详解Vue源码学习之双向绑定
原理
当你把一个普通的JavaScript对象传给Vue实例的data选项,Vue将遍历此对象所有的属性,并使用Object.defineProperty把这些属性全部转为getter/setter。Object.defineProperty是ES5中一个无法shim的特性,这也就是为什么Vue不支持IE8以及更低版本浏览器。
上面那段话是Vue官方文档中截取的,可以看到是使用Object.defineProperty实现对数据改变的监听。Vue主要使用了观察者模式来实现数据与视图的双向绑定。
functioninitData(vm){//将data上数据复制到_data并遍历所有属性添加代理
vm._data=vm.$options.data;
constkeys=Object.keys(vm._data);
leti=keys.length;
while(i--){
constkey=keys[i];
proxy(vm,`_data`,key);
}
observe(data,true/*asRootData*/)//对data进行监听
}
在第一篇数据初始化中,执行newVue()操作后会执行initData()去初始化用户传入的data,最后一步操作就是为data添加响应式。
实现
在Vue内部存在三个对象:Observer、Dep、Watcher,这也是实现响应式的核心。
Observer
Observer对象将data中所有的属性转为getter/setter形式,以下是简化版代码,详细代码请看这里。
exportfunctionobserve(value){
//递归子属性时的判断
if(!isObject(value)||valueinstanceofVNode){
return
}
...
ob=newObserver(value)
}
exportclassObserver{
constructor(value){
...//此处省略对数组的处理
this.walk(value)
}
walk(obj:Object){
constkeys=Object.keys(obj)
for(leti=0;i
创建Observer对象时,为data的每个属性都执行了一遍defineReactive方法,如果当前属性为对象,则通过递归进行深度遍历。该方法中创建了一个Dep实例,每一个属性都有一个与之对应的dep,存储所有的依赖。然后为属性设置setter/getter,在getter时收集依赖,setter时派发更新。这里收集依赖不直接使用addSub是为了能让Watcher创建时自动将自己添加到dep.subs中,这样只有当数据被访问时才会进行依赖收集,可以避免一些不必要的依赖收集。
Dep
Dep就是一个发布者,负责收集依赖,当数据更新是去通知订阅者(watcher)。源码地址
exportdefaultclassDep{
statictarget:?Watcher;//指向当前watcher
constructor(){
this.subs=[]
}
//添加watcher
addSub(sub:Watcher){
this.subs.push(sub)
}
//移除watcher
removeSub(sub:Watcher){
remove(this.subs,sub)
}
//通过watcher将自身添加到dep中
depend(){
if(Dep.target){
Dep.target.addDep(this)
}
}
//派发更新信息
notify(){
...
for(leti=0,l=subs.length;i
Watcher
源码地址
//解析表达式(a.b),返回一个函数
exportfunctionparsePath(path:string):any{
if(bailRE.test(path)){
return
}
constsegments=path.split('.')
returnfunction(obj){
for(leti=0;i
依赖收集的触发是在执行render之前,会创建一个渲染Watcher:
updateComponent=()=>{
vm._update(vm._render(),hydrating)//执行render生成VNode并更新dom
}
newWatcher(vm,updateComponent,noop,{
before(){
if(vm._isMounted){
callHook(vm,'beforeUpdate')
}
}
},true/*isRenderWatcher*/)
在渲染Watcher创建时会将Dep.target指向自身并触发updateComponent也就是执行_render生成VNode并执行_update将VNode渲染成真实DOM,在render过程中会对模板进行编译,此时就会对data进行访问从而触发getter,由于此时Dep.target已经指向了渲染Watcher,接着渲染Watcher会执行自身的addDep,做一些去重判断然后执行dep.addSub(this)将自身push到属性对应的dep.subs中,同一个属性只会被添加一次,表示数据在当前Watcher中被引用。
当_render结束后,会执行popTarget(),将当前Dep.target回退到上一轮的指,最终又回到了null,也就是所有收集已完毕。之后执行cleanupDeps()将上一轮不需要的依赖清除。当数据变化是,触发setter,执行对应Watcher的update属性,去执行get方法又重新将Dep.target指向当前执行的Watcher触发该Watcher的更新。
这里可以看到有deps,newDeps两个依赖表,也就是上一轮的依赖和最新的依赖,这两个依赖表主要是用来做依赖清除的。但在addDep中可以看到if(!this.newDepIds.has(id))已经对收集的依赖进行了唯一性判断,不收集重复的数据依赖。为何又要在cleanupDeps中再作一次判断呢?
while(i--){
constdep=this.deps[i]
if(!this.newDepIds.has(dep.id)){
dep.removeSub(this)
}
}
lettmp=this.depIds
this.depIds=this.newDepIds
this.newDepIds=tmp
this.newDepIds.clear()
tmp=this.deps
this.deps=this.newDeps
this.newDeps=tmp
this.newDeps.length=0
在cleanupDeps中主要清除上一轮中的依赖在新一轮中没有重新收集的,也就是数据刷新后某些数据不再被渲染出来了,例如: