乐趣区

关于vue.js:站在巨人的肩膀上分析vue3渲染过程

站在伟人的肩膀上剖析 vue3,次要用来记录本人看完 HcySunYang 文档的读后感并且加深本人的了解。倡议大家浏览 原文地址

一个组件最外围的就是 render 函数,残余的其余内容,如 data、compouted、props 等都是为 render 函数提供数据起源服务的,render 函数能够产出 Virtual DOM。

Virtual DOM 最终都要渲染成实在的 DOM,这个过程就叫做 patch。

什么是 Vnode

vue 首先会将 template 进行编译,这其中包含 parse、optimize、generate 三个过程。
parse 会应用正则等形式解析 template 模版中的指令、class、style 等数据,造成 AST。

<ul id='list' class='item'>
  <li class='item1'>Item 1</li>
  <li class='item3' style='font-size: 20px'>Item 2</li>
</ul>
var element = {
  tag: 'ul', // 节点标签名
  data: { // DOM 的属性,用一个对象存储键值对
    class: 'item',
    id: 'list'
  },
  children: [ // 该节点的子节点
    {tag: 'li', data: {class: 'item1'}, children: {tag: null, data: null, children: "Item 1"}},
    {tag: 'li', data: {class: 'item3', style: 'font-size: 20px'}, children: {tag: null, data: null, children: "Item 2"}},
  ]
}

如下面代码所述,一个 template 模版能够用 AST 语法树进行描述。咱们应用 tag 属性来存储标签的名字,用 data 属性来存储该标签的附加信息,比方 style、class、事件等。children 用来形容子节点。

Vnode 品种

下面讲述的是一些一般 HTML 标签,比方 div、span、p 这种,然而在理论的代码开发过程中咱们会抽离出很多组件

<div>
  <MyComponent />
</div>

像这种组件依然须要应用 VNode 来形容 <MyComponent/>。并给此类用来形容组件的 VNode 增加一个标识,以便在挂载的时候有方法辨别一个 VNode 到底是一般的 html 标签还是组件。

const elementVNode = {
  tag: 'div',
  data: null,
  children: {
    tag: MyComponent,
    data: null
  }
}

因而咱们能够应用 tag 来判断将要挂载的内容,通过不同的渲染函数去渲染对应的 HTML 构造。

组件能够分为两类,一种是函数式组件,一种是有状态组件。函数式组件只是一个纯函数,没有本身的状态,只接管内部数据;有组态组件是一个类,须要实例化,有本人的状态。

// 函数式组件
function MyComponent(props) {}

// 有状态组件
class MyComponent {}

除组件之外,还有两种类型须要形容,即 Fragment 和 Portal。

在 vue3 中 template 曾经不须要一个大盒子进行包裹所有的 html 内容了,也就是咱们简称的根元素。

<template>
  <td></td>
  <td></td>
  <td></td>
</template>

模板中不仅仅只有一个 td 标签,而是有多个 td 标签,即多个根元素,须要引入一个形象元素,也就是咱们要介绍的 Fragment。将 tag 标记为 Fragment,只须要把该 VNode 的子节点渲染到页面。

再来看看 Portal,什么是 Portal 呢?就是把子节点渲染到给定的指标。

const portalVNode = {
  tag: Portal,
  data: {target: '#app-root'},
  children: {
    tag: 'div',
    data: {class: 'overlay'}
  }
}

无论在何处应用 <Overlay/> 组件,它都会把内容渲染到 id=”app-root” 的元素下。

总的来说,咱们能够把 VNode 分成五类,别离是:html/svg 元素、组件、纯文本、Fragment 以及 Portal:

优化:flags 作为 VNode 的标识

既然 VNode 有类别之分,咱们就有必要应用一个惟一的标识,来表明某一个 VNode 属于哪一类。同时给 VNode 增加 flags 也是 Virtual DOM 算法的优化伎俩之一。

在 vue2 中辨别 vnode 类型步骤:

  1. 拿到 vnode 尝试把它作为组件去解决,如果胜利的创立了组建,那阐明该 vnode 就是组建的 vnode
  2. 如果没有胜利创立,则查看 vnode.tag 是否有定义,如果有定义当作一般标签解决
  3. 如果 vnode.tag 没有定义则查看是否是凝视节点
  4. 如果不是正文节点,则把他当作文本看待。

以上这些判断都是在挂载 (或 patch) 阶段进行的,换句话说,一个 VNode 到底形容的是什么是在挂载或 patch 的时候才晓得的。这就带来了两个难题:无奈从 AOT 的层面优化、开发者无奈手动优化。

为了解决这个问题,咱们的思路是在 VNode 创立的时候就把该 VNode 的类型通过 flags 表明,这样在挂载或 patch 阶段通过 flags 能够间接防止掉很多耗费性能的判断。这也是咱们须要讲的渲染器

// mount 函数的作用是把一个 VNode 渲染成实在 DOM,依据不同类型的 VNode 须要采纳不同的挂载形式
export function mount(vnode, container) {const { flags} = vnode
  if (flags & VNodeFlags.ELEMENT) {
    // 挂载一般标签
    mountElement(vnode, container)
  } else if (flags & VNodeFlags.COMPONENT) {
    // 挂载组件
    mountComponent(vnode, container)
  } else if (flags & VNodeFlags.TEXT) {
    // 挂载纯文本
    mountText(vnode, container)
  } else if (flags & VNodeFlags.FRAGMENT) {
    // 挂载 Fragment
    mountFragment(vnode, container)
  } else if (flags & VNodeFlags.PORTAL) {
    // 挂载 Portal
    mountPortal(vnode, container)
  }
}

至此,咱们曾经对 VNode 实现了肯定的设计,目前为止咱们所设计的 VNode 对象如下:

export interface VNode {
  // _isVNode 属性在上文中没有提到,它是一个始终为 true 的值,有了它,咱们就能够判断一个对象是否是 VNode 对象
  _isVNode: true
  // el 属性在上文中也没有提到,当一个 VNode 被渲染为实在 DOM 之后,el 属性的值会援用该实在 DOM
  el: Element | null
  flags: VNodeFlags
  tag: string | FunctionalComponent | ComponentClass | null
  data: VNodeData | null
  children: VNodeChildren
  childFlags: ChildrenFlags
}

h 函数创立 VNode

组件 的 flags 类型

下面讲到了如何应用 vnode 去形容一个 template 构造,咱们能够用函数去管制如何主动生成 vnode,这也是 vue 的一个外围 api,vue3 h 函数

h 函数返回一个”虚构节点“,通常缩写为 VNode:一个一般对象,其中蕴含向 Vue 形容它应在页面上渲染哪种节点的信息,包含所有子节点的形容。它的目标是用于手动编写的渲染函数:

render() {return h('h1', null, '')
}
function h() {
  return {
    _isVNode: true,
    flags: VNodeFlags.ELEMENT_HTML,
    tag: 'h1',
    data: null,
    children: null,
    childFlags: ChildrenFlags.NO_CHILDREN,
    el: null
  }
}

h 函数返回代码中的一些 vnode 信息,须要承受三个参数 tag、data 和 children。其中只须要确定 flags 和 childFlags 类型即可,其余的都是默认或者通过参数传递。

通过 tag 来确定 flags

须要留神的一点是 :在 vue2 中用一个对象(object) 作为组件的形容,在 vue3 中,有状态的组件是一个继承基类的类,是 Vue2 的对象式组件,咱们通过查看该对象的 functional 属性的虚实来判断该组件是否是函数式组件。在 Vue3 中,因为有状态组件会继承基类,所以通过原型链判断其原型中是否有 render 函数的定义来确定该组件是否是有状态组件

// 兼容 Vue2 的对象式组件
if (tag !== null && typeof tag === 'object') {
  flags = tag.functional
    ? VNodeFlags.COMPONENT_FUNCTIONAL       // 函数式组件
    : VNodeFlags.COMPONENT_STATEFUL_NORMAL  // 有状态组件
} else if (typeof tag === 'function') {
  // Vue3 的类组件
  flags = tag.prototype && tag.prototype.render
    ? VNodeFlags.COMPONENT_STATEFUL_NORMAL  // 有状态组件
    : VNodeFlags.COMPONENT_FUNCTIONAL       // 函数式组件
}

children 的 flags 类型

children 能够分为四种

  1. children 是一个数组 h(‘ul’, null, [h(‘li’),h(‘li’)])
  2. children 是一个 vnode 对象 h(‘div’, null, h(‘span’))
  3. children 是一个一般的文本字符串 h(‘div’, null, ‘ 我是文本 ’)
  4. 没有 children h(‘div’)

children 是数组能够分为两种,一种是有 key、另一种就是无 key 的状况,都会被标记为 KEYED_VNODES, 没有 key 的会调用 normalizeVNodes 进行人工干预生成 key

// 多个子节点,且子节点应用 key
childFlags = ChildrenFlags.KEYED_VNODES
children = normalizeVNodes(children)

render 函数 渲染 Vnode 成实在 DOM

渲染器的工作次要分为两个阶段:mount, path。如果旧的 vnode 存在,则会应用新的 vnode 与旧的 vnode 进行比照,试图以最小的资源开销实现 DOM 更新,这个过程就叫做patch,如果旧的 vnode 不存在,则间接将新的 vnode 挂载成全新的 DOM, 这给过程叫mount

render 函数接管两个参数,第一个参数是将要被渲染的 vnode 对象,第二个参数是一个用来承载内容的容器,通常也叫挂载点,

function render(vnode, container) {
  const prevVNode = container.vnode
  if (prevVNode == null) {if (vnode) {
      // 没有旧的 VNode,只有新的 VNode。应用 `mount` 函数挂载全新的 VNode
      mount(vnode, container)
      // 将新的 VNode 增加到 container.vnode 属性下,这样下一次渲染时旧的 VNode 就存在了
      container.vnode = vnode
    }
  } else {if (vnode) {
      // 有旧的 VNode,也有新的 VNode。则调用 `patch` 函数打补丁
      patch(prevVNode, vnode, container)
      // 更新 container.vnode
      container.vnode = vnode
    } else {
      // 有旧的 VNode 然而没有新的 VNode,这阐明应该移除 DOM,在浏览器中能够应用 removeChild 函数。container.removeChild(prevVNode.el)
      container.vnode = null
    }
  }
}
旧的 vnode 新的 vnode 操作
调用 mount 函数
移除 DOM
调用 patch 函数

渲染器的责任十分之大,是因为它不仅仅是一个把 VNode 渲染成实在 DOM 的工具,它还负责以下工作:

  • 管制局部组件生命周期钩子的调用,组件的挂载,卸载调用机会。
  • 多端渲染的桥梁。
    自定义渲染器的实质就是把特定平台操作 dom 的办法从外围算法中抽离,并提供可配置的计划
  • 异步渲染有间接的关系
    vue3 的异步渲染是基于调度器的实现,若要实现异步渲染,组件的挂载酒不能同步进行,dom 的变更就要在适合的机会,???
  • 蕴含最外围的算法 Diff 算法

渲染一般标签元素

下面在讲 flags 的时候也说过,不同 tag 会被 h 函数打上 flags, 通过 flags 的不同咱们就能够辨别出须要渲染的内容是什么类型。不同的 vnode 采纳不同的挂载函数

接下来咱们将围绕这三个问题去实现一般标签元素渲染的过程

  1. VNode 被渲染为实在 DOM 之后,没有援用实在 DOM 元素
  2. 没有将 VNodeData 利用到实在 DOM 元素上
  3. 没有持续挂载子节点,即 children

问题 1

function mountElement(vnode, container) {const el = document.createElement(vnode.tag)
  vnode.el = el
  container.appendChild(el)
}

问题 2 通过遍历 VNodeData,switch 取值作用到元素上

// 拿到 VNodeData
  const data = vnode.data
  if (data) {
    // 如果 VNodeData 存在,则遍历之
    for(let key in data) {
      // key 可能是 class、style、on 等等
      switch(key) {
        case 'style':
          // 如果 key 的值是 style,阐明是内联款式,一一将款式规定利用到 el
          for(let k in data.style) {el.style[k] = data.style[k]
          }
        break
      }
    }
  }

问题 2 递归挂载子节点

// 拿到 children 和 childFlags
const childFlags = vnode.childFlags
const children = vnode.children
// 检测如果没有子节点则无需递归挂载
if (childFlags !== ChildrenFlags.NO_CHILDREN) {if (childFlags & ChildrenFlags.SINGLE_VNODE) {
      // 如果是单个子节点则调用 mount 函数挂载
      mount(children, el)
    } else if (childFlags & ChildrenFlags.MULTIPLE_VNODES) {
      // 如果是单多个子节点则遍历并调用 mount 函数挂载
      for (let i = 0; i < children.length; i++) {mount(children[i], el)
      }
}

arrtibutes 跟 props 的区别:浏览器在加载页面之后会对页面中的标签进行解析,并生成与之相符的 DOM 对象,每个标签中都可能蕴含一些属性,如果这些属性是规范属性,那么解析生成的 DOM 对象中也会蕴含与之对应的属性。如果是非规范属性,那么则会当作是 props 解决

对于其余的比方 class、arrtibutes、props、事件的解决能够移步文档

渲染纯文本、Fragment 和 Portal

  1. 纯文本
    纯文本最简略,只须要将元素增加到页面上即可

    function mountText(vnode, container) {const el = document.createTextNode(vnode.children)
      vnode.el = el
      container.appendChild(el)
    }
  2. Fragment
    对于 Fragment 不须要渲染,只须要挂载 children, 如果有多个子节点的话遍历挂载调用 mount 即可,如果是空节点则创立一个空的文本节点 调用 mountText 挂载。

Fragment 类型的 VNode 来说,当它被渲染为实在 DOM 之后,其 el 属性的援用

如果只有一个节点,那么 el 属性就指向该节点;如果有多个节点,则 el 属性值是第一个节点的援用;如果片段中没有节点,即空片段,则 el 属性援用的是占位的空文本节点元素

那么这样设计有什么意义呢?

在 patch 阶段对 dom 元素进行挪动时,应该确保其放到正确的地位,而不应该始终应用 appendChild 函数,有时须要 insertBefore 函数,这时候咱们就须要拿到相应的节点利用,这时候 vnode.el 属性是必不可少的,即便 fragment 没有子节点咱们仍然须要一个占位的空文本节点作为地位的援用。

  1. Portal
    Portal 挂载跟 Fragment 一样,只须要特地留神的是 Portal 的 tag 是挂载点。

    Portal 类型的 VNode 其 el 属性应该指向谁

protal 所形容的内容能够挂载到任何地位,但依然须要一个占位元素,并且 protal 类型的 vnode 的 el 属性应该指向该占位元素 这是因为 Portal 的另外一个个性:尽管 Portal 的内容能够被渲染到任意地位,但它的行为依然像一般的 DOM 元素一样,如事件的捕捉 / 冒泡机制依然依照代码所编写的 DOM 构造施行。要实现这个性能就必须须要一个占位的 DOM 元素来承接事件。但目前来说,咱们用一个空的文本节点占位即可

渲染组件

还是通过 vnode.flags 来判断挂载的 vnode 是否属于有状态组件还是函数组件。

挂载一个有状态组件,也就是 class 类组件

class MyComponent {render() {
    return h(
      'div',
      {
        style: {background: 'green'}
      },
      [h('span', null, '我是组件的题目 1......'),
        h('span', null, '我是组件的题目 2......')
      ]
    )
  }
}
// 组件挂载
function mountStatefulComponent(vnode, container) {
  // 创立组件实例
  const instance = new vnode.tag()
  // 渲染 VNode
  instance.$vnode = instance.render()
  // 挂载
  mount(instance.$vnode, container)
  // el 属性值 和 组件实例的 $el 属性都援用组件的根 DOM 元素
  instance.$el = vnode.el = instance.$vnode.el
}

函数式组件间接返回 vnode 的函数

function MyFunctionalComponent() {
  // 返回要渲染的内容形容,即 VNode
  return h(
    'div',
    {
      style: {background: 'green'}
    },
    [h('span', null, '我是组件的题目 1......'),
      h('span', null, '我是组件的题目 2......')
    ]
  )
}

如下是 函数式组件 mountFunctionalComponent 函数的实现:

function mountFunctionalComponent(vnode, container, isSVG) {
  // 获取 VNode
  const $vnode = vnode.tag()
  // 挂载
  mount($vnode, container)
  // el 元素援用该组件的根元素
  vnode.el = $vnode.el
}

patch 函数更新渲染的 DOM

上一节讲没有旧的 vnode,应用 mount 函数挂载全新的 vnode。那么有 vnode 应该以何种适合的形式更新 DOM,这也就是咱们常说的 patch。

当应用 render 渲染一个全新的 vnode, 会调用 mount 函数挂载该 vnode,同时让容器元素存储该 vnode 对象的援用。这样在此调用渲染器渲染新的 vnode 对象到雷同的容器元素,因为旧的 vnode 曾经存在,所以会调用 patch 函数以适合的形式进行更新

计划一,vnode 之间有类型之分,只有雷同类型的 vnode 才有比照意义,如果不同,最优的计划就是用新的 vnode 替换旧的 vnode。如果新旧 VNode 的类型雷同,则依据不同的类型调用不同的比对函数

更新一般标签元素

计划二,不同的标签渲染的内容不同,比照没有意义

例如 ul 标签下只能渲染 li 标签,所以拿 ul 标签和一个 div 标签进行比对是没有任何意义的,这种状况下咱们不会对旧的标签元素打补丁,而是应用新的标签元素替换旧的标签元素。

如果新旧 vnode 标签雷同,那么不同的只有 VNodeData 和 children。实质上还是对这两个值做比照。

更新 VNodeData 时的思路分为以下几步:

  • 第 1 步:当新的 VNodeData 存在时,遍历新的 VNodeData。
  • 第 2 步:依据新的 VNodeData 中的 key,分贝尝试读取旧值和新值。即 prevValue 和 nextValue
  • 第 3 步:应用 switch…case 语句匹配不同的数据进行不同的更新操作。

以款式 (style) 的更新为例,如上代码所展现的更新过程是:

  1. 遍历新的款式数据,将新的款式数据全副利用到元素上
  2. 遍历旧的款式数据,将那些不存在新的款式数据中的款式从元素上移除

子节点的更新,次要是在 patchElement 函数中递归的调用 patchChildren。留神对于子节点的比拟只能是 同层级 的比拟。

// 调用 patchChildren 函数递归地更新子节点
  patchChildren(
    prevVNode.childFlags, // 旧的 VNode 子节点的类型
    nextVNode.childFlags, // 新的 VNode 子节点的类型
    prevVNode.children,   // 旧的 VNode 子节点
    nextVNode.children,   // 新的 VNode 子节点
    el                    // 以后标签元素,即这些子节点的父节点
  )

因为子节点的状态总共能够分为三种,一种是没有子节点,一种是子节点只有一个,最初一种就是子节点多个的状况,子节点同级比拟因而就会呈现九种状况。

实际上在整个新旧 children 的比对中,只有当新旧子节点都是多个子节点时才有必要进行真正的外围 diff,从而尽可能的复用子节点。 前面有章节也会着重解说 diff 如何尽可能的复用子节点。

更新纯文本、Fragment 和 Portal

  1. 纯文本
    纯文本的更新能够通过 DOM 对象的 nodeValue 属性读取或设置文本节点 (或正文节点) 的内容

    function patchText(prevVNode, nextVNode) {
      // 拿到文本元素 el,同时让 nextVNode.el 指向该文本元素
      const el = (nextVNode.el = prevVNode.el)
      // 只有当新旧文本内容不统一时才有必要更新
      if (nextVNode.children !== prevVNode.children) {el.nodeValue = nextVNode.children}
    }
  2. Fragment
    因为 Fragment 没有包裹元素,只有子节点,所以咱们对 Fragment 的更新实质上就是更新两个片段的“子节点”。间接调用标签元素的 patchChildren 函数,只须要留神 el 的指向。
  • 如果新的片段 children 是单个子节点,则意味着其 vnode.children 属性的值就是 VNode 对象 nextVNode.el = nextVNode.children.el
  • 如果新的片段 children 是空文本节点。prevVNode.el 属性援用就是该空文本节点 nextVNode.el = prevVNode.el
  • 如果新的片段 children 是多个子节点。nextVNode.el = nextVNode.children[0].el
  1. Portal
    portal 也是一样的,没有元素包裹只须要比拟子节点,并且留神 el 指向就能够nextVNode.el = prevVNode.el

如果新旧容器不同,才须要搬运。这块也就不扩大了,感兴趣的能够查看文档

更新组件

更新有状态组件

有状态组件更新能够分为两种,一种是 被动更新 被动更新

被动更新:就是组件本身的状态产生扭转所导致的更新。例如 data 的变动等等状况

被动更新:就是组件内部因素导致的,例如 props 的扭转

1. 被动更新

当组件的状态变动时,咱们须要做的就是从新执行渲染函数并产出新的 vnode,最初通过新旧 vnode 实现真是 dom 的更新。

比方咱们须要更新这样的组件该如何做呢?

class MyComponent {
  // 本身状态 or 本地状态
  localState = 'one'

  // mounted 钩子
  mounted() {// 两秒钟之后批改本地状态的值,并从新调用 _update() 函数更新组件
    setTimeout(() => {
      this.localState = 'two'
      this._update()}, 2000)
  }

  render() {return h('div', null, this.localState)
  }
}

能够回顾一下组件的挂载步骤:

  1. 创立组件的实例
  2. 调用组件的 render 取得 vnode
  3. 将 vnode 挂载到容器元素上
  4. el 属性值 和 组件实例的 $el 属性都援用组件的根 DOM 元素

咱们将所有的操作都封装到一个_update 函数里。

function mountStatefulComponent(vnode, container, isSVG) {
  // 创立组件实例
  const instance = new vnode.tag()

  instance._update = function() {
    // 1、渲染 VNode
    instance.$vnode = instance.render()
    // 2、挂载
    mount(instance.$vnode, container, isSVG)
    // 4、el 属性值 和 组件实例的 $el 属性都援用组件的根 DOM 元素
    instance.$el = vnode.el = instance.$vnode.el
    // 5、调用 mounted 钩子
    instance.mounted && instance.mounted()}

  instance._update()}

当更新时只须要再次调用_update 函数即,那么如何须要去判断一个组件是第一次渲染还是须要更新你,通过设置一个变量_mounted boolean 类型用来标记即可。

function mountStatefulComponent(vnode, container, isSVG) {
  // 创立组件实例
  const instance = new vnode.tag()

  instance._update = function() {
    // 如果 instance._mounted 为真,阐明组件已挂载,应该执行更新操作
    if (instance._mounted) {
      // 1、拿到旧的 VNode
      const prevVNode = instance.$vnode
      // 2、重渲染新的 VNode
      const nextVNode = (instance.$vnode = instance.render())
      // 3、patch 更新
      patch(prevVNode, nextVNode, prevVNode.el.parentNode)
      // 4、更新 vnode.el 和 $el
      instance.$el = vnode.el = instance.$vnode.el
    } else {
      // 1、渲染 VNode
      instance.$vnode = instance.render()
      // 2、挂载
      mount(instance.$vnode, container, isSVG)
      // 3、组件已挂载的标识
      instance._mounted = true
      // 4、el 属性值 和 组件实例的 $el 属性都援用组件的根 DOM 元素
      instance.$el = vnode.el = instance.$vnode.el
      // 5、调用 mounted 钩子
      instance.mounted && instance.mounted()}
  }

  instance._update()}

组件的更新大抵能够分为三步:

  1. 获取旧的 vnode
  2. 从新调用 render 函数产生新的 vnode
  3. 调用 patch 函数比照新旧 vnode

2. 被动更新

咱们能够在组件实例创立之后立刻初始化组件的 props,
instance.$props = vnode.data 这样子组件中就能够通过 this.$props.text 拜访从父组件传递进来的 props 数据

举个案例:

第一次渲染产出的 VNode 是:

const prevCompVNode = h(ChildComponent, {text: 'one'})
````

第二次渲染产出的 VNode 是:

const prevCompVNode = h(ChildComponent, {
text: ‘two’
})

因为渲染进去的 tag 都是组件,所以在 patch 函数外部会调用 patchComponent 函数进行更新
```js
function patchComponent(prevVNode, nextVNode, container) {
  // 查看组件是否是有状态组件
  if (nextVNode.flags & VNodeFlags.COMPONENT_STATEFUL_NORMAL) {
    // 1、获取组件实例
    const instance = (nextVNode.children = prevVNode.children)
    // 2、更新 props
    instance.$props = nextVNode.data
    // 3、更新组件
    instance._update()}
}
```
有状态组件更新能够分为三步:1. 通过 prevVNode.childredn 拿到组件实例
2. 更新 props,应用新的 VNodeData 从新设置组件实例的 `$props` 属性
3. 因为组件的 `$props` 曾经更新,所以在调用组件的 _update 办法,让组件从新渲染

> 不同的组件类型,则须要移除从新渲染,组件的 unmounted 生命周期函数将会执行。```js
function replaceVNode(prevVNode, nextVNode, container) {container.removeChild(prevVNode.el)
  // 如果将要被移除的 VNode 类型是组件,则须要调用该组件实例的 unmounted 钩子函数
  if (prevVNode.flags & VNodeFlags.COMPONENT_STATEFUL_NORMAL) {
    // 类型为有状态组件的 VNode,其 children 属性被用来存储组件实例对象
    const instance = prevVNode.children
    instance.unmounted && instance.unmounted()}
  mount(nextVNode, container)
}
```

** 特别强调 `shouldUpdateComponent`**

在 vue2 中没有 shouldUpdateComponent 这个生命周期。在某些状况下,组件不须要更新,然而组件仍旧跑了一次 update。因而咱们应用 patchFlag 和 props 的简略比照等形式来决定是否 update。这就是 shouldUpdateComponent 的作用。#### 更新函数式组件
无论是有状态组件和函数式组件都是组件通过执行 `_update` 产出新旧 vnode 做比照,从而实现更新。1. 函数式组件承受 props 只能在 mount 阶段传递过来
```js
function mountFunctionalComponent(vnode, container, isSVG) {
  // 获取 props
  const props = vnode.data
  // 获取 VNode 执行函数并将 props 传递到函数中
  const $vnode = (vnode.children = vnode.tag(props))
  // 挂载
  mount($vnode, container, isSVG)
  // el 元素援用该组件的根元素
  vnode.el = $vnode.el
}
```

2. 函数式组件通过定义一个函数在 VNode 中定义一个 handle 函数将整个挂载的过程都实现,下次更新的时候只须要执行 vnode 的 handle 函数即可。```js
vnode.handle = {
  prev: null,
  next: vnode,
  container,
  update() {/*...*/}
}
```
参数阐明:- prev:存储旧的函数式组件 vnode,在首次挂载时,没有旧的 vnode
- next:存储新的函数式组件 vnode, 在首次挂载时,被赋值为以后正在挂载的函数式组件
- container:存储的挂载容器

具体的实现过程也根本和有状态组件相似,具体的可参考[文档](http://hcysun.me/vue-design/zh/renderer-patch.html#%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BB%84%E4%BB%B6%E7%9A%84%E6%9B%B4%E6%96%B0)

## 下一节
下一节次要介绍 diff 算法如果尽可能的复用 dom 元素
退出移动版