Vue3源码解读之runtime(上)

前言

上一篇文章,我们提到packages中核心的源码主要分为三部分,接下来我们就开始阅读runtime部分的代码

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

接下来我们就以入口文件中的这行代码开始来一步步深入

初始化

上一篇文章中我们提到vue主入口文件中,引入导出了runtime-domcompiler,而createApp就是来自runtime-dom

// packages/runtime-dom/src/index.ts

export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)

  if (__DEV__) {
    injectNativeTagCheck(app) // 在dev环境下,注册一个方法isNativeTag,挂载到app.config下面
  }

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

  return app
}) as CreateAppFunction<Element>

在该函数内部中通过调用ensureRenderer()createApp(...args)创建了app实例并把实例返回出去,因此我们可以在app实例中安装插件,设置全局指令等等。这其中又是怎么实现的呢?

创建renderer

ensureRenderer()函数的用途是什么呢?

// packages/runtime-dom/src/index.ts

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

我们可以看到调用该函数后返回一个renderer,若没有renderer则调用createRenderer来进行创建。

而这边的createRenderer则是来自runtime-core

// packages/runtime-core/src/index.ts

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

该函数接收一个RendererOptions作为参数,其实际是调用了baseCreateRenderer并将options传入

// packages/runtime-core/src/renderer.ts

function baseCreateRenderer(
    options: RendererOptions,
    createHydrationFns?: typeof createHydrationFunctions
): any {
    const {
    insert: hostInsert,
    remove: hostRemove,
    patchProp: hostPatchProp,
    forcePatchProp: hostForcePatchProp,
    createElement: hostCreateElement,
    createText: hostCreateText,
    createComment: hostCreateComment,
    setText: hostSetText,
    setElementText: hostSetElementText,
    parentNode: hostParentNode,
    nextSibling: hostNextSibling,
    setScopeId: hostSetScopeId = NOOP,
    cloneNode: hostCloneNode,
    insertStaticContent: hostInsertStaticContent
  } = options
  
    // 声明了许多操作函数,约2000行

    return {
        render,
        hydrate,
        createApp: createAppAPI(render, hydrate)
    }
}

在调用完baseCreateRenderer后主要返回了三个函数:render,hydratecreateApp

此时renderer便创建完成了。

这其中有一处需要留心,即传入的RendererOptions是什么?为什么在runtime-dom传入,又在runtime-core拆解。

// packages/runtime-dom/src/index.ts

const rendererOptions = extend({ patchProp, forcePatchProp }, nodeOps)
// packages/runtime-dom/src/nodeOps.ts

export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
  insert: (child, parent, anchor) => {
    parent.insertBefore(child, anchor || null)
  },

  remove: child => {
    const parent = child.parentNode
    if (parent) {
      parent.removeChild(child)
    }
  },
    // ...
}

是不是很熟悉,其实就是对于dom操作的封装。那为什么要在runtime-dom中传入,runtime-core拆解?

其实是因为在Vue3中runtime-coreruntime-dom的拆分,runtime-core不应该关心实际的操作,这样当新平台要接入时(比如weex)就可以只实现属于自己平台的nodeOps

总结:创建renderer的函数调用顺序为

  1. ensureRenderer()
  2. createRenderer()
  3. baseCreateRenderer()

创建app

当创建完renderer后返回了3个函数,我们可以看到其中createApp实际上是引用了createAppAPI(render, hydrate),所以其实const app = ensureRenderer().createApp(...args)创建app实例时,调用的是createAppAPI的返回值(运用柯里化,返回的是一个函数)

// packages/runtime-core/src/apiCreateApp.ts

export function createAppContext(): AppContext {
  return {
    app: null as any, // 刚创建时为空
    config: {
      isNativeTag: NO,
      performance: false,
      globalProperties: {},
      optionMergeStrategies: {},
      isCustomElement: NO,
      errorHandler: undefined,
      warnHandler: undefined
    },
    mixins: [],
    components: {},
    directives: {},
    provides: Object.create(null)
  }
}

export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    // 检验root props
    if (rootProps != null && !isObject(rootProps)) {
      __DEV__ && warn(`root props passed to app.mount() must be an object.`)
      rootProps = null
    }
                      
    const context = createAppContext(); // 创建context
    const installedPlugins = new Set(); // 创建插件列表集合,储存已安装的插件

    let isMounted = false;

    const app: App = (context.app = {
      _component: rootComponent as Component,
      _props: rootProps,
      _container: null,
      _context: context,

      version,

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

      use() {},
      mixin() {},
      component() {},
      mount() {}                                                        
      // ...
                      
    })
        
        return app // 返回创建的app实例
  };
}

看完上面的代码后结果就很清楚了,当我们调用createApp时,返回的app上有着许多函数方法和属性,相信你对这些函数方法并不陌生,这些就是vue2.x中在Vue上的那些API:usemixincomponent,在vue3则是被挂载到了app实例上

需要注意的是:我们在应用中调用的createApp(App),其中的APP就是第一个参数,作为根组件

mount

当创建完app实例后,现在让我们开始进行mount('#app'),让我们重新进入createApp

// packages/runtime-dom/src/index.ts

export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)

  if (__DEV__) {
    injectNativeTagCheck(app)
  }

  const { mount } = app // 保存app实例上原本的mount
  // 重写mount
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    const container = normalizeContainer(containerOrSelector) // 获取根元素容器
    if (!container) return
    const component = app._component // 获取根组件,即App
    if (!isFunction(component) && !component.render && !component.template) {
      component.template = container.innerHTML // 使用根元素来作为模板
    }
    // clear content before mounting
    container.innerHTML = ''
    const proxy = mount(container) // 调用实例中的mount方法
    if (container instanceof Element) {
      container.removeAttribute('v-cloak') // 删除v-cloak属性
      container.setAttribute('data-v-app', '') // 添加data-v-app属性
    }
    return proxy
  }

  return app
}) as CreateAppFunction<Element>

我们可以看到在上面的代码中,在创建完app之后,先对app实例中的mount方法进行了保存,接着又对mount进行了重写。

重写的mount方法中,先是调用了normalizeContainer(containerOrSelector)来获取根元素容器,containerOrSelector即我们传入的#app

// packages/runtime-dom/src/index.ts

function normalizeContainer(
  container: Element | ShadowRoot | string
): Element | null {
  if (isString(container)) {
    const res = document.querySelector(container) // 进行dom操作选中容器
    if (__DEV__ && !res) {
      // ...
    }
    return res
  }
 // ...
  return container as any
}

在获取到根元素的容器之后,进行判断,将容器原本的html作为根组件的模板,然后清除了容器中原本的html

随后便开始调用app实例上原本的mount方法,正式进行挂载。

// packages/runtime-core/src/apiCreateApp.ts

mount(rootContainer: HostElement, isHydrate?: boolean): any {
        if (!isMounted) {
          // 1.创建vnode                                                                     
          const vnode = createVNode(
            rootComponent as ConcreteComponent, // App组件
            rootProps
          )

          vnode.appContext = context // 保存context在根节点上

          // HMR root reload
          if (__DEV__) {
            // ...
          }

          if (isHydrate && hydrate) {
            hydrate(vnode as VNode<Node, Element>, rootContainer as any)
          } else {
            render(vnode, rootContainer) // 2.进入render,函数来源于runtime-core
          }
                                                                                  
          isMounted = true // 修改状态
          app._container = rootContainer
          // for devtools and telemetry
          ;(rootContainer as any).__vue_app__ = app

          if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
            devtoolsInitApp(app, version)
          }

          return vnode.component!.proxy // 返回vnode.component的代理
        } else if (__DEV__) {
         // ...
        }
      },

runtime-coremount方法主要做了两件事:创建vnode和调用render进入渲染。这里我们先简略的介绍一下这两个函数的作用,后面我们将进行仔细的解析。

在创建vnode时调用了createVNode(),并将根组件作为参数传入。

在得到vnode之后又调用了render()开始进行渲染。

最后mount函数的返回值为vnode.component的代理。

createVNode

// packages/runtime-core/src/vnode.ts

export const createVNode = (__DEV__
  ? createVNodeWithArgsTransform
  : _createVNode) as typeof _createVNode

// 实际调用
function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  patchFlag: number = 0, // patch flag默认为0
  dynamicProps: string[] | null = null,
  isBlockNode = false
): VNode {
  // ...

  // ...

  // class & style normalization
  // 处理props,标准化calss和style
  if (props) {
    // for reactive or proxy objects, we need to clone it to enable mutation.
    if (isProxy(props) || InternalObjectKey in props) {
      props = extend({}, props)
    }
    let { class: klass, style } = props
    if (klass && !isString(klass)) {
      props.class = normalizeClass(klass) // 标准化class
    }
    if (isObject(style)) {
      // reactive state objects need to be cloned since they are likely to be
      // mutated
      if (isProxy(style) && !isArray(style)) {
        style = extend({}, style)
      }
      props.style = normalizeStyle(style) // 标准化style
    }
  }

  // 定义shapeFlag
  // encode the vnode type information into a bitmap
  const shapeFlag = isString(type)
    ? ShapeFlags.ELEMENT
    : __FEATURE_SUSPENSE__ && isSuspense(type)
      ? ShapeFlags.SUSPENSE
      : isTeleport(type)
        ? ShapeFlags.TELEPORT
        : isObject(type)
          ? ShapeFlags.STATEFUL_COMPONENT // 根组件shapeFlag
          : isFunction(type)
            ? ShapeFlags.FUNCTIONAL_COMPONENT
            : 0

  // ...
  
  // 创建vnode对象
  const vnode: VNode = {
    __v_isVNode: true,
    [ReactiveFlags.SKIP]: true,
    type,
    props,
    key: props && normalizeKey(props),
    ref: props && normalizeRef(props),
    scopeId: currentScopeId,
    children: null,
    component: null,
    suspense: null,
    ssContent: null,
    ssFallback: null,
    dirs: null,
    transition: null,
    el: null,
    anchor: null,
    target: null,
    targetAnchor: null,
    staticCount: 0,
    shapeFlag,
    patchFlag,
    dynamicProps,
    dynamicChildren: null,
    appContext: null
  }

  // ...

  normalizeChildren(vnode, children) // 标准化子节点
 // ...

  return vnode // 返回创建完的vnode
}

可以看到createVNode主要做了四件事:

  • 处理props:标准化classstyle,如果是响应式元素则会被克隆
  • 定义shapeFlagshapeFlag用于对元素进行标记,比如文本、注释、组件等等。主要是为了在render的时候可以根据不同的元素类型来进行不同的patch操作。
  • 创建vnode对象
  • 标准化子节点:把不同数据类型的 children 转成数组或者文本类型

shapeFlag的定义如下:

// packages/shared/src/shapeFlag.ts

export const enum ShapeFlags {
  ELEMENT = 1, // 普通元素
  FUNCTIONAL_COMPONENT = 1 << 1, // 函数组件
  STATEFUL_COMPONENT = 1 << 2, // 动态绑定style的元素
  TEXT_CHILDREN = 1 << 3, // 后代为文本元素
  ARRAY_CHILDREN = 1 << 4, // 后代为列表元素
  SLOTS_CHILDREN = 1 << 5, // 后代为插槽元素
  TELEPORT = 1 << 6, // teleport组件
  SUSPENSE = 1 << 7, // suspense组件
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8, // keep alive 路由组件
  COMPONENT_KEPT_ALIVE = 1 << 9, // keep alive组件
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT // 普通组件
}

render

当我们拥有这个vnode后,就开始进入渲染阶段了。render(vnode, rootContainer),可以看到传入的参数为vnode以及根元素的容器,接下来让我们继续深入。

不知道你是否还记得,这个render函数是在调用createAPI时传入的第一个参数,因此这个函数来源于runtime-core中的baseCreateRenderer

// packages/runtime-core/src/renderer.ts

const render: RootRenderFunction = (vnode, container) => {
    // (判断进行卸载还是渲染
    if (vnode == null) {
      if (container._vnode) {
        unmount(container._vnode, null, null, true) // 卸载
      }
    } else {
      patch(container._vnode || null, vnode, container) // 创建或更新组件,进行dom diff和渲染
    }
    flushPostFlushCbs() // 回调调度器,使用Promise实现,与Vue2的区别是Vue2是宏任务或微任务来处理的
    container._vnode = vnode // 缓存vnode节点,证明已经渲染完成,方便后续diff操作
  }

render函数中,对vnode的存在进行了判断,如果为空,则对组件进行销毁,否则将调用patch,创建组件。

接下来让我们继续进入patch函数

// packages/runtime-core/src/renderer.ts

const patch: PatchFn = (
    n1, // 旧
    n2, // 新
    container, // 容器
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    optimized = false
  ) => {
 
    // 如果两者类型不同,则直接卸载n1
    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
    // 根据不同的节点类型来进行不同的process方法
    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: // 片段(dom数组)
        processFragment(
            // ...
        )
        break
    default:
         if (shapeFlag & ShapeFlags.ELEMENT) { // 原生节点(div)
          processElement(
           // ...
          )
        } else if (shapeFlag & ShapeFlags.COMPONENT) { // 组件,根组件即通过processComponent处理
          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})`)
        }
    }
  
   // 处理节点之后处理ref
    if (ref != null && parentComponent) {
      setRef(ref, n1 && n1.ref, parentSuspense, n2)
    }
}

我们可以看到在创建vnode时定义的shapeFlag,在这里发挥了作用。根组件经过逻辑流程之后也进入了processComponent之中。

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

推荐阅读更多精彩内容

  • 前言 这是一篇一点都不讲究的文章记录下时间点吧 9 月 15 号把 Vue3 的 master 分支拉下来了,然后...
    zpkzpk阅读 1,065评论 0 0
  • 本次vue3.0源码是可以对vue3.0源码进行学习,这是测试代码 1.运行代码首先进入/package/runt...
    Viewwei阅读 1,457评论 0 0
  • 先看一下vue-next官方文档的介绍: 每个 Vue 应用都是通过用 createApp 函数创建一个新的应用实...
    RiverSouthMan阅读 9,554评论 0 4
  • 先看一下vue-next官方文档的介绍: 每个 Vue 应用都是通过用 createApp 函数创建一个新的应用实...
    RiverSouthMan阅读 46,243评论 0 8
  • 目录结构 当我们开始阅读源码之前,我们先来看一眼整体的目录结构 现在就正式进入放置源码的文件夹packages 整...
    Refrain37阅读 628评论 0 0