简单实现Vue的双向绑定__前端__Vue.js
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利
前言
前端面试时经常会问到Vue是如何实现双向绑定的,大部分人都会回答是通过数据劫持结合发布-订阅模式实现的,那么具体是怎么实现的,我们来简单的实现下。
双向绑定实现原理
实现双向绑定离不开三个类:Compile、Observer、Watcher、Dep
- Compile:扫描并解析每个元素节点,根据不同指令模板替换节点内容并绑定对应的更新函数。
- Observer:依附于每个被监听的对象,一旦依附,监听器转变目标对象的属性为响应式getter/setter方法,便于收集依赖项并分发更新操作。
- Watcher:连接Compile和Observer,解析表达式并收集依赖项,在表达式值更改时触发回调。可同时用于$watch api和指令集。
- Dep:依赖收集器,能被Watcher订阅
Vue 实例初始化
// 初始化
class Vue {
constructor(options) {
this.$options = options || {}
this.$el = typeof options.el === 'string' // 判断el是否为元素节点
? document.querySelector(options.el) // 若不是,则获取赋值
: options.el; // 若是,则直接赋值
this.$data = options.data;
if (this.$el) {
new Compile(this.$el, this)
}
}
}
Compile 指令解析器
class Compile {
constructor(el, vm) {
this.vm = vm
const childNodes = [...el.childNodes]; // 获取子节点,进行遍历
childNodes.forEach(node => {
// 根据不同节点类型进行编译
if (node.nodeType === 1) { //元素节点
this.compileElement(node)
} else if (node.nodeType === 3) { // 文本节点
this.compileText(node)
}
})
}
compileText(node) {
// 编译文本节点
}
compileElement(node) {
// 获取该节点的所有属性,对属性进行遍历
[...node.attributes].forEach(attr => {
const name = attr.name;
// 看当前name是否是一个指令
if (name.startsWith('v-')) { // v-if v-model v-show
// 对类似v-model进行操作, 去除v-
const attrName = name.substr(2);
this.update(node, attr.value, this, attrName)
// 在v-model="text"中 attr.value为"text"
} else if (name.startsWith('@')) { // @click
// 对事件进行处理 在这里处理的是@click
const eventName = name.split('@')[1];
this.event(node, attr.value, this, eventName)
// 在@click="clickEvent"中 attr.value为"clickEvent"
}
})
}
update(node, key, vm, attrName) {
const updater = {
// 使用的时候 和 Vue 的一样
text(node, key, value) {
node.textContent = value
},
// v-model
model(node, key, value) {
node.value = value
}
...
...
}
const fn = this.updater[attrName]
// 从 data 中获取值赋值给node
fn && fn(node, key, vm.$data[key])
}
event(node, key, vm, eventName) { // vm为Vue实例
// 从methods中获取方法,并做一个监听
const fn = vm.$options.methods && vm.$options.methods[key];
node.addEventListener(eventName, fn.bind(vm), false)
}
}
Observer 数据监听器
class Observer {
constructor(data) {
this.observe(data);
}
observe(data) {
// 判断 data 是否为非空对象
if (!data || typeof data !== 'object') return;
Object.keys(data).forEach(key => {
this.defineReactive(data, key)
})
}
defineReactive(obj, key) {
const value = obj[key]; // 如果 value 也为对象,递归遍历属性,添加响应式
this.observe(value);
Object.defineProperty(obj, key, {
// 设置可枚举
enumerable: true,
// 设置可配置
configurable: true,
get() {
return value;
},
set(newVal) {
if (newVal === value) return;
value = newVal // 设置新值
// 若新值为对象,递归遍历属性,添加响应式
this.observe(value)
}
})
}
}
修改Vue初始化方法
// 初始化
class Vue {
constructor(options) {
...
if (this.$data) {
// 使用 Observer 把data中的数据转为响应式
new Observer(this.$data)
}
}
}
Dep 发布者
class Dep {
constructor() {
// 观察者收集器
this.subs = []
}
addSub(sub) {
// 判断观察者是否存在且是否拥有update方法
if (!sub || !sub.update) return;
this.subs.push(sub)
}
notify() {
// 循环遍历观察者集合并调用他们update方法
this.subs.forEach(sub => {
sub.update();
})
}
}
修改Observer
...
defineReactive(obj, key) {
// 创建 Dep 对象
let dep = new Dep()
Object.defineProperty(obj, key, {
...
get() {
//订阅数据变化,往Dep中添加观察者
Dep.target && dep.addSub(Dep.target);
return value;
}
set(newVal) {
...
// 触发通知 更新视图
dep.notify()
...
}
...
})
}
...
Watcher 观察者
class Watcher {
constructor(vm, expr, callback) {
this.vm = vm; // Vue实例
this.expr = expr; // data中的属性名
this.cb = callback; // 回调
Dep.target = this; // 把观察者存放在 Dep.target
// 旧数据 更新视图的时候要进行比较
this.oldValue = vm[expr]; // vm[expr] 触发了 get 方法
// 数据赋值完之后Dep.target就可以置空了 不置空会发生错乱
Dep.target = null
}
// 观察者中的必备方法 用来更新视图
update() {
// 获取新值
let newValue = this.vm[this.expr]
// 比较旧值和新值
if (newValue === this.oldValue) return
// 调用具体的更新方法
this.cb(newValue)
}
}
修改Compile
update(node, key, vm, attrName) {
const updater = {
text(node, key, value) {
node.textContent = value
// 创建观察者
new Watcher(this.vm, key, newValue => {
node.textContent = newValue
})
},
model(node, key, value) {
node.value = value
// 创建观察者
new Watcher(this.vm, key, newValue => {
node.textContent = newValue
})
}
...
}
...
}
代理
我们在使用vue时可以直接通过this.name(vm.name)的方式来获取数据。这是因为vue内部也做了一层代理,把数据获取操作vm上的取值操作代理到vm.$data上
class Vue {
constructor(options) {
this.$options = options || {}
this.$el = typeof options.el === 'string' // 判断el是否为元素节点
? document.querySelector(options.el) // 若不是,则获取赋值
: options.el; // 若是,则直接赋值
this.$data = options.data;
if (this.$el) {
new Compile(this.$el, this)
}
if (this.$data) {
// 使用 Obsever 把data中的数据转为响应式
new Observer(this.$data)
}
this.proxyData(data)
}
proxyData(data) {
for (const name in data) {
Object.defineProperty(this, name, {
get() {
return data[name]
},
set(newVal) {
data[name] = newVal
}
})
}
}
}
参考资料
Vue的MVVM实现原理:https://juejin.cn/post/6844904183938678798#heading-12
手写一个简易vue响应式带你了解响应式原理:https://juejin.cn/post/6989106100582744072#heading-11
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 不好说君 原文链接:https://juejin.im/post/7012527585666007076