From b18a584673bec40398731152caf7a9f1b9e13f6e Mon Sep 17 00:00:00 2001 From: wuyw Date: Sun, 7 Apr 2019 18:30:11 +0800 Subject: [PATCH] dep/watcher --- vue/mvvm/1.md | 191 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 178 insertions(+), 13 deletions(-) diff --git a/vue/mvvm/1.md b/vue/mvvm/1.md index 669b94b..705033e 100644 --- a/vue/mvvm/1.md +++ b/vue/mvvm/1.md @@ -131,7 +131,7 @@ arr[0] = 'oh nanana'; // set 我们知道了数据劫持的基础实现,顺便再看看 Vue 源码是如何做的。 ``` javascript // observer/index.js -// 数据劫持前的判断方法 +// Observer 前的预处理方法 export function observe (value: any, asRootData: ?boolean): Observer | void { if (!isObject(value) || value instanceof VNode) { // 是否是对象或者虚拟dom return @@ -163,7 +163,7 @@ export class Observer { constructor (value: any) { this.value = value - this.dep = new Dep() // 给 Observer 添加 Dep 实例 + this.dep = new Dep() // 给 Observer 添加 Dep 实例,用于收集依赖,辅助 vm.$set/数组方法等 this.vmCount = 0 // 为被劫持的对象添加__ob__属性,指向自身 Observer 实例。作为是否 Observer 的唯一标识。 def(value, '__ob__', this) @@ -193,7 +193,7 @@ export class Observer { } } ``` -上面需要注意的是 `__ob__` 属性,避免重复创建。然后 Vue 将对象和数组分开处理,数组只深度监听了对象成员,这也是之前说的导致不能直接操作索引的原因。但是数组的一些方法是可以正常响应的,比如 push、pop 等,这便是因为上述判断响应对象是否是数组时,做的处理,我们来看看具体代码。 +上面需要注意的是 `__ob__` 属性,避免重复创建,`__ob__`上有一个 dep 属性,作为依赖收集的储存器,在 vm.$set、数组的 push 等多种方法上需要用到。然后 Vue 将对象和数组分开处理,数组只深度监听了对象成员,这也是之前说的导致不能直接操作索引的原因。但是数组的一些方法是可以正常响应的,比如 push、pop 等,这便是因为上述判断响应对象是否是数组时,做的处理,我们来看看具体代码。 ``` javascript // observer/index.js import { arrayMethods } from './array' @@ -268,7 +268,7 @@ methodsToPatch.forEach(function (method) { } if (inserted) ob.observeArray(inserted) // 原数组的新增部分需要重新 observe // notify change - ob.dep.notify() // 手动发布 + ob.dep.notify() // 手动发布,利用__ob__ 的 dep 实例 return result }) }) @@ -292,20 +292,20 @@ export function defineReactive ( // 兼容预定义的 getter/setter const getter = property && property.get const setter = property && property.set - if ((!getter || setter) && arguments.length === 2) { + if ((!getter || setter) && arguments.length === 2) { // 初始化 val val = obj[key] } - - let childOb = !shallow && observe(val) // 需要深度Watch就再次监听val,从 observe 开始,走完一套流程 + // 默认监听子对象,从 observe 开始,返回 __ob__ 属性 即 Observer 实例 + let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val // 执行预设的getter获取值 if (Dep.target) { // 依赖收集的关键 - dep.depend() // 依赖收集 - if (childOb) { // 如果有深度属性,则添加同样的依赖 - childOb.dep.depend() + dep.depend() // 依赖收集,利用了函数闭包的特性 + if (childOb) { // 如果有子对象,则添加同样的依赖 + childOb.dep.depend() // 即 Observer时的 this.dep = new Dep(); if (Array.isArray(value)) { // value 是数组的话调用数组的方法 dependArray(value) } @@ -316,6 +316,7 @@ export function defineReactive ( set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val // 原有值和新值比较,值一样则不做处理 + // newVal !== newVal && value !== value 这个比较有意思,但其实是为了处理 NaN if (newVal === value || (newVal !== newVal && value !== value)) { return } @@ -328,8 +329,8 @@ export function defineReactive ( } else { // 没有预设直接赋值 val = newVal } - childOb = !shallow && observe(newVal) // 是否要深度监听新设置的值 - dep.notify() // 发布 + childOb = !shallow && observe(newVal) // 是否要观察新设置的值 + dep.notify() // 发布,利用了函数闭包的特性 } }) } @@ -345,7 +346,171 @@ function dependArray (value: Array) { } ``` #### Dep -在上面的分析中,我们弄懂了 Vue 的数据劫持以及数组方法重写,但是又有了新的疑惑。 +在上面的分析中,我们弄懂了 Vue 的数据劫持以及数组方法重写,但是又有了新的疑惑,Dep 是做什么的?Dep 是一个发布者,可以被多个观察者订阅。 +``` javascript +// observer/dep.js + +let uid = 0 +export default class Dep { + static target: ?Watcher; + id: number; + subs: Array; + + constructor () { + this.id = uid++ // 唯一id + this.subs = [] // 观察者集合 + } + // 添加观察者 + addSub (sub: Watcher) { + this.subs.push(sub) + } + // 移除观察者 + removeSub (sub: Watcher) { + remove(this.subs, sub) + } + + depend () { // 核心,如果存在 Dep.target,则进行依赖收集操作 + if (Dep.target) { + Dep.target.addDep(this) + } + } + + notify () { + const subs = this.subs.slice() // 避免污染原来的集合 + // 如果不是异步执行,先进行排序,保证观察者执行顺序 + if (process.env.NODE_ENV !== 'production' && !config.async) { + subs.sort((a, b) => a.id - b.id) + } + for (let i = 0, l = subs.length; i < l; i++) { + subs[i].update() // 发布执行 + } + } +} + +Dep.target = null // 核心,用于闭包时,保存特定的值 +const targetStack = [] +// 给 Dep.target 赋值当前Watcher,并添加进target栈 +export function pushTarget (target: ?Watcher) { + targetStack.push(target) + Dep.target = target +} +// 移除最后一个Watcher,并将剩余target栈的最后一个赋值给 Dep.target +export function popTarget () { + targetStack.pop() + Dep.target = targetStack[targetStack.length - 1] +} +``` +#### Watcher +单个看 Dep 可能不太好理解,我们结合 Watcher 一起分析。 +``` javascript +// observer/watcher.js + +let uid = 0 +export default class Watcher { + // ... + constructor ( + vm: Component, // 组件实例对象 + expOrFn: string | Function, // 要观察的表达式,函数,或者字符串,只要能触发取值操作 + cb: Function, // 被观察者发生变化后的回调 + options?: ?Object, // 参数 + isRenderWatcher?: boolean // 是否是渲染函数的观察者 + ) { + this.vm = vm // Watcher有一个 vm 属性,表明它是属于哪个组件的 + if (isRenderWatcher) { + vm._watcher = this + } + vm._watchers.push(this) // 给组件实例的_watchers属性添加观察者实例 + // 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 { // 类似于 Obj.a 的字符串 + 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() + } + + get () { // 触发取值操作,进而触发属性的getter + pushTarget(this) // Dep 中提到的:给 Dep.target 赋值 + let value + const vm = this.vm + try { + // 核心,运行观察者表达式,进行取值,触发getter,从而在闭包中添加watcher + 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) { // 如果要深度监测,再对 value 执行操作 + traverse(value) + } + // 清理依赖收集 + 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)) { + dep.addSub(this) // dep 添加订阅者 + } + } + } + + update () { // 更新 + /* istanbul ignore else */ + if (this.lazy) { + this.dirty = true + } else if (this.sync) { + this.run() // 同步直接运行 + } else { // 否则加入异步队列等待执行 + queueWatcher(this) + } + } +} +``` +到这里,我们可以大概总结一些整个响应式系统的流程:第一步当然是通过 observer 进行数据劫持,然后在需要订阅的地方(如:模版编译),添加观察者(watcher),并立刻通过取值操作触发指定属性的 getter 方法,从而将观察者添加进 Dep (利用了闭包的特性),然后在 Setter 触发的时候,进行 notify,通知给所有观察者进行相应的 update。 ## Proxy ## MVVM ### 概念 \ No newline at end of file