berwin/Blog

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

berwin opened this issue · 11 comments

深入浅出 - 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方法来修改数据

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

第一张图挂了~ @berwin

页面修改后调用watch的set有详细介绍吗

@fuweiWork 是说在页面input修改数据后调用watch的原理么??

修改数据后怎么知道修改哪部分视图呢,还是没弄懂

@lujing2 简单来说,页面中有一个DOM节点使用了某个数据,那这个数据就会收集一个依赖,这个依赖就是这个节点,所以当数据发生变化的那一瞬间就可以通过这个数据的依赖列表得知应该更新哪些个DOM节点~ 这是Vue1的实现~

@berwin 这些dom节点是不是得有id 这样数据更改了才好更新视图

有个疑问,对于{{name}}is{{sex}} 这种,节点与对象绑定时的具体处理是怎样的。name修改了,会去访问sex的数据吗?

@francecil 会访问sex数据,目前的粒度同一个组件内的数据只要有一个有变动,其余的数据都会访问~ 想要深入学习的话,建议买一本 《深入浅出Vue.js》

Observer对基本数据类型 如何实现监听的?

Watcher是怎样将它的实例添加到Dep中的,是通过Watcher里面的那个get方法吗?麻烦回答的详细一点,谢谢!

哪位大神能跟我解释一下上面的那个问题,我是真的急啊,麻烦了