站在伟人的肩膀上剖析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 类型步骤:
- 拿到vnode 尝试把它作为组件去解决,如果胜利的创立了组建,那阐明该vnode 就是组建的vnode
- 如果没有胜利创立,则查看 vnode.tag 是否有定义,如果有定义当作一般标签解决
- 如果 vnode.tag 没有定义则查看是否是凝视节点
- 如果不是正文节点,则把他当作文本看待。
以上这些判断都是在挂载(或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 能够分为四种
- children 是一个数组 h('ul', null, [h('li'),h('li')])
- children 是一个 vnode 对象 h('div', null, h('span'))
- children 是一个一般的文本字符串 h('div', null, '我是文本')
- 没有children h('div')
children 是数组能够分为两种,一种是有key、另一种就是无key的状况,都会被标记为KEYED_VNODES, 没有key的会调用 normalizeVNodes 进行人工干预生成key
// 多个子节点,且子节点应用keychildFlags = ChildrenFlags.KEYED_VNODESchildren = 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 采纳不同的挂载函数
接下来咱们将围绕这三个问题去实现一般标签元素渲染的过程
- VNode 被渲染为实在DOM之后,没有援用实在DOM元素
- 没有将 VNodeData 利用到实在DOM元素上
- 没有持续挂载子节点,即 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 和 childFlagsconst childFlags = vnode.childFlagsconst 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
纯文本
纯文本最简略,只须要将元素增加到页面上即可function mountText(vnode, container) { const el = document.createTextNode(vnode.children) vnode.el = el container.appendChild(el)}
- Fragment
对于Fragment 不须要渲染,只须要挂载children,如果有多个子节点的话遍历挂载调用mount即可,如果是空节点则创立一个空的文本节点 调用mountText 挂载。
Fragment 类型的 VNode 来说,当它被渲染为实在DOM之后,其 el 属性的援用
如果只有一个节点,那么 el 属性就指向该节点;如果有多个节点,则 el 属性值是第一个节点的援用;如果片段中没有节点,即空片段,则 el 属性援用的是占位的空文本节点元素
那么这样设计有什么意义呢?
在 patch 阶段对 dom 元素进行挪动时,应该确保其放到正确的地位,而不应该始终应用 appendChild 函数,有时须要 insertBefore 函数,这时候咱们就须要拿到相应的节点利用,这时候 vnode.el 属性是必不可少的,即便 fragment 没有子节点咱们仍然须要一个占位的空文本节点作为地位的援用。
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)的更新为例,如上代码所展现的更新过程是:
- 遍历新的款式数据,将新的款式数据全副利用到元素上
- 遍历旧的款式数据,将那些不存在新的款式数据中的款式从元素上移除
子节点的更新,次要是在patchElement函数中递归的调用patchChildren。留神对于子节点的比拟只能是 同层级
的比拟。
// 调用 patchChildren 函数递归地更新子节点 patchChildren( prevVNode.childFlags, // 旧的 VNode 子节点的类型 nextVNode.childFlags, // 新的 VNode 子节点的类型 prevVNode.children, // 旧的 VNode 子节点 nextVNode.children, // 新的 VNode 子节点 el // 以后标签元素,即这些子节点的父节点 )
因为子节点的状态总共能够分为三种,一种是没有子节点,一种是子节点只有一个,最初一种就是子节点多个的状况,子节点同级比拟因而就会呈现九种状况。
实际上在整个新旧 children 的比对中,只有当新旧子节点都是多个子节点时才有必要进行真正的外围 diff,从而尽可能的复用子节点。 前面有章节也会着重解说diff如何尽可能的复用子节点。
更新纯文本、Fragment 和 Portal
纯文本
纯文本的更新能够通过 DOM 对象的 nodeValue 属性读取或设置文本节点(或正文节点)的内容function patchText(prevVNode, nextVNode) { // 拿到文本元素 el,同时让 nextVNode.el 指向该文本元素 const el = (nextVNode.el = prevVNode.el) // 只有当新旧文本内容不统一时才有必要更新 if (nextVNode.children !== prevVNode.children) { el.nodeValue = nextVNode.children }}
- 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
- 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) }}
能够回顾一下组件的挂载步骤:
- 创立组件的实例
- 调用组件的 render 取得vnode
- 将 vnode 挂载到容器元素上
- 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()}
组件的更新大抵能够分为三步:
- 获取旧的vnode
- 从新调用render 函数产生新的 vnode
- 调用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 函数进行更新```jsfunction 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生命周期函数将会执行。```jsfunction 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阶段传递过来```jsfunction 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 函数即可。```jsvnode.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元素