详解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;iWatcher
源码地址
//解析表达式(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中主要清除上一轮中的依赖在新一轮中没有重新收集的,也就是数据刷新后某些数据不再被渲染出来了,例如: