一只菜🐔的Vue3源码阅读记录

467 阅读22分钟

Vue3源码

Vue.js 2.x 的源码在 src 目录下,依据功不同分成 compiler(模板编译的相关代码)、core(与平台无关的通用运行时代码)、platforms(平台专有代码)、server(服务端渲染的相关代码)、sfc(.vue 单文件解析相关代码)、shared(共享工具代码) 等目录。

Vue.js 3.0中 ,源码通过 monorepo 的方式管理,根据功能将不同的模块拆分到 packages 目录下。

可以看出相对于 Vue.js 2.x 的源码组织方式,monorepo 把这些模块拆分到不同的 package 中,每个 package 有各自的 API、类型定义和测试。这样使得模块拆分更细化,职责划分更明确,模块之间的依赖关系也更加明确,开发人员也更容易阅读、理解和更改所有模块源码,提高代码的可维护性。

问题

  1. 克隆下git的代码之后,pnpm安装相关依赖,并且需要node版本12以上。
  2. 需要执行 pnpm run dev,打包dist,才可以使用packages下面的examples文件夹下的例子。

阅读步骤

应用初始化

// 在 Vue.js 3.0 中,初始化一个应用的方式如下
import { createApp } from 'vue'
import App from './app'
const app = createApp(App)
app.mount('#app')

1、创建app对象

  • createApp方法 packages/runtime-dom/src/index.ts

    const createApp = ((...args) => {
      // 创建 app 对象
      const app = ensureRenderer().createApp(...args)
      const { mount } = app
      // 重写 mount 方法
      app.mount = (containerOrSelector) => {
        // ...
      }
      return app
    })
    
  • 文件目录 vue-next/packages/runtime-dom/src/index.ts

    • 总的来说就是vue3中通过 ensureRenderer 创建一个渲染器,这个渲染器内部层层调用,最终会找到一个叫 createApp 的方法,它是执行 createAppAPI 方法返回的函数。 createApp 方法接受了 rootComponent 和 rootProps 两个参数,我们在应用层面执行 createApp(App) 方法时,会把 App 组件对象作为根组件传递给 rootComponent。 const app = createApp(App)

    • 使用 ensureRenderer().createApp() 来创建 app 对象

      1. 首先使用ensureRenderer()来创建一个渲染器,其内部包括rendererOptions配置和createRenderer方法。初始时会调用createRenderer方法来延时创建渲染器 。

        function ensureRenderer() {
          return renderer || (renderer = createRenderer(rendererOptions))
        }
        
      2. createRenderer方法里面调用baseCreateRenderer方法。

      3. baseCreateRenderer方法里面包括很多基础方法。比如patch,render等方法。其中render方法中就是组件渲染的核心逻辑

        patch、procesText、processCommentNode、mountStaticNode、patchStaticNode、moveStaticNode、removeStaticNode、processElement、mountElement、setScopeId、mountChildren、patchElement、patchBlockChildren、patchProps、processFragment、processComponent、mountComponent、updateComponent、setupRenderEffect、updateComponentPreRender、patchChildren、patchUnkeyedChildren、patchKeyedChildren、move、unmount、remove、removeFragment、unmountComponent、unmountChildren、getNextHostNode、render

        • baseCreateRenderer 的返回值, 也就是 ensureRenderer() 的返回值,返回上一步的CreateApp。CreateApp由createAppAPI实现。

          1. return {
                render,
                hydrate,
                createApp: createAppAPI(render, hydrate)
              }
            
        • createAppAPI内部会返回一个叫createApp的方法。

          1. return function createApp(rootComponent, rootProps = null) {
              const app = {
                  _component: rootComponent,
                  _props: rootProps,
                  mount(rootContainer) {
                    // 创建根组件的 vnode
                    const vnode = createVNode(rootComponent, rootProps)
                    // 利用渲染器渲染 vnode
                    render(vnode, rootContainer)
                    app._container = rootContainer
                    return vnode.component.proxy
                  },
                mixin(mixin: ComponentOptions) {
                  if (!context.mixins.includes(mixin)) {
                        context.mixins.push(mixin)
                      }
                    return app
                  },
                }
                return app
            }
            
          2. 这个function中定义了一个app实例,这个实例里面包括use、mixin、component、directive、mount、unmount、provide方法,为全局app实例添加一些扩展。

          3. createApp执行完之后会返回定义的app实例

  • 在整个 app 对象创建过程中,Vue.js 利用闭包和函数柯里化的技巧,很好地实现了参数保留。

    • 比如,在执行 app.mount 的时候,并不需要传入渲染器 render,这是因为在执行 createAppAPI 的时候渲染器 render 参数已经被保留下来了。

2、创建app.mount方法

const { mount } = app 要渲染到页面上,还需要调用mount,执行app.mount()= (containerOrSelector) => { ... }重写mount方法。

  1. 为什么要重写这个方法、而不把相关逻辑放到app实例上的mount方法内部去实现?
    • ( createApp 返回的app对象里面已经有了mount方法,但是在入口函数中,下面还要对app.mount()进行重写)
    • 原因:
      • 因为Vue.js不仅仅服务web平台,他的目标是支持跨平台渲染,而createApp函数内部的app.mount()方法是一个标准的可跨平台的组件渲染流程。
      • 标准的渲染流程是不包含任何特定平台相关的逻辑,也就是说这些代码的执行逻辑都是与平台无关的。
      • 我们需要在外部重写这个方法,来完善web平台下的逻辑渲染
  2. 一个标准化渲染流程会做什么?
    1. 创建根组件的 vnode
    2. 利用渲染器渲染 vnode
  3. 重写mount做了啥?
    1. 通过 normalizeContainer 标准化容器(可以传字符串选择器或者dom对象,如果是字符串选择器,就需要把他转换成dom对象,作为最终挂载容器。)
    2. 接着做一个if判断,如果组件对象没有定义 render 函数和 template 模板,则提取容器的 innerHTML 作为组件模板的内容。
    3. 接着在挂载之前清空容器内容,最终在调用 app.mount 的方法走标准的组件渲染流程。

其他问题

这些事情发生在mount中。 const { mount } = app

  1. 执行 render(vnode, rootContainer, isSVG) , 这个render其实是在baseCreateRenderer中定义的。那么baseCreateRenderer里面的render做了什么?

    render 是组件渲染的核心逻辑。

    判断vnode是否为空, 如不为空。执行patch方法。patch方法同样定义在baseCreateRenderer内部。 patch(container._vnode || null, vnode, container, null, null, null, isSVG)

  2. baseCreateRenderer内部的patch方法做了什么? 在 patch 中会判断 vnode typeshapeFlag执行对应的操作。 第一次patch的时候, vnode是一个组件,会进入 shapeFlags.COMPONENT 的判断, 执行processComponent方法进行处理。

  3. baseCreateRenderer内部的processComponent方法做了什么? processComponent 里面触发 mountComponent。

  4. baseCreateRenderer内部的mountComponent方法做了什么?

    • 定义了一个实例instance,并执行setupComponent(instance)
    • setupComponent方法在packages/runtime-core/src/component.ts中定义,具体做了什么?
      • 触发 setupComponent 房中初始化组件的 props slots setup等(initProps,initSlots),将需要proxy代理的数据处理好,返回setupResult。
      • 执行完了 setupComponent之后, 会执行setupRenderEffect。
  5. baseCreateRenderer内部的 setupRenderEffect 方法做了什么?

    • setupRenderEffect 内部首先定义了componentUpdateFn方法,接着会通过setupRenderEffect内部定义的effect方法使用componentUpdateFn方法。最后定义了update方法,执行effect方法。
    • componentUpdateFn里面实现了什么?
      • 第一次执行时, instance.isMounted 是 undefined。
      • 执行patch方法,通过patch递归创建子节点。
      • 结束后将instance.isMounted设置为true。
      • 在 patch 中会判断 vnode 的 type 或 shapeFlag 执行对应的操作。
    const setupRenderEffect: () => {
        const componentUpdateFn = () => {
          if (!instance.isMounted) {
            // 第一次执行时, instance.isMounted 是 undefined , 走这块逻辑
            
            // ...
            // 执行patch方法,通过patch递归创建子节点。
            patch()
            // 结束后将instance.isMounted设置为true。
            instance.isMounted = true
          } else {
          }
        }
        const effect = new ReactiveEffect(
              componentUpdateFn,
              () => queueJob(instance.update),
              instance.scope // track it in component's effect scope
            ) 
        const update = (instance.update = effect.run.bind(effect) as SchedulerJob)
        update()
    }
    

核心渲染流程

1、创建vnode

  • Vnode 本质上是用来描述DOM的 JavaScript对象。在Vue.js中可以描述不同类型的节点,比如普通元素节点、组件节点等。
  • vnode的优势?
    • 对抽象事物的描述。Vue.js 3.0 内部还针对 vnode 的 type,做了更详尽的分类,包括 Suspense、Teleport 等,且把 vnode 的类型信息做了编码,以便在后面的 patch 阶段,可以根据不同的类型执行相应的处理逻辑
    • 可以把渲染过程抽象化,从而使得组件的抽象能力提升。
    • 跨平台的优势,patch vnode 的过程对于不同的平台可以有自己的实现。
    • vnode的性能一定比操作原生DOM好吗? 不一定。首先基于vnode实现的MVVM框架,在每次render to vnode的过程中,渲染组件会有一定的JavaScript耗时。
  • 通过 createVNode 方法创建
    • 对 props 做标准化处理
    • 对 vnode 的类型信息编码
    • 创建 vnode 对象,标准化子节点 children 。

2、渲染vnode

render(vnode, rootContainer)
const render = (vnode, container) => {
  if (vnode == null) {
    // 销毁组件
    if (container._vnode) {
      unmount(container._vnode, null, null, true)
    }
  } else {
    // 创建或者更新组件
    patch(container._vnode || null, bnode. container)
  }
  // 缓存 vnode 节点, 表示已经渲染
  container._vnode = node
}
  1. render 渲染函数。逻辑简单,第一个参数vnode为空时,执行销毁组件的逻辑。否则执行创建或更新组件的逻辑。

  2. 渲染代码中的patch方法,是如何实现的。(render和patch都在baseCreateRenderer里面定义的)

    1. patch 本意是打补丁的意思,这个函数有两个功能.
      1. 一个是根据 vnode 挂载 DOM
      2. 一个是根据新旧 vnode 更新 DOM。
  3. 在创建的过程中,patch 函数接受多个参数,这里我们目前只重点关注前三个:

    第一个参数 n1 表示旧的 vnode,当 n1 为 null 的时候,表示是一次挂载的过程;

    第二个参数 n2 表示新的 vnode 节点,后续会根据这个 vnode 类型执行不同的处理逻辑;

    第三个参数 container 表示 DOM 容器,也就是 vnode 渲染生成 DOM 后,会挂载到 container 下面。

  4. 对于渲染的节点,我们这里重点关注两种类型节点的渲染逻辑:对组件的处理和对普通 DOM 元素的处理。

    1. 先来看对组件的处理。由于初始化渲染的是 App 组件,它是一个组件 vnode。

      1. 首先是用来处理组件的 processComponent 函数的实现。如果 n1 为 null,则执行挂载组件的逻辑,否则执行更新组件的逻辑。
      2. 挂载组件的 mountComponent 函数主要做三件事情。创建组件实例、设置组件实例、设置并运行带副作用的渲染函数。
        1. 首先是创建组件实例,Vue.js 3.0 虽然不像 Vue.js 2.x 那样通过类的方式去实例化组件,但内部也通过对象的方式去创建了当前渲染的组件实例。
        2. 其次设置组件实例,instance 保留了很多组件相关的数据,维护了组件的上下文,包括对 props、插槽,以及其他实例的属性的初始化处理。
        3. 最后是运行带副作用的渲染函数 setupRenderEffect。
          1. 该函数利用响应式库的 effect 函数创建了一个副作用渲染函数 componentEffect。副作用effect,当组件的数据发生变化时,effect 函数包裹的内部渲染函数 componentEffect 会重新执行一遍,从而达到重新渲染组件的目的。
          2. 渲染函数内部也会判断这是一次初始渲染还是组件更新。
            1. 初始渲染主要做两件事情:渲染组件生成 subTree、把 subTree 挂载到 container 中。
              1. 首先,是渲染组件生成 subTree,它也是一个 vnode 对象。这里要注意别把 subTree 和 initialVNode 弄混了(其实在 Vue.js 3.0 中,根据命名我们已经能很好地区分它们了,而在 Vue.js 2.x 中它们分别命名为 _vnode 和 $vnode)。
              2. 渲染生成子树 vnode 后,接下来就是继续调用 patch 函数把子树 vnode 挂载到 container 中了。
              3. 再次回到了 patch 函数,会继续对这个子树 vnode 类型进行判断。
    2. 对普通 DOM 元素的处理流程。

      1. processElement 函数。如果 n1 为 null,走挂载元素节点的逻辑,否则走更新元素节点逻辑。

      2. mountElement 函数。创建 DOM 元素节点、处理 props、处理 children、挂载 DOM 元素到 container 上。

      3. 创建 DOM 元素节点,通过 hostCreateElement 方法创建。调用了底层的 DOM API document.createElement 创建元素,所以本质上 Vue.js 强调不去操作 DOM ,只是希望用户不直接碰触 DOM,它并没有什么神奇的魔法,底层还是会操作 DOM。

      4. 创建完 DOM 节点后,接下来要做的是判断如果有 props 的话,给这个 DOM 节点添加相关的 class、style、event 等属性,并做相关的处理,这些逻辑都是在 hostPatchProp 函数内部做的。

      5. 接下来是对子节点的处理,我们知道 DOM 是一棵树,vnode 同样也是一棵树,并且它和 DOM 结构是一一映射的。

        1. 如果子节点是纯文本,则执行 hostSetElementText 方法。
        2. 如果子节点是数组,则执行 mountChildren 方法。

        子节点的挂载逻辑同样很简单,遍历 children 获取到每一个 child,然后递归执行 patch 方法挂载每一个 child 。

        mountChildren 函数的第二个参数是 container,而我们调用 mountChildren 方法传入的第二个参数是在 mountElement 时创建的 DOM 节点,这就很好地建立了父子关系。

        通过递归 patch 这种深度优先遍历树的方式,我们就可以构造完整的 DOM 树,完成组件的渲染。

      6. 处理完所有子节点后,最后通过 hostInsert 方法把创建的 DOM 元素节点挂载到 container 上。

    3. 其他类型。

完整的diff流程

更新组件主要做三件事情:更新组件 vnode 节点、渲染新的子树 vnode、根据新旧子树 vnode 执行 patch 逻辑。

重点关注一下patch 逻辑:

  • 首先判断新旧节点是否是相同的 vnode 类型,如果不同,比如一个 div 更新成一个 ul,那么最简单的操作就是删除旧的 div 节点,再去挂载新的 ul 节点。
  • 如果是相同的 vnode 类型,就需要走 diff 更新流程了,接着会根据不同的 vnode 类型执行不同的处理逻辑,这里我们只分析普通元素类型组件类型的处理过程。

组件更新

  • 组件的更新最终还是要转换成内部真实 DOM 的更新,而实际上普通元素的处理流程才是真正做 DOM 的更新。主要依据子组件 vnode 是否存在一些会影响组件更新的属性变化进行判断,如果存在就会更新子组件。

  • processComponent 作为处理组件更新的方法,主要通过执行 updateComponent 函数来更新子组件,updateComponent 函数在更新子组件的时候,会先执行 shouldUpdateComponent 函数,根据新旧子组件 vnode 来判断是否需要更新子组件。

    • processComponent 处理组件 vnode,本质上就是去判断子组件是否需要更新,如果需要则递归执行子组件的副作用渲染函数来更新,否则仅仅更新一些 vnode 的属性。
    • processComponent 处理组件 vnode,让子组件实例保留对组件 vnode 的引用,用于子组件自身数据变化引起组件重新渲染的时候,在渲染函数内部可以拿到新的组件 vnode。
  • 在 shouldUpdateComponent 函数的内部,主要是通过检测和对比组件 vnode 中的 props、chidren、dirs、transiton 等属性,来决定子组件是否需要更新。

一个组件重新渲染的两种场景:

  • 一种是组件本身的数据变化,这种情况下 next 是 null。
  • 一种是父组件在更新的过程中,遇到子组件节点,先判断子组件是否需要更新,如果需要则主动执行子组件的重新渲染方法,这种情况下 next 就是新的子组件 vnode。

子组件对应的新的组件 vnode 是什么时候创建的

  • 在父组件重新渲染的过程中,通过 renderComponentRoot 渲染子树 vnode 的时候生成。
  • 子树 vnode 是个树形结构,通过遍历它的子节点就可以访问到其对应的组件 vnode。

元素更新

processElement

更新元素(patchElement)的过程主要做两件事情:

  • 更新 props patchProps
  • 更新子节点 patchChildren
  • 一个 DOM 节点元素就是由它自身的一些属性和子节点构成的。

patchChildren

  • 旧子节点是纯文本

    • 如果新子节点也是纯文本,那么做简单地文本替换即可;

    • 如果新子节点是空,那么删除旧子节点即可;

    • 如果新子节点是 vnode 数组,那么先把旧子节点的文本清空,再去旧子节点的父容器下添加多个新子节点。

  • 旧子节点是空

    • 如果新子节点是纯文本,那么在旧子节点的父容器下添加新文本节点即可;
    • 如果新子节点也是空,那么什么都不需要做;
    • 如果新子节点是 vnode 数组,那么直接去旧子节点的父容器下添加多个新子节点即可
  • 旧子节点是 vnode 数组

    • 如果新子节点是纯文本,那么先删除旧子节点,再去旧子节点的父容器下添加新文本节点
    • 如果新子节点是空,那么删除旧子节点即可
    • 如果新子节点也是 vnode 数组,那么就需要做完整的 diff 新旧子节点了,这是最复杂的情况,内部运用了核心 diff 算法。

diff

diff主要发生在patch逻辑中,用来找出新旧子树 vnode 的不同,并找到一种合适的方式更新 DOM。(patch里面的processElement里面的patchChildren)

diff这次就先不展开写了,先给个思维导图。有时间写完了再更新文章~ assets.processon.com/chart_image…

组件渲染前的初始化过程

mountComponent

( packages/runtime-core/src/renderer.ts 的 baseCreateRenderer 中定义的)

vue3 不同于 vue2 通过类的方式去实例化组件了,但是内部也是通过设置对象的方式去创建了当前渲染的组件实例。

const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
    // 创建组件实例
    const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense))
    // 设置组件实例
    setupComponent(instance)
    // 设置并运行带副作用的渲染函数
    setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized)
}

主要流程

  1. 创建组件实例 createComponentInstance。里面定义了instance , 包括很多属性。props、attrs、refs、slots等。

  2. 组件实例的设置流程 setupComponent // Composition API 的核心 setup 函数的实现。

    1. 解构出一些属性 props children
    2. 判断是否是一个有状态的组件
    3. 初始化props
    4. 初始化插槽
    5. 返回 setupResult (执行 setupStatefulComponent
      • setupStatefulComponent
        • 创建渲染代理的属性访问缓存 instance.accessCache = {}
        • 创建渲染上下文代理 instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))
          1. 在执行组件渲染函数的时候,为了方便用户使用,会直接访问渲染上下文 instance.ctx 中的属性。
          2. 所以我们也要做一层 proxy,对渲染上下文 instance.ctx 属性的访问和修改,代理到对 setupState、ctx、data、props 中的数据的访问和修改。
          3. 明确了代理的需求后,我们接下来就要分析 proxy 的几个方法: get、set 和 has
            • PublicInstanceProxyHandlers (packages/runtime-core/src/componentPublicInstance.ts) 中包括get、set、has
        • 判断处理setup函数 const { setup } = Component if (setup) { }
          1. 如果 setup 函数带参数,则创建一个 setupContext。
            • 判断 setup 函数的参数长度,如果大于 1,则创建 setupContext 上下文。
            • const setupContext = (instance.setupContext =setup.length > 1 ? createSetupContext(instance) : null)
          2. 执行 setup 函数,获取结果 const setupResult = callWithErrorHandling(setup, instance, 0 /* SETUP_FUNCTION */, [instance.props, setupContext])
          3. 处理 setup 执行结果 handleSetupResult(instance, setupResult, isSSR)
        • 完成组件实例设置 finishComponentSetup(instance) if (!setup) { }
          • 做了两件事: 标准化模板或者渲染函数和兼容 Options API

响应式实现

响应式机制的主要功能就是,可以把普通的 JavaScript 对象封装成为响应式对象,拦截数据的获取和修改操作,实现依赖数据的自动化更新。

reactive

reactive 内部通过 createReactiveObject 函数把 target 变成了一个响应式对象。

createReactiveObject
  1. 首先判断target是不是数组或对象类型,如果不是直接返回target。 这里同时也说明了只有对象或者数组才能reactive

  2. target 已经是 Proxy 对象,直接返回。

    if (
        target[ReactiveFlags.RAW] &&
        !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
      ) {
        return target
      }
    

    注意如果是readonly作用于一个响应式对象上,不返回,继续执行。

  3. 如果target 已经有对应的 Proxy 了, 直接返回。(target already has corresponding Proxy)

    const existingProxy = proxyMap.get(target)
      if (existingProxy) {
        return existingProxy
      }
    // ...
    proxtMap.set(target, proxy)
    
  4. 只有在白名单里面的数据类型才能变成响应式。

    const targetType = getTargetType(target)
      if (targetType === TargetType.INVALID) { // 以前的canObserve  就是对target对象做进一步限制
        return target
      }
    

    TargetType.INVALID 不在白名单。

    也可能会看到有通过canObeserve 版本进行白名单判断的版本,道理都是一样的。

  5. 通过 Proxy API 劫持 target 对象,把它变成响应式。

    const proxy =  new Proxy(target, targetType = TargetType.COLLECTION ? collectionHandlers : baseHandlers )
    
    return proxy
    

    这里Proxy 对应的处理器会根据数据类型的不同而分成不同类型的处理器。

    mutableHandlers ,readonlyHandlers, shallowReactiveHandlers, shallowReadonlyHandlers

baseHandlers
处理器对象类型
  • mutableHandlers

    • 可变类型的处理器对象 (基本数据类型)

    • const mutableHandlers = {
        get, // 属性读取操作的捕捉器。
        set, // 属性设置操作的捕捉器。
        deleteProperty, // delete 操作符的捕捉器。
        has, // in 操作符的捕捉器。
        ownKeys // Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。
      }
      
    • 实现基本操作的拦截和自定义。

  • readonlyHandlers

    • 只读类型的处理器对象

    • const readonlyHandlers: ProxyHandler<object> = {
        get: readonlyGet, // createGetter(true)
        set(target, key) {
          return true
        },
        deleteProperty(target, key) {
          return true
        }
      }
      
    • set 和 deleteProperty 失效。 开发模式下有报错提示。

  • shallowReactiveHandlers

    • const shallowReactiveHandlers = Object.assign(
        {},
        mutableHandlers,
        {
          get: shallowGet,
          set: shallowSet
        }
      )
      
  • shallowReadonlyHandlers

    • const shallowReadonlyHandlers = Object.assign(
        {},
        readonlyHandlers,
        {
          get: shallowReadonlyGet
        }
      )
      
mutableHandlers

重点分析一下比较全面的基本数据类型的Proxy处理器对象 mutableHandlers

无论进行哪种处理,最终都会进行依赖收集track或者派发trigger通知两件事之一。

get
  • 访问对象属性会触发 get 函数

  • 就是对象直接 . 的时候

  • get函数就是执行createGetter()的返回值 const res = Reflect.get(target, key, receiver)

    1. 首先对特殊的key进行处理,对有ReactiveFlags.RAW属性的对象进行判断,如果有就返回对象本身。
    2. 如果对象不是只读的并且target是数组类型 Reflect.get(arrayInstrumentations, key, receiver) 对数组的每个元素做了依赖收集,保证可以监听到数组每个元素的变化
    3. 如果对象不是只读的,通过 trak 触发依赖收集。 track(target, TrackOpTypes.GET, key)
    4. 最后判断res是否是一个对象,是否需要递归执行reactive把res变成响应式的。return isReadonly ? readonly(res) : reactive(res)
      • 这么做的好处是,在初始化响应式对象时,通过Proxy劫持的是对象本身,并不劫持子对象的变化。
      • Proxy 只有在访问对象属性的时候才会进行递归操作,而Vue2.x中的Object.defineProperty触发对象劫持的时候就执行了递归操作。
      • 相比较之下,通过Proxy实现的只有在访问时才会触发递归会有性能上的提升。
set
  1. 设置对象属性会触发 set 函数

  2. 执行set操作,获取最新的value的值。

    • const result = Reflect.set(target, key, value, receiver)
  3. 通过trigger触依赖更新。

    if (target === toRaw(receiver)) {
          if (!hadKey) {
            trigger(target, TriggerOpTypes.ADD, key, value)
          } else if (hasChanged(value, oldValue)) {
            trigger(target, TriggerOpTypes.SET, key, value, oldValue)
          }
        }
    
  4. 如果目标的原型链也是一个 proxy,通过 Reflect.set 修改原型链上的属性会再次触发 setter,这种情况下就没必要触发两次 trigger 了。

    • const hadKey = hasOwn(target, key)
    • const hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key)
    • 判断key是否存在于target上来确定通知的类型。
      • TriggerOpTypes.ADD
      • TriggerOpTypes.SET
  5. 最后返回result。核心是如何通过trigger来派发通知,触发依赖。

deleteProperty

删除对象属性会触发 deleteProperty 函数

  1. 确认target是否有这个属性。const hadKey = Object.prototype.hasOwnProperty.call(val, key)
  2. 执行删除操作const result = Reflect.deleteProperty(target, key)
  3. 派发通知,trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  4. return result
has

in 操作符会触发 has 函数

  1. 执行has操作。const result = Reflect.has(target, key)
  2. 触发依赖收集 track(target, TrackOpTypes.HAS, key)
  3. return result
ownKeys

通过 Object.getOwnPropertyNames 访问对象属性名会触发 ownKeys 函数

  1. 进行依赖收集。track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
  2. Reflect.ownKeys(target) 返回一个由目标对象自身的属性键组成的数组。
track 🌟

收集依赖

  track(target, TrackOpTypes.GET, key)
  function track(target: object, type: TrackOpTypes, key: unknown){
    ...
  }

  const enum TrackOpTypes {
    GET = 'get',
    HAS = 'has',
    ITERATE = 'iterate'
  }

我们要收集的依赖就是数据变化后执行的副作用函数。

具体如何实现的:

  1. 创建全局的targetMap()用于存储target依赖,它的键是target,值是depsMap。

    • const targetMap = new WeakMap()
    • depsMap中键是target的key值,值是依赖收集的容器Dep,是一个Set。dep = createDep()
    • Dep存放了依赖的副作用函数effect。
  2. !(isTracking = shouldTrack && activeEffect !== undefined)

    • shouldTrack 是否应该收集依赖

    • activeEffect 当前激活的 effect

    • 当shouldTrack为false且activeEffect 为undefined时,track直接返回。

  3. 每个 target 对应一个 depsMap

    •     let depsMap = targetMap.get(target)
            if (!depsMap) {
              targetMap.set(target, (depsMap = new Map()))
            }
      
  4. 每个key对应一个dep 集合

    •     let dep = depsMap.get(key)
            if (!dep) {
              depsMap.set(key, (dep = createDep()))
            }
      
  5. 收集当前激活的**effect**作为依赖,也就是存储依赖的操作。trackEffects(dep)

    1. if (!dep.has(activeEffect!)) {
          // 收集当前激活的 effect 作为依赖
          dep.add(activeEffect!)
          // 当前激活的 effect 收集 dep 集合作为依赖
          activeEffect!.deps.push(dep)
        }
      
    2. activeEffect 是当前正在执行的effect

trigger 🌟

派发通知

      trigger(target, TriggerOpTypes.ADD, key, value)
      function trigger(
        target: object,
        type: TriggerOpTypes,
        key?: unknown,
        newValue?: unknown,
        oldValue?: unknown,
        oldTarget?: Map<unknown, unknown> | Set<unknown>
      ){
          ...
        }

      const enum TriggerOpTypes {
        SET = 'set',
        ADD = 'add',
        DELETE = 'delete',
        CLEAR = 'clear'
      }

执行trigger就是根据target和key从 targetMap中找到相关的所有副作用函数遍历执行一遍。

具体如何实现的:

  1. 通过 targetMap 拿到 target 对应的依赖集合 depsMap。

    •     const depsMap = targetMap.get(target)
            if (!depsMap) {
              // 如果没有依赖,直接返回。
              return
            }
      
  2. 创建依赖集合deps,根据 key 从 depsMap 中找到对应的 effects 添加到deps中。

    1. 当type为clear时。deps = [ ...depsMap.values()]

    2. 如果target为数组类型。

          depsMap.forEach((dep, key) => {
                if (key === 'length' || key >= (newValue as number)) {
                  deps.push(dep)
                }
              })
      
    3. SET | ADD | DELETE 操作,添加对应的effects。

    • deps.push(depsMap.get(key))
    • 会有一些针对不同类型的操作,不赘述了在这里。
  3. 收集到依赖集合之后,创建运行的 effects 集合const effects: ReactiveEffect[] = []。遍历依赖集合所有的依赖,然后添加到effects中。最后通过triggerEffects执行effects。

    1.     for (const dep of deps) {
            if (dep) {
              effects.push(...dep)
            }
          }
      
      triggerEffects(createDep(effects))
      
    2. triggerEffects 执行effects

          for (const effect of isArray(dep) ? dep : [...dep]) {
              if (effect !== activeEffect || effect.allowRecurse) {
                if (effect.scheduler) {
                  effect.scheduler()
                } else {
                  effect.run()
                }
              }
            }
      
effect 🌟
ReactiveEffect
  • 作用:用于生成effect实例,存放回调函数。其实就是响应式的副作用函数,当trigger派发通知时,执行的effect就是它。

  • 使用:例如trigger中定义的依赖集合effects,函数effect中的新建const _effect = new ReactiveEffect(fn)

  • 具体实现:

    • 实现思路

      • 把全局的activeEffect 指向它
      • 执行被包装的原始函数fn
    • 参数

      • active 是否激活 true
      • deps 依赖集合 cleanupEffect()时会从其中删除对应的effect,并清空它。
      • constructor中
        • fn
        • Scheduler
        • scope (非必填)
      • ...
    • 方法 effectStack是根据ReactiveEffect定义的。const effectStack: ReactiveEffect[] = []

      • run

        run() {
            if (!this.active) {
              return this.fn() // 执行fn 但是不收集依赖
            }
            if (!effectStack.includes(this)) {
              try {
                effectStack.push((activeEffect = this)) // 进行压栈操作,并设置activeEffect = effect
                enableTracking() // 开启 shouldTrack,设置为true,允许收集依赖
        
                trackOpBit = 1 << ++effectTrackDepth
        
                if (effectTrackDepth <= maxMarkerBits) {
                  initDepMarkers(this)
                } else {
                  cleanupEffect(this)
                }
                return this.fn() // 执行传入的fn
              } finally {
                if (effectTrackDepth <= maxMarkerBits) {
                  finalizeDepMarkers(this)
                }
        
                trackOpBit = 1 << --effectTrackDepth
        
                resetTracking() // 恢复开始之前的状态
                effectStack.pop() // 出栈操作
                const n = effectStack.length
                activeEffect = n > 0 ? effectStack[n - 1] : undefined // 给activeEffect赋值,只想最后一个effect
              }
            }
          }
        
        1. 首先判断effect的状态active,在active不为true时,直接执行原始函数fn。
        2. 接着判断 effectStack 中是否包含 effect,如果没有就把 effect 压入栈内。if (!effectStack.includes(effec))
          • effectStack.push((activeEffect = this)) 就相当于 effectStack.push(effect)activeEffect = effect
          • 入栈操作同时将activeEffect设置为当前的effect函数
        3. 执行enableTracking(),设置shouldTrack为true,进行依赖收集。
        4. 最后进行出栈操作,恢复开启依赖收集之前的状态,activeEffect指向栈内最后一个effect。
      • stop

        1. 执行cleanupEffect()操作,清空 reactiveEffect 函数对应的依赖,删除deps中每一项的effect,最后清空deps
        2. 设置active状态为false,变为飞机货的状态。
toReactive()

判断是否是对象,是的话进行响应式处理

isObject(value) ? reacive(value) : value

ref

实现

    function createRef(rawValue: unknown, shallow: boolean) {
      if (isRef(rawValue)) {
        return rawValue
      }
      return new RefImpl(rawValue, shallow)
    }
RefImpl
    class RefImpl<T> {
      private _value: T // 响应式处理后的值
      private _rawValue: T // 存储原始值,主要用于与 newVal 作比较

      public dep?: Dep = undefined // 依赖收集的容器,用于存放effect
      public readonly __v_isRef = true

      constructor(value: T, public readonly _shallow: boolean) {
        this._rawValue = _shallow ? value : toRaw(value)
        this._value = _shallow ? value : toReactive(value) // 响应式处理
      }

      get value() {
        trackRefValue(this) // 依赖收集
        return this._value
      }

      set value(newVal) {
        newVal = this._shallow ? newVal : toRaw(newVal)
        if (hasChanged(newVal, this._rawValue)) {
          this._rawValue = newVal
          this._value = this._shallow ? newVal : toReactive(newVal) // 发生改变时重新赋值
          triggerRefValue(this, newVal) // 派发通知,触发依赖,通知更新
        }
      }
    }
trackRefValue

ref依赖收集

  1. 判断ref.dep是否有值,没有的话初始化创建一个dep

  2. 执行trackEffects(ref.dep) 进行依赖收集

  3. 实现

        export function trackRefValue(ref: RefBase<any>) {
          if (isTracking()) {
            ref = toRaw(ref)
            if (!ref.dep) {
              ref.dep = createDep()
            }
            if (__DEV__) {
              trackEffects(ref.dep, {
                target: ref,
                type: TrackOpTypes.GET,
                key: 'value'
              })
            } else {
              trackEffects(ref.dep)
            }
          }
    
triggerRefValue
  1. ref.dep 有值时,执行triggerEffects(ref.dep) 触发依赖,派发通知

  2. 实现

        export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
          ref = toRaw(ref)
          if (ref.dep) {
            if (__DEV__) {
              triggerEffects(ref.dep, {
                target: ref,
                type: TriggerOpTypes.SET,
                key: 'value',
                newValue: newVal
              })
            } else {
              triggerEffects(ref.dep)
            }
          }
        }