vue响应式原理学习(三)— Watcher的实现
栏目: JavaScript · 发布时间: 6年前
内容简介:为什么我们改变了数据,
为什么我们改变了数据, 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() } } 复制代码
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
函数式算法设计珠玑
Richard Bird / 苏统华、孙芳媛、郝文超、徐琴 / 机械工业出版社 / 2017-4-1 / 69.00
本书采用完全崭新的方式介绍算法设计。全书由30个珠玑构成,每个珠玑单独列为一章,用于解决一个特定编程问题。这些问题的出处五花八门,有的来自游戏或拼图,有的是有趣的组合任务,还有的是散落于数据压缩及字串匹配等领域的更为熟悉的算法。每个珠玑以使用函数式编程语言Haskell对问题进行描述作为开始,每个解答均是诉诸于函数式编程法则从问题表述中计算得到。本书适用于那些喜欢学习算法设计思想的函数式编程人员、......一起来看看 《函数式算法设计珠玑》 这本书的介绍吧!