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

相比 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, 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,
    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 的代码,了解下核心科技。

你可能感兴趣的:(Vue3 源码首次瞎看——mount 流程)