本文内容转载于:
详细版可阅读——剖析Vue原理&实现双向绑定MVVM
简易版文字解答可直接阅读——Vue双向数据绑定原理
一:思路整理
vue的双向绑定式原理的思路总结:
要在vue中实现mvvm的双向绑定,是通过 数据劫持 和 发布-订阅者 功能来实现的。实现步骤:
-
实现一个数据监听器 Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
-
实现一个指令解析器 Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
-
实现一个订阅者Watcher,作为连接Observer 和 Compile 的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的回调函数,从而更新视图
Vue 是通过数据劫持的方式来做数据绑定的,其中
最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,达到监听数据变动的目的;Object.defineProperty(obj,prop,descriptor)方法,其中三个参数:
- obj:要在其上定义属性的对象
- prop:要定义或修改的属性名称
- desciption:将被定义或者修改的属性描述符【通过写入的方法来定义或者修改一个值】
关于该方法的简单应用:
var obj = {}
Object.defineProperty(obj,'hello',{
get: function() {
console.log('调用了get方法获取对象的属性')
return this
},
set: function() {
console.log('调用了set方法,方法的值是:' + newValue)
}
})
obj.hello; // 调用了get方法获取对象的属性
obj.hello = 'Hi'; // 调用了set方法,方法的值是:Hi
二:具体实现
1:实现 Observer
在这个函数中我们需要observer的数据对象进行递归遍历,包括子属性对象的属性,都加上 getter 和 setter
function observe(data){
if(!data || typeof data !== 'object') {
return
}
// 取出所有属性进行递归遍历
Object.keys(data).forEach(key => {
defineReactive(data, key, date[key])
})
}
// 定义可响应式,通过Object.defineProperty来进行数据劫持
function defineReactive(data, key, val) {
observe(val) // 递归监听子属性
Object.defineProperty(data, key, {
enumrable: true // 可枚举
configurable:false // 不能再define
get:function() {
return val
},
set:function(newVal) {
console.log('yeah,监听到了值变化',val ,'变为了', newVal)
val = newVal
}
})
}
这样可以实现监听每个数据的变化,但是监听到了变化如何通知订阅者。故也需要实现一个消息订阅器,维护一个数组即可,用来收集订阅者(watcher),数据变动触发 notify ,再调用订阅者的 update 方法,可改善如下:
function defineReactive(data, key, val) {
var dep = new Dep()
observe(val) // 递归监听子属性
Object.defineProperty(data, key, {
... // 省略
set:function(newVal) {
if(val === newVal ) return
console.log('yeah,监听到了值变化',val ,'变为了', newVal)
val = newVal
dep.notify() // 通知所有订阅者
}
})
}
// 实现的消息订阅器[数组形式]
function Dep() {
this.subs = []
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub)
},
notify: function() {
this.subs.forEach(sub => sub.update())
}
}
在上述的思路整理中,我们已明确订阅者应该就是 Watcher,而 var dep = new Dep()是在defineReactive方法内部定义的,故想通过dep添加订阅者,就必须在闭包内进行操作,所有我们可以在在getter里面进行修改:
function defineReactive(data, key, val) {
... // 省略
get:function() {
// 需要在闭包中添加watcher,故通过Dep定义一个全局target属性,暂存watcher,添加完再移除
Dep.target && dep.addSub(Dep.target)
return val
}
... // 省略
})
}
// Watcher.js
Watcher.prototype = {
get: function(key) {
Dep.target = this;
this.value = data[key] // 这里会触发属性的gtter,从而添加订阅者
}
}
目前已实现了一个Observer ,已具备监听数据和数据变化通知订阅者的功能;
2:实现Compile
compile 主要做的事情就是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加数据的订阅者,一旦数据有变动,收到通知,更新视图
实现Compile 的代码自己还看不懂…待后期补充
主要思路:
- 因为遍历解析的过程有多次操作dom节点,为提高性能和效率,会先将跟节点el转换成文档碎片fragment进行解析编译操作,解析完成,再将fragment添加回原来的真实dom节点中
- compileElement方法将遍历所有节点及其子节点,进行扫描解析编译,调用对应的指令渲染函数进行数据渲染,并调用对应的指令更新函数进行绑定
3:实现Watcher
Watcher 订阅者作为Observer 和 Compile 之间的通信桥梁,主要做的事情就是:
- 在自身实例化时往属性订阅器(dep)里添加自己
- 自身必须有个 update() 方法
- 待属性变动 dep.notice() 通知时,能调用自身的 update() 方法,并触发Compile 中绑定的回调
function Watcher(vm, exp, cb) {
this.cb = cb
this.vm = vm;
this.exp = exp;
// 此处为了触发属性的getter,从而在dep[消息订阅器数组]添加自己,结合Observer更易理解
this.value = this.get()
}
Watcher.prototype = {
update: function() {
this.run() // 属性值变化收到通知
}
run: function() {
var value = this.get(); // 取到最新值
var oldVal = this.value;
if(value !== oldVal) {
this.value = value
// 值有更新时调用Compile中绑定的回调函数,更新视图
this.cb.call(this.vm, value, oldVal)
}
},
get:function() {
Dep.target = this; // 将当前订阅者指向自己
var value = this.vm[exp]; // 触发 getter,添加自己到属性订阅器中
Dep.target = null // 添加完毕,重置
return value
}
}
// 这里再次列出Observer和Dep,方便理解
Object.defineProperty(data, key, {
get: function() {
// 由于需要在闭包内添加watcher,所以可以在Dep定义一个全局target属性,暂存watcher, 添加完移除
Dep.target && dep.addSub(Dep.target);
return val;
}
// ... 省略
});
// set中值有更新->消息订阅器notify()->订阅者update()->执行compile中回调函数
Dep.prototype = {
notify: function() {
this.subs.forEach(function(sub) {
sub.update(); // 调用订阅者的update方法,通知变化
});
}
};