2
头图
站在巨人的肩膀上分析vue3,主要用来记录自己看完HcySunYang文档的读后感并且加深自己的理解。建议大家阅读 原文地址

一个组件最核心的就是 render 函数,剩余的其他内容,如data、compouted、props 等都是为render函数提供数据来源服务的,render函数可以产出 Virtual DOM。

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

download.png

什么是 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:

download-1.png

优化: 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 采用不同的挂载函数

download-2.png

接下来我们将围绕这三个问题去完成普通标签元素渲染的过程

  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 的类型相同,则根据不同的类型调用不同的比对函数

download-3.png

更新普通标签元素

方案二,不同的标签渲染的内容不同,对比没有意义

例如 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                    // 当前标签元素,即这些子节点的父节点
  )

因为子节点的状态总共可以分为三种,一种是没有子节点,一种是子节点只有一个,最后一种就是子节点多个的情况,子节点同级比较因此就会出现九种情况。

download-4.png

download-6.png

download-5.png

实际上在整个新旧 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元素

WsmDyj
1.7k 声望493 粉丝

如何成为灭霸选中的人