vue响应式原理学习(三)— Watcher的实现
栏目: JavaScript · 发布时间: 7年前
内容简介:为什么我们改变了数据,
为什么我们改变了数据, Vue
能够自动帮我们刷新 DOM
。就是因为有 Watcher
。当然, Watcher
只是派发数据更新,真正的修改 DOM
,还需要借用 VNode
,我们这里先不讨论 VNode
。
computed
计算属性,内部实现也是基于 Watcher
watcher
选项的使用方法,我目前通过看文档和源码理解到的,有五种,如下:
new Vue ({
data: {
a: { x: 1 }
b: { y: 1 }
},
watch: {
a() {
// do something
},
'a.x'() {
// do something
},
a: {
hander: 'methodName',
deep: Boolean
immediate: Boolean
},
a: 'methodName',
a: ['methodName', 'methodName']
}
});
复制代码
Vue 初始化时在哪里对数据进行观察
代码来源:Vue项目下 src/core/instance/lifecycle.js
updateComponent = () => {
// vm._render 会根据我们的html模板和vm上的数据生成一个 新的 VNode
// vm._update 会将新的 VNode 与 旧的 Vnode 进行对比,执行 __patch__ 方法打补丁,并更新真实 dom
// 初始化时,肯定没有旧的 Vnode 咯,这个时候就会全量更新 dom
vm._update(vm._render(), hydrating)
}
// 当 new Watcher 时,会执行 updateComponent ,
// 执行 updateComponent 函数会访问 data 中的数据,相当于触发 data 中数据的 get 属性
// 触发 data 中数据的 get 属性,就相当于触发了 依赖收集
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
复制代码
如何收集依赖,如何派发更新
众所周知, Vue
是在触发数据的 get
时,收集依赖,改变数据时触发 set
, 达到派发更新的目的。
依赖收集 和 派发更新的 代码 在上一篇文章,有简单解释过。我们再来重温下代码
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 每个数据都有一个属于自己的 dep
const dep = new Dep()
// 省略部分代码...
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 省略部分代码...
if (Dep.target) {
// 收集依赖
dep.depend()
// 省略部分代码...
}
// 省略部分代码...
},
set: function reactiveSetter (newVal) {
// 省略部分代码...
// 派发更新
dep.notify()
}
})
}
复制代码
这里我省略了部分用于判断和兼容的代码,因为感觉一下子要看所有代码的话,会有些懵比。我们现在知道了 dep.depend
用于收集依赖, dep.notify
用于派发更新,我们按着这两条主线,去一步步摸索。
dep
是在代码开始的地方定义的: const dep = new Dep()
。
所以我们要先找到 Dep
这个构造函数,然后我们还要了解 Dep.target
是个啥东西
Dep 的实现
Dep 构造函数定义在 Vue 项目下: /src/core/observer/dep.js
我们可以发现 Dep
的实现就是一个观察者模式,很像一个迷你的事件系统。
Dep
中的 addSub, removeSub
,和 我们定义一个 Events
时里面的 on, off
是非常相似的。
// 用于当作 Dep 的标识
let uid = 0
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
// 定义一个 subs 数组,这个数组是用来存放 watcher 实例的
constructor () {
this.id = uid++
this.subs = []
}
// 将 watcher 实例添加到 subs 中
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 从 subs 中移除对应的 watcher 实例。
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// 依赖收集,这就是我们之前看到的 dep.dpend 方法
depend () {
// Dep.target 是 watcher 实例
if (Dep.target) {
// 看到这里应该能明白 watcher 实例上 有一个 addDep 方法,参数是当前 dep 实例
Dep.target.addDep(this)
}
}
// 派发更新,这就是我们之前看到的 dep.notify 方法
notify () {
// 复制一份,可能是因为下面要做排序,可是又不能影响 this.subs 数组内元素的顺序
// 所以就复制一份出来。
const subs = this.subs.slice()
// 这里做了个 排序 操作,具体原因是什么,我还不清楚
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
// 遍历 subs 数组,依次触发 watcher 实例的 update
for (let i = 0, l = subs.length; i < l; i++) {
// 看到这里应该能明白 watcher 实例上 有一个 update 方法
subs[i].update()
}
}
}
// 在 Dep 上挂一个静态属性,
// 这个 Dep.target 的值会在调用 pushTarget 和 popTarget 时被赋值,值为当前 watcher 实例对象。
Dep.target = null
// 维护一个栈结构,用于存储和删除 Dep.target
const targetStack = []
// pushTarget 会在 new Watcher 时被调用
export function pushTarget (_target: ?Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
// popTarget 会在 new Watcher 时被调用
export function popTarget () {
Dep.target = targetStack.pop()
}
复制代码
Watcher 是什么,它与 Dep 是什么关系
Dep
是一个类,用于依赖收集和派发更新,也就是存放 watcher实例
和触发 watcher实例
上的 update
。
Watcher
也是一个类,用于初始化 数据的 watcher实例
。它的原型上有一个 update
方法,用于派发更新。
一句话概括: Dep
是 watcher实例
的管理者。类似观察者模式的实现。
Watcher 的实现
Watcher 的代码比较多,我这里省略部分代码,并在主要代码上加上注释,方便大家理解。
export default class Watcher {
constructor(
vm: Component,
expOrFn: string | Function, // 要 watch 的属性名称
cb: Function, // 回调函数
options?: ?Object,
isRenderWatcher?: boolean // 是否是渲染函数观察者,Vue 初始化时,这个参数被设为 true
) {
// 省略部分代码... 这里代码的作用是初始化一些变量
// expOrFn 可以是 字符串 或者 函数
// 什么时候会是字符串,例如我们正常使用的时候,watch: { x: fn }, Vue内部会将 `x` 这个key 转化为字符串
// 什么时候会是函数,其实 Vue 初始化时,就是传入的渲染函数 new Watcher(vm, updateComponent, ...);
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// 在文章开头,我描述了 watch 的几种用法,
// 当 expOrFn 不为函数时,可能是这种描述方式:watch: {'a.x'(){ //do } },具体到了某个对象的属性
// 这个时候,就需要通过 parsePath 方法,parsePath 方法返回一个函数
// 函数内部会去获取 'a.x' 这个属性的值了
this.getter = parsePath(expOrFn)
// 省略部分代码...
}
// 这里调用了 this.get,也就意味着 new Watcher 时会调用 this.get
// this.lazy 是修饰符,除非用户自己传入,不然都是 false。可以先不管它
this.value = this.lazy
? undefined
: this.get()
}
get () {
// 将 当前 watcher 实例,赋值给 Dep.target 静态属性
// 也就是说 执行了这行代码,Dep.target 的值就是 当前 watcher 实例
// 并将 Dep.target 入栈 ,存入 targetStack 数组中
pushTarget(this)
// 省略部分代码...
try {
// 这里执行了 this.getter,获取到 属性的初始值
// 如果是初始化时 传入的 updateComponent 函数,这个时候会返回 udnefined
value = this.getter.call(vm, vm)
} catch (e) {
// 省略部分代码...
} finally {
// 省略部分代码...
// 出栈
popTarget()
// 省略部分代码...
}
// 返回属性的值
return value
}
// 这里再回顾一下
// dep.depend 方法,会执行 Dep.target.addDep(dep) 其实也就是 watcher.addDep(dep)
// watcher.addDep(dep) 会执行 dep.addSub(watcher)
// 将当前 watcher 实例 添加到 dep 的 subs 数组 中,也就是收集依赖
// dep.depend 和 这个 addDep 方法,有好几个 this, 可能有点绕。
addDep (dep: Dep) {
const id = dep.id
// 下面两个 if 条件都是去重的作用,我们可以暂时不考虑它们
// 只需要知道,这个方法 执行 了 dep.addSub(this)
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
// 将当前 watcher 实例添加到 dep 的 subs 数组中
dep.addSub(this)
}
}
}
// 派发更新
update () {
// 如果用户定义了 lazy ,this.lazy 是描述符,我们这里可以先不管它
if (this.lazy) {
this.dirty = true
// this.sync 表示是否改变了值之后立即触发回调。如果用户定义为true,则立即执行 this.run
} else if (this.sync) {
this.run()
// queueWatcher 内部也是执行的 watcher实例的 run 方法,只不过内部调用了 nextTick 做性能优化。
// 它会将当前 watcher 实例放入一个队列,在下一次事件循环时,遍历队列并执行每个 watcher实例的run() 方法
} else {
queueWatcher(this)
}
}
run () {
if (this.active) {
// 获取新的属性值
const value = this.get()
if (
// 如果新值不等于旧值
value !== this.value ||
// 如果新值是一个 引用 类型,那么一定要触发回调
// 举个例子,如果旧值本来就是一个对象,
// 在新值内,我们只改变对象内的某个属性值,那新值和旧值本身还是相等的
// 也就是说,如果 this.get 返回的是一个引用类型,那么一定要触发回调
isObject(value) ||
// 是否深度 watch
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
// this.user 是一个标志符,如果开发者添加的 watch 选项,这个值默认为 true
// 如果是用户自己添加的 watch ,就加一个 try catch。方便用户调试。否则直接执行回调。
if (this.user) {
try {
// 触发回调,并将 新值和旧值 作为参数
// 这也就是为什么,我们写 watch 时,可以这样写: function (newVal, oldVal) { // do }
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
// 省略部分代码...
// 以下是 Watcher 类的其他方法
cleanUpDeps() { }
evaluate() { }
depend() { }
teardown() { }
}
复制代码
Watcher 的代码较多,我就不将全部方法都解释一遍了。有兴趣的朋友可以自己去看下源码,了解下。
这里再顺带说下 parsePath
函数,其实这个函数的作用就是解析 watch
的 key
值是字符串,且为 obj.x.x
这种情况。
代码来源:Vue项目下 vue/src/core/util/lang.js
const bailRE = /[^\w.$]/
export function parsePath (path: string): any {
// 如果 path 参数,不包含 字母 或 数字 或 下划线,或者不包含 `.`、`$` ,直接返回
// 也就是说 obj-a, obj/a, obj*a 等值,会直接返回
if (bailRE.test(path)) {
return
}
// 假如传入的值是 'a.b.c',那么此时 segments 就是 ['a', 'b', 'c']
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
// 因为这个函数调用时,是 call(vm, vm) 的形式,所以第一个 obj 是 vm
// 注意这里的 vm 是形参
// 执行顺序如下
// obj = vm['a'] -> 拿到 a 对象 , 当前 obj 的值 为 vm.a
// obj = a['b'] -> 拿到 b 对象, 当前 obj 的值 为 a.b
// obj = b[c] -> 拿到 c 对象, 当前 obj 的值 是 a.b.c
// 循环结束
obj = obj[segments[i]]
}
return obj
}
}
复制代码
Vue 是怎么初始化我们传入的 watch 选项
代码来源:Vue项目下 src/core/instance/state.js
initWatch
// line - 286
// initWatch 会在 new Vue 初始化 的时候被调用
function initWatch (vm: Component, watch: Object) {
// 这里的 watch 参数, 就是我们 定义的 watch 选项
// 我们定义的 watch选项 是一个 Object,所以要用 for...in 循环遍历它。
for (const key in watch) {
// key 就是我们要 watch 的值的名称
const handler = watch[key]
// 如果 是这种调用方式 key: [xxx, xxx]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
复制代码
createWatcher
// line - 299
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
// 如果 handler 是一个对象, 如:key: { handler: 'methodName', deep: true } 这种方式调用
// 将 handler.handler 赋值给 handler,也就是说 handler 的值会被覆盖 为 'methodName'
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
// 如果handler 是一个字符串,则 从 vm 对象上去获取函数,赋值给 handler
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
复制代码
Vue.prototype.$watch
// line - 341
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
// 如果回调是对象的话,调用 createWatcher 将参数规范化, createWatcher 内部再调用 vm.$watch 进行处理。
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
// 设置 user 默认值 为 true,刚才我们分析的 Watcher 类,它的 run 方法里面就有关于 user 的判断
options.user = true
// 初始化 watcher
const watcher = new Watcher(vm, expOrFn, cb, options)
// 如果 immediate 为true, 立即触发一次回调
if (options.immediate ) {
cb.call(vm, watcher.value)
}
// 返回一个函数,可以用来取消 watch
return function unwatchFn () {
watcher.teardown()
}
}
复制代码
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Learn Python 3 the Hard Way
Zed A. Shaw / Addison / 2017-7-7 / USD 30.74
You Will Learn Python 3! Zed Shaw has perfected the world’s best system for learning Python 3. Follow it and you will succeed—just like the millions of beginners Zed has taught to date! You bring t......一起来看看 《Learn Python 3 the Hard Way》 这本书的介绍吧!