vue3- 实例挂载mount

先看一下vue-next官方文档的介绍:

每个 Vue 应用都是通过用 createApp 函数创建一个新的应用实例开始的

传递给 createApp 的选项用于配置根组件。当我们挂载应用时,该组件被用作渲染的起点。

一个应用需要被挂载到一个 DOM 元素中。例如,如果我们想把一个 Vue 应用挂载到<div id="app"></div>,我们应该传递 #app

我们将分为两部分进行渲染过程的理解:

  • 创建应用实例,函数createApp的剖析
  • 应用实例挂载, 函数mount方法挂载过程

本篇详细讲述调用应用实例方法mount过程

下面是一个简单的demo

<!-- template -->
  <div id="app">
    <input v-model="value"/>
    <p>双向绑定:{{value}}</p>
    <hello-comp person-name="zhangsan"/>
  </div>
const { createApp } = Vue
const helloComp = {
      name: 'hello-comp',
      props: {
        personName: {
          type: String,
          default: 'wangcong'
        }
      },
      template: '<p>hello {{personName}}!</p>'
    }
const app = {
  data() {
    return {
      value: '',
      info: {
        name: 'tom',
        age: 18
      }
    }
  },
  components: {
    'hello-comp': helloComp
  },
  mounted() {
    console.log(this.value, this.info)
  },
}
createApp(app).mount('#app')

现在我们从mount函数为入口,去了解应用挂载的过程。


image.png

挂载 mount

现在回顾一下在demo中我们调用的方法createApp(app).mount('#app')

此时调用的这个mount方法是在runtime-dom模块重写之后的,在内部依然会执行应用实例的app.mount方法。

重写mount方法,在app._component引用的根组件对象上面添加了template属性,用来获取html中的模板字符串。

并返回了proxy,也就是根组件实例vm

  // runtime-dom模块重写了`app.mount`方法
  const { mount } = app
  app.mount = (containerOrSelector: Element | string): any => {
    const container = normalizeContainer(containerOrSelector)
    if (!container) return
    // app._component引用的对象是根组件对象,就是我们传入createApp方法的根组件对象
    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
  }

proxy是调用了mount方法返回的:const proxy = mount(container)

现在看一下在应用实例app中定义的mount方法,它做了两件事情:

  • 执行createVNode方法生产VNode
  • 执行render方法。(上一篇文中所提:父级作用域函数createAppAPI(render, hydrate)接受的第一个参数)
mount(rootContainer: HostElement, isHydrate?: 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)
      }
    }

    if (isHydrate && hydrate) {
      hydrate(vnode as VNode<Node, Element>, rootContainer as any)
    } else {
      render(vnode, rootContainer)
    }
    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
  } 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)\``
    )
  }
}

createVNode

先看一下VNode包含的类型

export type VNodeTypes =
  | string
  | VNode
  | Component
  | typeof Text // 文本
  | typeof Static // 静态组件 纯html
  | typeof Comment
  | typeof Fragment // 多根组件
  | typeof TeleportImpl // 内置组件传送
  | typeof SuspenseImpl // 内置组件悬念 一般配合异步组件

createVNode函数内部实际会调用_createVNode函数

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

我们传入createApp方法的根组件对象会作为_createVNode方法接收的第一个参数type,并在执行后会返回一个VNode。VNode中包含一个shapeFlag标识类型,在后面patch过程中进入不同的处理逻辑。

此时传入的type参数类型符合定义的接口interface ClassComponent

  • data函数
  • mounted函数
function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  patchFlag: number = 0,
  dynamicProps: string[] | null = null,
  isBlockNode = false
): VNode {
  ...
  return vnode
}

此时返回的VNode:


image.png

render

在得到了VNode后,执行了render(vnode, rootContainer)

render函数是在 packages/runtime-core/src/renderer.js 中函数baseCreateRenderer闭包内部声明的。

const render: RootRenderFunction = (vnode, container) => {
    if (vnode == null) {
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
      patch(container._vnode || null, vnode, container)
    }
    flushPostFlushCbs()
    container._vnode = vnode
  }

内部调用了patch方法完成对VNode的解析与渲染

const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    optimized = false
  ) => {
 
    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) {
     /*...*/
    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
          )
        }
       /*...*/
    }
}

在这个switch语句中会根据vnodeshapeFlag字段对不同类型的vnode进行处理。

ShapeFlags

export const enum ShapeFlags {
  ELEMENT = 1,
  FUNCTIONAL_COMPONENT = 1 << 1,
  STATEFUL_COMPONENT = 1 << 2,
  TEXT_CHILDREN = 1 << 3,
  ARRAY_CHILDREN = 1 << 4,
  SLOTS_CHILDREN = 1 << 5,
  TELEPORT = 1 << 6,
  SUSPENSE = 1 << 7,
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
  COMPONENT_KEPT_ALIVE = 1 << 9,
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}

通过位运算符>>|定义了枚举类型 ShapeFlags; 在逻辑语句中使用& 位运算符进行匹配;

左移:a << b 将 a 的二进制形式向左移 b (< 32) 比特位,右边用0填充。

按位或:a | b 对于每一个比特位,当两个操作数相应的比特位至少有一个1时,结果为1,否则为0。

按位与:a & b 对于每一个比特位,只有两个操作数相应的比特位都是1时,结果才为1,否则为0。

例如当前的demo:_createVNode方法在生成根组件VNode时,
属性赋值操作:shapeFlag = ShapeFlags.STATEFUL_COMPONENT
因此会匹配到ShapeFlags.COMPONENT

const shapeFlag = isString(type)
    ? ShapeFlags.ELEMENT
    : __FEATURE_SUSPENSE__ && isSuspense(type)
      ? ShapeFlags.SUSPENSE
      : isTeleport(type)
        ? ShapeFlags.TELEPORT
        : isObject(type)
          ? ShapeFlags.STATEFUL_COMPONENT
          : isFunction(type)
            ? ShapeFlags.FUNCTIONAL_COMPONENT
            : 0

并执行了方法processComponent;

const processComponent = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) => {
    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)
    }
  }

在判断当前组件不是keep-alive类型后继续执行了mountComponent方法

mountComponent

下面详细看mountComponent方法中的省略后的核心逻辑:

const mountComponent: MountComponentFn = (
    initialVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    const instance = createComponentInstance(initialVNode, parentComponent, parentSuspense)
    setupComponent(instance)
    setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized)
  }

创建组件实例

createComponentInstance

初始化并returninstance对象,

export function createComponentInstance(
  vnode: VNode,
  parent: ComponentInternalInstance | null,
  suspense: SuspenseBoundary | null
) {
  const type = vnode.type as ConcreteComponent
  // inherit parent app context - or - if root, adopt from root vnode
  const appContext =
    (parent ? parent.appContext : vnode.appContext) || emptyAppContext
  const instance = {/*...*/}
  if (__DEV__) {
    instance.ctx = createRenderContext(instance)
  } else {
    instance.ctx = { _: instance }
  }
  instance.root = parent ? parent.root : instance
  instance.emit = emit.bind(null, instance)

  if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
    devtoolsComponentAdded(instance)
  }

  return instance
  }

其中instance.ctx 做了一层引用instance.ctx = { _: instance }

setupComponent: 解析props、slots、setup

创建instance后,然后执行了setupComponent(instance)

setupComponent核心代码

function setupComponent (instance) {
  initProps(instance, props, isStateful, isSSR)
  initSlots(instance, children)
  const setupResult = setupStatefulComponent(instance, isSSR)
  return setupResult
}

initProps:初始化props、将其处理为响应式的
initSlots: 处理插槽
setupStatefulComponent: 处理setup配置选项

解析模板、初始化选项

setupStatefulComponent简略后的逻辑:

function setupStatefulComponent(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  const Component = instance.type as ComponentOptions
  // 创建渲染代理属性访问缓存
  instance.accessCache = {}
  instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
  // 2. call setup()
  const { setup } = Component
  if (setup) {
    const setupResult = setup()
    if (isPromise(setupResult)) {
      return setupResult.then((resolvedResult: unknown) => {
        handleSetupResult(instance, resolvedResult, isSSR)
      })
    } else {
      handleSetupResult(instance, setupContext)
    }
  } else {
    finishComponentSetup(instance, isSSR)
  }
}

如果组件选项里配置了setup,则会调用handleSetupResult对返回值setupResult进行处理;

setupStatefulComponent内最终逻辑都会调用finishComponentSetup方法:

function finishComponentSetup(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  const Component = instance.type as ComponentOptions
  Component.render = compile(Component.template, {
    isCustomElement: instance.appContext.config.isCustomElement,
    delimiters: Component.delimiters
  })
  applyOptions(instance, Component)
}
  1. 解析模板: 将template模板转换为render函数;
    备注:compile函数是在packages/vue/src/index.js中调用registerRuntimeCompiler(compileToFunction)完成的编译器注册

  2. 执行applyOptions方法。并在初始化选项前调用beforeCreate钩子:

选项初始化顺序保持了与Vue 2的一致:

  • props (上一步initProps中已经完成了初始化)
  • inject
  • methods
  • data (由于它依赖于this访问而推迟)
  • computed
  • watch (由于它依赖于this访问而推迟)

完成上面初始化后调用了 created钩子,然后注册其余声明周期钩子:

  beforeMount?(): void
  mounted?(): void
  beforeUpdate?(): void
  updated?(): void
  activated?(): void
  deactivated?(): void
  /** @deprecated use `beforeUnmount` instead */
  beforeDestroy?(): void
  beforeUnmount?(): void
  /** @deprecated use `unmounted` instead */
  destroyed?(): void
  unmounted?(): void
  renderTracked?: DebuggerHook
  renderTriggered?: DebuggerHook
  errorCaptured?: ErrorCapturedHook

到此setupComponent函数结束,下面继续执行setupRenderEffect

setupRenderEffect:为渲染创建响应性效果

下面是setupRenderEffect的伪代码:

const setupRenderEffect = (
  instance,
  initialVNode,
  container,
  anchor,
  parentSuspense,
  isSVG,
  optimized
) {
      instance.update = effect(function componentEffect() {
      const subTree = (instance.subTree = renderComponentRoot(instance))
      patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)
      initialVNode.el = subTree.el
  },  EffectOptions)
}

调用了命名为effect的函数,第一个参数接收一个函数,第二个参数是配置选项;并将函数执行结果赋值给instance.update;
componentEffect回调中得到子节点的VNode,并递归调用了patch方法。
通过initialVNode.el = subTree.el实现dom的挂载

何时创建的dom?

通过递归执行patch方法,会遍历整个vnode树;
这个过程中非dom节点类类型的Vnode会重复上面的过程,继续调用patch

递归函数最终会结束,那么这个函数内一定有一个分支不再调用自己。这个就是普通vnode节点

举例:当Vnode类型是Text,会走到处理逻辑processText方法中

const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
    if (n1 == null) {
      hostInsert(
        (n2.el = hostCreateText(n2.children as string)),
        container,
        anchor
      )
    } else {
      const el = (n2.el = n1.el!)
      if (n2.children !== n1.children) {
        hostSetText(el, n2.children as string)
      }
    }
  }

hostCreateText创建了Text dom。该方法定义在packages/runtime-dom/src/nodeOps.ts

const createText = document.createTextNode(text)

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

推荐阅读更多精彩内容