浅析Vue3数据响应系统
国庆长假还没结束,尤大大就把Vue3源码给放了出来,还能不能好好让人好好放个假:fearful:,目前的版本还是 Pre-Alpha
,源码地址: Vue-next
, 而正式版本可能要明年才发布了,于是放假回来,便第一时间clone了源码,生啃了起来,还别说,真香~~~:joy:
源码整体结构还是比较清晰,相比较于Vue2.x做了比较大的调整,代码话说98%都是用ts编写,所以看源码还需要大概了解ts的一些知识。而在还没发布Vue3源码之前,官网也已经给出了 Vue Composition API RFC
,可以初步了解Vue3的一些写法和特性,这两天花了些时间大概看了下数据响应部分( 网上已经有大佬给出了调试技巧
)
熟悉Vue2.x的童鞋应该知道,其数据响应是基于 ES5
的API Object.defineProperty
来操作属性的 getter/setter
实现的,本身API存在一些不足:比如对于数组,不能通过数组下标来观测其变化,也不能观测数组长度length的变化,为此对数组7个原生方法做了拦截处理实现对数组的观测。对于对象,由于defineProperty的局限性,Vue2不能检测对象属性的添加或删除。
为了优化解决这些不足,更好的实现方式是通过 ES6
提供的 Proxy
对象。
Vue3 就是基于 Proxy
对其数据响应系统进行了重写,现在这部分可以作为独立的模块配合其他框架使用。数据响应可分为三个阶段: 初始化阶段 –> 依赖收集阶段 –> 数据响应阶段
Proxy代理须知
用 Proxy
做代理时,我们需要了解几个问题:
1、 Proxy
代理是如何对其 trap
进行处理来实现数据响应的?也就是其 get/set
里面是如何做拦截处理(其实这里的trap默认行为可以通过 Reflect
来返回, Reflect
对象的方法与 Proxy
对象的方法一一对应,只要是 Proxy
对象的方法,就能在 Reflect
对象上找到对应的方法。这就让 Proxy
对象可以方便地调用对应的 Reflect
方法,完成默认行为,作为修改行为的基础。这里具体可以查看阮大神的 ES6入门
)
2、 Proxy
代理的对象只能代理到第一层,当代理的对象多层嵌套时,那么对象内部的深度监测需要如何去实现?
3、当代理对象是数组时,比如push操作会触发多次 get/set
,因为push操作除了增加数组的数据项之外,也会引发数组本身其他相关属性的改变,因此会多次触发 get/set
,那么要如何解决呢?
下面我们会稍微分析下 Vue3 针对这几个问题做了哪些优化处理。
初始化阶段
初始化过程相对比较简单,通过 reactive()
方法将数据转化成 Proxy
对象,这里注意一个比较重要的对象 targetMap
,它在依赖收集阶段起着比较重要的作用,具体下面会有分析。
export function reactive(target: object) { // if trying to observe a readonly proxy, return the readonly version. if (readonlyToRaw.has(target)) { return target } // target is explicitly marked as readonly by user if (readonlyValues.has(target)) { return readonly(target) } return createReactiveObject( target, rawToReactive, reactiveToRaw, mutableHandlers, mutableCollectionHandlers ) } ... // 创建proxy对象 function createReactiveObject( target: any, toProxy: WeakMap, toRaw: WeakMap, baseHandlers: ProxyHandler, collectionHandlers: ProxyHandler ) { if (!isObject(target)) { if (__DEV__) { console.warn(`value cannot be made reactive: ${String(target)}`) } return target } // target already has corresponding Proxy let observed = toProxy.get(target) if (observed !== void 0) { return observed } // target is already a Proxy if (toRaw.has(target)) { return target } // only a whitelist of value types can be observed. if (!canObserve(target)) { return target } const handlers = collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers observed = new Proxy(target, handlers) toProxy.set(target, observed) toRaw.set(observed, target) if (!targetMap.has(target)) { targetMap.set(target, new Map()) } return observed }
Vue3 如何进行深度观测的?先看下面这段代码
let data = { x: {y: {z: 1 } } } let p = new Proxy(data, { get(target, key, receiver) { const res = Reflect.get(target, key, receiver) console.log('get value:', key) console.log(res) return res }, set(target, key, value, receiver) { console.log('set value:', key, value) return Reflect.set(target, key, value, receiver) } }) p.x.y = 2 // get value: x // {y: 2}
上面代码我们可以知道 Proxy
只会代理一层,因为这里只是触发了一次最外层属性 x
的 get
,而重新赋值的其内部属性 y
,此时 set
并没有被触发,所以改变内部属性是不会监测到的。继续看,Reflect.get返回的结果正是 target
的内层结构,此时 p.x.y
的值也已经变成 2
了,我们可以判断当前 Reflect.get
返回的值是否为 object
,若是则再通过 reactive
做代理,这样就达到了深度观测的目的了。
Vue3实现过程具体我们可以看下面源码:
function createGetter(isReadonly: boolean) { return function get(target: any, key: string | symbol, receiver: any) { const res = Reflect.get(target, key, receiver) if (typeof key === 'symbol' && builtInSymbols.has(key)) { return res } if (isRef(res)) { return res.value } track(target, OperationTypes.GET, key) // 当代理的对象是多层结构时,Reflect.get会返回对象的内层结构,我们可以拿到当前res再做判断是否为object,进而进行reactive,就达到了深度观测的目的了 return isObject(res) ? isReadonly ? // need to lazy access readonly and reactive here to avoid // circular dependency readonly(res) : reactive(res) : res } }
依赖收集阶段
所谓的依赖在Vue3可简单理解为各种 effect
响应式函数,其中包括了属性依赖的 effect
,计算属性 computedEffect
以及组件视图的 componentEffect
1、在视图挂载渲染时会执行一个 componentEffect
,触发相关数据属性getter操作来完成视图依赖收集。
2、 effect
函数执行也会触发相关属性的getter操作,此时操作了某个属性的 effect
也会被该属性对应进行收集(注意这里的属性是可观测的)。
之所以说是响应式的,是因为effect方法回调中关联了被观测的数据属性,而effect一般是立即执行的,此时触发了该属性的 getter
,进行依赖收集,当该属性触发 setter
时,便会触发执行收集的依赖。另外,这里每次effect执行时,当前的effect会被压入一个名为 activeReactiveEffectStack
的栈中,是在依赖收集的时候使用。
export function effect( fn: Function, options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect { if ((fn as ReactiveEffect).isEffect) { fn = (fn as ReactiveEffect).raw } const effect = createReactiveEffect(fn, options) if (!options.lazy) { // effect立即执行,触发effect回调函数fn中相关响应数据属性的getter操作,从而进行依赖收集 effect() } return effect } ... // 触发getter操作,进行依赖收集 export function track( target: any, type: OperationTypes, key?: string | symbol ) { if (!shouldTrack) { return } const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1] if (effect) { if (type === OperationTypes.ITERATE) { key = ITERATE_KEY } let depsMap = targetMap.get(target) if (depsMap === void 0) { targetMap.set(target, (depsMap = new Map())) } let dep = depsMap.get(key!) if (dep === void 0) { depsMap.set(key!, (dep = new Set())) } // 防止依赖重复收集 if (!dep.has(effect)) { dep.add(effect) effect.deps.push(dep) if (__DEV__ && effect.onTrack) { effect.onTrack({ effect, target, type, key }) } } } }
开头说过 targetMap
对象在依赖收集过程中的重要作用,看源码我们大概知道了,它维护了一个依赖收集的关系表, targetMap
是一个 WeakMap
,其 key
值是当前被代理的对象 target
,而 value
则是该对象所对应的 depsMap
,它是一个 Map
, key
值为触发 getter
时的属性值,而 value
值则是触发过该属性值所对应的各个 effect
。
故 targetMap
的关系映射可以看成 target --> key --> effect
,可以看出 target
被观测后,其属性 key
在被触发 getter
操作时,收集了所依赖的 effect
,可以说 targetMap
是Vue3进行依赖收集的一个核心对象。
响应阶段
当触发属性 setter
时,通过 trigger
函数会执行属性对应收集的 effects
,也包括 computedEffects
,此时通过 scheduleRun
逐个调用 effect
,最后完成视图更新。
上面我们讲过监测数组的时候可能触发多次 get/set
, 那么如何防止触发多次的呢?先看Vue3的源码(简写省略了部分代码):
// setter操作触发响应 function set( target: any, key: string | symbol, value: any, receiver: any ): boolean { value = toRaw(value) // 判断key是否为当前target自身属性 const hadKey = hasOwn(target, key) // 获取旧值 const oldValue = target[key] if (isRef(oldValue) && !isRef(value)) { oldValue.value = value return true } const result = Reflect.set(target, key, value, receiver) ... if (!hadKey) { // 若属性不存在标记为add操作 trigger(target, OperationTypes.ADD, key) } else if (value !== oldValue) { // 若值不相等在触发,并且标记为set操作 trigger(target, OperationTypes.SET, key) } ... return result } export function trigger( target: any, type: OperationTypes, key?: string | symbol, extraInfo?: any ) { const depsMap = targetMap.get(target) if (depsMap === void 0) { // never been tracked return } const effects = new Set() const computedRunners = new Set() // 这里遍历找出相关依赖的effect if (type === OperationTypes.CLEAR) { // collection being cleared, trigger all effects for target depsMap.forEach(dep => { addRunners(effects, computedRunners, dep) }) } else { // schedule runs for SET | ADD | DELETE if (key !== void 0) { addRunners(effects, computedRunners, depsMap.get(key)) } // also run for iteration key on ADD | DELETE if (type === OperationTypes.ADD || type === OperationTypes.DELETE) { // 这里当改变数组length长度时也会触发相关effect进行响应 const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY addRunners(effects, computedRunners, depsMap.get(iterationKey)) } } // 遍历执行依赖的effect const run = (effect: ReactiveEffect) => { scheduleRun(effect, target, type, key, extraInfo) } computedRunners.forEach(run) effects.forEach(run) } function scheduleRun( effect: ReactiveEffect, target: any, type: OperationTypes, key: string | symbol | undefined, extraInfo: any ) { ... if (effect.scheduler !== void 0) { effect.scheduler(effect) } else { effect() } }
由源码我们可以分析出:1、判断key是否为当前被代理对象target自身属性; 2、判断旧值与新值是否相等。只有这两个条件其中一个满足,才有可能执行 trigger
。
怎么理解呢,我们举个:chestnut:,可以实现一个小的 reactive
方法来做数据代理,代码如下:
function hasOwn(val, key) { const hasOwnProperty = Object.prototype.hasOwnProperty return hasOwnProperty.call(val, key) } function reactive(data) { let observed = new Proxy(data, { get(target, key, receiver) { const res = Reflect.get(target, key, receiver) return res }, set(target, key, value, receiver) { console.log(target, key, value) const hadKey = hasOwn(target, key) const oldValue = target[key] const result = Reflect.set(target, key, value, receiver) if (!hadKey) { console.log('trigger add operation...') } else if(value !== oldValue) { console.log('trigger set operation...') } return result } }) return observed } let data = ['a', 'b'] let state = reactive(data) state.push('c') // ["a", "b"] "2" "c" // trigger add operation... // ["a", "b", "c"] "length" 3
state.push(‘c’)
会触发两次 set
,一次是push的值 c
,一次是 length
属性设置。
1、设置值 c
时,新增了索引 key
为 2,*target 是原始的代理对象 [‘a’, ‘c’]
,这是一个 add
操作, 故 hasOwn(target, key)
返回的是false,此时执行 trigger add operation…
。注意在 trigger
方法中, length
没有对应的 effect
,所以就没有执行相关的 effect
。
2、当传入 key
为 length
时, length
是自身属性,故 hasOwn(target, key)
返回 true
, 此时 value
是 3, 而 oldValue
即为 target[‘length’]
也是 3,故 value !== oldValue
不成立,不执行 trigger
方法
故只有当 hasOwn(target, key)
返回true或者 value !== oldValue
的时候才执行 trigger
。
总结
在分析源码之前我们先列举了用Proxy做代理实现数据响应需要解决的几个问题,并带着这些问题一步一步揭开Vue在数据响应系统处理这些问题的面纱,也让我们进一步了解了Vue源码编写有许多巧妙的地方,比如利用 Reflect.get
返回值为 target
当前触发的第一层属性 key
值对应的 value
值,从而再来判断是否为Object来进行深度观测,并且观测的值存放在一个WeakMap下,这样相比较递归Proxy,Vue的这种实现方式大大提高了数据响应的性能。
这两天就大致的阅读了这部分源码,有些细节地方还没去深入理解,存在不足或者错误的地方还请各位多指教,大家一起学习交流哈。