Vue3源码04: Vue3响应式系统源码实现1/2

2022-09-27 14:23:39 浏览数 (1)

  • Vue3源码01 : 代码管理策略-monorepo
  • Vue3源码02: 项目构建流程和源码调试方法
  • Vue3源码03: Vue3响应式核心原理

“我们在前一篇文章中手写实现了一个极简版的响应式系统,接下来将会开始带着大家分析reactivity中的具体实现。关于reactivity的源码实现细节分析会通过两篇文章完成。本文将会从观察reactivity的代码文件结构开始,并从中选出最核心最重要的几个文件(reactive.ts、effect.ts、dep.ts、baseHandlers.ts、collectionHanders.ts)分析其代码关系,接着会分析这其中的一些关键逻辑。在下一篇文章中再探索一些具体api的功能以及对应的源码实现细节。 ”

代码组织结构

我们先来看看reactivity由哪些代码文件组成:

从图中可以看出,reactivity的具体实现由12个代码文件组成。看过上一篇文章的朋友可能会觉得惊讶,当时极简版的代码是这样的简单,不足50行。没错,当时写的极简版响应式系统仅用少量代码就实现了修改对象的属性,自动触发页面更新的功能。相信50行代码和12个文件之间的差距不仅仅是边界条件处理那么简单。但是也不用担心,只要抓住了这些文件间的关系就可以消除心中的疑惑,因为根本的原理确确实实就是我们前面手写的极简版。我们先来看看这12个文件具体的分工是什么:

代码语言:javascript复制
reactive.ts-----------------对外提供响应式能力(对象)
ref.ts----------------------对外提供响应式能力(原始类型值)
baseHandlers.ts-------------Proxy处理器(普通对象)
collectionHandlers.ts-------Proxy处理器(集合对象)
effect.ts-------------------响应式对象属性依赖管理器
dep.ts----------------------工具函数

computed.ts-----------------计算属性
deferredComputed.ts---------下一个tick执行的计算属性
effectScope.ts--------------effect相关的一个管理器
index.ts--------------------集合整个库的能力,对外暴露
operations.ts---------------操作类型相关的常量
warning.ts------------------工具函数

本文只会涉及上面列出的前5个文件。之所以这样安排,是因为这5个文件的内容,完整的呈现了响应式系统的工作流程,而且reactivive.ts暴露的reactive函数在某种程度上可以认为是日常开发中最常用的api,理解了上面几个文件中的代码,对于响应式系统剩下的内容就能比较轻松的理解。

如果对这几个文件的分工,此时还是比较疑惑,可以回想一下上一篇文章中实现的极简版响应式系统相关代码:

代码语言:javascript复制
<!--代码片段1-->
<html>
    <head></head>
    <body>
        <div id="app"></div>
    </body>
    <script>
        const objMap = new Map()
        let activeEffect = null
        const reactive = (obj) => {
            return new Proxy(obj, {
                get: function (target, property) {
                    let propertyMap = objMap.get(target) || new Map()
                    let effectArr = propertyMap.get(property) || []
                    if (effectArr.indexOf(activeEffect) === -1 && !!activeEffect) { 
                        effectArr.push(activeEffect)
                        propertyMap.set(property, effectArr)
                        objMap.set(target, propertyMap)
                    }
                    return target[property] 
                },
                set: function (target, property, val) {
                    target[property] = val
                    let propertyMap = objMap.get(target) || new Map()
                    let effectArr = propertyMap.get(property) || []
                    effectArr.forEach(item => {
                        item()  
                    })
                }
            })
        }
        const effect = (fn) => {
            activeEffect = fn
            fn()
        }

        let dataObj = {name: 'yangyitao'}
        let reactiveDataObj = reactive(dataObj)

        const functionA = () => {
            document.getElementById('app').innerText = reactiveDataObj.name // Id为`anyRealId`的元素真实存在 
        }
        effect(functionA)

        setTimeout(() => {
            reactiveDataObj.name = '杨艺韬'
        }, 3000)
    </script>
</html>

我们从极简本中要抓住几个重点:

  1. effect函数
  2. 调用函数reactive,让普通对象具备响应式能力
  3. 创建Proxy对象实例,给Proxy对象设置处理器对象

这个时候我们再回头看我们今天要分析的几个文件,由于dep.ts只是一个工具函数,可以暂时忽略。而collectionHandlers.ts只是为SetMap之类的集合对象添加响应式能力而存在的Proxy处理器对象相关的内容,我们这里也可以暂时忽略,只分析为普通对象添加响应式能力的代码。因此可以先简单的认为:

代码语言:javascript复制
reactive.ts-----------------极简版中的reactive函数所在的地方
baseHandlers.ts-------------极简版中的创建proxy实例的地方
effect.ts-------------------极简版中的调用effect函数的地方

在具体进入代码之前,请先看下面这张思维导图:

此时看这张图片可能会觉得有些复杂,但没关系,分析完本文的内容再回过头相信就会更加容易理解了。

函数reactive

按照极简版中的理解,reactive函数,就是为某个对象创建一个Proxy实例并返回。我们先看看源码中的实现:

代码语言:javascript复制
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
  // 此处省略一些次要逻辑...
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  // 此处省略一些次要逻辑...
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  const proxy = new Proxy(
    target,
    //此时暂不考虑集合对象的处理,可以认为该参数值就是baseHandler
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}

到目前为止,逻辑和极简版还是高度相似的,只不过这里的Proxy实例的处理器相关代码是放在文件baseHandlers中维护的。但是,我们会发现在文件reactive.ts中除了函数reactive,还有函数shallowReactivereadonlyshallowReadonly,这几个函数内部和函数reactive一样也调用了createReactiveObject,也就是说都最终创建了Proxy实例,只不过传入的参数不太一样。具体这些函数的功能,可以查阅vue3的官方文档,就不在此处赘述了。另外reactive.ts中还有7个工具函数,逻辑相对简单朋友们可以自行阅读,不作为本文重点进行介绍了。

函数effect

先看看函数effect的具体代码:

代码语言:javascript复制
export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  // 省去一些参处理代码...
  const _effect = new ReactiveEffect(fn)
  // 省去和effectScope相关逻辑以及一些条件判断的代码...
  _effect.run()
  // 省去返回值相关逻辑...
}

对该函数代码进行精简后,逻辑很简单,创建一个ReactiveEffect实例,并执行其run方法。我们来看看类ReactiveEffect的代码实现:

代码语言:javascript复制
export class ReactiveEffect<T = any> {
  active = true
  deps: Dep[] = []
  parent: ReactiveEffect | undefined = undefined
  // 省略许多属性声明代码...
  constructor(
    public fn: () => T,
    public scheduler: EffectScheduler | null = null,
    scope?: EffectScope
  ) {
    // 省略代码...
  }

  run() {
    // 省略许多代码...
    return this.fn()
    // 省略许多代码...
  }

  stop() {
    // 省略许多代码...
  }
}

去除支持,发现run方法只是执行了传入的fn这个函数参数并返回结果。也就是说,我们调用effect函数,最重要的工作,就是执行了传入的函数参数。朋友们可能会问,执行了又怎么样呢?

mutableHandlers

还记得我们上面分析reactive函数时候提到的baseHandler吗,当我们将某个普通对象传入reactive的时候,会为该对象创建Proxy实例,并设置baseHandler,而baseHander又做了细致的区分,对于我们调用reactive函数而言,其真实的处理器是baseHandlers.ts中的mutableHandlers,具体代码如下:

代码语言:javascript复制
export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}
const get = /*#__PURE__*/ createGetter()
const set = /*#__PURE__*/ createSetter()

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    // 省略若干代码...
    track(target, TrackOpTypes.GET, key)
    // 省略若干代码...
    return res
  }
}

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    // 省略若干代码...
    trigger(target, TriggerOpTypes.SET, key, value)
    // 省略若干代码
    return result
  }
}

还记得我们在极简版中的响应式系统中,访问对象属性的时候,记录其对应的从effect传入的函数,改变对象属性值的时候,遍历记录中的函数依次执行。这个记录和触发的任务在这里交给了tracktrigger,那这两个函数在哪里实现的呢,答案是在effect.ts中实现。大家想想为什么要在effect.ts中维护这两个函数呢?其实不难理解,track收集的是对象属性和对应的ReactiveEffect对象实例。我们在极简版中是收集的传给effect的函数,这里源码实现中收集的是ReactiveEffect对象实例。而trigger是遍历对象属性所对应的ReactiveEffect实例集合并执行实例的run方法。

这时候再回过头看上文中的思维导图,相信就更加容易理解了,如果还是阅读起来有困难可以尝试断点调试,具体调试方法在本系列文章的第2篇中有介绍。

小结

本文介绍了reactivity中的代码文件组成。选取了其中最关键的三个文件reactive.tseffect.ts,baseHandlers.ts,介绍了reactive函数到实现,进而分析了Proxy实例的处理器,引出了tracktrigger两个函数。同时也介绍了effect函数,涉及了类ReactiveEffect最关键的实现。有了这些基础,下一篇文章中,将会讨论一些reactivity暴露的具体api的功能及其对应的源码细节。

0 人点赞