第一次写文章,写得不对的地方但愿各位大神指出。vue
Vue的响应式原理是经过“观察者/订阅者”模式实现的。react
首先,Vue会给data及data下的数组、对象循环调用Object.defineProperty
方法来设置getter和setter,以此来拦截data的赋值和取值。也就是说,当咱们赋值(如:this.property='string'
)时,会调用Object.defineProperty
方法设置的set方法,当咱们取值时(如:<div v-if="title">{{title}}</div>
),会调用设置的get方法。算法
get方法会判断当前Dep.target
(Dep对象用于维护依赖,Dep.target
用于保存当前Watcher对象)是否为空,若是不为空,则将Dep.target
加入到dep对象的subs数组中用以记录依赖,也就是说这个subs数组中记录了全部会取该data的Watcher对象,这样的话当该data发生改变时,咱们就能经过这个数组来通知全部的依赖去进行更新,从而完成响应式。数组
这个通知过程就是经过set方法来完成的。set方法会调用dep.notify方法来通知全部依赖的Watcher对象,让他们调用本身的update方法来进行更新。函数
接下来讲一下这个Watcher是如何建立的。性能
当Vue组件在渲染时,会先经过compileToFunctions函数将组件的template来生成一个render函数(这个render函数是用于生成VNode虚拟DOM树的),而后会建立一个Watcher对象,并将生成的render函数传给这个Watcher对象用于更新。当Watcher对象建立时,会调用咱们传进去的render函数,调用render函数时会去获取template中使用到的data的值,这样的话就会触碰到getter,将这个Watcher对象添加到依赖中。这样咱们整一个链路就完成了。this
当data发生改变,就会通知这个Watcher对象去更新,这个Watcher对象就会调用render函数去从新渲染。因为Vue2.0使用的是Virtual DOM,因此当data改变时,从新渲染的就只有改变的部分,不用担忧整个组件从新渲染形成性能问题,因此整一个render就只须要一个Watcher对象去维护而不是像Vue1.0时那样每个Directive对应一个Watcher对象。3d
1. 为何有时候个人数据响应式会失效?code
因为这个响应式的创建是在Vue组件渲染时就进行的,因此在代码中给data添加属性就没法实现响应式,由于这些属性并无加上setter和getter,当它被修改时没法通知Watcher对象去进行更新。cdn
若是咱们须要在组件渲染完以后去添加一个响应式的属性,须要用Vue.$set(obj,'name',value)
来为data对象中已有的对象添加属性。也就是说data中的根属性必需要一开始就定义好,不然没法实现响应式。
举个例子:
咱们能够经过Vue.$set
方法给dialog添加一个响应式的callback属性,可是没法添加一个响应式的data根属性productId(假设productId这个属性一开始没有定义)。
2. 为何计算属性也能实现响应式?
在Vue2.0中,data改变时Watcher对象调用render函数从新渲染,因此使用到计算属性的地方也会被从新计算,从而实现了响应式。
3. 为何有时响应会有延时?
好比当咱们修改数据后立刻去获取DOM时会发现获取到DOM彷佛尚未改变,这是由于当数据发生变化时,Vue会将数据的变化放到一个队列中,等到下一个‘tick’再去执行DOM的更新,从而避免反复地去更新DOM。若是咱们有一些须要依赖更新后的DOM的操做,咱们能够将这些操做做为回调放到vm.$nextTick(callback)
里,这样在下一个‘tick’就会执行咱们回调函数。
先从创建一个Vue实例开始看。
能够看到,建立Vue实例时,会调用this._init
方法,接下来看一下this._init
方法中的关键代码。
这里面调用了一个initState方法,看一下initState方法干了什么。
能够看到,在initState方法中,会调用initProps,initMethods,initData,initComputed,initWatch等方法。它们会根据组件的props,methods,data,computed,watch等进行初始化。
咱们主要关注initData方法。
首先,组件options中的data会被赋给vm._data
,而后会执行observe(data,true)
,接下来看看observe方法是怎么定义的。
若是value已经有ob对象的话,会返回value.ob,不然通过一系列判断后(如value是否为数组或对象,value是否可拓展,value是否为Vue实例等)使用value来建立一个Observer对象并返回。接下来看看Observer类是如何定义的。
这里首先会给Observer对象new一个Dep对象,Dep对象是用于处理数据依赖的,它有一个id和subs(用于收集依赖)。而后def方法会经过defineProperty把该Observer对象做为ob属性添加给value。而后判断value是否为数组,若是是,则调用observeArray方法对数组中的元素调用observe方法;若是不是数组的话会调用walk方法。walk方法会对value中的属性循环调用definereactivity方法。下面看看definereactivity中的关键代码。
首先咱们会对value的属性进行observe(let childOb = !shallow && observe(val)
)。在definereactivity中会调用defineProperty方法给value设置getter和setter,这样咱们就能够拦截到value的get和赋值。也就是说当咱们使用value时会调用getter来取值,给value赋值时会调用setter而不是想原来同样直接赋值。
当getter被调用时,若是Dep.target
不为空,则将调用dep.depend
方法,在depend方法中会调用Dep.target.addDep
方法(addDep是Watcher类的一个方法)将dep对象push到Dep.target的newDeps数组中,同时会调用Dep类的addSub方法将Watcher对象push到Dep对象中用来记录依赖的subs数组中。而后会调用childOb.dep.depend()
将Watcher对象收集到value的childOb的dep对象中。
有一个问题是,childOb的dep对象是Observer类中的dep,而当咱们调用setter时,调用dep.notify()来通知依赖该数据的Watcher时,使用的是在definereactivity方法中定义的dep,因此这一步暂时意义不明,可能有别的用途。
当Watcher对象调用getter时,经过以上代码就能够将依赖该value的Watcher收集起来。
再来看看setter。setter首先是会设置新的值,而后从新observe这个新的值,最后调用dep.notify()
通知依赖该数据的Watcher对象调用update方法。
接下来看一下Vue组件的渲染过程。
在_init方法中,会调用vm.$mount
方法将template或el编译成render函数。这个生成的render函数会在vm._render
方法中被调用,生成VNode对象。而后通过DOM Diff算法查找差别,生成真正的DOM树,从而实现渲染。
下面看一下具体的实现过程,看看$mount方法到底作了什么。
在$mount方法中,会调用compileToFunctions方法生成render和staticRenderFns。render就是render函数,staticRenderFns是一个数组,包含着不会发生变化的VNode节点所生成的函数。
接下来,$mount方法会调用这个mount方法,而这个mount方法会调用mountComponent方法。
能够看到在mountComponent方法中,会建立一个新的Watcher对象,并传入updateComponent函数,这个函数会返回vm._update(vm._render(), hydrating)
。
前面咱们知道vm._render
方法会调用生成的render函数来返回一个VNode对象,vm._update
方法会调用vm.__patch__
方法将这个VNode对象与以前的VNode对象比较,把差别的部分渲染到真正的DOM树上。
最后来看一看Watcher类的定义。
首先它会将getter设为expOrFn,从上面看到,在渲染时,这个expOrFn就是updateComponent函数。而后会调用get方法。
在get方法中,首先会调用pushTarget函数将Watcher对象设为Dep.target,而后会调用getter函数获取value,也就是会调用vm._update(vm._render(), hydrating)
,从而调用compileToFunctions函数生成的render函数。在调用render函数的时候,会去获取模板中所使用到的数据,从而触发数据Observer的getter。
因为设置了Dep.target
,因此触发getter时,数据的Dep对象会将Watcher对象收集为依赖,这样就完成了渲染的依赖收集。每当咱们去修改响应式数据时,setter就会经过dep.notify
方法来调用Watcher的update方法。在调用完getter函数后,会经过popTarget函数将Dep.target
置空。
能够看到update方法中会调用run方法或queueWatcher方法,queueWatcher会将Watcher对象加入到队列中,在nextTick调用它的run方法。因此这两种方式最终都会调用Watcher对象的run方法。在run方法中会再次调用Watcher对象的get方法,从新取值并收集依赖。上面能够看到Watcher对象的get方法会调用getter函数,这个getter函数会去调用vm._update(vm._render(), hydrating)
,从而从新渲染。
这样当咱们修改数据时,就完成了响应式的DOM变化。