为何咱们改变了数据,
Vue
可以自动帮咱们刷新DOM
。就是由于有Watcher
。固然,Watcher
只是派发数据更新,真正的修改DOM
,还须要借用VNode
,咱们这里先不讨论VNode
。javascript
computed
计算属性,内部实现也是基于Watcher
html
watcher
选项的使用方法,我目前经过看文档和源码理解到的,有五种,以下:vue
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项目下 src/core/instance/lifecycle.js
java
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
, 达到派发更新的目的。node
依赖收集 和 派发更新的 代码 在上一篇文章,有简单解释过。咱们再来重温下代码react
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
用于派发更新,咱们按着这两条主线,去一步步摸索。express
dep
是在代码开始的地方定义的:const dep = new Dep()
。数组
因此咱们要先找到 Dep
这个构造函数,而后咱们还要了解 Dep.target
是个啥东西性能优化
Dep 构造函数定义在 Vue 项目下:/src/core/observer/dep.js
dom
咱们能够发现 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()
}
复制代码
Dep
是一个类,用于依赖收集和派发更新,也就是存放watcher实例
和触发watcher实例
上的update
。
Watcher
也是一个类,用于初始化 数据的watcher实例
。它的原型上有一个update
方法,用于派发更新。
一句话归纳:Dep
是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项目下 src/core/instance/state.js
// 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)
}
}
}
复制代码
// 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)
}
复制代码
// 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()
}
}
复制代码
还在画图中...
谢谢阅读。若是文章有错误的地方,烦请指出。