【源码阅读】vue3 - reactive 源码探究

974 阅读4分钟

在vue设计与实现4.3设计一个完善的响应系统中提到一个响应式系统的工作流程如下:

  1. 当读取操作发生时,将副作用函数收集到“桶”中;
  2. 当设置操作发生时,从“桶”中取出副作用函数并执行。

读取操作会调用 track 方法,设置操作会调用 trigger 方法。将断点卡在 track 和 trigger 方法上,看看 vue3 都干了什么。

getter 和 setter 执行过程探究

下面是用于 debugger 的一个简单的例子:

<div id="app">
    <demo />
</div>
<script src="../../dist/vue.global.js"></script>
<script type="text/x-template" id="item-template">
    <div>{{data.count}}</div>
    <button @click="add">+1</button>
</script>
<script>
    const { reactive, createApp } = Vue
    const demo = {
        template: '#item-template',
        setup() {
            const data = reactive({
                count: 0,
            })
            const add = () => {
                data.count++
            }
            return { data, add }
        }
    }
​
    Vue.createApp({
        components: {
            demo
        },
    }).mount("#app");
</script>

断点卡在 track 上时,刷新页面会发现会有两次进入,调用顺序及调用栈信息分别如下:

第一次:

image.png

第二次:

image.png

为什么执行两次目前先不去探究,但是从两次的调用栈及函数名称大概可以推测出在 track 前vue执行的操作:vue 会解析模板转变成虚拟 DOM,经 render 函数处理,在此过程中触发 get 操作收集副作用函数

断点卡在 trigger 上,点击 +1 ,调用顺序和调用栈信息如下:

第一次:

image.png

第二次:

image.png

第三次:

image.png

执行三次很好理解,在第二次过程中存在异步操作其作用是将副作用函数的执行放入到异步队列中,从 4.7 调度执行可以了解到此部分实现的功能是:在 vue 中如果多次修改同一个响应式数据但是只会触发一次更新。通过这一点你也可以理解为什么会有 nextTick 这个 api 了。

reactive 执行过程探究

reactive 是在源码的 packages --> reactivity --> src --> reactive.ts 中:

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: /*#__PURE__*/ createInstrumentationGetter(false, false)
}
export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (isReadonly(target)) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}

这段代码很好理解:如果传入的对象是一个只读的代理对象则直接返回,判断是否为只读的代理对象是通过判断属性上有没有 __v_isReadonly。接着直接返回 createReactiveObject 的执行结果,createReactiveObject 代码如下:

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
){
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // target already has corresponding Proxy
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // only specific value types can be observed.
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}

该段代码逻辑也很简单,如果传入的对象满足以下4种情况,直接返回原对象:

  • 不是一个对象
  • 该对象已经是一个代理对象
  • 该对象已经有了关联的代理对象,如果存在相关联的对象在 proxyMap 中能够找到,因为 proxyMap 保存了target 和 proxy 的映射关系
  • 传入的对象需要符合代理对象的要求,即 TargetType 属于两大类:common(object,Array)和 collection (Map,Set,WeakMap,WeakSet)
function targetTypeMap(rawType: string) {
  switch (rawType) {
    case 'Object':
    case 'Array':
      return TargetType.COMMON
    case 'Map':
    case 'Set':
    case 'WeakMap':
    case 'WeakSet':
      return TargetType.COLLECTION
    default:
      return TargetType.INVALID
  }
}

最后根据 TargetType 返回代理对象

// 当 TargetType === common 时,返回的代理对象为
export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}
new Proxy(
    target,
    mutableHandlers
)
  
// 当 TargetType === collection 时,返回的代理对象为
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: /*#__PURE__*/ createInstrumentationGetter(false, false)
}
new Proxy(
    target,
    mutableCollectionHandlers
)

对于 TargetType === common 即传入的对象为 object 和 Array 时,handler 除了拦截 get,set 外,同时还拦截了 deleteProperty,has 和 ownKeys 操作。拦截这三个操作的目的如下:

  • deleteProperty:拦截的是 delete 操作符,例如 obj.foo
  • has :拦截的是 in 操作符,例如 key in obj
  • ownKeys : 拦截的是Object.getOwnPropertyNamesObject.getOwnPropertySymbols,例如 for(const key in obj)

了解了代码逻辑外,createReactiveObject 接收 5 个参数:

  • target 表示被代理的对象

  • isReadonly 表示是否创建只读的响应式对象,以下方法创建的都是只读的响应式对象

    // 接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理
    export function readonly<T extends object>( target: T ): DeepReadonly<UnwrapNestedRefs<T>> {
      return createReactiveObject(
        target,
        true,
        readonlyHandlers,
        readonlyCollectionHandlers,
        readonlyMap
      )
    }
    ​
    // readonly() 的浅层作用形式
    export function shallowReadonly<T extends object>(target: T): Readonly<T> {
      return createReactiveObject(
        target,
        true,
        shallowReadonlyHandlers,
        shallowReadonlyCollectionHandlers,
        shallowReadonlyMap
      )
    }
    
  • baseHandlers 表示 target 类型为 common (Object ,Array )代理对象的 handler,拦截的操作包括 get,set,deleteProperty,has,ownKeys

  • collectionHandlers 表示 target 类型为 collection (Map,Set,WeakMap,WeakSet)代理对象的 handler。拦截的操作只有 get

  • proxyMap 表示是“桶”,记录了 target 和 代理对象的映射关系

    export const reactiveMap = new WeakMap<Target, any>()
    export const shallowReactiveMap = new WeakMap<Target, any>()
    export const readonlyMap = new WeakMap<Target, any>()
    export const shallowReadonlyMap = new WeakMap<Target, any>()
    

最后总结下 reactive 函数的执行过程:

  1. reactive 函数接收单个参数 target ,其值必须对象。内部调用 createReactiveObject 方法将对象变成响应式。
  2. 对于 target 类型也只能接收两大类:common (Object ,Array )和 collection(Map,Set,WeakMap,WeakSet),common 会被劫持 get,set,deleteProperty,has,ownKeys 操作,collection 仅劫持 get 操作,其他的对象类型则不会进行响应式处理