这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战
前言
在Vue中存在三种Watcher:
- 负责视图变化的渲染Watcher
- 负责执行计算属性更新的的computed Watcher
- 用户通过watcher api自定义的user Watcher
在前边看响应式对象的时候我们知道数据在执行getter逻辑时依赖收集(dep.depend()),执行setter逻辑时派发更新(dep.notify())。
在整个响应式实现的过程中有一个重点是Dep,它是用来管理所有Watcher的,需要配合Watcher来一起理解。这篇就来详细的看一下Watcher的实现部分。
定义
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
...
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
...
}
/**
* Add a dependency to this directive.
*/
addDep (dep: Dep) {
...
}
/**
* Clean up for dependency collection.
*/
cleanupDeps () {
...
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
...
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
...
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
...
}
/**
* Depend on all deps collected by this watcher.
*/
depend () {
...
}
/**
* Remove self from all dependencies' subscriber list.
*/
teardown () {
...
}
}
Watcher接收5个参数:
- vm:Vue实例
- expOrFn: 监听的数据表达式(可以使用字符串表示一个路径,也可以使用函数返回要监听的数据)
- cb:回调函数,用于数据变更后去执行
- options:设置一些配置参数
- isRenderWatcher:是否是渲染Watcher
在看代码之前,先来看下Watcher类中,部分属性的含义:
参数 | 含义 |
---|---|
expression | 保存expOrFn表达式,用来log输出 |
getter | 要监听的数据 |
deep | 是否深层次监听对象内部的变化 |
user | 是否是用户通过watcher API创建的user Watcher |
lazy | 是否惰性计算,与computed Watcher有关 |
sync | 值发生变化后,是否同步执行回调函数。即不需要将该Watcher推入队列,而是直接在当前Tick执行回调。 |
dirty | 标记计算属性是否脏了,要被重新计算,与computed Watcher有关 |
active | 标记Watcher是否已经从所有订阅的Sub中移除 |
deps | 上一次的Dep实例数组 |
newDeps | 新添加的Dep实例数组 |
depIds | 保存与deps对应的id Set |
newDepIds | 保存与newDeps对应的id Set |
getter | Watcher的getter函数 |
before | 定义钩子函数,数据变化后触发更新前执行(如beforeUpdate钩子的执行就是在渲染Watcher的before函数) |
constructor
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
构造函数的部分比较好理解,给属性设置默认值。这里主要来看下getter和value两个属性。
getter
从代码里可以看到expOrFn可以是一个字符串,也可以是函数。对于我们开发者来讲,传入的都是字符串;如果是渲染Watcher,则会传入一个函数。
-
首先判断要被监听的数据变量expOrFn是不是函数,如果是直接将expOrFn赋值给getter。
-
如果不是函数,则调用parsePath函数处理expOrFn字符串。这里主要处理我们平时写的'a.b.c'这种场景
parsePath函数定义在src/core/util/lang.js中:
export const unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/
const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\d]`)
export function parsePath (path: string): any {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
首先通过正则来判断path的合法性。如果path满足bailRE正则,则直接返回,否则返回一个函数,在获取Watcher值时调用(后边看get函数时具体来看)。
最后回到getter函数,如果getter为undefined,则直接将loop赋值给getter,并且在开发环境抛出异常,告诉开发者Watcher只接收点分割的path,如果想用全部的js语法,可以考虑使用函数。
noop函数定义在src/shared/utils.js
export function noop (a?: any, b?: any, c?: any) {}
value
this.value = this.lazy ? undefined : this.get()
如果lazy值为true,也就是computed Watcher,则将value的值设置为undefined,否则执行get获取初始值。
接下来看一下get函数:
get
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
总体来看get函数的实现就是使用try...catch...finally语句获得value值。
首先调用pushTarget函数,看到这个函数,是不是很眼熟,在前边看Dep class的时候是看到过的。
pushTarget
pushTarget定义在src/core/observer/dep.js
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
这里是将当前的Watch push到targetStack数组中,并且把Dep.target设置为当前的Watcher
获取value值
接下来调用this.getter获取value值。
value = this.getter.call(vm, vm)
上边看parsePath函数的时候,我们知道此时的getter函数是通过遍历segments数组来一步步访问path的属性值,每一步都会触发数据的get拦截函数。
具体可以看一个下边这个例子,我把每一步的obj[segments[i]]都具体打出来了:
traverse
finally语句的逻辑是无论try...catch是否抛出异常都要执行。首先判断deep值,如果为true说明要对watch的值进行深度监听,使用traverse函数来处理。
想一下我们使用deep的场景:
data() {
return{
a: {
b: 'init'
}
}
},
watch: {
a: {
handler(newVal) {
console.log(newVal)
},
//如果不设置deep值,将不会触发watcher函数
deep: true
}
}
看一下traverse函数怎么实现的,traverse函数定义在src/core/observer/traverse.js
const seenObjects = new Set()
export function traverse (val: any) {
_traverse(val, seenObjects)
seenObjects.clear()
}
function _traverse (val: any, seen: SimpleSet) {
let i, keys
const isA = Array.isArray(val)
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
if (val.__ob__) {
const depId = val.__ob__.dep.id
if (seen.has(depId)) {
return
}
seen.add(depId)
}
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
traverse函数通过递归调用来深度处理数据,让对象及数组属性的每一层都被依赖收集。
popTarget
popTarget函数定义在src/core/observer/dep.js
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
cleanupDeps
最后调用cleanupDeps
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
cleanupDeps方法用来移除无用的Dep。首先遍历deps数组,如果其中项在newDeps不存在,则移除该项。
移除完无用的依赖以后,将newDepIds和newDeps赋值给depIds和deps,然后清空newDepIds和newDeps