乐趣区

关于javascript:Vue30源码解析之组件渲染vnode-到真实-DOM

在 Vue.js 中,组件是一个十分重要的概念,整个利用的页面都是通过组件渲染来实现的,然而你晓得当咱们编写这些组件的时候,它的外部是如何工作的吗?从咱们编写组件开始,到最终实在的 DOM 又是怎么的一个转变过程呢?

首先,组件是一个形象的概念,它是对一棵 DOM 树的形象,咱们在页面中写一个组件节点:

<hello-world></hello-world>

这段代码并不会在页面上渲染一个 <hello-world> 标签,而它具体渲染成什么,取决于你怎么编写 HelloWorld 组件的模板。举个例子,HelloWorld 组件外部的模板定义是这样的:

<template>
  <div>
    <p>Hello World</p>
  </div>
</template>

能够看到,模板外部最终会在页面上渲染一个 div,外部蕴含一个 p 标签,用来显示 Hello World 文本。

所以,从体现上来看,组件的模板决定了组件生成的 DOM 标签,而在 Vue.js 外部,一个组件想要真正的渲染生成 DOM,还须要经验“创立 vnode – 渲染 vnode – 生成 DOM”这几个步骤:


你可能会问,什么是 vnode,它和组件什么关系呢?先不要焦急,咱们在前面会具体阐明。这里,你只须要记住它就是一个能够形容组件信息的 JavaScript 对象即可。

接下来,咱们就从应用程序的入口开始,逐渐来看 Vue.js 3.0 中的组件是如何渲染的。

应用程序初始化

一个组件能够通过“模板加对象形容”的形式创立,组件创立好当前是如何被调用并初始化的呢?因为整个组件树是由根组件开始渲染的,为了找到根组件的渲染入口,咱们须要从应用程序的初始化过程开始剖析。

在这里,我别离给出了通过 Vue.js 2.x 和 Vue.js 3.0 来初始化利用的代码:

// 在 Vue.js 2.x 中,初始化一个利用的形式如下
import Vue from 'vue'
import App from './App'
const app = new Vue({render: h => h(App)
})
app.$mount('#app')
// 在 Vue.js 3.0 中,初始化一个利用的形式如下
import {createApp} from 'vue'
import App from './app'
const app = createApp(App)
app.mount('#app')

能够看到,Vue.js 3.0 初始化利用的形式和 Vue.js 2.x 差异并不大,实质上都是把 App 组件挂载到 id 为 app 的 DOM 节点上。

然而,在 Vue.js 3.0 中还导入了一个 createApp,其实这是个入口函数,它是 Vue.js 对外裸露的一个函数,咱们来看一下它的外部实现:

const createApp = ((...args) => {
  // 创立 app 对象
  const app = ensureRenderer().createApp(...args)
  const {mount} = app
  // 重写 mount 办法
  app.mount = (containerOrSelector) => {// ...}
  return app
})

从代码中能够看出 createApp 次要做了两件事件:创立 app 对象和重写 app.mount 办法。接下来,咱们就具体来剖析一下它们。

1. 创立 app 对象

首先,咱们应用 ensureRenderer().createApp() 来创立 app 对象:

 const app = ensureRenderer().createApp(...args)

其中 ensureRenderer() 用来创立一个渲染器对象,它的外部代码是这样的:

// 渲染相干的一些配置,比方更新属性的办法,操作 DOM 的办法
const rendererOptions = {
  patchProp,
  ...nodeOps
}
let renderer
// 延时创立渲染器,当用户只依赖响应式包的时候,能够通过 tree-shaking 移除外围渲染逻辑相干的代码
function ensureRenderer() {return renderer || (renderer = createRenderer(rendererOptions))
}
function createRenderer(options) {return baseCreateRenderer(options)
}
function baseCreateRenderer(options) {function render(vnode, container) {// 组件渲染的外围逻辑}
  return {
    render,
    createApp: createAppAPI(render)
  }
}
function createAppAPI(render) {
  // createApp createApp 办法承受的两个参数:根组件的对象和 prop
  return function createApp(rootComponent, rootProps = null) {
    const app = {
      _component: rootComponent,
      _props: rootProps,
      mount(rootContainer) {
        // 创立根组件的 vnode
        const vnode = createVNode(rootComponent, rootProps)
        // 利用渲染器渲染 vnode
        render(vnode, rootContainer)
        app._container = rootContainer
        return vnode.component.proxy
      }
    }
    return app
  }
}

能够看到,这里先用 ensureRenderer() 来延时创立渲染器,这样做的益处是当用户只依赖响应式包的时候,就不会创立渲染器,因而能够通过 tree-shaking 的形式移除外围渲染逻辑相干的代码。

这里波及了渲染器的概念,它是为跨平台渲染做筹备的,之后我会在自定义渲染器的相干内容中具体阐明。在这里,你能够简略地把渲染器了解为蕴含平台渲染外围逻辑的 JavaScript 对象。

咱们联合下面的代码持续深刻,在 Vue.js 3.0 外部通过 createRenderer 创立一个渲染器,这个渲染器外部会有一个 createApp 办法,它是执行 createAppAPI 办法返回的函数,承受了 rootComponent 和 rootProps 两个参数,咱们在利用层面执行 createApp(App) 办法时,会把 App 组件对象作为根组件传递给 rootComponent。这样,createApp 外部就创立了一个 app 对象,它会提供 mount 办法,这个办法是用来挂载组件的。

在整个 app 对象创立过程中,Vue.js 利用闭包和函数颗粒化的技巧,很好地实现了参数保留。比方,在执行 app.mount 的时候,并不需要传入渲染器 render,这是因为在执行 createAppAPI 的时候渲染器 render 参数曾经被保留下来了。

2. 重写 app.mount 办法

接下来,是重写 app.mount 办法。

依据后面的剖析,咱们晓得 createApp 返回的 app 对象曾经领有了 mount 办法了,但在入口函数中,接下来的逻辑却是对 app.mount 办法的重写。先思考一下,为什么要重写这个办法,而不把相干逻辑放在 app 对象的 mount 办法外部来实现呢?

这是因为 Vue.js 不仅仅是为 Web 平台服务,它的指标是反对跨平台渲染,而 createApp 函数外部的 app.mount 办法是一个规范的可跨平台的组件渲染流程:

mount(rootContainer) {
  // 创立根组件的 vnode
  const vnode = createVNode(rootComponent, rootProps)
  // 利用渲染器渲染 vnode
  render(vnode, rootContainer)
  app._container = rootContainer
  return vnode.component.proxy
}

规范的跨平台渲染流程是先创立 vnode,再渲染 vnode。此外参数 rootContainer 也能够是不同类型的值,比方,在 Web 平台它是一个 DOM 对象,而在其余平台(比方 Weex 和小程序)中能够是其余类型的值。所以这外面的代码不应该蕴含任何特定平台相干的逻辑,也就是说这些代码的执行逻辑都是与平台无关的。因而咱们须要在内部重写这个办法,来欠缺 Web 平台下的渲染逻辑。

接下来,咱们再来看 app.mount 重写都做了哪些事件:

app.mount = (containerOrSelector) => {
  // 标准化容器
  const container = normalizeContainer(containerOrSelector)
  if (!container)
    return
  const component = app._component
   // 如组件对象没有定义 render 函数和 template 模板,则取容器的 innerHTML 作为组件模板内容
  if (!isFunction(component) && !component.render && !component.template) {component.template = container.innerHTML}
  // 挂载前清空容器内容
  container.innerHTML = ''
  // 真正的挂载
  return mount(container)
}

首先是通过 normalizeContainer 标准化容器(这里能够传字符串选择器或者 DOM 对象,但如果是字符串选择器,就须要把它转成 DOM 对象,作为最终挂载的容器),而后做一个 if 判断,如果组件对象没有定义 render 函数和 template 模板,则取容器的 innerHTML 作为组件模板内容;接着在挂载前清空容器内容,最终再调用 app.mount 的办法走规范的组件渲染流程。

在这里,重写的逻辑都是和 Web 平台相干的,所以要放在内部实现。此外,这么做的目标是既能让用户在应用 API 时能够更加灵便,也兼容了 Vue.js 2.x 的写法,比方 app.mount 的第一个参数就同时反对选择器字符串和 DOM 对象两种类型。

从 app.mount 开始,才算真正进入组件渲染流程,那么接下来,咱们就重点看一下外围渲染流程做的两件事件:创立 vnode 和渲染 vnode。

外围渲染流程:创立 vnode 和渲染 vnode

1. 创立 vnode

首先,是创立 vnode 的过程。

vnode 实质上是用来形容 DOM 的 JavaScript 对象,它在 Vue.js 中能够形容不同类型的节点,比方一般元素节点、组件节点等。

什么是一般元素节点呢?举个例子,在 HTML 中咱们应用 <button> 标签来写一个按钮:

<button class="btn" style="width:100px;height:50px">click me</button>

咱们能够用 vnode 这样示意 <button> 标签:

const vnode = {
  type: 'button',
  props: { 
    'class': 'btn',
    style: {
      width: '100px',
      height: '50px'
    }
  },
  children: 'click me'
}

其中,type 属性示意 DOM 的标签类型,props 属性示意 DOM 的一些附加信息,比方 style、class 等,children 属性示意 DOM 的子节点,它也能够是一个 vnode 数组,只不过 vnode 能够用字符串示意简略的文本。

什么是组件节点呢?其实,vnode 除了能够像下面那样用于形容一个实在的 DOM,也能够用来形容组件。

咱们先在模板中引入一个组件标签 <custom-component>:

<custom-component msg="test"></custom-component>

咱们能够用 vnode 这样示意 <custom-component> 组件标签:

const CustomComponent = {// 在这里定义组件对象}
const vnode = {
  type: CustomComponent,
  props: {msg: 'test'}
}

组件 vnode 其实是对形象事物的形容,这是因为咱们并不会在页面上真正渲染一个 <custom-component> 标签,而是渲染组件外部定义的 HTML 标签。

除了上两种 vnode 类型外,还有纯文本 vnode、正文 vnode 等等,但鉴于咱们的主线只须要钻研组件 vnode 和一般元素 vnode,所以我在这里就不赘述了。

另外,Vue.js 3.0 外部还针对 vnode 的 type,做了更详尽的分类,包含 Suspense、Teleport 等,且把 vnode 的类型信息做了编码,以便在前面的 patch 阶段,能够依据不同的类型执行相应的解决逻辑:

const shapeFlag = isString(type)
  ? 1 /* ELEMENT */
  : isSuspense(type)
    ? 128 /* SUSPENSE */
    : isTeleport(type)
      ? 64 /* TELEPORT */
      : isObject(type)
        ? 4 /* STATEFUL_COMPONENT */
        : isFunction(type)
          ? 2 /* FUNCTIONAL_COMPONENT */
          : 0

晓得什么是 vnode 后,你可能会好奇,那么 vnode 有什么劣势呢?为什么肯定要设计 vnode 这样的数据结构呢?

首先是形象,引入 vnode,能够把渲染过程抽象化,从而使得组件的形象能力也失去晋升。

其次是跨平台,因为 patch vnode 的过程不同平台能够有本人的实现,基于 vnode 再做服务端渲染、Weex 平台、小程序平台的渲染都变得容易了很多。

不过这里要特地留神,应用 vnode 并不意味着不必操作 DOM 了,很多同学会误以为 vnode 的性能肯定比手动操作原生 DOM 好,这个其实是不肯定的。

因为,首先这种基于 vnode 实现的 MVVM 框架,在每次 render to vnode 的过程中,渲染组件会有肯定的 JavaScript 耗时,特地是大组件,比方一个 1000 10 的 Table 组件,render to vnode 的过程会遍历 1000 10 次去创立外部 cell vnode,整个耗时就会变得比拟长,加上 patch vnode 的过程也会有肯定的耗时,当咱们去更新组件的时候,用户会感觉到显著的卡顿。尽管 diff 算法在缩小 DOM 操作方面足够优良,但最终还是免不了操作 DOM,所以说性能并不是 vnode 的劣势。

那么,Vue.js 外部是如何创立这些 vnode 的呢?

回顾 app.mount 函数的实现,外部是通过 createVNode 函数创立了根组件的 vnode:

 const vnode = createVNode(rootComponent, rootProps)

咱们来看一下 createVNode 函数的大抵实现:

function createVNode(type, props = null
,children = null) {if (props) {// 解决 props 相干逻辑,标准化 class 和 style}
  // 对 vnode 类型信息编码
  const shapeFlag = isString(type)
    ? 1 /* ELEMENT */
    : isSuspense(type)
      ? 128 /* SUSPENSE */
      : isTeleport(type)
        ? 64 /* TELEPORT */
        : isObject(type)
          ? 4 /* STATEFUL_COMPONENT */
          : isFunction(type)
            ? 2 /* FUNCTIONAL_COMPONENT */
            : 0
  const vnode = {
    type,
    props,
    shapeFlag,
    // 一些其余属性
  }
  // 标准化子节点,把不同数据类型的 children 转成数组或者文本类型
  normalizeChildren(vnode, children)
  return vnode
}

通过上述代码能够看到,其实 createVNode 做的事件很简略,就是:对 props 做标准化解决、对 vnode 的类型信息编码、创立 vnode 对象,标准化子节点 children。

咱们当初领有了这个 vnode 对象,接下来要做的事件就是把它渲染到页面中去。

2. 渲染 vnode

接下来,是渲染 vnode 的过程。

回顾 app.mount 函数的实现,外部通过执行这段代码去渲染创立好的 vnode:

render(vnode, rootContainer)
const render = (vnode, container) => {if (vnode == null) {
    // 销毁组件
    if (container._vnode) {unmount(container._vnode, null, null, true)
    }
  } else {
    // 创立或者更新组件
    patch(container._vnode || null, vnode, container)
  }
  // 缓存 vnode 节点,示意曾经渲染
  container._vnode = vnode
}

这个渲染函数 render 的实现很简略,如果它的第一个参数 vnode 为空,则执行销毁组件的逻辑,否则执行创立或者更新组件的逻辑。

接下来咱们接着看一下下面渲染 vnode 的代码中波及的 patch 函数的实现:

const patch = (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
  }
  const {type, shapeFlag} = n2
  switch (type) {
    case Text:
      // 解决文本节点
      break
    case Comment:
      // 解决正文节点
      break
    case Static:
      // 解决动态节点
      break
    case Fragment:
      // 解决 Fragment 元素
      break
    default:
      if (shapeFlag & 1 /* ELEMENT */) {
        // 解决一般 DOM 元素
        processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
      }
      else if (shapeFlag & 6 /* COMPONENT */) {
        // 解决组件
        processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
      }
      else if (shapeFlag & 64 /* TELEPORT */) {// 解决 TELEPORT}
      else if (shapeFlag & 128 /* SUSPENSE */) {// 解决 SUSPENSE}
  }
}

patch 本意是打补丁的意思,这个函数有两个性能,一个是依据 vnode 挂载 DOM,一个是依据新旧 vnode 更新 DOM。对于首次渲染,咱们这里只剖析创立过程,更新过程在前面的章节剖析。

在创立的过程中,patch 函数承受多个参数,这里咱们目前只重点关注前三个:

  1. 第一个参数 n1 示意旧的 vnode,当 n1 为 null 的时候,示意是一次挂载的过程;
  2. 第二个参数 n2 示意新的 vnode 节点,后续会依据这个 vnode 类型执行不同的解决逻辑;
  3. 第三个参数 container 示意 DOM 容器,也就是 vnode 渲染生成 DOM 后,会挂载到 container 上面。

对于渲染的节点,咱们这里重点关注两种类型节点的渲染逻辑:对组件的解决和对一般 DOM 元素的解决。

先来看对组件的解决。因为初始化渲染的是 App 组件,它是一个组件 vnode,所以咱们来看一下组件的解决逻辑是怎么的。首先是用来解决组件的 processComponent 函数的实现:

const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {if (n1 == null) {
   // 挂载组件
   mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
  }
  else {
    // 更新组件
    updateComponent(n1, n2, parentComponent, optimized)
  }
}

该函数的逻辑很简略,如果 n1 为 null,则执行挂载组件的逻辑,否则执行更新组件的逻辑。

咱们接着来看挂载组件的 mountComponent 函数的实现:

const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
  // 创立组件实例
  const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense))
  // 设置组件实例
  setupComponent(instance)
  // 设置并运行带副作用的渲染函数
  setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized)
}

能够看到,挂载组件函数 mountComponent 次要做三件事件:创立组件实例、设置组件实例、设置并运行带副作用的渲染函数。

首先是创立组件实例,Vue.js 3.0 尽管不像 Vue.js 2.x 那样通过类的形式去实例化组件,但外部也通过对象的形式去创立了以后渲染的组件实例。

其次设置组件实例,instance 保留了很多组件相干的数据,保护了组件的上下文,包含对 props、插槽,以及其余实例的属性的初始化解决。

创立和设置组件实例这两个流程咱们这里不开展讲,会在前面的章节详细分析。

最初是运行带副作用的渲染函数 setupRenderEffect,咱们重点来看一下这个函数的实现:

const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
  // 创立响应式的副作用渲染函数
  instance.update = effect(function componentEffect() {if (!instance.isMounted) {
      // 渲染组件生成子树 vnode
      const subTree = (instance.subTree = renderComponentRoot(instance))
      // 把子树 vnode 挂载到 container 中
      patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)
      // 保留渲染生成的子树根 DOM 节点
      initialVNode.el = subTree.el
      instance.isMounted = true
    }
    else {// 更新组件}
  }, prodEffectOptions)
}

该函数利用响应式库的 effect 函数创立了一个副作用渲染函数 componentEffect(effect 的实现咱们前面讲响应式章节会具体说)。副作用,这里你能够简略地了解为,当组件的数据发生变化时,effect 函数包裹的外部渲染函数 componentEffect 会从新执行一遍,从而达到从新渲染组件的目标。

渲染函数外部也会判断这是一次初始渲染还是组件更新。这里咱们只剖析初始渲染流程。

初始渲染次要做两件事件:渲染组件生成 subTree、把 subTree 挂载到 container 中。

首先,是渲染组件生成 subTree,它也是一个 vnode 对象。这里要留神别把 subTree 和 initialVNode 弄混了(其实在 Vue.js 3.0 中,依据命名咱们曾经能很好地区分它们了,而在 Vue.js 2.x 中它们别离命名为 _vnode 和 $vnode)。我来举个例子阐明,在父组件 App 中里引入了 Hello 组件:

<template>
  <div class="app">
    <p>This is an app.</p>
    <hello></hello>
  </div>
</template>

在 Hello 组件中是 <div> 标签包裹着一个 <p> 标签:

<template>
  <div class="hello">
    <p>Hello, Vue 3.0!</p>
  </div>
</template>

在 App 组件中,<hello> 节点渲染生成的 vnode,对应的就是 Hello 组件的 initialVNode,为了好记,你也能够把它称作“组件 vnode”。而 Hello 组件外部整个 DOM 节点对应的 vnode 就是执行 renderComponentRoot 渲染生成对应的 subTree,咱们能够把它称作“子树 vnode”。

咱们晓得每个组件都会有对应的 render 函数,即便你写 template,也会编译成 render 函数,而 renderComponentRoot 函数就是去执行 render 函数创立整个组件树外部的 vnode,把这个 vnode 再通过外部一层标准化,就失去了该函数的返回后果:子树 vnode。

渲染生成子树 vnode 后,接下来就是持续调用 patch 函数把子树 vnode 挂载到 container 中了。

那么咱们又再次回到了 patch 函数,会持续对这个子树 vnode 类型进行判断,对于上述例子,App 组件的根节点是 <div> 标签,那么对应的子树 vnode 也是一个一般元素 vnode,那么咱们接下来看对一般 DOM 元素的解决流程。

首先咱们来看一下解决一般 DOM 元素的 processElement 函数的实现:

const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
  isSVG = isSVG || n2.type === 'svg'
  if (n1 == null) {
    // 挂载元素节点
    mountElement(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
  }
  else {
    // 更新元素节点
    patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
  }
}

该函数的逻辑很简略,如果 n1 为 null,走挂载元素节点的逻辑,否则走更新元素节点逻辑。

咱们接着来看挂载元素的 mountElement 函数的实现:

const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
  let el
  const {type, props, shapeFlag} = vnode
  // 创立 DOM 元素节点
  el = vnode.el = hostCreateElement(vnode.type, isSVG, props && props.is)
  if (props) {
    // 解决 props,比方 class、style、event 等属性
    for (const key in props) {if (!isReservedProp(key)) {hostPatchProp(el, key, null, props[key], isSVG)
      }
    }
  }
  if (shapeFlag & 8 /* TEXT_CHILDREN */) {
    // 解决子节点是纯文本的状况
    hostSetElementText(el, vnode.children)
  }
  else if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
    // 解决子节点是数组的状况
    mountChildren(vnode.children, el, null, parentComponent, parentSuspense, isSVG && type !== 'foreignObject', optimized || !!vnode.dynamicChildren)
  }
  // 把创立的 DOM 元素节点挂载到 container 上
  hostInsert(el, container, anchor)
}

能够看到,挂载元素函数次要做四件事:创立 DOM 元素节点、解决 props、解决 children、挂载 DOM 元素到 container 上。

首先是创立 DOM 元素节点,通过 hostCreateElement 办法创立,这是一个平台相干的办法,咱们来看一下它在 Web 环境下的定义:

function createElement(tag, isSVG, is) {isSVG ? document.createElementNS(svgNS, tag)
    : document.createElement(tag, is ? { is} : undefined)
}

它调用了底层的 DOM API document.createElement 创立元素,所以实质上 Vue.js 强调不去操作 DOM,只是心愿用户不间接碰触 DOM,它并没有什么神奇的魔法,底层还是会操作 DOM。

另外,如果是其余平台比方 Weex,hostCreateElement 办法就不再是操作 DOM,而是平台相干的 API 了,这些平台相干的办法是在创立渲染器阶段作为参数传入的。

创立完 DOM 节点后,接下来要做的是判断如果有 props 的话,给这个 DOM 节点增加相干的 class、style、event 等属性,并做相干的解决,这些逻辑都是在 hostPatchProp 函数外部做的,这里就不开展讲了。

接下来是对子节点的解决,咱们晓得 DOM 是一棵树,vnode 同样也是一棵树,并且它和 DOM 构造是一一映射的。

如果子节点是纯文本,则执行 hostSetElementText 办法,它在 Web 环境下通过设置 DOM 元素的 textContent 属性设置文本:

function setElementText(el, text) {el.textContent = text}

如果子节点是数组,则执行 mountChildren 办法:

const mountChildren = (children, container, anchor, parentComponent, parentSuspense, isSVG, optimized, start = 0) => {for (let i = start; i < children.length; i++) {
    // 预处理 child
    const child = (children[i] = optimized
      ? cloneIfMounted(children[i])
      : normalizeVNode(children[i]))
    // 递归 patch 挂载 child
    patch(null, child, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
  }
}

子节点的挂载逻辑同样很简略,遍历 children 获取到每一个 child,而后递归执行 patch 办法挂载每一个 child。留神,这里有对 child 做预处理的状况(前面编译优化的章节会详细分析)。

能够看到,mountChildren 函数的第二个参数是 container,而咱们调用 mountChildren 办法传入的第二个参数是在 mountElement 时创立的 DOM 节点,这就很好地建设了父子关系。

另外,通过递归 patch 这种深度优先遍历树的形式,咱们就能够结构残缺的 DOM 树,实现组件的渲染。

解决完所有子节点后,最初通过 hostInsert 办法把创立的 DOM 元素节点挂载到 container 上,它在 Web 环境下这样定义:

function insert(child, parent, anchor) {if (anchor) {parent.insertBefore(child, anchor)
  }
  else {parent.appendChild(child)
  }
}

这里会做一个 if 判断,如果有参考元素 anchor,就执行 parent.insertBefore,否则执行 parent.appendChild 来把 child 增加到 parent 下,实现节点的挂载。

因为 insert 的执行是在解决子节点后,所以挂载的程序是先子节点,后父节点,最终挂载到最外层的容器上。

常识延长:嵌套组件

仔细的你可能会发现,在 mountChildren 的时候递归执行的是 patch 函数,而不是 mountElement 函数,这是因为子节点可能有其余类型的 vnode,比方组件 vnode。

在实在开发场景中,嵌套组件场景是再失常不过的了,后面咱们举的 App 和 Hello 组件的例子就是嵌套组件的场景。组件 vnode 次要保护着组件的定义对象,组件上的各种 props,而组件自身是一个形象节点,它本身的渲染其实是通过执行组件定义的 render 函数渲染生成的子树 vnode 来实现,而后再 patch。通过这种递归的形式,无论组件的嵌套层级多深,都能够实现整个组件树的渲染。

最初,用一张图来带你更加直观地感触下整个组件渲染流程:

转载自网络,若有不妥请分割删除!

退出移动版