本文分析下Vue是如何实现响应式的,个人感觉Vue能如此的受欢迎,一个很重要的原因就是因为通过响应式,只要修改数据就可以操作DOM。从架构的角度看,响应式承载了一个ViewModel的作用,是MVVM架构的核心。

我们先简单了解下Vue是如何实现这个功能的,当然了,这只是Vue源码的一部分,后续我们在一起分析下编译,VirtualDOM等其他模块。

Vue简单使用

下面看一个HelloWorld实现,通过在构造vue的传入一个el参数或者template参数,告诉Vue需要编译的模板,vue会将这个template编译成js,比如我们通过如下的方式初始化Vue。

这个template就是vue需要编译的模板,当然一般情况下这个模板会非常复杂,比如嵌套很多的组件,这里我们为了演示原理只写一个div。

template会被编译成vue实例(或组件)的_render函数,比如上面的template会被编译成如下内容。

这段js其实就是生成VirtualDOM了,在Vue中是一个VNode对象。上面的_c_v的都是Vue的内置渲染函数,我们可以先看下。

在core/instance/render-helpers目录下:

这些方法最终会定义在Vue的原型上。

渲染暂时介绍到这,本文主要是讲响应式,响应式指其中{{text}}就和data中的text相绑定了,即vm.data.text改变,div中的text也会发生改变。

vue 初始化

构造Vue实例是通过new Vue()进行的。

最终调用this._init函数,这个函数的作用是根据传入的options初始化vue实例中的字段,我们看下实现。

代码比较长,这里只取核心逻辑。

initState这个方法是初始化了Vue中props,data,computed,watcher等字段。
也就是说我们构造Vue实例的options对象,在这个阶段会初始化到Vue实例上。

接下来我们以initData为例介绍下data是如何变成响应化对象并暴露到Vue实例上的。

响应化与字段暴露

首先我们看下initData的实现。

下面我们把这个流程通过一张图来描述。

我们可以看到,在initData的过程中,存在一个 (3)proxy (4)observe 的过程,其中proxy是将this._data中的字段通过定义get/set映射到Vue实例上去。

具体逻辑在proxy函数中。

看到这里我们有了一个概念,我们之所以能够通过vue实例访问到定义在data中的数据,其根本是通过proxy中定义的proxyGetter/proxySetter暴露的,实际上是访问的this._data对象。

而this._data对象是响应式的,具体是怎么变成响应式的呢?接着往下看。
在proxy之后会调用observer将这个对象及子对象(若有的话)变成响应式的。

这个方法的逻辑是,如果该对象没有__ob__字段并__ob__不是一个Observer,那么就初始化一个Observer对象,并作为__ob__字段。

new Observer的过程中,遍历该对象的所有key,然后对每个key都添加一个get/set,之后每次访问这个对象的时候,都可以触发我们get/set中的逻辑了。

可以看到,通过walk进行遍历,将每个字段都用defineReactive定义get/set。

defineReactive可以说是核心了,下面看下实现。

总结下:
使用defineReactive对this._data中每个字段递归的定义get/set,这个get/set名字称为reactiveGetter/reactiveSetter,正是通过这些get/set,我们可以知道对象字段的变化,比如有逻辑获取这个对象的值,我们可以通过get得知。对象的某个字段发生改变了,我们可以通过set得知,并及时通知依赖方(订阅者)进行更新。

Vue上某字段的访问过程如下图:

下面将详细分析下这个defineReactive,这是响应式的核心。

Vue 依赖收集

刚才讲了通过定义get/set捕获对象的修改和访问,那么如何在对象修改的过程中,通知到依赖方数据发生变化了呢?

我们首先可以想到可以使用观察者模式,这是一种使用相当广泛的设计模式,在Java/Android开发中有大量使用。

Vue中是也是使用观察者模式实现的,它注册依赖(订阅者)的过程称为依赖收集

注册依赖的过程是在reactiveGetter的过程中完成的,我们在看下刚才的代码:

reactiveGetter的过程中涉及到一个Dep对象,我们看下这个Dep对象的定义。

这个Dep对象是可以理解为JDK中的Observable,它会通过notify()方法告诉所有的依赖方(订阅者)该依赖发生变化了。

依赖方可以理解为订阅者,这里的订阅者是watcher对象,每一个组件会持有一个watcher对象观测数据的变化,如果数据发生变化了,我们的wacherupdate()会被调用,然后触发UI的更新。

我们通过一个类图来看下Observer/Dep/Watcher三者的关系。

那Watcher是何时进行初始化的呢?

Watcher的初始化

我们先看下watcher的定义:
为了方便阅读,删了些次要的代码。

vue在进行mount的时候会递归mount所有子组件,我们看下代码。

我们看下这个new Watcher的过程,watcher的的expOrFnupdateComponent,这个方法是刷新DOM的我们先不管它,认为调用后可以更新DOM就可以了。

这个expOrFn是关键,每次数据改变时,watcher都会重新调用expOrFn,这里是调用updateComponent,也就更新DOM了。

Watcher求值并调用到reactiveGetter

在watcher的构造器中,会首先调用get()进行求值。

而get()过程,会把自己(watcher实例)放到Dep的静态变量里。

其中这段代码

会最终通过访问vue实例属性的流程调用到reactiveGetter,这个时候就触发reactiveGetter的依赖收集机制了。

reactiveGetter中收集依赖 (watcher)

这段代码会将当前watcher放入Dep的静态属性中。

我们看下pushTarget的实现。

简单说就是将watcher放到这个Dep对象的静态变量里,然后在reactiveGetter中我们就能获取这个watcher了。

可以看到如果判断Dep.target存在的话,那么就执行dep.depend(),这段逻辑是将Dep.target中的watcher,添加到这个dep对象的sub数组中,然后当有逻辑更新这个变量时,dep会通知watcher进行更新(调用update()),逻辑在reactiveSetter中。

可以说这个就是依赖收集的全过程了,简单总结下:

watcher对象是我们所说的依赖,也就是订阅者,watcher对象在vue实例的$mount过程中生成,此时的watcher中的getter是vue实例的_render(),_render()上文介绍过,通过模板编译而成,可以直接调用到依赖的字段,比如上文的{{text}}

new Watcher的过程中,会触发一个get(),并将自己放入Dep的target中,此时对字段的调用触发到reactiveGetter,reactiveGetter把正在Dep.target中的watcher收集起来,此时当前调用的watcher被收集到了dep.sub中。

用图片描述方法调用:

总结下整个依赖收集流程:

reactiveSetter派发更新

依赖通过reactiveGetter收集到并保存在dep中。

当vue上某个字段被修改时,会调用到reactiveSetter,此时在setter内部会通过dep.notify将更新传递给watcher。

然后watcher收到更新后,会更新DOM。
这个过程称为派发更新
reactiveSetter实现:

notify()实现:

watcher的update()实现:

queueWatcher其实就是在nextTick的时候再调用一次getter,并且执行callback(this.cb字段)

Vue在每个组件中注册一个Watcher,这个watcher的getter就是当前组件的渲染函数(_render),如果watcher重新调用getter的话,就意味着组件进行了重新渲染。

总结下派发更新这个过程:

Vue响应式的实现主要由依赖收集,派发更新这两个部分组成,到这里我们就介绍完了。

接下来我们会分析Vue源码的其他模块,大家如果有什么地方没看明白,欢迎留言。

发表评论

电子邮件地址不会被公开。 必填项已用*标注