Vue3组件初始化流程(一): 从createApp到mountComponent [Vue3源码系列_xiaolu]

19 篇文章 9 订阅

前言

前不久,Vue3已经成为正式版了,因此打算将Vue3源码分析输出为博客,顺便巩固一下知识

对于Vue3中的代码,在博客中,我会去去除掉一些有关dev,ssr的代码,这些代码不影响我们分析流程。

对于分析的方法,我都会在之前给出放名以及路径,方便大家查看。

在每一部分之后,我会通过链接给出这一部分的注释,大家可以点击链接去查看当前部分的注释。

好了,让我们开始搭建环境吧。

分析环境搭建

开始搭建分析环境

1.获取源码

git clone一份Vue3源码到本地

git clone https://github.com/vuejs/core.git

clone之后的目录结构如下

2.安装pnpm

因为Vue3使用的包管理工具是pnpm,因此先全局安装pnpm

npm install -g pnpm

3.修改package.json

进入到package.json文件中,修改一下scripts的dev,加上–sourcemap,以便之后断点调试

"dev": "node scripts/dev.js --sourcemap",

注: 可以删除掉devDependencies中的puppeteer,安装依赖时,这个依赖太大了,经常卡着

4.安装依赖

pnpm install

5.dev打包

pnpm run dev

运行打包之后,去看看packages/vue/dist文件夹下是否有两个文件,如图

在这里插入图片描述

如果有的话,就代表前面的步骤成功了,ctrl + c 结束打包。

进入下一步,找入口

怎么找入口

上次vue2中我是通过打包的路径寻找入口文件的,这次打算使用断点调试的方法去寻找。

在哪打断点

找到packages/vue/expamles/composition中的todomvc.html,如图

在这里插入图片描述

ctrl+alt+o用浏览器打开todomvc.html (vscode的open插件)

打开F12,source,找到todomvc.html文件,找到createApp方法,在这里打上断点,如图

在这里插入图片描述

运行断点

刷新页面,让我们来点击这个按钮,如图

在这里插入图片描述

点击之后,进入到下一步,也就是说todomvc里面的createApp方法进入到了这里,

现在要找到当前文件的位置,右键点击当前文件的选项卡,有reveal in sidebar,此时就能看到当前文件的目录了,如图

在这里插入图片描述

此时文件路径为: packages/runtime-dom/src/index.ts

这就是我们要去往的路径

createApp

createApp方法 路径: core\packages\runtime-dom\src\index.ts

export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)
  // 下面先不看 
}) as CreateAppFunction<Element>

createApp方法做了什么?

  1. 方法内部调用了ensureRenderer方法
  2. 使用ensureRenderer方法调用后的返回值去调用createApp方法

从断点给出的路径,我们成功找到了createApp方法,方法内部却是调用了ensureRenderer方法,并用其返回值再调用createApp

因此我们要去找ensureRenderer方法(找的过程我就不说了,方法的路径我都会给出的)

ensureRenderer

ensureRenderer方法 路径: core\packages\runtime-dom\src\index.ts

function ensureRenderer() {
  return (
    renderer ||
    (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
  )
}

ensureRenderer方法做了什么?

  1. 返回一个renderer
  2. renderer由createRenderer方法创建

ensureRenderer方法内部返回一个renderer(渲染器),但我们这是首次执行,renderer肯定是不存在的,因此会调用后面的createRenderer方法来创建renderer,所以接着看createRenderer方法的实现

createRenderer

createRenderer方法 路径: core\packages\runtime-core\src\renderer.ts

export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options)
}

createRenderer方法做了什么?

  1. 返回baseCreateRenderer方法调用后的返回值

好家伙,createRenderer内部又返回了一个baseCreateRenderer方法调用之后的返回值,行,那继续看baseCreateRenderer方法的实现

baseCreateRenderer

baseCreateRenderer方法 路径: core\packages\runtime-core\src\renderer.ts

  // 前面的2000行实现先忽略,先看返回值
  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }

baseCreateRenderer方法做了什么?

  1. 返回了一个对象,此对象就是渲染器对象,并且拥有三个方法: render、hydrate、createApp: createAppAPI()

这个baseCreateRenderer函数,2200行代码,极其恐怖哦,先忽略内部实现,让我们看看这个函数的返回值

返回值是一个包含render,hydrate,createApp三个方法的渲染器对象(注意: createApp的值是createAppAPI执行后返回的函数)

这个渲染器对象会作为createRenderer的返回值赋值给ensureRenderer内部的renderer

而renderer对象作为ensureRenderer方法调用后的返回值回到createApp内部,此时createApp内部如下

export const createApp = ((...args) => {
  // renderer是ensureRenderer方法的返回值
  const app = renderer.createApp(...args)
}) as CreateAppFunction<Element>

所以,此时createApp内部是通过renderer渲染器调用createApp方法

我们知道渲染器对象上的createApp方法的值是createAppAPI方法调用后的返回值,因此来看createAppAPI的实现

createAppAPI

createAppAPI方法 路径: core\packages\runtime-core\src\apiCreateApp.ts

export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    // 忽略内部实现
  }
}

createAppAPI方法做了什么?

可以看到,createAppAPI内部返回了一个createApp方法

也就是说baseCreateRenderer返回的渲染器对象中的createApp方法就是调用这createAppAPI方法返回的createApp方法

结合上一步的renderer.createApp(…args)来看,renderer的createApp方法其实就是调用createAppAPI方法返回的createApp方法

因此可以得出,todomvc里的createApp方法其实就是执行当前createAppAPI内部的createApp方法

文字描述可能不太好理解,可以结合代码和图和注释(通过github commit的diff)来看 如图

在这里插入图片描述

知道了实际调用的是哪个createApp之后,那我们来看这个返回的方法的实现

createAppAPI返回的createApp

createAppAPI方法内部返回的方法(其实也就是我们调用createApp) 路径: core\packages\runtime-core\src\apiCreateApp.ts

function createApp(rootComponent, rootProps = null) {
    if (rootProps != null && !isObject(rootProps)) {
      __DEV__ && warn(`root props passed to app.mount() must be an object.`)
      rootProps = null
    }

    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() {},

      set config(v) {},

      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) {}
    })

    if (__COMPAT__) {
      installAppCompatProperties(app, context, render)
    }

    return app
  }

createApp方法做了什么?

  1. 初始化app实例,上面有一些属性和方法
  2. 返回app实例

可以看到,此方法就是初始化一个app实例并返回,app上带有的这几个方法,是不是在vue3中经常使用的?

use,mixin,component,directive,mount,provide以及一些属性

到此,createAppAPI的createApp方法已经执行完毕,返回了一个app实例

但是实际调用的createApp还没执行完,所以接着看后面的操作

mount

继续看之前我们进入的createApp内部

createApp方法内部 路径: core\packages\runtime-dom\src\index.ts

const app = ensureRenderer().createApp(...args)

const { mount } = app

app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {}

return app

在执行完createApp之后还做了什么?

省去了一些dev和实现,能看到在我们经过上面的返回的渲染器执行createApp之后,还执行了相关操作

  1. 解构出mount方法,以便添加副作用
  2. 对app.mount添加副作用,并执行解构出的mount
  3. 返回app实例

对1、2两步不懂得可以看我的注释 解构mount,添加副作用并执行

此时,用户调用的createApp方法执行完毕,得到一个app实例,此实例的mount方法被添加了副作用。

那么用户在调用createApp后会做什么?通常都是链式调用mount方法

createApp().mount()

因此我们来看mount方法,先不管副作用

mount执行

在第二步里面执行了mount方法,因此我们来看mount方法

mount方法 路径: core\packages\runtime-core\src\apiCreateApp.ts

mount(
  rootContainer: HostElement,
  isHydrate?: boolean,
  isSVG?: boolean
): any {
  if (!isMounted) {
    const vnode = createVNode(
      rootComponent as ConcreteComponent,
      rootProps
    )
    // store app context on the root VNode.
    // this will be set on the root instance on initial mount.
    vnode.appContext = context
    // HMR root reload
    if (__DEV__) {
      context.reload = () => {
        render(cloneVNode(vnode), rootContainer, isSVG)
      }
    }  
    if (isHydrate && hydrate) {
      hydrate(vnode as VNode<Node, Element>, rootContainer as any)
    } else {
      render(vnode, rootContainer, isSVG)
    }
    isMounted = true
    app._container = rootContainer
    // for devtools and telemetry
    ;(rootContainer as any).__vue_app__ = app
    if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
      app._instance = vnode.component
      devtoolsInitApp(app, version)
    }
    //todo To: getExposeProxy
    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方法做了什么?

  1. 首先判断isMounted变量,在首次挂载时,isMounted为false,因此走if分支
  2. 通过createVnode方法创建vnode
  3. 通过render方法将vnode转换为dom并挂载 (这里的render方法是baseCreateRenderer返回的render方法)
  4. 将isMounted变量置为true,下次再进来就走else分支报错了
  5. 返回vnode.component.proxy这些

可以看到最主要的步骤就是通过render方法将vnode进行挂载,因此来看render方法的实现

这一部分的注释在: mount

render

这里的render方法是baseCreateRenderer中的render方法

render方法 路径: core\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(container._vnode || null, vnode, container, null, null, null, isSVG)
  }
  flushPostFlushCbs()
  container._vnode = vnode
}

render方法做了什么?

  1. 判断vnode(新的虚拟节点)是否为空
  2. 如果vnode为空,并且container._vnode(老的虚拟节点)存在,代表要执行卸载操作,因此执行unmout方法
  3. 如果vnode不为空,调用patch方法

我们这次分析,是组件初始化挂载,所以vnode肯定是有值的,因此会走else分支,执行patch方法,继续看patch方法的实现

这一部分的注释在:render

patch

patch方法 路径: core\packages\runtime-core\src\renderer.ts

const patch: PatchFn = (
  n1,
  n2,
  container,
  anchor = null,
  parentComponent = null,
  parentSuspense = null,
  isSVG = false,
  slotScopeIds = null,
  optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
  // 上面的忽略,先看大体功能
  const { type, ref, shapeFlag } = n2
  switch (type) {
    case Text:
      processText(n1, n2, container, anchor)
      break
    case Comment:
      processCommentNode(n1, n2, container, anchor)
      break
    case Static:
      if (n1 == null) {
        mountStaticNode(n2, container, anchor, isSVG)
      } else if (__DEV__) {
        patchStaticNode(n1, n2, container, isSVG)
      }
      break
    case Fragment:
      processFragment()
      break
    default:
      if (shapeFlag & ShapeFlags.ELEMENT) {
        processElement()
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
        processComponent()
      } else if (shapeFlag & ShapeFlags.TELEPORT) {
        ;(type as typeof TeleportImpl).process()
      } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
        ;(type as typeof SuspenseImpl).process()
      } else if (__DEV__) {
        warn('Invalid VNode type:', type, `(${typeof type})`)
      }
  }
  // 后面忽略
}

patch方法做了什么?

  1. 判断n2的类型
  2. 根据类型执行相应的process方法

忽略一些其他处理,让我们来看主要功能,根据n2的类型来判断(n2就是新的vnode)

根据n2类型来执行相应的process+Type方法

因为此次分析,我们是组件初始化挂载,因此是component类型,所以会执行processComponent(),所以来看看processComponent实现

这一部分的注释在这 patch

processComponent

processComponent方法 路径: core\packages\runtime-core\src\renderer.ts

const processComponent = (
  n1: VNode | null,
  n2: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) => {
  n2.slotScopeIds = slotScopeIds
  if (n1 == null) {
    if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
      ;(parentComponent!.ctx as KeepAliveContext).activate(
        n2,
        container,
        anchor,
        isSVG,
        optimized
      )
    } else {
      mountComponent(
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    }
  } else {
    updateComponent(n1, n2, optimized)
  }
}

processComponent方法做了什么?

  1. 判断n1(老的虚拟节点)是否为空
  2. 如果n1为空,并且shapeFlags是COMPONENT_KEPT_ALIVE类型(补丁,涉及位运算,之后会说),会执行activate方法
  3. 如果n1为空,shapeFlags不是COMPONENT_KEPT_ALIVE类型,执行mountComponent方法
  4. 如果n1不为空,执行updateComponent方法

首先判断n1,如果n1是null,然后判断是否是keepAlive组件,如果是keepAlive组件,用activate将组件从虚拟容器中移出来

如果不是keepAlive组件,就执行mountComponent进行组件挂载

如果n1不为null,那么就是更新,因此会调用updateComponent

这次分析是组件初始化,所以n1为null,并且不是keepAlive组件,因此调用mountComponent进行组件挂载,所以来看用mountComponent方法的实现

这一步的注释: processCompoent

mountComponet

mountComponent方法 路径: core\packages\runtime-core\src\renderer.ts

const mountComponent: MountComponentFn = (
  initialVNode,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  optimized
) => {
  // 2.x compat may pre-create the component instance before actually
  // mounting
  const compatMountInstance =
    __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
  const instance: ComponentInternalInstance =
    compatMountInstance ||
    (initialVNode.component = createComponentInstance())
    
  // resolve props and slots for setup context
  if (!(__COMPAT__ && compatMountInstance)) {
    if (__DEV__) {
      startMeasure(instance, `init`)
    }
    setupComponent(instance)
    if (__DEV__) {
      endMeasure(instance, `init`)
    }
  }

  setupRenderEffect(
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  )
}

mountComponent方法做了什么?

  1. createComponentInstance
  2. setupCompoent
  3. setupRenderEffect

可以看到把一些其他处理去掉之后,mountComponent调用了三个方法

这三个方法及其重要,因此将其留在下一章中讲解

这一部分的注释在: mountComponent

总结

这一章做了什么?

  1. 获取源码,通过在createApp方法打断点调试,找到ensureRenderer方法
  2. 经过一系列复杂的操作,得知调用的createApp实际是调用createAppAPI方法返回的createApp方法
  3. 调用完createApp方法后,解构出mount方法,对其添加副作用,并执行mount方法
  4. mount方法内部调用了render方法去挂载
  5. render方法内部调用了patch方法
  6. patch方法根据类型执行相应操作,这一次执行了processComponent方法
  7. processComponent根据n1是否存在,去执行相应操作,这一次执行mountComponent方法进行挂载
  8. mountComponent方法调用了三个重要的方法,这三个方法下一期会讲解(todo)

这篇留的todo 如图
在这里插入图片描述

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值