技术专栏 | Vue源码分析之 Watcher 和 Dep

Posted TalkingData

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了技术专栏 | Vue源码分析之 Watcher 和 Dep相关的知识,希望对你有一定的参考价值。

本文由TalkingData原创,转载请获取授权。


之前在学习 Vue 官方文档深入响应式原理(https://cn.vuejs.org/v2/guide/reactivity.html)时,大概对响应式有一个概念性的了解以及它的效果是什么样子的。最近通过阅读 Vue 的源码并查询相关资料,对响应式原理有了更进一步的认识,并且大体上可以还原出它的实现过程。因此写这篇文章记录一下自己的理解过程。

响应式原理我理解可以分为两步,第一步是依赖收集的过程,第二步是触发-重新渲染的过程。先来看依赖收集的过程,有三个很重要的类,分别是 Watcher、Dep、Observer。Watcher 是观察者模式中的观察者;我把 Dep 看成是观察者模式中的主题,它也是管理和保存观察者的地方;Observer 用来使用 Object.defineProperty 把指定属性转为 getter/setter,它是观察者模式中充当发布者的角色。对于观察者模式不熟悉的话可以查看上篇文章。

接下来,从最基本的实例化 Vue 开始讲起,本篇文章会以下面这段代码作为实际例子,来剖析依赖收集的过程。

(* 左右滑动即可查看完整代码,下同)

var vm = new Vue({
 el: '#demo',
 data: {
   firstName: 'Hello',
   fullName: ''
 },
 watch: {
   firstName(val) {
     this.fullName = val + 'TalkingData';
   },
 }
})

在源码中,通过还原Vue 进行实例化的过程,我把相关代码做了梳理。从实例化开始一步一步到实例化了 Watcher 类的源码依次为(省略了很多不在本篇文章讨论的代码):

var vm = new Vue({
 el: '#demo',
 data: {
   firstName: 'Hello',
   fullName: ''
 },
 watch: {
   firstName(val) {
     this.fullName = val + 'TalkingData';
   },
 }
})

在源码中,通过还原Vue 进行实例化的过程,我把相关代码做了梳理。从实例化开始一步一步到实例化了 Watcher 类的源码依次为(我省略了很多不在本篇文章讨论的代码)
// src/core/instance/index.js
function Vue (options{
 if (process.env.NODE_ENV !== 'production' &&
   !(this instanceof Vue)
 ) {
   warn('Vue is a constructor and should be called with the `new` keyword')
 }
 this._init(options)
}

// src/core/instance/init.js
Vue.prototype._init = function (options?: Object{
 const vm: Component = this
 // ...
 initState(vm)
 // ...
}

// src/core/instance/state.js
export function initState (vm: Component{
 // ...
 const opts = vm.$options
 if (opts.data) {
   initData(vm)
 }
 // ...
 if (opts.watch && opts.watch !== nativeWatch) {
   initWatch(vm, opts.watch)
 }
}
function initWatch (vm: Component, watch: Object{
 for (const key in watch) {
   // ...
   createWatcher(vm, key, handler)
 }
}
function createWatcher (
 vm: Component,
 keyOrFn: string | Function,
 handler: any,
 options?: Object
{
 // ...
 return vm.$watch(keyOrFn, handler, options)
}
Vue.prototype.$watch = function (
 expOrFn: string | Function,
 cb: any,
 options?: Object
): Function 
{
 const vm: Component = this
 // ...

 // 注意:在这里实例化了 Watcher
 const watcher = new Watcher(vm, expOrFn, cb, options)
 // ...
}

下面我们来看 Watcher 类究竟起了什么作用:

// src/core/observer/watcher.js
import Dep, { pushTarget, popTarget } from './dep'
export default class Watcher {
 constructor (
   vm: Component,
   expOrFn: string | Function,
   cb: Function,
   options?: ?Object,
   isRenderWatcher?: boolean
 ) {
   this.vm = vm
   // ...
   if (typeof expOrFn === 'function') {
     this.getter = expOrFn
   } else {
     this.getter = parsePath(expOrFn)
     // ...
   }
   this.value = this.lazy
     ? undefined
     : this.get()
 }
}

注意这里 this.getter 的赋值,实际上是把 new Vue() 时, watch 字段里键值对(key/value)的键(key)- 也就是这里的参数 expOrFn - 赋值给了 this.getter。watch 字段里键值对(key/value)的键(key)可以是字符串,也可以是函数。如果 expOrFn 是函数,那么直接让 this.getter 等于该函数。如果是我们之前示例里的代码,那么 expOrFn = 'firstName',它是字符串,因此会调用 parsePath 方法。

下面的代码块是 parsePath 方法:

var bailRE = /[^\w.$]/;
function parsePath (path) {
 if (bailRE.test(path)) {
   return
 }
 var segments = path.split('.');
 // 返回一个匿名函数
 return function (obj) {
   for (var i = 0; i < segments.length; i++) {
     if (!obj) { return }
     obj = obj[segments[i]];
   }
   return obj
 }
}

该方法返回一个匿名函数,接受一个对象作为参数。在 Watcher类中的 get() 方法执行this.getter.call(vm,vm)时,会调用这个返回的匿名函数。在匿名函数中,segments[i]是 watch 字段里键值对(key/value)的键(key),如果 segments[i] 被 Object.defineProperty 设置了 get 方法的话,那么执行到 obj[segments[i]] 时,会触发 segments[i] 属性的 get 方法。在我们的示例里,就相当于执行了 vm[firstName],所以会触发 firstName 上的 get 方法(如果设置了的话)。

在 Watcher 的 constructor 中,除了调用了 parsePath(expOrFn),this.value= this.lazy ? undefined : this.get()还调用了 this.get() 方法。下面我们来看一下 Watcher 类里面 get 方法的代码:

// src/core/observer/watcher.js

//...
get() {
 pushTarget(this)

 let value
 const vm = this.vm
 try {
   value = this.getter.call(vm, vm)
 }
 // ...
 return value
}

// addDep方法,会在 Dep 类的 depend 方法中调用
addDep (dep: Dep) {
 const id = dep.id
 if (!this.newDepIds.has(id)) {
   this.newDepIds.add(id)
   this.newDeps.push(dep)
   if (!this.depIds.has(id)) {
     // Dep 类中的 addSub 方法
     dep.addSub(this)
   }
 }
}

pushTarget 是 Dep 类中的方法,它的作用是给 Dep.target 赋值,所以 pushTarget 执行了之后Dep.target = 当前这个 watcher 实例,在执行到value = this.getter.call(vm, vm)时,会调用之前 parsePath 里面返回的匿名函数,这时就会调用 Observer 类通过 Object.defineProperty 给 key 设置的 get 方法,而这个 get 函数执行过程中,因为 Dep.target 已经存在,那么就会进入依赖收集的过程。Observer类相关及其 get/set 方法的内容会在之后的文章里介绍,这里只需要知道,依赖收集的过程的开始,是执行了下面的代码:

// 实例化 Dep
const dep = new Dep();
// 调用 Dep 的 depend 方法
dep.depend();

这段代码实际上是在 Observer 类的一个方法中 Object.defineProperty 时的 get 方法中的,这个也是非常重要的内容,我会在之后的文章里介绍。

接下来我们来看 Dep 类的代码:

// src/core/observer/dep.js
export default class Dep {
 static target: ?Watcher;
 id: number;
 subs: Array<Watcher>;

 constructor () {
   this.id = uid++
   this.subs = []
 }

 addSub (sub: Watcher) {
   this.subs.push(sub)
 }

 removeSub (sub: Watcher) {
   remove(this.subs, sub)
 }

 depend () {
   if (Dep.target) {
     Dep.target.addDep(this)
   }
 }

 notify () {
   // stabilize the subscriber list first
   const subs = this.subs.slice()
   for (let i = 0, l = subs.length; i < l; i++) {
     subs[i].update()
   }
 }
}

之前已经介绍过,Dep.target 是 Watcher 类的实例,执行 Wachter 类的 this.get() 时,就会给 Dep.target 赋值。所以开始依赖收集而执行dep.depend();时,在这里的 depend 方法就会进而执行Dep.target.addDep(this)。addDep 是之前介绍过的 Watcher 类中的方法,addDep 会反过来调用 Dep 类的 addSub 方法,向 Dep 管理的 this.subs 观察者列表里 push 进当前 Watcher 实例。当列表中添加了该属性 - 比如示例里的 firstName - 的 Watcher 实例之后,依赖收集的过程在这里就结束了。也就是下面这张图中render - touch - Data[getter] - Collect as Dependency -Watcher的这个过程结束了。

那图中的Data[setter]- Notify - Watcher - Trigger re-render的过程是体现在哪里呢?根据上篇文章介绍的观察者模式,每次被 watch 的属性即被当作依赖收集起来的属性 - 比如示例里的 firstName - 发生变化时,会触发 Observer 类中的 Object.defineProperty 给该属性设置的 set 方法。set 方法里有回调用dep.notify。该方法会遍历观察者列表中的每一个 Watcher 实例即每一个观察者,然后触发每一个观察者的 update 方法进行更新,最终能更新 Virtual DOM TREE。

介绍完了 Watcher 和 Dep 类,接下来的文章该介绍 Observer 类了。

- To be continued -

更多技术干货:

①  

②  

③  

以上是关于技术专栏 | Vue源码分析之 Watcher 和 Dep的主要内容,如果未能解决你的问题,请参考以下文章

Vue源码实现之watcher拾遗

Vue源码实现之watcher拾遗

Vue源码之计算属性watcher

vue 源码分析之如何实现 observer 和 watcher

Vue源码之用户watcher

Vue源码之用户watcher