Vue源码分析之 Watcher和Dep

作者:TalkingData 张成斌

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

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

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

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

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

varvm =newVue({

el:'#demo',

data: {

firstName:'Hello',

fullName:''

},

watch: {

firstName(val) {

this.fullName = val +'TalkingData';

},

}

})

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

varvm =newVue({

el:'#demo',

data: {

firstName:'Hello',

fullName:''

},

watch: {

firstName(val) {

this.fullName = val +'TalkingData';

},

}

})

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

// src/core/instance/index.js

functionVue(options){

if(process.env.NODE_ENV !=='production'&&

!(thisinstanceofVue)

) {

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){

constvm: Component =this

// ...

initState(vm)

// ...

}

// src/core/instance/state.js

exportfunctioninitState(vm: Component){

// ...

constopts = vm.$options

if(opts.data) {

initData(vm)

}

// ...

if(opts.watch && opts.watch !== nativeWatch) {

initWatch(vm, opts.watch)

}

}

functioninitWatch(vm: Component, watch:Object){

for(constkeyinwatch) {

// ...

createWatcher(vm, key, handler)

}

}

functioncreateWatcher(

vm: Component,

keyOrFn:string|Function,

handler:any,

options?:Object

){

// ...

returnvm.$watch(keyOrFn, handler, options)

}

Vue.prototype.$watch =function(

expOrFn:string|Function,

cb:any,

options?:Object

):Function{

constvm: Component =this

// ...

// 注意:在这里实例化了 Watcher

constwatcher =newWatcher(vm, expOrFn, cb, options)

// ...

}

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

// src/core/observer/watcher.js

importDep, { pushTarget, popTarget }from'./dep'

exportdefaultclassWatcher{

constructor(

vm: Component,

expOrFn: string | Function,

cb: Function,

options?: ?Object,

isRenderWatcher?: boolean

) {

this.vm = vm

// ...

if(typeofexpOrFn ==='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 方法:

varbailRE =/[^\w.$]/;

functionparsePath(path){

if(bailRE.test(path)) {

return

}

varsegments = path.split('.');

// 返回一个匿名函数

returnfunction(obj){

for(vari =; i

if(!obj) {return}

obj = obj[segments[i]];

}

returnobj

}

}

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

// src/core/observer/watcher.js

//...

get() {

pushTarget(this)

letvalue

constvm =this.vm

try{

value =this.getter.call(vm, vm)

}

// ...

returnvalue

}

// addDep方法,会在 Dep 类的 depend 方法中调用

addDep (dep: Dep) {

constid = 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)

}

}

}

// 实例化 Dep

constdep =newDep();

// 调用 Dep 的 depend 方法

dep.depend();

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

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

// src/core/observer/dep.js

exportdefaultclassDep {

statictarget: ?Watcher;

id:number;

subs:Array;

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

constsubs =this.subs.slice()

for(leti =, l = subs.length; i

subs[i].update()

}

}

}

那图中的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 -

更多技术干货:

①技术专栏 | 微服务架构初探

②技术专栏 | 走进分布式一致性协议

③技术专栏 | 利用HDFS备份实现Elasticsearch容灾

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180322B0OPGZ00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码关注腾讯云开发者

领取腾讯云代金券