Vue3 源码首次瞎看——mount 流程

前言

这是一篇一点都不讲究的文章
记录下时间点吧 9 月 15 号把 Vue3 的 master 分支拉下来了,然后 10 月 15 号开始白嫖,

没看过大佬们的分享,大致扫了一眼,相比 2x 版本最直观的感受是:

  1. (runtime-) core 代码移至 packages 内,
  2. reactivity 的抽离,只看名字我猜这个是响应式的核心代码?抽出来做成框架无关的了?
  3. 在根目录下看到了 rollup 配置文件,虽然 vue2x 较新的版本也是基于 rollup 的打包出来的 esm 文件,因为 rollup-plugin-alias 的加持,没有做到 tree shaking。3x 版本可以通过 esm-bundler 做到 tree shaking。
  4. 一些 2x 版本印象深刻的入口函数搜索不到了,比如 _init、initState、initData,一些响应式相关的关键字段也没了,比如 new Dep、new Watcher 之类的。
  5. 进一步发现,几乎搜不到 class 了,感觉对我这种初学者来说,这种函数式的编程可读性要差一些

然后跑一个 demo,我的 vue-cli 版本够新,可以直接创建 vue3 的项目,跑起来以后,入口居然是 main.js!组件还是那个2x 版本的样子!除了这里不太一样。。。

createApp(App).mount('#app')

抱歉,竟然是 cli 没用好,重新搞一下,都勾上回车!变成了 main.ts,然后看到了 ts 的组件:

import { Options, Vue } from 'vue-class-component';

@Options({
  props: {
    msg: String
  }
})
export default class HelloWorld extends Vue {
  msg!: string
}

之前公司的项目用的是 ts + mobx + vue2.6.11,类似:import { Vue } from 'vue-property-decorator' ,所以感觉不是很唐突(但是我看网文不都是什么 setup 函数?为啥我这还是类 + 装饰器,先不考究了,看源码和业务代码的风格也没啥关系,慢慢尝试之后你会发现 vue3 对 data 函数依旧是兼容的),入口文件因为 cli 钩的多,所以变长了一点,之前太短都没有注意到链式调用

createApp(App).use(store).use(router).mount('#app')

不禁让我想到了头条的面试。。。你给我写个链式调用吧,你给我写个科里化吧,你给我实现一个 array.reduce 吧

初始化流程的源码瞎看

createApp + mount

跑 demo 就是为了拿个入口。。。直接看源码

export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args) // 这个地方大约有 2000 行相关源码

  if (__DEV__) {
    injectNativeTagCheck(app)
  }

  const { mount } = app
  app.mount = (containerOrSelector: Element | string): any => {
    const container = normalizeContainer(containerOrSelector)
    if (!container) return
    const component = app._component
    if (!isFunction(component) && !component.render && !component.template) {
      component.template = container.innerHTML
    }
    // clear content before mounting
    container.innerHTML = ''
    const proxy = mount(container)
    container.removeAttribute('v-cloak')
    container.setAttribute('data-v-app', '')
    return proxy
  }

  return app
}) as CreateAppFunction<Element>

相比 2x 版本的 new Vue 然后 $mount 不同(子组件也会执行 new Vue 的操作),这里的 createApp 是最底层的,只执行一次

追一下 app 的相关代码
ensureRenderer => createRenderer => baseCreateRenderer

// packages/runtime-core/src/renderer.ts
function baseCreateRenderer () {
  ... // render 下面会用到,列一下
  const render: RootRenderFunction = (vnode, container) => {
    if (vnode == null) { // unmount 的逻辑
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else { // mount 逻辑
      patch(container._vnode || null, vnode, container)
    }
    flushPostFlushCbs()
    container._vnode = vnode
  }
  ... // 此处省略约 1800 行代码
  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }
}

然后我们看下 app 的结构

// packages/runtime-core/src/apiCreateApp.ts
function createAppAPI(render: RootRenderFunction, hydrate?: RootHydrateFunction) {
  ... //
  const context = createAppContext()
  ... //
  return function createApp(rootComponent, rootProps = null): App {
    const app: App = { // app 上挂有以下属性,方法内容省略了
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _container: null,
      _context: context,

      version,

      get config() {},
      set config(v) {},

      use(plugin: Plugin, ...options: any[]): App {}, // 安装 plugin
      mixin(mixin: ComponentOptions): App {}, // 往 context 的 mixins 里面添加 mixin
      component(name: string, component?: Component): any {}, // 往 context 的 components 里面添加 component || 获取 component
      directive(name: string, directive?: Directive) {}, // 往 context 的 directives 里面添加 directive || 获取 directive
      mount(rootContainer: HostElement, isHydrate?: boolean): any {}, // 初始化
      unmount() {}, // 销毁:通过 render 传入 null
      provide(key, value) {} // context.provides 上添加键值对
    }
    return app
  }
}

再回过头来看,mount 主要干了啥:

  1. 判断 isMounted,主流程肯定是没 mount
  2. const vnode = createVNode(rootComponent as ConcreteComponent, rootProps),然后引用 context: vnode.appContext = context
  3. 判断入参 isHydrate && createAppAPI 入参 hydrate
    a. 为 true 执行 hydrate(),这在 vue2 里是一个和 vdom patch 相关的函数,在这里看上去也是这么个意思
    if (isHydrate && hydrate) {
        hydrate(vnode as VNode<Node, Element>, rootContainer as any)
    }
    
    b. 为 false 执行 render(vnode, rootContainer),首次加载都是 false,所以会执行这里,render 就是上文在 baseCreateRenderer 中提到的 render: RootRenderFunction 函数,本质就是首次 vdom 的 patch
  4. 修改 isMounted 状态为 true
  5. 为 app 和 rootContainer 相互添加引用,app._container = rootContainer(rootContainer as any).__vue_app__ = app
  6. return vnode.component!.proxy

createComponentInstance + patch

感觉上述内容都比较明朗,但是 mount 逻辑是 APP 级的,那子组件是怎么初始化的?
其实都发生在上述步骤 3 中

vue-next-master/packages/runtime-core/src/renderer.ts的 baseCreateRenderer下,有大致如下的执行顺序。

patch

其中 patch 函数的源码:

function baseCreateRenderer(
    options: RendererOptions<Node, Element>,
    createHydrationFns: typeof createHydrationFunctions
): HydrationRenderer

function baseCreateRenderer(
    options: RendererOptions,
    createHydrationFns?: typeof createHydrationFunctions
): any {
    // ...
    const patch: PatchFn = (
        n1,
        n2,
        container,
        anchor = null,
        parentComponent = null,
        parentSuspense = null,
        isSVG = false,
        optimized = false
    ) => {
        // patching & not same type, unmount old tree
        if (n1 && !isSameVNodeType(n1, n2)) {
            anchor = getNextHostNode(n1)
            unmount(n1, parentComponent, parentSuspense, true)
            n1 = null
        }

        if (n2.patchFlag === PatchFlags.BAIL) {
            optimized = false
            n2.dynamicChildren = null
        }

        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(
                    n1,
                    n2,
                    container,
                    anchor,
                    parentComponent,
                    parentSuspense,
                    isSVG,
                    optimized
                )
                break
            default:
                if (shapeFlag & ShapeFlags.ELEMENT) {
                    processElement(
                        n1,
                        n2,
                        container,
                        anchor,
                        parentComponent,
                        parentSuspense,
                        isSVG,
                        optimized
                    )
                } else if (shapeFlag & ShapeFlags.COMPONENT) {
                    processComponent(
                        n1,
                        n2,
                        container,
                        anchor,
                        parentComponent,
                        parentSuspense,
                        isSVG,
                        optimized
                    )
                } else if (shapeFlag & ShapeFlags.TELEPORT) {
                    ; (type as typeof TeleportImpl).process(
                        n1 as TeleportVNode,
                        n2 as TeleportVNode,
                        container,
                        anchor,
                        parentComponent,
                        parentSuspense,
                        isSVG,
                        optimized,
                        internals
                    )
                } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
                    ; (type as typeof SuspenseImpl).process(
                        n1,
                        n2,
                        container,
                        anchor,
                        parentComponent,
                        parentSuspense,
                        isSVG,
                        optimized,
                        internals
                    )
                } else if (__DEV__) {
                    warn('Invalid VNode type:', type, `(${typeof type})`)
                }
        }

        // set ref
        if (ref != null && parentComponent) {
            setRef(ref, n1 && n1.ref, parentComponent, parentSuspense, n2)
        }
    }
}

而子组件的创建最终落实于 mountComponentcreateComponentInstance 的调用,即创建出对应的组件实例,对应 2x 版本的 VueComponent 的创建

完~ 其实缕一缕感觉套路都差不多,错过了比较多的细节,都追也不太现实,下次准备直接看 reactivity 的代码,了解下核心科技。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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