上个学习笔记讲到 Vue 的响应式数据原理,我们知道 vue 通过 Object.defineProperty
对数据进行 get
和 set
来完成数据的响应式,而从源码中我们得知,每个对象进行响应式处理时会有各自的 dep
实例,在 get
的时候,会调用 dep.depend
来收集依赖,在 set
的时候执行 dep.notify
来更新依赖,这期学习笔记将介绍收集依赖的主要过程;
一、依赖收集
我们可以先看看 Dep 的主要内容
// src/core/observer/dep.js
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
// 用来收集依赖,里面存放 watcher 实例
this.subs = []
}
// 添加依赖
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 移除依赖
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
// 如果当前全局存在依赖的 watcher,则添加到 subs
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
// 通知依赖
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// Dep.target 是全局唯一的 watcher 指向
Dep.target = null
// 模拟栈结构来存放 watcher
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
从上面的源码中我们可以看到 Dep
的主要作用是用一个数组收集 watcher
实例,当我们调用 dep.depend
的时候可以将当前存在的 watcher
收集到 subs
,然后在 dep.notify
的时候遍历 subs
,依次通知 watcher
实例调用 update
更新,那 Watcher 是怎么定义的呢?继续往下走:
// src/core/observer/watcher.js
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
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
this.active = true
this.dirty = this.lazy // 计算属性时使用
this.deps = [] // 存放 dep
this.newDeps = []
this.depIds = new Set() // 去重
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// 传进来的如果是对象中的某个值,如 form.name,Vue 会将字符串转化为实例对应的值
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
)
}
}
// 如果不是计算属性,则调用一次 get 方法,获取并保存一次旧值
this.value = this.lazy
? undefined
: this.get()
}
get () {
// 将当前的 watcher 实例挂到 Dep.target
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 {
if (this.deep) {
// 如果 deep 属性为真,则递归对象的嵌套属性,进行深度依赖收集
traverse(value)
}
// 将当前的 watcher 从 Dep.target 中移除
popTarget()
this.cleanupDeps()
}
return value
}
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)) {
// 将当前实例化的 watcher 添加到 dep 的 subs 中
dep.addSub(this)
}
}
}
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
}
// 更新渲染 watcher
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
run () {
if (this.active) {
// 获取新的值
const value = this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
// 旧的值
const oldValue = this.value
this.value = value
// 如果是用户 watcher,执行用户传进来的回调函数,并将新值旧值传出去
if (this.user) {
const info = `callback for watcher "${
this.expression}"`
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
} else {
// 执行渲染 watcher
this.cb.call(this.vm, value, oldValue)
}
}
}
}
// 计算属性时使用
evaluate () {
this.value = this.get()
this.dirty = false
}
// 计算属性时使用
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
// 销毁 watcher,在 destory 生命钩子函数调用
teardown () {
if (this.active) {
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
从 Watcher
类来看,我们主要是通过 get
方法来获取新的值,在数据更新时调用 update
来渲染 watcher
,除了这些以外,还有其他的属性和方法,这些在计算属性的时候会用到,后面再补充;
二、侦听属性
在之前的文章里面,我们知道 Vue 在初始化的时候会调用 initWatch
对 watch
属性进行初始化,初始化的时候做了些什么,我们可以从源码中查看:
// src/core/instance/state.js
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
// 自定义 watch 的写法可以是数组、对象、函数、字符串
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
// 遍历数组创建 watcher
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
// 创建 watcher 的核心方法
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
// 如果传入的是对象,例如:
/**
watchData: {
handler(nv, ov) {
...
},
deep: true,
immediate: true
}
**/
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
// 如果传入的是字符串,则 handler 为定义好的方法
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
// $watch 原型方法是创建用户自定义 watch 的核心
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
// 如果 handler 是对象,需要调用一次 createWatcher,拿到处理好的参数
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
// 用户自定义选项,包括 deep, immediate等
options = options || {
}
// 用户自定义 watch 标识
options.user = true
// 传入相应的参数,创建 watch 实例
const watcher = new Watcher(vm, expOrFn, cb, options)
// 如果 immediate 为 true,则马上执行一次渲染 watcher
if (options.immediate) {
const info = `callback for immediate watcher "${
watcher.expression}"`
pushTarget()
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
popTarget()
}
return function unwatchFn () {
watcher.teardown()
}
}
从源码中我们可以得知几个事情:
initWatch
会遍历用户自定义的watch
,挨个进行创建,我们知道自定义watch
的写法可以是数组、对象、函数、字符串,创建实例时会进行兼容性处理,如果是数组,则遍历数组进行创建;createWatcher
方法是创建watch
的核心,主要是对handler
进行判断处理,然后通过vm.$watch
创建实例;$watch
原型方法是创建用户自定义watch
的核心,它会给options
打上标识user: true
,表示是用户自定义watch
,然后通过new Watcher
创建一个实例,如果设置了immediate
,则执行一次用户传进来的回调函数;
总结:
- Vue 在数据响应式处理时,给每个数据创建一个
dep
的实例,用来收集依赖,当数据变化时,通过调用dep.notify
来通知watcher
进行更新,watcher
更新时主要方法是get
和update
,通过get
来获取新的值,通过update
来执行渲染watcher
; - Vue 在初始化时会执行
initWatch
,对用户自定义的watch
进行兼容性处理,通过createWacther
方法来创建watch
,createWacther
方法会将处理好的参数通过vm.$watch
原型来创建watch
实例;