站在巨人的肩膀上分析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
// 多个子节点,且子节点使用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 采用不同的挂载函数
接下来我们将围绕这三个问题去完成普通标签元素渲染的过程
- 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 和 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
纯文本
纯文本最简单,只需要将元素添加到页面上即可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 函数进行更新
```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元素
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。