深入 Vue3 源码,学习初始化流程

搭建调试环境

为了弄清楚 Vue3 的初始化,建议先克隆 Vue3 到本地。

git clone https://github.com/vuejs/vue-next.git

安装依赖

npm install

修改 package.json,将 dev 命令加上 --sourcemap 方便调试,并运行 npm run dev

// package.json
...
"scripts": {
  "dev": "node scripts/dev.js --sourcemap",
  ...
}
...

在 packages/vue 目录下增加 index.html,内容如下

<!-- index.html -->
<div id="app">
  {{ count }}
</div>
<script src="./dist/vue.global.js"></script>
<script>
  Vue.createApp({
    setup() {
      const count = Vue.ref(0);
      return { count };
    }
  }).mount('#app');
</script>

在浏览器打开 index.html,程序正常运行则可以开始进行下一步调试。

进行调试

假如在下面的流程中迷失了方向,建议先看一下结尾的总结,再回过头来看这一段。

调试

在 createApp 的位置打上断点,然后刷新页面进断点,开始调试。

具体大家可以自行调试,我在这里就大概描述一下初始化流程。

createApp

进入 createApp 内部,会跳转到 packages/runtime-dom/src/index.ts 的 createApp,执行完成返回 app 实例。

// packages/runtime-dom/src/index.ts
export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)
  ...
  return app
}) as CreateAppFunction<Element>

接下来继续看 ensureRenderer 的实现。

// packages/runtime-dom/src/index.ts
function ensureRenderer() {
  return (
    renderer ||
    (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
  )
}

这里的 renderer 是个单例,初始时会调用 createRenderer 创建,再继续深入。

来到 packages/runtime-core/src/renderer.ts,可以看到 createRenderer 又会调用 baseCreateRenderer。

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

baseCreateRenderer 的实现有 2000 行,我们只需要关注几个关键点就可以了。

image

其返回值也就是 ensureRenderer() 的返回值

// packages/runtime-core/src/renderer.ts
function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
    // 此处省略 2000 行
  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }
}

接下来回到开始的位置,在执行完 ensureRenderer() 后会接着执行 createApp,这个 createApp 就是上一步返回的 createApp,再接着看看做了哪些工作。

// packages/runtime-dom/src/index.ts
export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)
  ...
  return app
}) as CreateAppFunction<Element>

ensureRenderer 返回的 createApp 由 createAppAPI 实现,接下来再看看 createAppAPI 是如何实现的吧。

// packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    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) {}
    })
    return app
  }
}

可以看到 createAppAPI 会返回一个 createApp 函数,也就是我们调用 createApp,当 createApp 执行完之后会返回 app 实例。app 实例还会有 use、mixin、component、directive 等方法,可以为全局 app 添加一些扩展。

案例如下,传入的第一个参数为上一步的 rootComponent 也就是根组件。

// index.html
const app = Vue.createApp({})
    .use(xxx)
    .component(xxx)
    .mount(xxx)

mount

createApp 创建 app 实例后,要渲染到页面上还需要调用 mount。接下来看看 mount 又做了什么工作。还是在 createAppAPI 内部

// packages/runtime-core/src/apiCreateApp.ts
mount(
  rootContainer: HostElement,
  isHydrate?: boolean,
  isSVG?: boolean
): any {
  if (!isMounted) {
    const vnode = createVNode(
      rootComponent as ConcreteComponent,
      rootProps
    )
        ...
    if (isHydrate && hydrate) {
      hydrate(vnode as VNode<Node, Element>, rootContainer as any)
    } else {
      render(vnode, rootContainer, isSVG)
    }
    isMounted = true
    app._container = rootContainer
    ...
    return vnode.component!.proxy
  } else if (__DEV__) {
    // 开发环境警告提醒,app 不可以重复挂载
  }
}

最终会执行 render(vnode, rootContainer, isSVG) 这一行代码,接下来看看调用 createAppAPI 时传入的 renderer。

回到 baseCreateRenderer 中,可以看到在 return 时调用 createAppAPI 传入的 renderer。

// packages/runtime-core/src/renderer.ts
function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
    // 此处省略 2000 行
  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }
}

其中 renderer 在 baseCreateRenderer 中定义了

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
}

在 index.html 的例子中,第一次执行 render 时 vnode 是由 rootComponent 创建出来,rootComponent 则是 createApp 时传入的对象。

container 为 app 容器,也就是 id 为 app 的 div , container._vnodeundefined

所以最终会进入 patch 。

patch 的逻辑同样位于 baseCreateRenderer 中。代码太长了,这里就讲一下思路。在 patch 中会判断 vnode 的 type 或 shapeFlag 执行对应的操作。

image

因为第一次 patch 时,vnode 是一个组件,会进入 ShapeFlags.COMPONENT 的判断内,执行 processComponent进行组件的处理。

然后会触发 mountComponent 挂载组件,从而触发 setupComponent(instance) 初始化组件的 props、slots、setup 等将需要 proxy 代理的数据做好准备,以及将 template 进行编译为 render。

// packages/runtime-core/src/renderer.ts
const mountComponent: MountComponentFn = (
  initialVNode,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  optimized
) => {
  // 2.x compat may pre-creaate the component instance before actually
  // mounting
  const compatMountInstance =
        __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
  const instance: ComponentInternalInstance =
        compatMountInstance ||
        (initialVNode.component = createComponentInstance(
          initialVNode,
          parentComponent,
          parentSuspense
        ))
    ...
  // resolve props and slots for setup context
  if (!(__COMPAT__ && compatMountInstance)) {
    ...
    setupComponent(instance)
    ...
  }
  ...
  setupRenderEffect(
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  )
  ...
}

紧接着会执行 setupRenderEffect,该方法会将渲染函数封装成一个副作用,当依赖的响应式数据发生变化时,会自动重新执行。

重点关注 componentUpdateFn,代码太长了,这里也简单讲下吧,第一次会执行 instance.isMounted 为 undefined,则会进入创建流程,其中会执行 const subTree = (instance.subTree = renderComponentRoot(instance)) 创建子树,然后通过 patch 递归创建子节点,结束后 instance.isMounted = true

一旦依赖发生变化,componentUpdateFn会被重新执行,instance.isMounted 为 true,则会尽心更新的处理,具体就不再展开了。

至于为什么依赖发生变化 componentUpdateFn会被重新执行,这个我们留在下一篇文章中介绍,记得关注我。

总结

  1. 在 index.html 调用 createApp 时会先经过ensureRendererbaseCreateRenderer 生成下面的对象
// baseCreateRenderer 返回值
return {
  render,
  hydrate,
  createApp: createAppAPI(render, hydrate)
}
  1. 继续调用 baseCreateRenderer 返回的 createApp,这里的 createApp 实际上调用的是 createAppAPI 返回的函数。

createAppAPI执行完成返回 app 实例。

  1. index.html 在创建好 app 后接着调用 mount 进行挂载,mount 的实现在 createAppAPI 内部。

mount 执行时会调用 render函数,该 renderbaseCreateRenderer 传入。

  1. render 则开始 patch 进行渲染,patch 内部会进行递归渲染子节点。

以上就是 Vue3 的 createApp 和 mount 的大致流程。至于第一步为什么需要经过 ensureRendererbaseCreateRenderer

baseCreateRenderer 主要是平台无关的逻辑处理,存放在 runtime-core 中。

patch 的时候需要操作 dom,则会调用外部传入的方法进行操作,这样就可以更方便实现跨端。

ensureRenderer 存放在 runtime-dom 中,主要为 baseCreateRenderer 提供一系列 dom 操作的函数。

假如我们要自定义渲染器,那么只需要实现ensureRenderer 即可。而不是像 Vue2 需要 fork 一份,大大提高了 Vue3 的应用范围。


好了,这篇文章就水到这里吧。如有错误的地方,希望还能在评论区指出,感谢!

下篇文章将解析 Vue3 的响应式原理,如果有兴趣的话别忘了关注我呀,我们一起学习、进步。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容