深入浅出 - vue之深入响应式原理

@berwin 2016-12-23 10:06:49发表于 berwin/Blog javascriptvue.js

深入浅出 - vue之深入响应式原理

本文讲的内容是 vue 1.0 版本,同时为了阅读者的阅读心情,本文尽量做到不枯燥,特别适合那些想明白内部原理又讨厌看枯燥的源码的同学~

说到响应式原理其实就是双向绑定的实现,说到 双向绑定 其实有两个操作,数据变化修改dom,input等文本框修改值的时候修改数据

1. 数据变化 -> 修改dom
2. 通过表单修改value -> 修改数据

先说第一步,数据变化更改DOM的一个前提条件是能够知道数据什么时候变了,像这种需求如果不考虑兼容性的话,用屁股想都知道可以通过 gettersetter 来实现,每当触发 setter 的时候更新DOM

但这就引发了一个问题,我们怎么知道当 setter 触发的时候更新哪个DOM?

一个解决思路是,我们先知道哪些dom需要用到数据,当触发 setter 的时候把所有使用到该数据的dom更新

所以我们需要一个收集依赖关系的功能,每当触发 getter 的时候如果是 DOM 中触发的,我把这个 Key 和 DOM 记录起来,这样当这个 Key 触发 setter 的时候,我把这个 Key 所对应的所有 DOM都更新一遍,这样一个简单的单向绑定就实现了

下面说说第二步,其实第二步要比第一步简单的多,以input为例:

<input v-model="name" />

很明显,我只需要使用 getAttribute 方法读取 v-model 拿到的值就是 Key,在通过 input.value 拿到 value,直接就可以用key和value把数据改了






一切看起来都是那么的美好...






可是...






如果像 vue 这样的一个能投入生产环境下使用,而非玩具的框架的实现要考虑的事情要比上面那个多的多,实现方式也要复杂的多。






下面看看vue的实现方式

Data

上图是vue官方文档中的一张图片

可以看到最右侧绿色的圆代表数据,里面紫色的圆代表属性,属性被 gettersetter 拦截

有一条黑色虚线指向 getter,标注的英文是 Collect Dependencies,代表触发 getter 的时候收集依赖(其实就是把watcher实例推到依赖列表里)

setter 处有一条红线指向 Watcher,标注是 Notify,代表触发 setter 时,会发送消息到 Watcher

可以看到 gettersetter 的部分与我上面的猜测基本一致,但是多了个 WatcherDirective,其实这就是我上面说到的,作为vue来讲,并不是简单的更新dom就可以了,vue中有很多指令,不同的指令有不同的更新DOM的方式而 Directive 就是用来处理这方面的事情用的

那中间那个 Watcher 是个什么鬼?

Watcher 可以先暂时理解为 房产中介 用户买房子找中介,中介帮忙找房主,房主卖房子找中介,中介帮房主把房子卖给用户。。。。。。。。。。。。。。

setter 触发消息到 Watcher watcher帮忙告诉 Directive 更新DOM,DOM中修改了数据也会通知给 Watcher,watcher 帮忙修改数据

关于Directive 和 Watcher 后面会细说

其实站在原理的角度上讲,上图中的内容是不全的,上图中是为了使用者更好的理解响应式画的图,而不是为了研究者画的图

这张图片是《Vue.js 权威指南》中源码篇的一个章节中画的图,专门画给研究者看的

可以看到 多了一个 DepObserver

下面我们就要说说 ObserverDepWatcherDirective 他们之间的关系以及vue是如何通过他们实现的双向绑定

先说说 Observer

Observer,正如它的名字,Observer就是观察者模式的实现,它用来观察数据的变化,触发消息。

Observer会观察两种类型的数据,ObjectArray

对于Array类型的数据,会先重写操作数组的原型方法,重写后能达到两个目的,

  1. 当数组发生变化时,触发 notify
  2. 如果是 push,unshift,splice 这些添加新元素的操作,则会使用observer观察新添加的数据

重写完原型方法后,遍历拿到数组中的每个数据 使用observer观察它

而对于Object类型的数据,则遍历它的每个key,使用 defineProperty 设置 getter 和 setter,当触发getter的时候,observer则开始收集依赖,而触发setter的时候,observe则触发notify。

那怎么收集的依赖呢?

这个时候 Dep 改闪亮登场了。

当数据的 getter 触发后,会收集依赖,但也不是所有的触发方式都会收集依赖,只有通过watcher 触发的 getter 会收集依赖,而所谓的被收集的依赖就是当前 watcher

这里需要特殊说一下,因为只有watcher触发的 getter 才会收集依赖,所以DOM中的数据必须通过watcher来绑定,就是说DOM中的数据必须通过watcher来读取!

Dep 提供了一些方法,我先简单帖两个

export default function Dep () {
  this.id = uid++
  this.subs = []
}

Dep.prototype.addSub = function (sub) {
  this.subs.push(sub)
}

Dep.prototype.notify = function () {
  // stablize the subscriber list first
  var subs = toArray(this.subs)
  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}

...

可以看到其实挺简单的,当通过 watcher 触发 getter时,watcher会使用 dep.addSub(this) 把自己的实例推到 subs

当触发setter的时候,会触发notify,而notify则会把watcher的update方法执行一遍。

到这里 observer dep watcher 已经缕清了

那么watcher的update方法是如何配合Directive改变视图的呢??

说到这里就要从编译模板的时候说起了。。。。。

Directive

看上图,我们上一节讲的是左边的那部分内容,这一节我们讲右边那部分内容

关于编译这块vue分了两种类型,一种是文本节点,一种是元素节点,如果以最简单的文本节点为例,首先需要知道什么是文本节点?

hello {{name}}

这就是一个文本节点,它包含两部分,普通文本节点 hello 和一个特殊的节点{{name}}

第一步

首先第一步vue会通过正则来解析文本节点,把普通文本节点和特殊节点区分开,解析后大概长下面这样

[{
  value: 'hello '
}, {
  value: 'name',
  tag: true,
  html: false,
  oneTime: false
}]

第二步

第二步是遍历Array,将所有tag为true的添加扩展对象扩展属性包括指令方法

像文本节点的特殊节点只有两种类型,text和html,所以简单判断html的值就可以知道,应该给扩展类型添加那种指令的接口

添加扩展对象后大概长成下面的样子

[{
  value: 'hello '
}, {
  value: 'name',
  tag: true,
  html: false,
  oneTime: false,
  descriptor: {
    def: {
      update: function,
      bind: function
    },
    expression: xx,
    filters: xx,
    name: 'text'
  }
}]

可以看到vue内置了这么多的指令,这些指令都会抛出两个接口 bind 和 update,这两个接口的作用是,编译的最后一步是执行所有用到的指令的bind方法,而 update 方法则是当 watcher 触发 update 时,Directive会触发指令的update方法

observe -> 触发setter -> watcher -> 触发update -> Directive -> 触发update -> 指令

第三步
第三步是将所有 tagtrue 的数据中的扩展对象拿出来生成一个Directive实例并添加到 _directives 中(_directives是当前vm中存储所有directive实例的地方)。

this._directives.push(
  new Directive(descriptor, this, node, host, scope, frag)
)

第四步

循环 _directives 执行所有 directive实例的 _bind 方法。

Directive 中 _bind 方法的作用有几点:

  1. 调用所有已绑定的指令的 bind 方法
  2. 实例化一个Watcher,将指令的update与watcher绑定在一起(这样就实现了watcher接收到消息后触发的update方法,指令可以做出对应的更新视图操作
  3. 调用指令的update,首次初始化视图

这里有一个点需要注意一下,实例化 Watcher 的时候,Watcher会将自己主动的推入Dep依赖中

好了,到这里整体的流程已经结束了,来一段总结吧

总结

响应式原理共有四个部分,observeDepwatcherDirective

observer可以监听数据的变化

Dep 可以知道数据变化后通知给谁

Watcher 可以做到接收到通知后将执行指令的update操作

Directive 可以把 Watcher 和 指令 连在一起

不同的指令都会有update方法来使用自己的方式更新dom

必须使用watcher触发getter,Dep才会收集依赖

执行流:

当数据触发 setter 时,会发消息给所有watcher,watcher会跟执行指令的update方法来更新视图

当指令在页面上修改了数据会触发watcher的set方法来修改数据

您的赞助是我最大的动力~