技术专栏 | 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的主要内容,如果未能解决你的问题,请参考以下文章