最近在深入学习 Virtual DOM
的相关知识,参考了许多资料,也拜读了许多大神的文章,所以在这里大致的整理成了比较适合自己理解的方式,方便时不时回来翻翻,复习一下,篇幅较长,主要会分为三个部分来分别进行介绍,具体章节如下,目录名就差不多代表了章节的相关内容
在上篇的 什么是 Virtual DOM?
章节当中我们介绍过 Virtual DOM
的相关概念,以及如何将 Virtual DOM
渲染为真实的 DOM
节点和一个辅助创建 VNode
的 h
函数,本章是第二部分,我们就接着之前的内容来介绍渲染器相关内容,参考的是 HcySunYang/vue-design ,本章相关内容如下
什么是渲染器?
mount
阶段
挂载普通标签元素
挂载文本节点
挂载 Fragment
挂载 Portal
挂载有状态组件
挂载函数式组件
patch
阶段
类型不同则替换 VNode
更新标签元素
更新文本节点
更新 Fragment
更新 Portal
更新有状态组件
更新函数式组
什么是渲染器? 所谓渲染器,简单的说就是将 Virtual DOM
渲染成特定平台下真实 DOM
的工具(就是一个函数,通常叫 render
),渲染器的工作流程通常分为两个阶段 mount
和 patch
如果旧的 VNode
存在,则会使用新的 VNode
与旧的 VNode
进行对比,试图以最小的资源开销完成 DOM
的更新,这个过程就叫 patch
(打补丁)
如果旧的 VNode
不存在,则直接将新的 VNode
挂载成全新的 DOM
,这个过程叫做 mount
渲染器通常接收两个参数,第一个参数是将要被渲染的 VNode
对象,第二个参数是一个用来承载内容的容器(container
),通常也叫挂载点,如下代码所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 export default function render (vnode, container ) { const prevVNode = container.vnode if (prevVNode == null ) { if (vnode) { mount(vnode, container) container.vnode = vnode } } else { if (vnode) { patch(prevVNode, vnode, container) container.vnode = vnode } else { container.removeChild(prevVNode.el) container.vnode = null } } }
当然渲染器不仅仅是一个把 VNode
渲染成真实 DOM
的工具,它还可以负责控制部分组件生命周期钩子的调用,又与异步渲染有直接关系,但是在这里,我们主要介绍的是其挂载和更新的操作,至于核心的 Diff
算法将会放到下一章单独介绍
mount 阶段 在前面已经简单的介绍过了,渲染器的工作流程通常分为两个阶段 mount
和 patch
,那我们下面就来看看如何使用 mount
函数挂载全新的 VNode
挂载普通标签元素 这部分我们主要探讨渲染器的 mount
函数是如何把 VNode
渲染成真实 DOM
的,mount
函数的作用是把一个 VNode
渲染成真实 DOM
,根据不同类型的 VNode
需要采用不同的挂载方式,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function mount (vnode, container, isSVG, refNode ) { const { flags } = vnode if (flags & VNodeFlags.ELEMENT) { mountElement(vnode, container, isSVG, refNode) } else if (flags & VNodeFlags.COMPONENT) { mountComponent(vnode, container, isSVG) } else if (flags & VNodeFlags.TEXT) { mountText(vnode, container) } else if (flags & VNodeFlags.FRAGMENT) { mountFragment(vnode, container, isSVG) } else if (flags & VNodeFlags.PORTAL) { mountPortal(vnode, container, isSVG) } }
我们根据 VNode
的 flags
属性值能够区分一个 VNode
对象的类型,不同类型的 VNode
采用不同的挂载函数
在之前的章节当中,我们曾经简单的实现过一个 mountElement
的方法
1 2 3 4 function mountElement (vnode, container ) { const el = document .createElement(vnode.tag) container.appendChild(el) }
这是一个极简的用于挂载普通标签元素的 mountElement
函数,但它具有以下缺陷
VNode
被渲染为真实 DOM
之后,没有引用真实 DOM
元素
没有将 VNodeData
应用到真实 DOM
元素上
没有继续挂载子节点,即 children
不能严谨地处理 SVG
标签
这里有一点需要说明,我们之所以设计 vnode.el
这个字段
是因为在 patch
阶段对 DOM
元素进行移动时,应该确保将其放到正确的位置,而不应该始终使用 appendChild
函数 有时需要使用 insertBefore
函数,这时候我们就需要拿到相应的节点引用,这时候 vnode.el
属性是必不可少的
下面我们就来针对上面的四个问题,我们逐个去解决,代码已经整合,详细的可以参见注释部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 function mountElement (vnode, container, isSVG ) { const domPropsRE = /\W|^(?:value|checked|selected|muted)$/ isSVG = isSVG || vnode.flags & VNodeFlags.ELEMENT_SVG const el = isSVG ? document .createElementNS('http://www.w3.org/2000/svg' , vnode.tag) : document .createElement(vnode.tag) vnode.el = el const data = vnode.data if (data) { for (let key in data) { switch (key) { case 'style' : for (let k in data.style) { el.style[k] = data.style[k] } break case 'class' : if (isSVG) { el.setAttribute('class' , data[key]) } else { el.className = data[key] } break default : if (key[0 ] === 'o' && key[1 ] === 'n' ) { el.addEventListener(key.slice(2 ), data[key]) } else if (domPropsRE.test(key)) { el[key] = data[key] } else { el.setAttribute(key, data[key]) } break } } } const childFlags = vnode.childFlags const children = vnode.children if (childFlags !== ChildrenFlags.NO_CHILDREN) { if (childFlags & ChildrenFlags.SINGLE_VNODE) { mount(children, el, isSVG) } else if (childFlags & ChildrenFlags.MULTIPLE_VNODES) { for (let i = 0 ; i < children.length; i++) { mount(children[i], el, isSVG) } } } refNode ? container.insertBefore(el, refNode) : container.appendChild(el) }
自此,我们用于挂载普通标签元素的 mountElement
函数算是暂时告一段落,下面来看看如何挂载文本节点
挂载文本节点 如果一个 VNode
的类型是 VNodeFlags.TEXT
,那么 mount
函数会调用 mountText
函数挂载该纯文本元素,mountText
函数实现起来很简单,由于纯文本类型的 VNode
其 children
属性存储着与之相符的文本字符串,所以只需要调用 document.createTextNode
函数创建一个文本节点即可,然后将其添加到 container
中即可
1 2 3 4 5 function mountText (vnode, container ) { const el = document .createTextNode(vnode.children) vnode.el = el container.appendChild(el) }
挂载 Fragment 其实挂载 Fragment
和单纯地挂载一个 VNode
的 children
是没什么区别的,只是在没有 Fragment
的时候我们如果要想挂载一个片段,这个片段必须使用包裹元素包裹(比如 div
),有了 Fragment
则不需要包裹元素,实际上对于 Fragment
类型的 VNode
的挂载,就等价于只挂载一个 VNode
的 children
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 function mountFragment (vnode, container, isSVG ) { const { children, childFlags } = vnode switch (childFlags) { case ChildrenFlags.SINGLE_VNODE: mount(children, container, isSVG) vnode.el = children.el break case ChildrenFlags.NO_CHILDREN: const placeholder = createTextVNode('' ) mountText(placeholder, container) vnode.el = placeholder.el break default : for (let i = 0 ; i < children.length; i++) { mount(children[i], container, isSVG) } vnode.el = children[0 ].el } }
挂载 Portal 实际上 Portal
可以『不严谨地认为是可以被到处挂载』的 Fragment
,实现 Portal
的关键是要将其 VNode
的 children
中所包含的子 VNode
挂载到 tag
属性所指向的挂载点,『而非 container
』(这个很重要)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 function mountPortal (vnode, container ) { const { tag, children, childFlags } = vnode const target = typeof tag === 'string' ? document .querySelector(tag) : tag if (childFlags & ChildrenFlags.SINGLE_VNODE) { mount(children, target) } else if (childFlags & ChildrenFlags.MULTIPLE_VNODES) { for (let i = 0 ; i < children.length; i++) { mount(children[i], target) } } const placeholder = createTextVNode('' ) mountText(placeholder, container, null ) vnode.el = placeholder.el }
挂载有状态组件 组件还分为有状态组件和函数式组件,所以在 mountComponent
函数内部,我们需要再次对组件的类型进行区分,并使用不同的挂载方式
1 2 3 4 5 6 7 8 function mountComponent (vnode, container, isSVG ) { if (vnode.flags & VNodeFlags.COMPONENT_STATEFUL) { mountStatefulComponent(vnode, container, isSVG) } else { mountFunctionalComponent(vnode, container, isSVG) } }
我们先来看看有状态组件的挂载方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 function mountStatefulComponent (vnode, container, isSVG ) { const instance = (vnode.children = new vnode.tag()) instance.$props = vnode.data instance._update = function ( ) { if (instance._mounted) { const prevVNode = instance.$vnode const nextVNode = (instance.$vnode = instance.render()) patch(prevVNode, nextVNode, prevVNode.el.parentNode) instance.$el = vnode.el = instance.$vnode.el } else { instance.$vnode = instance.render() mount(instance.$vnode, container, isSVG) instance._mounted = true instance.$el = vnode.el = instance.$vnode.el instance.mounted && instance.mounted() } } instance._update() }
挂载函数式组件 在挂载函数式组件的时候,比挂载有状态组件少了一个实例化的过程,如果一个 VNode
描述的是函数式组件,那么其 tag
属性值就是该函数的引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 function mountFunctionalComponent (vnode, container, isSVG ) { vnode.handle = { prev: null , next: vnode, container, update: () => { if (vnode.handle.prev) { const prevVNode = vnode.handle.prev const nextVNode = vnode.handle.next const prevTree = prevVNode.children const props = nextVNode.data const nextTree = (nextVNode.children = nextVNode.tag(props)) patch(prevTree, nextTree, vnode.handle.container) } else { const props = vnode.data const $vnode = (vnode.children = vnode.tag(props)) mount($vnode, container, isSVG) vnode.el = $vnode.el } } } vnode.handle.update() }
以上就是 mount
阶段的全部内容了,主要就是针对各种不同的挂载类型来进行对应的处理,下面我们再来看看 patch
阶段
patch 阶段 在上面我们已经介绍过了渲染器的挂载逻辑,其实 mount
阶段的本质就是将各种类型的 VNode
渲染成真实 DOM
的过程,渲染器除了将全新的 VNode
挂载成真实 DOM
之外,它的另外一个职责是负责对新旧 VNode
进行比对,并以合适的方式更新 DOM
,也就是我们常说的 patch
当使用 render
渲染器渲染一个全新的 VNode
时,会调用 mount
函数挂载该 VNode
,同时让容器元素存储对该 VNode
对象的引用,这样当再次调用渲染器渲染新的 VNode
对象到相同的容器元素时,由于旧的 VNode
已经存在,所以会调用 patch
函数以合适的方式进行更新
1 2 3 4 5 6 7 8 9 10 11 const prevVNode = h('div' )const nextVNode = h('span' )render(prevVNode, document .getElementById('app' )) render(nextVNode, document .getElementById('app' ))
patch
函数会对新旧 VNode
进行比对,但是在这里,我们需要设定一定的规则,因为只有相同类型的 VNode
才有比对的意义,例如我们有两个 VNode
,其中一个 VNode
的类型是标签元素,而另一个 VNode
的类型是组件,当这两个 VNode
进行比对时,最优的做法是使用新的 VNode
完全替换旧的 VNode
,按照这个思路,我们先来实现一个基本版本的 patch
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function patch (prevVNode, nextVNode, container ) { const nextFlags = nextVNode.flags const prevFlags = prevVNode.flags if (prevFlags !== nextFlags) { replaceVNode(prevVNode, nextVNode, container) } else if (nextFlags & VNodeFlags.ELEMENT) { patchElement(prevVNode, nextVNode, container) } else if (nextFlags & VNodeFlags.COMPONENT) { patchComponent(prevVNode, nextVNode, container) } else if (nextFlags & VNodeFlags.TEXT) { patchText(prevVNode, nextVNode) } else if (nextFlags & VNodeFlags.FRAGMENT) { patchFragment(prevVNode, nextVNode, container) } else if (nextFlags & VNodeFlags.PORTAL) { patchPortal(prevVNode, nextVNode) } }
其核心原则是
如果类型不同,则直接调用 replaceVNode
函数使用新的 VNode
替换旧的 VNode
,否则根据不同的类型调用与之相符的比对函数
如下图所示
类型不同则替换 VNode 替换操作并不复杂,本质就是把旧的 VNode
所渲染的 DOM
移除,再挂载新的 VNode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function replaceVNode (prevVNode, nextVNode, container ) { container.removeChild(prevVNode.el) if (prevVNode.flags & VNodeFlags.COMPONENT_STATEFUL_NORMAL) { const instance = prevVNode.children instance.unmounted && instance.unmounted() } mount(nextVNode, container) }
更新标签元素 首先即使两个 VNode
的类型同为标签元素,但它们也可能是不同的标签,也就是说它们的 tag
属性值不尽相同,所以我们可以认为『不同的标签渲染的内容不同』,例如 ul
标签下只能渲染 li
标签,所以拿 ul
标签和一个 div
标签进行比对是没有任何意义的,这种情况下我们会使用新的标签元素替换旧的标签元素
1 2 3 4 5 6 7 function patchElement (prevVNode, nextVNode, container ) { if (prevVNode.tag !== nextVNode.tag) { replaceVNode(prevVNode, nextVNode, container) return } }
如果新旧 VNode
描述的是相同的标签,那两个 VNode
之间的差异就只会出现在 VNodeData
和 children
上,所以这里主要分为两部分
VNodeData
的比对
children
的比对
我们首先先来看一下如何更新 VNodeData
,在这里我们遵循的原则是
将新的 VNodeData
全部应用到元素上,再把那些已经不存在于新的 VNodeData
上的数据从元素上移除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 function patchElement (prevVNode, nextVNode, container ) { if (prevVNode.tag !== nextVNode.tag) { replaceVNode(prevVNode, nextVNode, container) return } const el = (nextVNode.el = prevVNode.el) const prevData = prevVNode.data const nextData = nextVNode.data if (nextData) { for (let key in nextData) { const prevValue = prevData[key] const nextValue = nextData[key] switch (key) { case 'style' : for (let k in nextValue) { el.style[k] = nextValue[k] } for (let k in prevValue) { if (!nextValue.hasOwnProperty(k)) { el.style[k] = '' } } break default : break } } } }
是不是有一点似成相识的感觉,实际上无论是 mountElement
函数中用来处理 VNodeData
的代码还是 patchElement
函数中用来处理 VNodeData
的代码,它们的本质都是将 VNodeData
中的数据应用到 DOM
元素上,唯一的区别就是在 mountElement
函数中没有旧数据可言,所以我们可以来稍微的封装一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 function patchElement (prevVNode, nextVNode, container ) { if (prevVNode.tag !== nextVNode.tag) { replaceVNode(prevVNode, nextVNode, container) return } const el = (nextVNode.el = prevVNode.el) const prevData = prevVNode.data const nextData = nextVNode.data if (nextData) { for (let key in nextData) { const prevValue = prevData[key] const nextValue = nextData[key] patchData(el, key, prevValue, nextValue) } } if (prevData) { for (let key in prevData) { const prevValue = prevData[key] if (prevValue && !nextData.hasOwnProperty(key)) { patchData(el, key, prevValue, null ) } } } } function patchData (el, key, prevValue, nextValue ) { const domPropsRE = /\W|^(?:value|checked|selected|muted)$/ switch (key) { case 'style' : for (let k in nextValue) { el.style[k] = nextValue[k] } for (let k in prevValue) { if (!nextValue.hasOwnProperty(k)) { el.style[k] = '' } } break case 'class' : el.className = nextValue break default : if (key[0 ] === 'o' && key[1 ] === 'n' ) { if (prevValue) { el.removeEventListener(key.slice(2 ), prevValue) } if (nextValue) { el.addEventListener(key.slice(2 ), nextValue) } } else if (domPropsRE.test(key)) { el[key] = nextValue } else { el.setAttribute(key, nextValue) } break } }
在了解完如何更新 VNodeData
以后,我们再来看看最后一小部分,那就是如何更新子节点,针对子节点,我们只需在 patchElement
函数中最后递归地更新子节点即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 function patchElement (prevVNode, nextVNode, container ) { if (prevVNode.tag !== nextVNode.tag) { } if (nextData) { } if (prevData) { } patchChildren( prevVNode.childFlags, nextVNode.childFlags, prevVNode.children, nextVNode.children, el ) }
接下来我们就可以思考如何来实现 patchChildren
函数,简单来说,无论是新标签还是旧标签,该标签的子节点都可以分为三种情况
至于一个标签的子节点属于哪种类型是可以通过该标签所对应的 VNode
对象的 childFlags
属性得知的,所以根据以上规则,我们可以得出 patchChildren
函数最基本的样子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 function patchChildren ( prevChildFlags, nextChildFlags, prevChildren, nextChildren, container ) { switch (prevChildFlags) { case ChildrenFlags.SINGLE_VNODE: switch (nextChildFlags) { case ChildrenFlags.SINGLE_VNODE: break case ChildrenFlags.NO_CHILDREN: break default : break } break case ChildrenFlags.NO_CHILDREN: switch (nextChildFlags) { case ChildrenFlags.SINGLE_VNODE: break case ChildrenFlags.NO_CHILDREN: break default : break } break default : switch (nextChildFlags) { case ChildrenFlags.SINGLE_VNODE: break case ChildrenFlags.NO_CHILDREN: break default : break } break } }
由于新旧 children
各有三种情况,所以合起来共有九种情况,我们会根据这九种情况分别来进行完善,具体处理逻辑可以参考下面几个图
所以完善后的 patchChildren
函数如下所示,一些需要注意的地方都已经标注在注释当中了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 function patchChildren ( prevChildFlags, nextChildFlags, prevChildren, nextChildren, container ) { switch (prevChildFlags) { case ChildrenFlags.SINGLE_VNODE: switch (nextChildFlags) { case ChildrenFlags.SINGLE_VNODE: patch(prevChildren, nextChildren, container) break case ChildrenFlags.NO_CHILDREN: container.removeChild(prevChildren.el) break default : container.removeChild(prevChildren.el) for (let i = 0 ; i < nextChildren.length; i++) { mount(nextChildren[i], container) } break } break case ChildrenFlags.NO_CHILDREN: switch (nextChildFlags) { case ChildrenFlags.SINGLE_VNODE: mount(nextChildren, container) break case ChildrenFlags.NO_CHILDREN: break default : for (let i = 0 ; i < nextChildren.length; i++) { mount(nextChildren[i], container) } break } break default : switch (nextChildFlags) { case ChildrenFlags.SINGLE_VNODE: for (let i = 0 ; i < prevChildren.length; i++) { container.removeChild(prevChildren[i].el) } mount(nextChildren, container) break case ChildrenFlags.NO_CHILDREN: for (let i = 0 ; i < prevChildren.length; i++) { container.removeChild(prevChildren[i].el) } break default : for (let i = 0 ; i < prevChildren.length; i++) { container.removeChild(prevChildren[i].el) } for (let i = 0 ; i < nextChildren.length; i++) { mount(nextChildren[i], container) } break } break } }
更新文本节点 相较于标签元素,文本节点的更新非常简单,如果一个 DOM
元素是文本节点或注释节点,那么可以通过调用该 DOM
对象的 nodeValue
属性读取或设置文本节点(或注释节点)的内容
1 2 3 4 5 6 7 8 9 function patchText (prevVNode, nextVNode ) { const el = (nextVNode.el = prevVNode.el) if (nextVNode.children !== prevVNode.children) { el.nodeValue = nextVNode.children } }
更新 Fragment 实际上片段的更新是『简化版的标签元素的更新』,我们知道对于标签元素来说更新的过程分为两个步骤
首先需要更新标签本身的 VNodeData
其次更新其子节点
然而由于 Fragment
没有包裹元素,只有子节点,所以我们对 Fragment
的更新本质上就是更新两个片段的子节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 function patchFragment (prevVNode, nextVNode, container ) { patchChildren( prevVNode.childFlags, nextVNode.childFlags, prevVNode.children, nextVNode.children, container ) switch (nextVNode.childFlags) { case ChildrenFlags.SINGLE_VNODE: nextVNode.el = nextVNode.children.el break case ChildrenFlags.NO_CHILDREN: nextVNode.el = prevVNode.el break default : nextVNode.el = nextVNode.children[0 ].el } }
更新 Portal 实际上 Portal
的更新与 Fragment
类似,我们需要更新其子节点,但由于 Portal
可以被到处挂载,所以新旧 Portal
的挂载目标可能不同,所以对于 Portal
的更新除了要更新其子节点之外,还要对比新旧挂载目标是否相同,如果新的 Portal
的挂载目标变了我们就需要将 Portal
的『内容从旧的容器中搬运到新的容器中』
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 function patchPortal (prevVNode, nextVNode ) { patchChildren( prevVNode.childFlags, nextVNode.childFlags, prevVNode.children, nextVNode.children, prevVNode.tag ) nextVNode.el = prevVNode.el if (nextVNode.tag !== prevVNode.tag) { const container = typeof nextVNode.tag === 'string' ? document .querySelector(nextVNode.tag) : nextVNode.tag switch (nextVNode.childFlags) { case ChildrenFlags.SINGLE_VNODE: container.appendChild(nextVNode.children.el) break case ChildrenFlags.NO_CHILDREN: break default : for (let i = 0 ; i < nextVNode.children.length; i++) { container.appendChild(nextVNode.children[i].el) } break } } }
更新有状态组件 对于有状态组件来说它的更新方式有两种,主动更新和被动更新
主动更新,指的是组件自身的状态发生变化所导致的更新,例如组件的 data
数据发生了变化就必然需要重渲染
被动更新,一个组件所渲染的内容是很可能包含其它组件的,也就是子组件,对于子组件来讲,它除了自身状态之外,很可能还包含从父组件传递进来的外部状态(props
),所以父组件自身状态的变化很可能引起子组件外部状态的变化,此时就需要更新子组件,像这种因为外部状态变化而导致的组件更新就叫做被动更新
我们先来看看主动更新,关于主动更新的关键点在于数据变化之后需要重新执行渲染函数,得到新的 VNode
,这里就用到了我们在 mountStatefulComponent
方法中定义的 instance._update()
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 function patchComponent (prevVNode, nextVNode, container ) { if (nextVNode.tag !== prevVNode.tag) { replaceVNode(prevVNode, nextVNode, container) } else if (nextVNode.flags & VNodeFlags.COMPONENT_STATEFUL_NORMAL) { const instance = (nextVNode.children = prevVNode.children) instance.$props = nextVNode.data instance._update() } else { } }
更新函数式组件 其实无论是有状态组件还是函数式组件,它们的更新原理都是一样的,用组件新产出的 VNode
与之前产出的旧 VNode
进行比对,从而完成更新,这里就用到了我们在 mountFunctionalComponent
方法中定义的 update()
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function patchComponent (prevVNode, nextVNode, container ) { if (nextVNode.tag !== prevVNode.tag) { } else if (nextVNode.flags & VNodeFlags.COMPONENT_STATEFUL_NORMAL) { } else { const handle = (nextVNode.handle = prevVNode.handle) handle.prev = prevVNode handle.next = nextVNode handle.container = container handle.update() } }
参考 如果想了解更多的相关内容,可以参考以下链接