先看张图,了解一下大体流程和要做的事
初始化
在 new Vue 初始化的时候,会对我们组件的数据 props 和 data 进行初始化,由于本文主要就是介绍响应式,所以其他的不做过多说明来,看一下源码
源码地址:src/core/instance/init.js - 15行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| export function initMixin (Vue: Class<Component>) { Vue.prototype._init = function (options?: Object) { ... vm._self = vm initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) initState(vm) initProvide(vm) callHook(vm, 'created') ... } }
|
初始化这里调用了很多方法,每个方法都做着不同的事,而关于响应式主要就是组件内的数据 props
、data
。这一块的内容就是在 initState()
这个方法里,所以我们进入这个方法源码看一下
initState()
源码地址:src/core/instance/state.js - 49行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| export function initState(vm: Component) { vm._watchers = [] const opts = vm.$options if (opts.props) initProps(vm, opts.props) if (opts.methods) initMethods(vm, opts.methods) if (opts.data) { initData(vm) } else { observe((vm._data = {}), true ) } if (opts.computed) initComputed(vm, opts.computed) if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } }
|
又是调用一堆初始化的方法,我们还是直奔主题,取我们响应式数据相关的,也就是 initProps()
、initData()
、observe()
一个一个继续扒,非得整明白响应式的全部过程
initProps()
源码地址:src/core/instance/state.js - 65行
这里主要做的是:
- 遍历父组件传进来的
props
列表
- 校验每个属性的命名、类型、default 属性等,都没有问题就调用
defineReactive
设置成响应式
- 然后用
proxy()
把属性代理到当前实例上,如把 vm._props.xx
变成 vm.xx
,就可以访问
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| function initProps(vm: Component, propsOptions: Object) { const propsData = vm.$options.propsData || {} const props = (vm._props = {}) const keys = (vm.$options._propKeys = []) const isRoot = !vm.$parent if (!isRoot) { toggleObserving(false) } for (const key in propsOptions) { keys.push(key) const value = validateProp(key, propsOptions, propsData, vm) if (process.env.NODE_ENV !== "production") { const hyphenatedKey = hyphenate(key) if ( isReservedAttribute(hyphenatedKey) || config.isReservedAttr(hyphenatedKey) ) { warn(`hyphenatedKey 是保留属性,不能用作组件 prop`) } defineReactive(props, key, value, () => { if (!isRoot && !isUpdatingChildComponent) { warn(`避免直接改变 prop`) } }) } else { defineReactive(props, key, value) } if (!(key in vm)) { proxy(vm, `_props`, key) } } toggleObserving(true) }
|
initData()
源码地址:src/core/instance/state.js - 113行
这里主要做的是:
- 初始化一个 data,并拿到 keys 集合
- 遍历 keys 集合,来判断有没有和 props 里的属性名或者 methods 里的方法名重名的
- 没有问题就通过
proxy()
把 data 里的每一个属性都代理到当前实例上,就可以通过 this.xx
访问了
- 最后再调用
observe
监听整个 data
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| function initData(vm: Component) { let data = vm.$options.data data = vm._data = typeof data === "function" ? getData(data, vm) : data || {} if (!isPlainObject(data)) { data = {} process.env.NODE_ENV !== "production" && warn(`数据函数应该返回一个对象`) } const keys = Object.keys(data) const props = vm.$options.props const methods = vm.$options.methods let i = keys.length while (i--) { const key = keys[i] if (process.env.NODE_ENV !== "production") { if (methods && hasOwn(methods, key)) { warn(`Method 方法不能重复声明`) } } if (props && hasOwn(props, key)) { process.env.NODE_ENV !== "production" && warn(`属性不能重复声明`) } else if (!isReserved(key)) { proxy(vm, `_data`, key) } } observe(data, true ) }
|
observe()
源码地址:src/core/observer/index.js - 110行
这个方法主要就是用来给数据加上监听器的
这里主要做的是:
- 如果是 vnode 的对象类型或者不是引用类型,就直接跳出
- 否则就给没有添加 Observer 的数据添加一个 Observer,也就是监听者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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
源码地址:src/core/observer/index.js - 37行
这是一个类,作用是把一个正常的数据成可观测的数据
这里主要做的是:
- 给当前 value 打上已经是响应式属性的标记,避免重复操作
- 然后判断数据类型
- 如果是对象,就遍历对象,调用 defineReactive()创建响应式对象
- 如果是数组,就遍历数组,调用 observe()对每一个元素进行监听
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| export class Observer { value: any dep: Dep vmCount: number constructor(value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, "__ob__", this) if (Array.isArray(value)) { 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]) } } }
|
defineReactive()
源码地址:src/core/observer/index.js - 135行
这个方法的作用是定义响应式对象
这里主要做的是:
- 先初始化一个 dep 实例
- 如果是对象就调用 observe,递归监听,以保证不管结构嵌套多深,都能变成响应式对象
- 然后调用 Object.defineProperty() 劫持对象属性的 getter 和 getter
- 如果获取时,触发 getter 会调用 dep.depend() 把观察者 push 到依赖的数组 subs 里去,也就是依赖收集
- 如果更新时,触发 setter 会做以下操作
- 新值没有变化或者没有 setter 属性的直接跳出
- 如果新值是对象就调用 observe() 递归监听
- 然后调用 dep.notify() 派发更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| export function defineReactive( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { const dep = new Dep() const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } 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() { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter(newVal) { const value = getter ? getter.call(obj) : val if (newVal === value || (newVal !== newVal && value !== value)) { return } if (process.env.NODE_ENV !== "production" && customSetter) { customSetter() } if (getter && !setter) return if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) dep.notify() }, }) }
|
上面说了通过 dep.depend 来做依赖收集,可以说 Dep 就是整个 getter 依赖收集的核心了
依赖收集
依赖收集的核心是 Dep,而且它与 Watcher 也是密不可分的,我们来看一下
Dep
源码地址:src/core/observer/dep.js
这是一个类,它实际上就是对 Watcher 的一种管理
这里首先初始化一个 subs 数组,用来存放依赖,也就是观察者,谁依赖这个数据,谁就在这个数组里,然后定义几个方法来对依赖添加、删除、通知更新等
另外它有一个静态属性 target,这是一个全局的 Watcher,也表示同一时间只能存在一个全局的 Watcher
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| 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) } depend () { if (Dep.target) { Dep.target.addDep(this) } } notify () { ... } }
Dep.target = null const targetStack = []
export function pushTarget (target: ?Watcher) { targetStack.push(target) Dep.target = target }
export function popTarget () { targetStack.pop() Dep.target = targetStack[targetStack.length - 1] }
|
Watcher
源码地址:src/core/observer/watcher.js
Watcher 也是一个类,也叫观察者(订阅者),这里干的活还挺复杂的,而且还串连了渲染和编译
先看源码吧,再来捋一下整个依赖收集的过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| let uid = 0 export default class Watcher { ... constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() this.value = this.lazy ? undefined : this.get() if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) } } get () pushTarget(this) let value const vm = this.vm try { value = this.getter.call(vm, vm) } catch (e) { ... } finally { if (this.deep) { 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) } } } cleanupDeps () { ... } update () { ... } run () { ... } depend () { let i = this.deps.length while (i--) { this.deps[i].depend() } } }
|
补充:
我们自己组件里写的 watch,为什么自动就能拿到新值和老值两个参数?
就是在 watcher.run() 里面会执行回调,并且把新值和老值传过去
为什么要初始化两个 Dep 实例数组
因为 Vue 是数据驱动的,每次数据变化都会重新 render,也就是说 vm.render() 方法就又会重新执行,再次触发 getter,所以用两个数组表示,新添加的 Dep 实例数组 newDeps 和上一次添加的实例数组 deps
依赖收集过程
在首次渲染挂载的时候,还会有这样一段逻辑
mountComponent
源码地址:src/core/instance/lifecycle.js - 141行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| export function mountComponent (...): Component { callHook(vm, 'beforeMount') let updateComponent updateComponent = () => { vm._update(vm._render(), hydrating) } new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true ) if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted') } return vm }
|
依赖收集:
- 挂载之前会实例化一个渲染
watcher
,进入 watcher
构造函数里就会执行 this.get()
方法
- 然后就会执行
pushTarget(this)
,就是把 Dep.target
赋值为当前渲染 watcher
并压入栈(为了恢复用)
- 然后执行
this.getter.call(vm, vm)
,也就是上面的 updateComponent() 函数,里面就执行了 vm._update(vm._render(), hydrating)
- 接着执行
vm._render()
就会生成渲染 vnode
,这个过程中会访问 vm 上的数据,就触发了数据对象的 getter
- 每一个对象值的 getter 都有一个
dep
,在触发 getter 的时候就会调用 dep.depend()
方法,也就会执行 Dep.target.addDep(this)
- 然后这里会做一些判断,以确保同一数据不会被多次添加,接着把符合条件的数据 push 到 subs 里,到这就已经完成了依赖的收集,不过到这里还没执行完,如果是对象还会递归对象触发所有子项的 getter,还要恢复 Dep.target 状态
移除订阅
移除订阅就是调用 cleanupDeps() 方法。比如在模板中有 v-if 我们收集了符合条件的模板 a 里的依赖。当条件改变时,模板 b 显示出来,模板 a 隐藏。这时就需要移除 a 的依赖
这里主要做的是:
- 先遍历上一次添加的实例数组 deps,移除 dep.subs 数组中的 Watcher 的订阅
- 然后把 newDepIds 和 depIds 交换,newDeps 和 deps 交换
- 再把 newDepIds 和 newDeps 清空
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| cleanupDeps () { let i = this.deps.length while (i--) { const dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } 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 }
|
派发更新
notify()
触发 setter 的时候会调用 dep.notify()
通知所有订阅者进行派发更新
1 2 3 4 5 6 7 8 9 10 11 12
| 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() } }
|
update()
触发更新时调用
1 2 3 4 5 6 7 8 9 10
| update () { if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } }
|
queueWatcher()
源码地址:src/core/observer/scheduler.js - 164行
这是一个队列,也是 Vue 在做派发更新时的一个优化点。就是说在每次数据改变的时候不会都触发 watcher 回调,而是把这些 watcher 都添加到一个队列里,然后在 nextTick 后才执行
这里和下一小节 flushSchedulerQueue()
的逻辑有交叉的地方,所以要联合起来理解
主要做的是:
- 先用 has 对象查找 id,保证同一个 watcher 只会 push 一次
- else 如果在执行 watcher 期间又有新的 watcher 插入进来就会到这里,然后从后往前找,找到第一个待插入的 id 比当前队列中的 id 大的位置,插入到队列中,这样队列的长度就发生了变化
- 最后通过 waiting 保证 nextTick 只会调用一次
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| export function queueWatcher(watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true if (!flushing) { queue.push(watcher) } else { let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } if (!waiting) { waiting = true if (process.env.NODE_ENV !== "production" && !config.async) { flushSchedulerQueue() return } nextTick(flushSchedulerQueue) } } }
|
flushSchedulerQueue()
源码地址:src/core/observer/scheduler.js - 71行
这里主要做的是:
- 先排序队列,排序条件有三点,看注释
- 然后遍历队列,执行对应 watcher.run()。需要注意的是,遍历的时候每次都会对队列长度进行求值,因为在 run 之后,很可能又会有新的 watcher 添加进来,这时就会再次执行到上面的 queueWatcher
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| function flushSchedulerQueue() { currentFlushTimestamp = getNow() flushing = true let watcher, id
queue.sort((a, b) => a.id - b.id)
for (index = 0; index < queue.length; index++) { watcher = queue[index] if (watcher.before) { watcher.before() } id = watcher.id has[id] = null watcher.run() if (process.env.NODE_ENV !== "production" && has[id] != null) { circular[id] = (circular[id] || 0) + 1 if (circular[id] > MAX_UPDATE_COUNT) { warn(`无限循环了`) break } } } const activatedQueue = activatedChildren.slice() const updatedQueue = queue.slice() resetSchedulerState() callActivatedHooks(activatedQueue) callUpdatedHooks(updatedQueue) }
|
updated()
终于可以更新了,updated
大家都熟悉了,就是生命周期钩子函数
上面调用 callUpdatedHooks()
的时候就会进入这里, 执行 updated
了
1 2 3 4 5 6 7 8 9 10
| function callUpdatedHooks(queue) { let i = queue.length while (i--) { const watcher = queue[i] const vm = watcher.vm if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) { callHook(vm, "updated") } } }
|
至此 Vue2 的响应式原理流程的源码基本就分析完毕了,接下来就介绍一下上面流程中的不足之处
defineProperty 缺陷及处理
使用 Object.defineProperty 实现响应式对象,还是有一些问题的
- 比如给对象中添加新属性时,是无法触发 setter 的
- 比如不能检测到数组元素的变化
而这些问题,Vue2 里也有相应的解决文案
Vue.set()
给对象添加新的响应式属性时,可以使用一个全局的 API,就是 Vue.set() 方法
源码地址:src/core/observer/index.js - 201行
set 方法接收三个参数:
- target:数组或普通对象
- key:表示数组下标或对象的 key 名
- val:表示要替换的新值
这里主要做的是:
- 先判断如果是数组,并且下标合法,就直接使用重写过的 splice 替换
- 如果是对象,并且 key 存在于 target 里,就替换值
- 如果没有
__ob__
,说明不是一个响应式对象,直接赋值返回
- 最后再把新属性变成响应式,并派发更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| export function set(target: Array<any> | Object, key: any, val: any): any { if ( process.env.NODE_ENV !== "production" && (isUndef(target) || isPrimitive(target)) ) { warn( `Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}` ) } if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key) target.splice(key, 1, val) return val } if (key in target && !(key in Object.prototype)) { target[key] = val return val } const ob = (target: any).__ob__ if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== "production" && warn( "Avoid adding reactive properties to a Vue instance or its root $data " + "at runtime - declare it upfront in the data option." ) return val } if (!ob) { target[key] = val return val } defineReactive(ob.value, key, val) ob.dep.notify() return val }
|
重写数组方法
源码地址:src/core/observer/array.js
这里做的主要是:
- 保存会改变数组的方法列表
- 当执行列表里有的方法的时候,比如 push,先把原本的 push 保存起来,再做响应式处理,再执行这个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [ "push", "pop", "shift", "unshift", "splice", "sort", "reverse", ]
methodsToPatch.forEach(function (method) { const original = arrayProto[method] def(arrayMethods, method, function mutator(...args) { const result = original.apply(this, args) const ob = this.__ob__ let inserted switch (method) { case "push": case "unshift": inserted = args break case "splice": inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) ob.dep.notify() return result }) })
|