组件的产出是什么?
一个组件就是一个函数,给我什么样的数据,我就渲染对应的 html 内容
例证 最早的组件,给定一个模板,和数据,输出相应的html内容;当数据改变时,重新渲染模板。
区别 过去的输出为html字符串,现在是Virtual Dom ,组件的产出就是 Virtual DOM(VNode)
本质:为什么要改为输出VNode? 分离组件,增加复用性,减少耦合度。比如一个stepper,可能多处会使用;分层设计,使得渲染过程抽象,可以渲染到web之外的平台(如微信)
**
何为VNode
VNode 是真实 DOM 的描述
// html标签(string)
const elementVnode = {
tag: 'div'
}
// 组件
const componentVnode = {
tag: MyComponent
}
渲染器(抽象) 接收两个参数,分别是将要渲染的 vnode 和 元素挂载点(真实 DOM 被渲染的位置)
// 把 elementVnode 渲染到 id 为 app 的元素下
render(elementVnode, document.getElementById('app'))
设计VNode
用 VNode 描述真实 DOM
一个 html
标签有它的名字、属性、事件、样式、子节点等诸多信息,这些内容都需要在 VNode
中体现。
那么我们设计的VNode可能会有这些属性: tag(标签名div, span等),data(node的对应数据),children(子节点信息,变成了一棵树),当然可以添加更多属性,但是维护更麻烦,所以现在先加入这些属性。
// 红色背景的正方形 div 元素
const elementVNode = {
tag: 'div',
data: {
style: {
width: '100px',
height: '100px',
backgroundColor: 'red'
}
}
}
// 描述一个有子节点的 div 元素
const elementVNode = {
tag: 'div',
data: null,
children: {
tag: 'span',
data: null
}
}
// 若有多个子节点,则可以把 children 属性设计为一个数组:
const elementVNode = {
tag: 'div',
data: null,
children: [
{
tag: 'h1',
data: null
},
{
tag: 'p',
data: null
}
]
}
文本节点 tag为null
const textVNode = {
tag: null,
data: null,
children: '文本内容'
}
组件节点 对应的tag属性值则引用组件类(或函数)本身
const elementVNode = {
tag: 'div',
data: null,
children: {
tag: MyComponent,
data: null
}
}
// 对应
<div>
<MyComponent />
</div>
Fragment节点
const Fragment = Symbol()
const fragmentVNode = {
// tag 属性值是一个唯一标识
tag: Fragment,
data: null,
children: [
{
tag: 'td',
data: null
},
{
tag: 'td',
data: null
},
{
tag: 'td',
data: null
}
]
}
const elementVNode = {
tag: 'td',
data: null
}
<-- 使用场景 -->
<template>
<table>
<tr>
<Columns />
</tr>
</table>
</template>
<-- 对应模板 -->
<template>
<td></td>
<td></td>
<td></td>
</template>
Potral节点
其最终效果是,无论你在何处使用
const Portal = Symbol()
const portalVNode = {
tag: Portal,
data: {
target: '#app-root'
},
children: {
tag: 'div',
data: {
class: 'overlay'
}
}
}
<-- 使用场景 -->
<template>
<div id="box" style="z-index: -1;">
<Overlay />
</div>
</template>
<-- 对应模板 -->
<template>
<Portal target="#app-root">
<div class="overlay"></div>
</Portal>
</template>
VNode的种类
总的来说,我们可以把 VNode 分成五类,分别是:html/svg 元素、组件、纯文本、Fragment 以及 Portal:
VNodeFlag
以现有的节点表示,如果需要知道VNode是什么类型,需要在挂载阶段,使用排除法得到,为了能够更快辨明节点类型,节省Node的render时间,可以通过flag实现
例如:
// html 元素节点
const htmlVnode = {
flags: VNodeFlags.ELEMENT_HTML,
tag: 'div',
data: null
}
// svg 元素节点
const svgVnode = {
flags: VNodeFlags.ELEMENT_SVG,
tag: 'svg',
data: null
}
VNode的children和对应flag
有无children,有单个/多个子节点,多个子节点有无key
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
}
辅助创建 VNode 的 h 函数
何为h函数
我们可以看到这一个个的VNode,但是可以发现它非常长,生成起来相当复杂,冗余度也很高。不难想象,我们希望有一种形式化写法,可以简化这一问题,这就是h函数的需求。
用来创建VNode对象的函数封装,简单模型如下:
// 创建一个空的 <h1></h1> 标签
function h() {
return {
_isVNode: true,
flags: VNodeFlags.ELEMENT_HTML,
tag: 'h1',
data: null,
children: null,
childFlags: ChildrenFlags.NO_CHILDREN,
el: null
}
}
考虑,如果希望h函数的可用性变强,可以生成更多VNode可以增加输入,如:
function h(tag, data = null, children = null) {
//...
}
在返回的值中,flags和childrenFlags都是根据tag和children判断得到的。
举例:
<template>
<div>
<span></span>
</div>
</template>
const elementVNode = h('div', null, h('span'))
const elementVNode = {
_isVNode: true,
flags: 1, // VNodeFlags.ELEMENT_HTML
tag: 'div',
data: null,
children: {
_isVNode: true,
flags: 1, // VNodeFlags.ELEMENT_HTML
tag: 'span',
data: null,
children: null,
childFlags: 1, // ChildrenFlags.NO_CHILDREN
el: null
},
childFlags: 2, // ChildrenFlags.SINGLE_VNODE
el: null
}
生成组件,应当注意:
class MyStatefulComponent extends Component {}
class Component {
render() {
throw '组件缺少 render 函数'
}
}
一个组件来说它的 render 函数就是它的一切,它说明了一个组件如何渲染在页面上,状态型组件继承与Component类,其实现方法中应当由render()方法,如果没有,在渲染时会报错。
渲染器的挂载
何为渲染器
渲染器,简单的说就是将 Virtual DOM 渲染成特定平台下真实 DOM 的工具(就是一个函数,通常叫 render)
渲染器的工作流程分为两个阶段:mount 和 patch,如果旧的 VNode 存在,则会使用新的 VNode 与旧的 VNode 进行对比,试图以最小的资源开销完成 DOM 的更新,这个过程就叫 patch,或“打补丁”。如果旧的 VNode 不存在,则直接将新的 VNode 挂载成全新的 DOM,这个过程叫做 mount。
通常渲染器接收两个参数,第一个参数是将要被渲染的 VNode 对象,第二个参数是一个用来承载内容的容器(container),通常也叫挂载点,如下:
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,mount的方法各不相同:
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)
}
}
先看mountElement基础代码:
function mountElement(vnode, container) {
// 生成DOM
const el = document.createElement(vnode.tag)
// 将DOM放到应该放的地方
container.appendChild(el)
}
这段代码有4个问题:
1、VNode 被渲染为真实DOM之后,没有引用真实DOM元素
2、没有将 VNodeData 应用到真实DOM元素上
3、没有继续挂载子节点,即 children
4、不能严谨地处理 SVG 标签
对应2,节点有style如下:
const elementVnode = h(
'div',
{
style: {
height: '100px',
width: '100px',
background: 'red'
},
class: 'cls-a cls-b',
type: 'checkbox',
}
)
新代码:
function mountElement(vnode, container, isSVG) {
const el = document.createElement(vnode.tag)
// 问题4 处理SVG 之所以增加输入isSVG是因为所有<svg>的子标签(如<circle>都应该是svg)
isSVG = isSVG || vnode.flags & VNodeFlags.ELEMENT_SVG
const el = isSVG
? document.createElementNS('http://www.w3.org/2000/svg', vnode.tag)
: document.createElement(vnode.tag)
// 问题1 将真是DOM绑定到VNode上,用来索引
vnode.el = el
// 问题2
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
case 'class':
el.className = data[key]
// 判断是否为DOM属性
default:
if (key[0] === 'o' && key[1] === 'n') {
// 事件
el.addEventListener(key.slice(2), data[key])
} else if (domPropsRE.test(key)) {
// 当作 DOM Prop 处理
el[key] = data[key]
} else {
// 当作 Attr 处理
el.setAttribute(key, data[key])
}
break
}
}
}
// 问题3 拿到 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(child, el)
}
}
}
container.appendChild(el)
}
应用层面的设计
如果某些class会动态变化,那么VNode的data结构和render的方法都应该发生相应改变。
<template>
<div class="cls-a" :class="dynamicClass"></div>
</template>
// 动态class可以是数组,也可以是对象
dynamicClass = ['class-b', 'class-c']
dynamicClass = {
'class-b': true,
'class-c': true
}
// VNode h函数
h('div', {
class: ['class-a', dynamicClass]
})
// 对象对应VNode输出
h('div', {
class: [
'class-a',
{
'class-b': true,
'class-c': true
}
]
})
Attributes 和 DOM Properties
attr: 每个标签中都可能包含一些属性,如果这些属性是标准属性,那么解析生成的DOM对象中也会包含与之对应的属性,例如:
<body id="page"></body>
可以通过 document.body.id 来访问它的值,这些属性可以认为是DOM对象的标准属性。
prop: 非标准属性,通过document.body.custom访问,会得到undefined
<body custom="val"></body>
但是可以通过setAttribute 设置属性为标准属性。但是设置的属性值会被先转为字符串,再传给属性值:
const checkboxEl = document.querySelector('input')
checkboxEl.setAttribute('checked', false)
// 等价于
checkboxEl.setAttribute('checked', 'false')
console.log(checkboxEl.checked) // true
事件的处理
在 mount 阶段为 DOM 元素添加事件很容易,我们只需要在元素对象上调用 addEventListener 方法即可
<div @click="handler"></div>
// 在事件的属性前增加on 与其他属性进行区分
const elementVNode = h('div', {
onclick: handler
})
渲染器的patch
何为patch
渲染器需要对新旧 VNode 进行比对,并以合适的方式更新DOM,也就是我们常说的 patch。
VNode的对比
function patch(prevVNode, nextVNode, container) {
// 分别拿到新旧 VNode 的类型,即 flags
const nextFlags = nextVNode.flags
const prevFlags = prevVNode.flags
// 检查新旧 VNode 的类型是否相同,如果类型不同,则直接调用 replaceVNode 函数替换 VNode
// 如果新旧 VNode 的类型相同,则根据不同的类型调用不同的比对函数
if (prevFlags !== nextFlags) {
replaceVNode(prevVNode, nextVNode, container)
// &是由于flag设计时用的是1<<k的循环移位方式设计,同时有的flag是多种flag的或值,因此用&可以判断是否包含
} 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)
}
}
替换VNode
新旧节点类型不同时,先删除旧节点,再挂载新节点
function replaceVNode(prevVNode, nextVNode, container) {
// 将旧的 VNode 所渲染的 DOM 从容器中移除
container.removeChild(prevVNode.el)
// 再把新的 VNode 挂载到容器中
mount(nextVNode, container)
}
patch基础标签
新旧VNode
// 旧的 VNode
const prevVNode = h('div', {
style: {
width: '100px',
height: '100px',
backgroundColor: 'red'
}
})
// 新的 VNode
const nextVNode = h('div', {
style: {
width: '100px',
height: '100px',
border: '1px solid green'
}
})
仅针对这个案例而言,我们的更新规则应该是:先将红色背景从元素上移除,再为元素添加绿色边框。此即:将新的 VNodeData 全部应用到元素上,再把那些已经不存在于新的 VNodeData 上的数据从元素上移除
function patchElement(prevVNode, nextVNode, container) {
// 如果新旧 VNode 描述的是不同的标签,则调用 replaceVNode 函数,使用新的 VNode 替换旧的 VNode
if (prevVNode.tag !== nextVNode.tag) {
replaceVNode(prevVNode, nextVNode, container)
return
}
// 拿到 el 元素,注意这时要让 nextVNode.el 也引用该元素
const el = (nextVNode.el = prevVNode.el)
// 拿到 新旧 VNodeData
const prevData = prevVNode.data
const nextData = nextVNode.data
// 新的 VNodeData 存在时才有必要更新
if (nextData) {
// 遍历新的 VNodeData
for (let key in nextData) {
// 根据 key 拿到新旧 VNodeData 值
const prevValue = prevData[key]
const nextValue = nextData[key]
switch (key) {
case 'style':
// 遍历新 VNodeData 中的 style 数据,将新的样式应用到元素
for (let k in nextValue) {
el.style[k] = nextValue[k]
}
// 遍历旧 VNodeData 中的 style 数据,将已经不存在于新的 VNodeData 的数据移除
for (let k in prevValue) {
if (!nextValue.hasOwnProperty(k)) {
el.style[k] = ''
}
}
break
default:
break
}
}
}
}
如上高亮代码所示,我们在更新 VNodeData 时的思路分为以下几步:
第 1 步:当新的 VNodeData 存在时,遍历新的 VNodeData。
第 2 步:根据新 VNodeData 中的 key,分别尝试读取旧值和新值,即 prevValue 和 nextValue。
第 3 步:使用 switch…case 语句匹配不同的数据进行不同的更新操作
以样式(style)的更新为例,如上代码所展示的更新过程是:
1 :遍历新的样式数据(prevValue),将新的样式数据全部应用到元素上
2 :遍历旧的样式数据(nextValue),将那些已经不存在于新的样式数据中的样式从元素上移除,最终我们完成了元素样式的更新。
这个过程实际上就是更新标签元素的基本规则。
更新VNodeData
和mount渲染data的方式类似,知识变为了patch
function patchElement(prevVNode, nextVNode, container) {
// 如果新旧 VNode 描述的是不同的标签,则调用 replaceVNode 函数,使用新的 VNode 替换旧的 VNode
if (prevVNode.tag !== nextVNode.tag) {
replaceVNode(prevVNode, nextVNode, container)
return
}
// 拿到 el 元素,注意这时要让 nextVNode.el 也引用该元素
const el = (nextVNode.el = prevVNode.el)
const prevData = prevVNode.data
const nextData = nextVNode.data
if (nextData) {
// 遍历新的 VNodeData,将旧值和新值都传递给 patchData 函数
for (let key in nextData) {
const prevValue = prevData[key]
const nextValue = nextData[key]
patchData(el, key, prevValue, nextValue)
}
}
if (prevData) {
// 遍历旧的 VNodeData,将已经不存在于新的 VNodeData 中的数据移除
for (let key in prevData) {
const prevValue = prevData[key]
if (prevValue && !nextData.hasOwnProperty(key)) {
// 第四个参数为 null,代表移除数据
patchData(el, key, prevValue, null)
}
}
}
}
export function patchData(el, key, prevValue, nextValue) {
switch (key) {
case 'style':
// 省略处理样式的代码...
case 'class':
// 省略处理 class 的代码...
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)) {
// 当作 DOM Prop 处理
el[key] = nextValue
} else {
// 当作 Attr 处理
el.setAttribute(key, nextValue)
}
break
}
}
更新子节点
function patchElement(prevVNode, nextVNode, container) {
// 如果新旧 VNode 描述的是不同的标签,则调用 replaceVNode 函数,使用新的 VNode 替换旧的 VNode
if (prevVNode.tag !== nextVNode.tag) {
replaceVNode(prevVNode, nextVNode, container)
return
}
// 拿到 el 元素,注意这时要让 nextVNode.el 也引用该元素
// ...处理data变化
// 调用 patchChildren 函数递归地更新子节点
patchChildren(
prevVNode.childFlags, // 旧的 VNode 子节点的类型
nextVNode.childFlags, // 新的 VNode 子节点的类型
prevVNode.children, // 旧的 VNode 子节点
nextVNode.children, // 新的 VNode 子节点
el // 当前标签元素,即这些子节点的父节点
)
}
function patchChildren(
prevChildFlags,
nextChildFlags,
prevChildren,
nextChildren,
container
) {
switch (prevChildFlags) {
// 旧的 children 是单个子节点,会执行该 case 语句块
case ChildrenFlags.SINGLE_VNODE:
switch (nextChildFlags) {
case ChildrenFlags.SINGLE_VNODE:
// 此时 prevChildren 和 nextChildren 都是 VNode 对象 直接更新
patch(prevChildren, nextChildren, container)
break
case ChildrenFlags.NO_CHILDREN:
// 新节点没有孩子 删除原节点孩子
container.removeChild(prevChildren.el)
break
default:
// 新的 children 中有多个子节点时,会执行该 case 语句块
// 移除旧的单个子节点
container.removeChild(prevChildren.el)
// 遍历新的多个子节点,逐个挂载到容器中
for (let i = 0; i < nextChildren.length; i++) {
mount(nextChildren[i], container)
}
break
}
break
// 旧的 children 中没有子节点时,会执行该 case 语句块
case ChildrenFlags.NO_CHILDREN:
switch (nextChildFlags) {
case ChildrenFlags.SINGLE_VNODE:
// 新的 children 是单个子节点时,会执行该 case 语句块
break
case ChildrenFlags.NO_CHILDREN:
// 新的 children 中没有子节点时,会执行该 case 语句块
break
default:
// 新的 children 中有多个子节点时,会执行该 case 语句块
break
}
break
// 旧的 children 中有多个子节点时,会执行该 case 语句块
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
}
}
有状态组件的更新
有状态组件来说它的更新方式有两种:主动更新 和 被动更新。
主动更新: 指的是组件自身的状态发生变化所导致的更新,例如组件的 data 数据发生了变化就必然需要重渲染;
被动更新:对于子组件来讲,它除了自身状态之外,很可能还包含从父组件传递进来的外部状态(props),所以父组件自身状态的变化很可能引起子组件外部状态的变化,此时就需要更新子组件,像这种因为外部状态变化而导致的组件更新就叫做被动更新。
主动更新
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()
}
}
// mounted 钩子
mounted() {
// 两秒钟之后修改本地状态的值,并重新调用 _update() 函数更新组件
setTimeout(() => {
this.localState = 'two'
this._update()
}, 2000)
}
instance._update()
}
组件必定会走_update方法,无论是挂载还是更新,但是_update方法会区分挂载与更新。
被动更新
增加$prop属性,内部保存父组件对应的data,当父组件数据变化时,$prop相应变化,此时子组件的mounted钩子会根据$prop的数据改变,进而改变VNode
渲染器的核心 Diff 算法
只有当新旧子节点的类型都是多个子节点时,核心 Diff 算法才派得上用场。
演化:
示例: 排序列表的重排
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
// origin 1, 2, 3
[
h('li', null, 1),
h('li', null, 2),
h('li', null, 3)
]
// current 3, 1, 2
[
h('li', null, 3),
h('li', null, 1),
h('li', null, 2)
]
简单 Diff 算法:遍历旧的子节点,将其全部移除;再遍历新的子节点,将其全部添加
当子节点等长,每个对应节点patch
长度不同,移除长的节点(尽量能修改就修改)
最佳期望: 移动元素位置,而非更新节点信息
新旧 children 中的节点都是 li 标签,以新 children 的第一个 li 标签为例,你能说出在旧 children 中哪一个 li 标签可被它复用吗?不能,所以,为了明确的知道新旧 children 中节点的映射关系,我们需要在 VNode 创建伊始就为其添加唯一的标识,即 key 属性
// 旧 children
[
h('li', { key: 'a' }, 1),
h('li', { key: 'b' }, 2),
h('li', { key: 'c' }, 3)
]
// 新 children
[
h('li', { key: 'c' }, 3)
h('li', { key: 'a' }, 1),
h('li', { key: 'b' }, 2)
]
**
知道了映射关系,我们就很容易判断新 children 中的节点是否可被复用:只需要遍历新 children 中的每一个节点,并去旧 children 中寻找是否存在具有相同 key 值的节点。
// 遍历新的 children
for (let i = 0; i < nextChildren.length; i++) {
const nextVNode = nextChildren[i]
let j = 0
// 遍历旧的 children
for (j; j < prevChildren.length; j++) {
const prevVNode = prevChildren[j]
// 如果找到了具有相同 key 值的两个节点,则调用 `patch` 函数更新之
if (nextVNode.key === prevVNode.key) {
patch(prevVNode, nextVNode, container)
break // 这里需要 break
}
}
}
如何移动:
// 用来存储寻找过程中遇到的最大索引值
let lastIndex = 0
// 遍历新的 children
for (let i = 0; i < nextChildren.length; i++) {
const nextVNode = nextChildren[i]
let j = 0
// 遍历旧的 children
for (j; j < prevChildren.length; j++) {
const prevVNode = prevChildren[j]
// 如果找到了具有相同 key 值的两个节点,则调用 `patch` 函数更新之
if (nextVNode.key === prevVNode.key) {
patch(prevVNode, nextVNode, container)
if (j < lastIndex) {
// 需要移动
// refNode 是为了下面调用 insertBefore 函数准备的
const refNode = nextChildren[i - 1].el.nextSibling
// 调用 insertBefore 函数移动 DOM
container.insertBefore(prevVNode.el, refNode)
} else {
// 更新 lastIndex
lastIndex = j
}
break // 这里需要 break
}
}
}
举例: 0 1 2 3 4 5 => 3 2 1 5 4
过程: 3(key) 的VNode被更新,绑定原先3的DOM,作为新的基准点;然后1,2VNode分别被更新,其对应的DOM变为3的DOM的右兄弟DOM。然后5被更新,再来一次。
总结:最大值永远总为基准点(不用动),然后小的值落在其右边,无关于顺序(key只是身份的标识)
移动中的新元素
应该使用 mount 函数将 li-d 节点作为全新的 VNode 挂载到合适的位置
第一,在找节点的算法中添加boolean变量find标识有无找到;
第二,如果没找到,mount新节点,让后插入到合适位置;
第三,如果新节点是第一个节点,应该mount时append到element中.
移除不存在的元素
在循环结束后,对prev children进行循环,查看有无删除,如果删除了将真实DOM移除。
双端比较
上述演化算法(React)的优化,思想在于原算法在尾部变为头部后会一直移动元素,
但实际只应把尾部移至新头部即可,减少很多操作。
思想:双端比较,然后递归实现