Vue3源码解读之初始化流程

这篇文章我们来了解下Vue3的初始化流程,总共包括以下几个流程👇:

思维导图

下面我们来看看每个步骤都做了什么,逐个击破🚀

createApp

createApp源码

js 复制代码
// packages/runtime-dom/src/index.ts 

// 这里的 createApp 方法是在写页面时实际调用的方法
export const createApp = ((...args) => {
  // 1、获取渲染器,并执行渲染器的 createApp 方法,创建 app 应用实例
  const app = ensureRenderer().createApp(...args)
  
  // ...
  // 2、扩展 mount 方法
  const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    // 获取根节点,即页面上 id 为 app 的div标签
    const container = normalizeContainer(containerOrSelector)
    
    //...
    
    const component = app._component
    // 获取模板
    if (!isFunction(component) && !component.render && !component.template) {
      // 将根节点下的HTML内容添加到组件的 template 上
      component.template = container.innerHTML
      // ...
    }
    
    const proxy = mount(container, false, container instanceof SVGElement)
    
    // ...
    
    return proxy
  }
  
  return app
}) as CreateAppFunction<Element>

在Vue3中创建Vue应用实例使用的 createApp 方法,调用的就是在 packages/runtime-dom/src/index.ts 中定义的 createApp 函数。

createApp 中主要做了两件事情:

  1. 调用 ensureRenderer 函数获取渲染器,然后执行渲染器的 createApp 方法创建 app 应用实例。
  2. 获取 app 应用实例的 mount 方法,对 mount 方法进行扩展。

createRenderer

createApp源码

js 复制代码
// packages/runtime-core/src/renderer.ts
export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options)
}

createRenderer 函数中,仅仅只是调用了 baseCreateRenderer 函数,传入 options,创建一个渲染器。在baseCreateRenderer 函数中,返回的对象就是渲染器,渲染器中的 createApp 方法,就是创建 app 应用实例最终执行的方法。

createAppAPI

createAppAPI源码

js 复制代码
// packages/runtime-core/src/apiCreateApp.ts

export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {

  // 返回开发者使用的app工厂函数
  return function createApp(rootComponent, rootProps = null) {
    // rootComponent 就是 createApp函数的 args,也就是options

    // ...

    const context = createAppContext()
    const installedPlugins = new Set()

    let isMounted = false

    // 应用程序实例
    const app: App = (context.app = {
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _container: null,
      _context: context,
      _instance: null,

      version,

      get config() {
        return context.config
      },

      set config(v) {
        // ...
      },

      // 加载插件,和vue2不同的是,vue2的插件是全局的,这里只针对一个vue实例
      use(plugin: Plugin, ...options: any[]) {
        // ...
      },

      // 混入
      mixin(mixin: ComponentOptions) {
        // ...
      },

      // 加载组件
      component(name: string, component?: Component): any {
        // ...
      },

      // 指令
      directive(name: string, directive?: Directive) {
        // ...
      },

      // 挂载,核心渲染逻辑
      mount(
        rootContainer: HostElement,
        isHydrate?: boolean,
        isSVG?: boolean
      ): any {
        // ...
      },

      // 卸载
      unmount() {
        // ...
      },

      // 注入
      provide(key, value) {
        // ...
      }
    })

    return app
  }
}

createAppAPI 函数中,返回的工厂函数 createApp 就是开发者在页面中调用的createApp方法,在工厂函数中创建了一个app应用程序实例,并将其添加到context对象的app属性上,最后将其返回出去。

mount

js 复制代码
// packages/runtime-core/src/apiCreateApp.ts

// 挂载,核心渲染逻辑,将 vnode 转换成真实DOM
// 注意:mount方法只会执行一次
mount(
  rootContainer: HostElement,
  isHydrate?: boolean,
  isSVG?: boolean
): any {
  // 判断是否已挂载
  if (!isMounted) {
    // 用于检测当前是否已经有一个 Vue 应用实例挂载在指定的 DOM 元素上。
    // 首先判断当前是否已经挂载了 Vue 应用实例,如果没有挂载,就继续执行下面的代码。如果已经挂载了,就在开发环境下使用 warn 函数输出警告信息,提示开发者需要先卸载之前的应用实例,然后再挂载新的应用实例。
    if (__DEV__ && (rootContainer as any).__vue_app__) {
        warn(
            `There is already an app instance mounted on the host container.\n` +
            ` If you want to mount another app on the same host container,` +
            ` you need to unmount the previous app by calling \`app.unmount()\` first.`
        )
    }
    // 首次挂载,创建根组件对应的vnode,即虚拟DOM
    const vnode = createVNode(
      rootComponent as ConcreteComponent,
      rootProps
    )
    // 应用上下文(app context)是一个全局的对象,用于存储应用实例、全局配置、全局 API 等信息。在应用初始化时,会创建一个应用上下文,并将其传递给组件实例、指令、插件等。
    vnode.appContext = context

    // 开发环境下 热更新
    if (__DEV__) {
      context.reload = () => {
        render(cloneVNode(vnode), rootContainer, isSVG)
      }
    }

    // 转换 VNode 的处理逻辑
    if (isHydrate && hydrate) {
      // ssr 服务端渲染 的 VNode 转换逻辑
      hydrate(vnode as VNode<Node, Element>, rootContainer as any)
    } else {
      // spa 前端渲染 的 VNode 转换逻辑
      render(vnode, rootContainer, isSVG)
    }
    isMounted = true
    app._container = rootContainer
    // 根容器上设置一个特殊标记 __vue_app__,用于判断一个 DOM 元素上是否已经挂载了 Vue 应用实例
    ;(rootContainer as any).__vue_app__ = app

    // 如果是开发环境或者开启了生产环境下的开发工具支持,还会将应用实例的 _instance 属性设置为根 VNode 对应的组件实例,然后调用 devtoolsInitApp 函数,将应用实例注册到开发工具中。这样,开发者就可以在开发工具中查看应用的状态和行为,方便调试和排查问题
    if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
        app._instance = vnode.component
        devtoolsInitApp(app, version)
    }
          
    return getExposeProxy(vnode.component!) || vnode.component!.proxy
  } else if (__DEV__) {
    warn(
    `App has already been mounted.\n` +
        `If you want to remount the same app, move your app creation logic ` +
        `into a factory function and create fresh app instances for each ` +
        `mount - e.g. \`const createMyApp = () => createApp(App)\``
    )
  }
},

mount 方法是 app 应用实例的一个属性,用来挂载节点,它会将根组件下的HTML内容渲染出来,并建立更新机制。

mount 方法只会执行一次,即在app应用实例挂载时执行。它调用createVNode函数创建根组件对应的VNode,然后判断是否是服务端渲染,如果是,则执行外部传入的hydrate方法,将VNode转换成真实DOM;如果不是,则调用外部传入的render方法,将VNode转换成真实DOM,将HTML内容渲染出来。

render

render源码

js 复制代码
// packages/runtime-core/src/renderer.ts

const render: RootRenderFunction = (vnode, container, isSVG) => {
  if (vnode == null) {
    if (container._vnode) {
      // 卸载组件
      unmount(container._vnode, null, null, true)
    }
  } else {
    // 执行patch过程,即 diff 过程
    patch(container._vnode || null, vnode, container, null, null, null, isSVG)
  }
  flushPostFlushCbs()
  container._vnode = vnode
}

mount 方法中,Vue会判断当前的渲染时服务端渲染还是前端渲染,如果是前端渲染,就会执行从外部传入的render方法,将VNode转换成真实DOM,将HTML内容渲染出来。

在 render 方法中,如果vnode不为null,即根组件对应的vnode(即虚拟DOM)存在,则执行 patch 过程,即 Diff 过程。

patch

patch源码

js 复制代码
// packages/runtime-core/src/renderer.ts

const patch: PatchFn = (
  n1, // 旧vnode  初始化时n1 为 null
  n2, // 新vnode  初始化时n2 是根节点
  container, // div#app
  anchor = null, // 定位锚点dom,用于往锚点前插入节点
  parentComponent = null,
  parentSuspense = null,
  isSVG = false,
  slotScopeIds = null,
  optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
  // ...

  const { type, ref, shapeFlag } = n2
  // 这里的 type 是 component,也就是API  createApp 的入参 args
  switch (type) {
    // ...
    default:
      if (shapeFlag & ShapeFlags.ELEMENT) {
       // ...
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
        // 首次初始化时会进入这里,处理组件
        processComponent(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      }
      // ...
  }

  // ...
}

patch 过程,也就是 Diff 过程。patch的过程就是以新的VNode为基准,去更新旧的VNode 。在首次初始化时,会执行 processComponent 方法挂载组件。

processComponent

processComponent源码

js 复制代码
// packages/runtime-core/src/renderer.ts

const processComponent = (
  n1: VNode | null, // 旧vnode  初始化时 n1 为 null
  n2: VNode, // 新vnode  初始化时 n2 是根节点
  container: RendererElement,  // 初始化时是 div#app
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) => {
  // ...
  
  // 旧vnode为null,说明这是首次初始化
  if (n1 == null) {
    if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
      // ...
    } else {
      // 首次初始化时 挂载组件
      mountComponent(
        n2, // 初始化时 n2 就是根节点的vnode
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    }
  } else {
    // 这里是 更新阶段
    //...
  }
}

processComponent 中,判断 n1 是否为null,即旧的VNode是否为null,如果为null,说明是首次初始化,则调用 mountComponent 挂载组件,其中传入 mountComponentn2 就是根节点的VNode

下面我们来看下 mountComponent 挂载组件的实现过程

mountComponent

mountComponent源码

js 复制代码
// packages/runtime-core/src/renderer.ts

const mountComponent: MountComponentFn = (
  initialVNode, // 在初始化时 initialVNode 是根节点的vnode
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  optimized
) => {
  // 在 Vue.js 2.x 中,因为组件实例的创建和挂载是分开进行的,即先创建组件实例,然后将组件实例挂载到指定的 DOM 元素上。而在 Vue.js 3 中,组件实例的创建和挂载是合并在一起的,即在 createComponentInstance 函数中同时创建组件实例和对应的 VNode。为了兼容 Vue.js 2.x,Vue.js 3 中提供了一个 __COMPAT__ 标记,用于判断当前是否处于 Vue.js 2.x 的兼容模式下。
  const compatMountInstance =
    __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
  
  // 1、创建根组件实例
  const instance: ComponentInternalInstance =
    compatMountInstance ||
    (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ))

  // ...

  // 在 <keep-alive> 组件的上下文中,注入渲染器的内部实现,以便在组件缓存和激活时可以调用相应的方法
  if (isKeepAlive(initialVNode)) {
    ;(instance.ctx as KeepAliveContext).renderer = internals
  }

  // 调用 setupComponent 函数,用于解析 props 和 slots,并将解析后的结果存储到组件实例的上下文 ctx 中
  // 以便于在 setup 函数中,可以访问到组件的 props、slots、attrs 等信息
  if (!(__COMPAT__ && compatMountInstance)) {
    // ... 
    // 2、组件实例安装,相当于组件初始化,this._init
    // 属性声明,响应式等等
    setupComponent(instance)

    // ..
  }

  // ...
  
  // 3、设置并运行带有副作用的渲染函数,相当于是 updateComponent + watcher
  setupRenderEffect(
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  )

  // ...
}

mountComponent 的作用就是挂载组件,在 mountComponent 中,首先调用 createComponentInstance 创建根组件实例,然后将创建好的根组件实例传入 setupComponent 中,进行 props 以及 slots 的初始化,最后设置并运行带有副作用的渲染函数 setupRenderEffect

再来看看 setupComponent 中做了什么事情

setupComponent

setupComponent源码

js 复制代码
// packages/runtime-core/src/component.ts

export function setupComponent(
  instance: ComponentInternalInstance,
  isSSR = false
) {
  isInSSRComponentSetup = isSSR

  const { props, children } = instance.vnode
  
  // 是否是状态型组件
  const isStateful = isStatefulComponent(instance)
  // 初始化组件的属性
  initProps(instance, props, isStateful, isSSR)
  // 初始化组件的插槽
  initSlots(instance, children)
  
  // 如果是状态型组件,则为其挂载setup信息
  // 非状态型组件仅用来纯UI展示,不需要挂载状态信息
  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR)
    : undefined
  isInSSRComponentSetup = false
  return setupResult
}

看起来不复杂,setupComponent 用于初始化propsslots 等。首先判断当前根组件实例是否状态型组件,如果是状态型组件,则初始化组件的属性props,并为根组件实例挂载setup信息。

setupStatefulComponent

setupStatefulComponent源码

js 复制代码
// packages/runtime-core/src/component.ts

// 状态型组件生成setup结果并进行信息挂载
function setupStatefulComponent(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  // 组件的 options,也就是 vnode.type
  const Component = instance.type as ComponentOptions

  // ...

  // 0. 创建渲染代理属性访问缓存
  instance.accessCache = Object.create(null)
  // 1. 为组件实例创建渲染代理,同时将代理标记为 raw,
  // 为的是在后续过程中不会被误转化为响应式数据,
  // 渲染代理源对象是组件实例上下文
  instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))

  // ...

  // 2. 调用 setup 函数
  // 这里的setup是开发者调用 createApp 时传入的 setup 函数
  const { setup } = Component
  if (setup) {
    
    // 创建 setup上下文并挂载到组件实例上
    const setupContext = (instance.setupContext =
      setup.length > 1 ? createSetupContext(instance) : null)
   
    // 记录当前正在初始化的组件实例
    setCurrentInstance(instance)
    
    // 执行 setup 前暂停依赖收集
    // PS: 执行setup期间是不允许进行依赖收集的,setup只是为了获取需要为组件提供的状态信息,在它里面不应该有其它非必要的副作用
    // 真正的依赖收集等有较强副作用的操作应该放到 setup挂载之后,以免产生不可预测的问题
    pauseTracking()
    
    // 执行 setup 函数,并获得安装结果信息,setup执行结构就是我们定义的响应式数据、函数、钩子等
    const setupResult = callWithErrorHandling(
      setup, // 开发者调用 createApp 时定义的 setup函数
      instance, // 根组件实例
      ErrorCodes.SETUP_FUNCTION,
      [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
    )
    
    // setup执行完毕后恢复依赖收集
    resetTracking()
    
    // 重置当前根组件实例
    unsetCurrentInstance()

    // 挂载 setup执行的结果
    // 在 SSR 服务端渲染或者 suspense 时 setup 返回的是 promise
    // 因此需要判断 setupResult 是否是 promise,进行不同的操作
    if (isPromise(setupResult)) {
      setupResult.then(unsetCurrentInstance, unsetCurrentInstance)

      if (isSSR) {
        // 在SSR或者suspense时setup返回promise
        // suspense因为有节点fallback,而setup中是正式渲染内容,因此是一个异步resolve的过程
        return setupResult
          .then((resolvedResult: unknown) => {
            handleSetupResult(instance, resolvedResult, isSSR)
          })
          .catch(e => {
            handleError(e, instance, ErrorCodes.SETUP_FUNCTION)
          })
      } 
      // ...
    } else {
      // setupResult 返回的不是 promise
      // 直接将 setup的执行结果挂载到组件实例上
      handleSetupResult(instance, setupResult, isSSR)
    }
  } else {
    // ...
  }
}

setupStatefulComponent 为状态型组件生成setup结果并进行信息挂载。

  1. 它首先会为组件实例创建渲染代理来访问缓存。
  2. 如果开发者创建app应用实例时传入了setup函数,则调用 createSetupContext 创建setup上下文并挂载到组件实例上,然后暂停 effect 的依赖收集,等到执行完setup并拿到其返回的结果setupResult后再继续进行effect的依赖收集。
  3. 最后将setup的执行结果挂载到组件实例上

再来看看我们最常用的 setup 函数

setup

这里的setup是开发者调用 createApp 时传入的 setup 函数。setup函数执行后,其执行结果会被挂载到根组件实例上。

html 复制代码
<script>
  Vue.createApp({
    setup() {
      const state = reactive({})

      return {
        state
      }
    },
  }).mount('#app')
< /script>

handleSetupResult

handleSetupResult 将 setup的执行结果挂载到根组件实例上。

handleSetupResult源码

js 复制代码
// packages/runtime-core/src/component.ts

export function handleSetupResult(
  instance: ComponentInternalInstance,
  setupResult: unknown,
  isSSR: boolean
) {
  
  // setup的执行结果是一个函数
  if (isFunction(setupResult)) {
    // 如果是函数,则说明 setup 函数返回了一个内联渲染函数,需要将其挂载到组件实例的 render 或 ssrRender 属性上,以便在组件渲染时调用。
    if (__SSR__ && (instance.type as ComponentOptions).__ssrInlineRender) {
      // 如果是SSR 服务端渲染,将 setup 执行结果挂载到根组件实例的 ssrRender 属性上
      instance.ssrRender = setupResult 
    } else {
      // SPA 前端渲染,将 setup执行结果挂载到根组件实例的 render 属性上
      instance.render = setupResult as InternalRenderFunction
    }
  } else if (isObject(setupResult)) {

    // ...

    // setup的执行结果是一个Object对象
    // 将setup的执行结果挂载到根组件实例的 setupState 属性上
    instance.setupState = proxyRefs(setupResult)

    // ...
  } 

  //...

  finishComponentSetup(instance, isSSR)
}

handleSetupResult 会将setup的执行结果进行不同的处理。如果setup的执行结果是一个函数,则将其挂载到根组件实例的 render 属性上。如果setup的执行结果是一个Object对象,则将其挂载到根组件实例的 setupState 属性上。

finishComponentSetup

finishComponentSetup源码

js 复制代码
// packages/runtime-core/src/component.ts

export function finishComponentSetup(
  instance: ComponentInternalInstance,
  isSSR: boolean,
  skipOptions?: boolean
) {
  
  // 组件的 options,也就是 vnode.type,也就是API: createApp 的参数
  const Component = instance.type as ComponentOptions

  // ...

  // 首先判断根组件是否已经存在渲染函数。如果不存在,则需要对根组件的模板进行编译
  if (!instance.render) {
    // 如果当前不处于 SSR 服务端渲染模式,并且存在编译器 compile,并且根组件选项中没有指定 render 属性,则将根组件的模板编译为渲染函数,并将其挂载到 Component.render 属性上。
    if (!isSSR && compile && !Component.render) {
      // 获取根组件的 innerHTML 内容
      const template =
        (__COMPAT__ &&
          instance.vnode.props &&
          instance.vnode.props['inline-template']) ||
        Component.template
      if (template) {

        // ...
        const { isCustomElement, compilerOptions } = instance.appContext.config
        const { delimiters, compilerOptions: componentCompilerOptions } =
          Component
        // 编译的 options
        const finalCompilerOptions: CompilerOptions = extend(
          extend(
            {
              isCustomElement,
              delimiters
            },
            compilerOptions
          ),
          componentCompilerOptions
        )
        if (__COMPAT__) {
          // 在编译模板时,需要根据组件选项和全局配置中的编译选项进行合并,并调用编译器 compile 函数进行编译
          finalCompilerOptions.compatConfig = Object.create(globalCompatConfig)
          if (Component.compatConfig) {
            extend(finalCompilerOptions.compatConfig, Component.compatConfig)
          }
        }
        
        // 编译 template,即编译 根组件下的innerHTML内容
        // 并把编译后的内容挂载到 Component 的render 属性上
        // 开发者调用createApp 时传入的参数中是没有render属性,
        // 我们再打印出createApp 时的参数看到的render属性就是在这里挂载上去的
        Component.render = compile(template, finalCompilerOptions)

        // ...
      }
    }

    // 将编译结果挂载到根组件实例上
    instance.render = (Component.render || NOOP) as InternalRenderFunction

    // ...
  }

  // ...

}

根组件的模板或渲染函数可以通过组件选项的 templaterender 属性来指定。如果指定了 template 属性,则需要将其编译为渲染函数,以便在组件渲染时调用。如果指定了 render 属性,则可以直接使用该渲染函数

如果开发者创建app应用实例时没有传入setup函数,则执行finishComponentSetup来挂载编译后的内容。首先获取根组件的 innerHTML 内容,对其进行编译,然后把编译后的内容挂载到 Componentrender 属性上,再将编译结果挂载到根组件实例的render属性上。

setupRenderEffect

setupRenderEffect 函数的作用运行带有副作用的render函数,其实现了 instance.update 方法,这个方法其实就是一个effect。

setupRenderEffect源码

js 复制代码
// packages/runtime-core/src/renderer.ts

const setupRenderEffect: SetupRenderEffectFn = (
  instance,
  initialVNode,
  container,
  anchor,
  parentSuspense,
  isSVG,
  optimized
) => {
  // 创建更新函数
  const componentUpdateFn = () => {
    // 如果是首次挂载 isMounted 为 false,因此这里是初始化流程
    if (!instance.isMounted) {

      // 首次挂载阶段
      
      // 首先会执行 组件实例的 render 函数获取VNode,
      // 然后进入patch过程,将VNode转换成真实DOM,渲染到界面上

      let vnodeHook: VNodeHook | null | undefined
      const { el, props } = initialVNode
      const { bm, m, parent } = instance
      const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)

      toggleRecurse(instance, false)
      // beforeMount hook
      if (bm) {
        invokeArrayFns(bm)
      }
      // onVnodeBeforeMount
      if (
        !isAsyncWrapperVNode &&
        (vnodeHook = props && props.onVnodeBeforeMount)
      ) {
        invokeVNodeHook(vnodeHook, parent, initialVNode)
      }
      if (
        __COMPAT__ &&
        isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
      ) {
        instance.emit('hook:beforeMount')
      }
      toggleRecurse(instance, true)

      if (el && hydrateNode) {
        
        // 服务端渲染
        
        // 在水合操作中,首先执行根组件的渲染函数,获取根组件的 VNode。然后,调用 hydrateNode 函数进行水合操作,将服务器端渲染的 HTML 内容与客户端渲染的 VNode 进行比对,并尽可能地复用已有的 DOM 元素。
        const hydrateSubTree = () => {
          // ...
          
          // 开始渲染根组件,执行当前组件实例的 render 函数获取VNode
          instance.subTree = renderComponentRoot(instance)

          // ... 

          hydrateNode!(
            el as Node,
            instance.subTree,
            instance,
            parentSuspense,
            null
          )

          // ...
        }

        // 首先判断初始 VNode 是否是异步组件的占位符 VNode
        if (isAsyncWrapperVNode) {
          ;(initialVNode.type as ComponentOptions).__asyncLoader!().then(
            //如果是,则需要等待异步组件加载完成后再进行水合操作。在等待异步组件加载完成时,会调用异步组件的加载函数 __asyncLoader,并在加载完成后执行水合操作。
            () => !instance.isUnmounted && hydrateSubTree()
          )
        } else {
          // 如果初始 VNode 不是异步组件的占位符 VNode,则直接执行水合操作。
          hydrateSubTree()
        }
      } else {

        // ...
        
        // 前端渲染
        
        // 开始渲染根组件,执行当前组件实例的 render 函数获取VNode
        // 首先获取 组件vnode,其实就是调用组件的 render
        // 这次调用触发了依赖收集
        
        // 生成子树结构,创建组件的vnode
        // 执行组件内部的render函数 (手写或template编译生成) 生成渲染vnode,渲染vnode就是组件实际要渲染出来的
        // 内容对应的vnode,并将渲染vnode存储到组件实例上
        // 注意:执行render函数会访问组件实例上的响应式数据,从而触发依赖收集,当前定义的renderEffect会被收集到依赖仓库
        // 当后续发生数据变化时,renderEffect则会被派发,触发re-render
        const subTree = (instance.subTree = renderComponentRoot(instance))

        // ...

        // 向下递归更新
        // 首次渲染是完整递归创建
        // 进入 Diff 过程,将子树渲染到container中
        patch(
          null,
          subTree,
          container,
          anchor,
          instance,
          parentSuspense,
          isSVG
        )

        // ... 

        initialVNode.el = subTree.el
      }
      // mounted hook
      // 执行mount(挂载之后)钩子函数
      if (m) {
        queuePostRenderEffect(m, parentSuspense)
      }
      // onVnodeMounted
      if (
        !isAsyncWrapperVNode &&
        (vnodeHook = props && props.onVnodeMounted)
      ) {
        const scopedInitialVNode = initialVNode
        queuePostRenderEffect(
          () => invokeVNodeHook(vnodeHook!, parent, scopedInitialVNode),
          parentSuspense
        )
      }
      if (
        __COMPAT__ &&
        isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
      ) {
        queuePostRenderEffect(
          () => instance.emit('hook:mounted'),
          parentSuspense
        )
      }

      // 判断初始 VNode 是否是 keep-alive 组件的 VNode。如果是,则需要在组件渲染后执行 activated 钩子函数。
      if (initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
        // 在执行 activated 钩子函数时,需要注意钩子函数可能是由子组件的 keep-alive 传递而来的。因此,需要在组件的首次渲染后再执行 activated 钩子函数。
        instance.a && queuePostRenderEffect(instance.a, parentSuspense)
        // 如果开启了 Vue.js 2.x 的兼容模式,并且当前组件实例已经被标记为具有兼容性问题,还会触发 hook:activated 事件,以兼容 Vue.js 2.x 中的 activated 钩子函数。
        if (
          __COMPAT__ &&
          isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
        ) {
          queuePostRenderEffect(
            () => instance.emit('hook:activated'),
            parentSuspense
          )
        }
      }
      instance.isMounted = true

      // ...

    } else {

      // updateComponent

      // 更新阶段

      // ...
     }
  }

  // 创建 副作用,类似于 react 的 useEffect hooks
  // 副作用:如果 componentUpdateFn 执行的过程中有响应式数据发生变化,则按照
  // 参数2 () => queueJob(instance.update) 的方式执行参数1
  const effect = (instance.effect = new ReactiveEffect(
    componentUpdateFn, // 在响应式数据发生变化时 componentUpdateFn 会再次执行
    () => queueJob(instance.update), // 执行instance.update 就是执行 componentUpdateFn
    instance.scope // track it in component's effect scope
  ))

  // 创建 update,这个update就是 effect 的run方法,
  // 其实就是执行实例化 ReactiveEffect 时传入的 componentUpdateFn
  const update = (instance.update = effect.run.bind(effect) as SchedulerJob)
  update.id = instance.uid

  // ...

  // 执行更新
  update()
}

最后,我们来总结下整个过程:

  1. setupRenderEffect函数中,首先创建了一个componentUpdateFn函数,这个函数用来执行当前组件实例的render函数获取VNode,然后进入 Diff 过程,将子树渲染到container中。

  2. 在SSR 服务端渲染时,需要将服务器端渲染的 HTML 内容与客户端渲染的 VNode 进行比对,以便在客户端渲染时尽可能地复用已有的 DOM 元素,提高渲染性能。这个过程称为水合(hydration)。

  3. 异步组件可以通过 defineAsyncComponent 函数来定义。当异步组件被渲染时,会先渲染一个占位符 VNode,然后在异步组件加载完成后再替换为真正的组件 VNode

  4. keep-alive 组件的 activated 钩子函数实现原理:

    • 首先判断初始 VNode 是否是 keep-alive 组件的 VNode。如果是,则需要在组件渲染后执行 activated 钩子函数。
    • 在执行 activated 钩子函数时,需要注意钩子函数可能是由子组件的 keep-alive 传递而来的。因此,需要在组件的首次渲染后再执行 activated 钩子函数。
    • 如果开启了 Vue.js 2.x 的兼容模式,并且当前组件实例已经被标记为具有兼容性问题,还会触发 hook:activated 事件,以兼容 Vue.js 2.x 中的 activated 钩子函数。
  5. 创建完更新函数componentUpdateFn后,将componentUpdateFn传入 new ReactiveEffect() 中创建一个副作用。如果 componentUpdateFn 在执行的过程中有响应式数据发生了变化,则按照参数2: () => queueJob(instance.update) 的方式执行参数1。

  6. 接下来执行effect.run 方法创建一个 update,这个update其实就是 effectrun 方法,也就是执行实例化 ReactiveEffect 时传入的 componentUpdateFn

  7. 最后是执行这个update,其实就是执行componentUpdateFn,通过执行componentUpdateFn,来完成更新操作。

相关推荐
还是大剑师兰特25 分钟前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解26 分钟前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~32 分钟前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding37 分钟前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT41 分钟前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓42 分钟前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶213642 分钟前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web
发现你走远了42 分钟前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js
Mr.咕咕44 分钟前
Django 搭建数据管理web——商品管理
前端·python·django
张张打怪兽1 小时前
css-50 Projects in 50 Days(3)
前端·css