Vue3源码阅读

657 阅读6分钟

Vue3源码

印入你眼帘的是Vue3源码中关于vnode渲染和响应式的部分实现,技术有限,求各位老板们别喷我哈🐶。

目前Vue3的整体生态进入了一个相对稳定的阶段,本着跳出舒适圈,拥抱新世界的原则,公司新开的几个项目都选择使用了Vue3。但是对于Vue2.x版本,本着不抛弃不放弃的原则,老项目还是会坚持一段时间的。经常会听到同事在写新项目时对Vue3或吐槽或赞美,自己也不甘心只当一个API调用工程师,那么就在开发中学习如何使用,在内卷里学习一哈源码吧。

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文件夹下的例子。
  3. 阅读源码时,如果需要source map模式。可以在package.json文件的script里面加上 -- sourcemap

阅读步骤

应用初始化

// 在 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()
    }