之前学习了 vue 实例化及渲染函数的实现,其中的数据初始化 initData
中的 observe(data)
我们没有继续深入分析。因为内容比较多,所以今天单独开了篇来介绍 vue 的响应式原理。
对数据劫持与订阅发布比较生疏的,可以先阅读 浅谈订阅发布实现vue打打基础。
从入口开始
initState -> initData -> observe(data)
observe
我们从 src/core/observer/index.js
observe
开始
export function observe (value: any, asRootData: ?boolean): Observer | void { if (!isObject(value) || value instanceof VNode) { return } // 如果已经监听数据则直接返回 let ob: Observer | void if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ } else if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { // 实例化监听者 ob = new Observer(value) } if (asRootData && ob) { ob.vmCount++ } return ob } 复制代码
主要就是实例化 Observer
Observer
我们再来看看 Observer
实例化都做了什么
export class Observer { value: any; dep: Dep; vmCount: number; // number of vms that have this object as root $data constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 // 定义__ob__属性 def(value, '__ob__', this) // 数组处理 if (Array.isArray(value)) { // 我们知道Object.defineProperty无法监听数组的改变 // 所以这边其实是重写了数组方法,使其支持监听,例如splice if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { this.walk(value) } } walk (obj: Object) { // 遍历属性 const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } } observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } } 复制代码
Observer
的实例化也比较简单,判断为数组时遍历数组进行数据监听。再就是为数据对象的属性遍历调用 defineReactive
defineReactive
export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { // 每个属性会有一个dep实例 const dep = new Dep() const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // 这里会存着属性自身的 set get 函数 // cater for pre-defined getter/setters const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments.length === 2) { val = obj[key] } // 递归子属性进行数据监听 let childOb = !shallow && observe(val) // 重点:数据劫持 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { // 如有开发者自定义的get则执行 const value = getter ? getter.call(obj) : val // Dep.target作为连接dep和watcher的纽带 if (Dep.target) { // dep添加订阅者Watcher实例 // Watcher实例的来源后面会说到 dep.depend() // 未知作用:暂不讲解 if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { // 如有开发者自定义的get则执行,注意执行的也是get,因为这里是取值 const value = getter ? getter.call(obj) : val // 判断数据更新 (newVal !== newVal && value !== value) 暂不清楚是干嘛的 猜测是处理某些情况导致数据修改的异常 /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } // #7981: for accessor properties without setter if (getter && !setter) return if (setter) { // 如有开发者自定义的set则执行 setter.call(obj, newVal) } else { // 设置新值 val = newVal } // 对新值也要进行监听 childOb = !shallow && observe(newVal) // dep进行发布通知 这里将在get中收集的watcher逐一进行通知 dep.notify() } }) } 复制代码
defineReactive
的实现其实就是数据劫持的实现,利用 Object.getOwnPropertyDescriptor
对数据的具体属性进行 set
get
定义。之后便可以在 get
中进行订阅 dep.depend()
,当属性值修改时,进行发布通知 dep.notify()
Dep
在 defineReactive
函数中,我们实例化了订阅发布中心 const dep = new Dep()
,并且调用了 dep.notify
现在我们再来看看 Dep
的实现
let uid = 0 export default class Dep { static target: ?Watcher; id: number; subs: Array<Watcher>; constructor () { this.id = uid++ // 订阅者列表 this.subs = [] } // 添加订阅者 addSub (sub: Watcher) { this.subs.push(sub) } removeSub (sub: Watcher) { remove(this.subs, sub) } // 将当前watcher实例作为订阅者添加 depend () { 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() } } } // 缓存watcher实例 // 同一时间仅有一个正在实例化的watcher Dep.target = null 复制代码
Dep
的实现就是个简单的订阅发布中心,同时给 Dep
添加了个静态属性 target
用于缓存当前的 watcher
实例。
Watcher
我们接着再来看看 Watcher
实现,会比较复杂一些
let uid = 0 export default class Watcher { vm: Component; expression: string; cb: Function; id: number; deep: boolean; user: boolean; lazy: boolean; sync: boolean; dirty: boolean; active: boolean; deps: Array<Dep>; newDeps: Array<Dep>; depIds: SimpleSet; newDepIds: SimpleSet; before: ?Function; getter: Function; value: any; constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm // 顾名思义渲染watch if (isRenderWatcher) { vm._watcher = this } // _watchers添加当前实例 vm._watchers.push(this) // 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') { // expOrFn是函数 this.getter = expOrFn } else { // expOrFn是表达式则转化为函数 this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = noop } } // 执行get this.value = this.lazy ? undefined : this.get() } get () { // 将静态属性设置为当前watcher实例Dep.target = this pushTarget(this) let value const vm = this.vm try { // 执行getter也就是expOrFn // expOrFn函数将访问到我们的vm数据,所以将触发我们在defineReactive函数定义的get函数 // 此处便解答了defineReactive中Dep.target的来源 // new Watcher -> 执行get -> Dep.target = this -> 执行expOrFn -> 触发数据get函数 -> dep.depend() -> 属性的dep添加订阅者(this) // 这样就完成了数据属性的订阅 value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { // 深度watcher 暂时跳过 if (this.deep) { traverse(value) } popTarget() this.cleanupDeps() } return value } addDep (dep: Dep) { // 在newDeps列表中存储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 cleanupDeps () { let i = this.deps.length // 仅存在deps中的dep清除订阅 while (i--) { const dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } // 清空depIds // 这边使用了两个数组交换 我猜测是为了节约内存 如果单纯设置 newDepIds = [] 的话需要新开辟内存 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 } // 数据更新 update () { if (this.lazy) { this.dirty = true } else if (this.sync) { // 同步watcher this.run() } else { // 异步watcher queueWatcher(this) } } // 执行回调 run () { if (this.active) { const value = this.get() if ( // 判断新旧值是否有改变 value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // set new value const oldValue = this.value this.value = value if (this.user) { const info = `callback for watcher "${this.expression}"` invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info) } else { // 执行回调 this.cb.call(this.vm, value, oldValue) } } } } // ... } 复制代码
通过阅读 Watcher
实现,我们可以大致了解其主要功能,在实例化的时候将调用 get
方法,在 get
方法中定义 Dep.target = this
将实例赋值给 Dep.target
。之后再调用 getter
函数将访问 vm
数据属性的 get
函数,在 get
中调用 dep.depend()
实现订阅。
同时 watcher
实例有一些其它的方法,当前面我们说的数据改变触发属性 set
函数时,将通知 dep
进行发布通知,此时将会调用到订阅了该 dep
的所有 watcher
的update
函数。通过 update
函数我们可以看到如果时同步 watcher
其主要就是直接调用 run
函数进行回调函数 cb
的执行。
流程梳理
通过前面源码的分析,我们来梳理下 订阅发布模式
的实现
- 首先对数据
data
调用Observer
进行实例化进行数据监听
- 遍历递归数据属性,调用
defineReactive
- 每个属性生成唯一
dep
订阅发布中心实例 - 劫持
get
,在get
中调用dep.depend()
收集订阅者 - 劫持
set
,在set
中调用dep.notify()
发布通知
Dep
订阅发布中心的实现,具备收集订阅者及发布通知的能力Watcher
实例化生成订阅者,在实例化的同时访问数据get
属性,通过Dep.target
纽带完成订阅- 开发者修改数据
Data
触发set
通知,订阅中心dep
依次调用watcher
的update
函数,update
中执行回调函数完成整个流程
结语
本文从实际源码出发,研究了 Vue
内部的响应式实现,收获满满。但虽然我们弄清楚了其整个流程,还有很多细节我们是没有去深入研究分析的,例如 Watcher
实例化的各种参数配置,及同步异步 Watcher
的区别及实现,后面我们将继续分析继续嗨。