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

在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种情况,直接返回原对象:

  • 不是一个对象
  • 该对象已经是一个代理对象
  • 该对象已经有了关联的代理对象
  • 传入的对象需要符合代理对象的要求,即 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.getOwnPropertyNames 和Object.getOwnPropertySymbols,例如 for(const key in obj)

猜你喜欢

转载自juejin.im/post/7219899306946330682