很早之前,我曾写过一篇文章,分析并实现过一版简易的 vdom
。想看的可以点击 传送门
聊聊为什么又想着写这么一篇文章,实在是项目里,不管自己还是同事,都或多或少会遇到这块的坑。所以这里当给小伙伴们再做一次总结吧,希望大伙看完,能对 vue
中的 vdom
有一个更好的认知。好了,接下来直接开始吧
一、抛出问题
在开始之前,我先抛出一个问题,大家可以先思考,然后再接着阅读后面的篇幅。先上下代码
<template>
<el-select
class="test-select"
multiple
filterable
remote
placeholder="请输入关键词"
:remote-method="remoteMethod"
:loading="loading"
@focus="handleFoucs"
v-model="items">
<!-- 这里 option 的 key 直接绑定 vfor 的 index -->
<el-option
v-for="(item, index) in options"
:key="index"
:label="item.label"
:value="item.value">
<el-checkbox
:label="item.value"
:value="isChecked(item.value)">
{{ item.label }}
</el-checkbox>
</el-option>
</el-select>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
@Component
export default class TestSelect extends Vue {
options: Array<{ label: string, value: string }> = []
items: Array<string> = []
list: Array<{ label: string, value: string }> = []
loading: boolean = false
states = ['Alabama', 'Alaska', 'Arizona',
'Arkansas', 'California', 'Colorado',
'Connecticut', 'Delaware', 'Florida',
'Georgia', 'Hawaii', 'Idaho', 'Illinois',
'Indiana', 'Iowa', 'Kansas', 'Kentucky',
'Louisiana', 'Maine', 'Maryland',
'Massachusetts', 'Michigan', 'Minnesota',
'Mississippi', 'Missouri', 'Montana',
'Nebraska', 'Nevada', 'New Hampshire',
'New Jersey', 'New Mexico', 'New York',
'North Carolina', 'North Dakota', 'Ohio',
'Oklahoma', 'Oregon', 'Pennsylvania',
'Rhode Island', 'South Carolina',
'South Dakota', 'Tennessee', 'Texas',
'Utah', 'Vermont', 'Virginia',
'Washington', 'West Virginia', 'Wisconsin',
'Wyoming']
mounted () {
this.list = this.states.map(item => {
return { value: item, label: item }
})
}
remoteMethod (query) {
if (query !== '') {
this.loading = true
setTimeout(() => {
this.loading = false
this.options = this.list.filter(item => {
return item.label.toLowerCase()
.indexOf(query.toLowerCase()) > -1
})
}, 200)
} else {
this.options = this.list
}
}
handleFoucs (e) {
this.remoteMethod(e.target.value)
}
isChecked (value: string): boolean {
let checked = false
this.items.forEach((item: string) => {
if (item === value) {
checked = true
}
})
return checked
}
}
</script>
输入筛选后效果图如下
然后我在换一个关键词进行搜索,结果就会出现以下展示的问题
我并没有进行选择,但是 select 选择框中展示的值却发生了变更。老司机可能一开始看代码,就知道问题所在了。其实把 option 里面的 key
绑定换一下就OK,换成如下的
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value">
<el-checkbox
:label="item.value"
:value="isChecked(item.value)">
{{ item.label }}
</el-checkbox>
</el-option>
那么问题来了,这样可以避免问题,但是为什么可以避免呢?其实,这块就牵扯到 vdom 里 patch 相关的内容了。接下来我就带着大家重新把 vdom 再捡起来一次
开始之前,看几个下文中经常出现的 API
isDef()
export function isDef (v: any): boolean %checks {
return v !== undefined && v !== null
}
isUndef()
export function isUndef (v: any): boolean %checks {
return v === undefined || v === null
}
isTrue()
export function isTrue (v: any): boolean %checks {
return v === true
}
二、class VNode
开篇前,先讲一下 VNode ,vue
中的 vdom
其实就是一个 vnode
对象。
对 vdom
稍作了解的同学都应该知道,vdom
创建节点的核心首先就是创建一个对真实 dom 抽象的 js 对象树,然后通过一系列操作(后面我再谈具体什么操作)。该章节我们就只谈 vnode
的实现
1、constructor
首先,我们可以先看看, VNode 这个类对我们这些使用者暴露了哪些属性出来,挑一些我们常见的看
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component
) {
this.tag = tag // 节点的标签名
this.data = data // 节点的数据信息,如 props,attrs,key,class,directives 等
this.children = children // 节点的子节点
this.text = text // 节点对应的文本
this.elm = elm // 节点对应的真实节点
this.context = context // 节点上下文,为 Vue Component 的定义
this.key = data && data.key // 节点用作 diff 的唯一标识
}
2、for example
现在,我们举个例子,假如我需要解析下面文本
<template>
<div class="vnode" :class={ 'show-node': isShow } v-show="isShow">
This is a vnode.
</div>
</template>
使用 js 进行抽象就是这样的
function render () {
return new VNode(
'div',
{
// 静态 class
staticClass: 'vnode',
// 动态 class
class: {
'show-node': isShow
},
/**
* directives: [
* {
* rawName: 'v-show',
* name: 'show',
* value: isShow
* }
* ],
*/
// 等同于 directives 里面的 v-show
show: isShow,
[ new VNode(undefined, undefined, undefined, 'This is a vnode.') ]
}
)
}
转换成 vnode 后的表现形式如下
{
tag: 'div',
data: {
show: isShow,
// 静态 class
staticClass: 'vnode',
// 动态 class
class: {
'show-node': isShow
},
},
text: undefined,
children: [
{
tag: undefined,
data: undefined,
text: 'This is a vnode.',
children: undefined
}
]
}
然后我再看一个稍微复杂一点的例子
<span v-for="n in 5" :key="n">{{ n }}</span>
假如让大家使用 js 对其进行对象抽象,大家会如何进行呢?主要是里面的 v-for 指令,大家可以先自己带着思考试试。
OK,不卖关子,我们现在直接看看下面的 render 函数对其的抽象处理,其实就是循环 render 啦!
function render (val, keyOrIndex, index) {
return new VNode(
'span',
{
directives: [
{
rawName: 'v-for',
name: 'for',
value: val
}
],
key: val,
[ new VNode(undefined, undefined, undefined, val) ]
}
)
}
function renderList (
val: any,
render: (
val: any,
keyOrIndex: string | number,
index?: number
) => VNode
): ?Array<VNode> {
// 仅考虑 number 的情况
let ret: ?Array<VNode>, i, l, keys, key
ret = new Array(val)
for (i = 0; i < val; i++) {
ret[i] = render(i + 1, i)
}
return ret
}
renderList(5)
转换成 vnode 后的表现形式如下
[
{
tag: 'span',
data: {
key: 1
},
text: undefined,
children: [
{
tag: undefined,
data: undefined,
text: 1,
children: undefined
}
]
}
// 依次循环
]
3、something else
我们看完了 VNode Ctor
的一些属性,也看了一下对于真实 dom vnode 的转换形式,这里我们就稍微补个漏,看看基于 VNode
做的一些封装给我们暴露的一些方法
// 创建一个空节点
export const createEmptyVNode = (text: string = '') => {
const node = new VNode()
node.text = text
node.isComment = true
return node
}
// 创建一个文本节点
export function createTextVNode (val: string | number) {
return new VNode(undefined, undefined, undefined, String(val))
}
// 克隆一个节点,仅列举部分属性
export function cloneVNode (vnode: VNode): VNode {
const cloned = new VNode(
vnode.tag,
vnode.data,
vnode.children,
vnode.text
)
cloned.key = vnode.key
cloned.isCloned = true
return cloned
}
捋清楚 VNode
相关方法,下面的章节,将介绍 vue
是如何将 vnode
渲染成真实 dom
三、render
1、createElement
在看 vue 中 createElement 的实现前,我们先看看同文件下私有方法 _createElement
的实现。其中是对 tag 具体的一些逻辑判定
- tagName 绑定在 data 参数里面
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
- tagName 不存在时,返回一个空节点
if (!tag) {
return createEmptyVNode()
}
- tagName 是 string 类型的时候,直接返回对应 tag 的 vnode 对象
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
- tagName 是非 string 类型的时候,则执行
createComponent()
创建一个 Component 对象
vnode = createComponent(tag, data, context, children)
- 判定 vnode 类型,进行对应的返回
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
// namespace 相关处理
if (isDef(ns)) applyNS(vnode, ns)
// 进行 Observer 相关绑定
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
createElement()
则是执行 _createElement()
返回 vnode
return _createElement(context, tag, data, children, normalizationType)
2、render functions
i. renderHelpers
这里我们先整体看下,挂载在 Vue.prototype
上的都有哪些 render 相关的方法
export function installRenderHelpers (target: any) {
target._o = markOnce // v-once render 处理
target._n = toNumber // 值转换 Number 处理
target._s = toString // 值转换 String 处理
target._l = renderList // v-for render 处理
target._t = renderSlot // slot 槽点 render 处理
target._q = looseEqual // 判断两个对象是否大体相等
target._i = looseIndexOf // 对等属性索引,不存在则返回 -1
target._m = renderStatic // 静态节点 render 处理
target._f = resolveFilter // filters 指令 render 处理
target._k = checkKeyCodes // checking keyCodes from config
target._b = bindObjectProps // v-bind render 处理,将 v-bind="object" 的属性 merge 到VNode属性中
target._v = createTextVNode // 创建文本节点
target._e = createEmptyVNode // 创建空节点
target._u = resolveScopedSlots // scopeSlots render 处理
target._g = bindObjectListeners // v-on render 处理
}
然后在 renderMixin()
方法中,对 Vue.prototype
进行 init 操作
export function renderMixin (Vue: Class<Component>) {
// render helps init 操作
installRenderHelpers(Vue.prototype)
// 定义 vue nextTick 方法
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
Vue.prototype._render = function (): VNode {
// 此处定义 vm 实例,以及 return vnode。具体代码此处忽略
}
}
ii. AST 抽象语法树
到目前为止,我们看到的 render
相关的操作都是返回一个 vnode
对象,而真实节点的渲染之前,vue 会对 template 模板中的字符串进行解析,将其转换成 AST 抽象语法树,方便后续的操作。关于这块,我们直接来看看 vue 中在 flow 类型里面是如何定义 ASTElement
接口类型的,既然是开篇抛出的问题是由 v-for
导致的,那么这块,我们就仅仅看看 ASTElement
对其的定义,看完之后记得举一反三去源码里面理解其他的定义哦
declare type ASTElement = {
tag: string; // 标签名
attrsMap: { [key: string]: any }; // 标签属性 map
parent: ASTElement | void; // 父标签
children: Array<ASTNode>; // 子节点
for?: string; // 被 v-for 的对象
forProcessed?: boolean; // v-for 是否需要被处理
key?: string; // v-for 的 key 值
alias?: string; // v-for 的参数
iterator1?: string; // v-for 第一个参数
iterator2?: string; // v-for 第二个参数
};
iii. generate 字符串转换
renderList
在看 render function
字符串转换之前,先看下 renderList
的参数,方便后面的阅读
export function renderList (
val: any,
render: (
val: any,
keyOrIndex: string | number,
index?: number
) => VNode
): ?Array<VNode> {
// 此处为 render 相关处理,具体细节这里就不列出来了,上文中有列出 number 情况的处理
}
genFor
上面看完定义,紧接着我们再来看看,generate
是如何将 AST 转换成 render function 字符串的,这样同理我们就看对 v-for
相关的处理
function genFor (
el: any,
state: CodegenState,
altGen?: Function,
altHelper?: string
): string {
const exp = el.for // v-for 的对象
const alias = el.alias // v-for 的参数
const iterator1 = el.iterator1 ? `,${el.iterator1}` : '' // v-for 第一个参数
const iterator2 = el.iterator2 ? `,${el.iterator2}` : '' // v-for 第二个参数
el.forProcessed = true // 指令需要被处理
// return 出对应 render function 字符串
return `${altHelper || '_l'}((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${(altGen || genElement)(el, state)}` +
'})'
}
genElement
这块集成了各个指令对应的转换逻辑
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.staticRoot && !el.staticProcessed) { // 静态节点
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) { // v-once 处理
return genOnce(el, state)
} else if (el.for && !el.forProcessed) { // v-for 处理
return genFor(el, state)
} else if (el.if && !el.ifProcessed) { // v-if 处理
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget) { // template 根节点处理
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') { // slot 节点处理
return genSlot(el, state)
} else {
// component or element 相关处理
}
}
generate
generate
则是将以上所有的方法集成到一个对象中,其中 render
属性对应的则是 genElement
相关的操作,staticRenderFns
对应的则是字符串数组。
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`, // render
staticRenderFns: state.staticRenderFns // render function 字符串数组
}
}
3、render 栗子
看了上面这么多,对 vue 不太了解的一些小伙伴可能会觉得有些晕,这里直接举一个 v-for
渲染的例子给大家来理解。
i. demo
<div class="root">
<span v-for="n in 5" :key="n">{{ n }}</span>
</div>
这块首先会被解析成 html 字符串
let html = `<div class="root">
<span v-for="n in 5" :key="n">{{ n }}</span>
</div>`
ii. 相关正则
拿到 template 里面的 html 字符串之后,会对其进行解析操作。具体相关的正则表达式在 src/compiler/parser/html-parser.js
里面有提及,以下是相关的一些正则表达式以及 decoding map
的定义。
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
const comment = /^<!\--/
const conditionalComment = /^<!\[/
const decodingMap = {
'<': '<',
'>': '>',
'"': '"',
'&': '&',
' ': '\n',
'	': '\t'
}
const encodedAttr = /&(?:lt|gt|quot|amp);/g
const encodedAttrWithNewLines = /&(?:lt|gt|quot|amp|#10|#9);/g
iii. parseHTML
vue
解析 template 都是使用 while
循环进行字符串匹配的,每每解析完一段字符串都会将已经匹配完的部分去除掉,然后 index
索引会直接对剩下的部分继续进行匹配。具体有关 parseHTML
的定义如下,由于文章到这篇幅已经比较长了,我省略掉了正则循环匹配指针的一些逻辑,想要具体了解的小伙伴可以自行研究或者等我下次再出一篇文章详谈这块的逻辑。
export function parseHTML (html, options) {
const stack = [] // 用来存储解析好的标签头
const expectHTML = options.expectHTML
const isUnaryTag = options.isUnaryTag || no
const canBeLeftOpenTag = options.canBeLeftOpenTag || no
let index = 0 // 匹配指针索引
let last, lastTag
while (html) {
// 此处是对标签进行正则匹配的逻辑
}
// 清理剩余的 tags
parseEndTag()
// 循环匹配相关处理
function advance (n) {
index += n
html = html.substring(n)
}
// 起始标签相关处理
function parseStartTag () {
let match = {
tagName: start[1],
attrs: [],
start: index
}
// 一系列匹配操作,然后对 match 进行赋值
return match
}
function handleStartTag (match) {}
// 结束标签相关处理
function parseEndTag (tagName, start, end) {}
}
经过 parseHTML()
进行一系列正则匹配处理之后,会将字符串 html 解析成以下 AST 的内容
{
'attrsMap': {
'class': 'root'
},
'staticClass': 'root', // 标签的静态 class
'tag': 'div', // 标签的 tag
'children': [{ // 子标签数组
'attrsMap': {
'v-for': "n in 5",
'key': n
},
'key': n,
'alias': "n", // v-for 参数
'for': 5, // 被 v-for 的对象
'forProcessed': true,
'tag': 'span',
'children': [{
'expression': '_s(item)', // toString 操作(上文有提及)
'text': '{{ n }}'
}]
}]
}
到这里,再结合上面的 generate
进行转换便是 render
这块的逻辑了。
四、diff and patch
哎呀,终于到 diff 和 patch 环节了,想想还是很鸡冻呢。
1、一些 DOM 的 API 操作
看进行具体 diff 之前,我们先看看在 platforms/web/runtime/node-ops.js
中定义的一些创建真实 dom 的方法,正好温习一下 dom
相关操作的 API
createElement()
创建由 tagName 指定的 HTML 元素
export function createElement (tagName: string, vnode: VNode): Element {
const elm = document.createElement(tagName)
if (tagName !== 'select') {
return elm
}
if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
elm.setAttribute('multiple', 'multiple')
}
return elm
}
createTextNode()
创建文本节点
export function createTextNode (text: string): Text {
return document.createTextNode(text)
}
createComment()
创建一个注释节点
export function createComment (text: string): Comment {
return document.createComment(text)
}
insertBefore()
在参考节点之前插入一个拥有指定父节点的子节点
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
parentNode.insertBefore(newNode, referenceNode)
}
removeChild()
从 DOM 中删除一个子节点
export function removeChild (node: Node, child: Node) {
node.removeChild(child)
}
appendChild()
将一个节点添加到指定父节点的子节点列表末尾
export function appendChild (node: Node, child: Node) {
node.appendChild(child)
}
parentNode()
返回父节点
export function parentNode (node: Node): ?Node {
return node.parentNode
}
nextSibling()
返回兄弟节点
export function nextSibling (node: Node): ?Node {
return node.nextSibling
}
tagName()
返回节点标签名
export function tagName (node: Element): string {
return node.tagName
}
setTextContent()
设置节点文本内容
export function setTextContent (node: Node, text: string) {
node.textContent = text
}
2、一些 patch 中的 API 操作
提示:上面我们列出来的 API 都挂在了下面的 nodeOps
对象中了
createElm()
创建节点
function createElm (vnode, parentElm, refElm) {
if (isDef(vnode.tag)) { // 创建标签节点
vnode.elm = nodeOps.createElement(tag, vnode)
} else if (isDef(vnode.isComment)) { // 创建注释节点
vnode.elm = nodeOps.createComment(vnode.text)
} else { // 创建文本节点
vnode.elm = nodeOps.createTextNode(vnode.text)
}
insert(parentElm, vnode.elm, refElm)
}
insert()
指定父节点下插入子节点
function insert (parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) { // 插入到指定 ref 的前面
if (ref.parentNode === parent) {
nodeOps.insertBefore(parent, elm, ref)
}
} else { // 直接插入到父节点后面
nodeOps.appendChild(parent, elm)
}
}
}
addVnodes()
批量调用createElm()
来创建节点
function addVnodes (parentElm, refElm, vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
createElm(vnodes[startIdx], parentElm, refElm)
}
}
removeNode()
移除节点
function removeNode (el) {
const parent = nodeOps.parentNode(el)
if (isDef(parent)) {
nodeOps.removeChild(parent, el)
}
}
removeNodes()
批量移除节点
function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx]
if (isDef(ch)) {
removeNode(ch.elm)
}
}
}
sameVnode()
是否为相同节点
function sameVnode (a, b) {
return (
a.key === b.key &&
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
)
}
sameInputType()
是否有相同的 input type
function sameInputType (a, b) {
if (a.tag !== 'input') return true
let i
const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
return typeA === typeB
}
3、节点 diff
i. 相关流程图
谈到这,先挪(盗)用下我以前文章中相关的两张图
ii. diff、patch 操作合二为一
看过我以前文章的小伙伴都应该知道,我之前文章中关于 diff 和 patch 是分成两个步骤来实现的。而 vue
中则是将 diff 和 patch 操作合二为一了。现在我们来看看,vue
中对于这块具体是如何处理的
function patch (oldVnode, vnode) {
// 如果老节点不存在,则直接创建新节点
if (isUndef(oldVnode)) {
if (isDef(vnode)) createElm(vnode)
// 如果老节点存在,新节点却不存在,则直接移除老节点
} else if (isUndef(vnode)) {
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
removeVnodes(parentElm, , 0, [oldVnode].length -1)
} else {
const isRealElement = isDef(oldVnode.nodeType)
// 如果新旧节点相同,则进行具体的 patch 操作
if (isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode)
} else {
// 否则创建新节点,移除老节点
createElm(vnode, parentElm, nodeOps.nextSibling(oldElm))
removeVnodes(parentElm, [oldVnode], 0, 0)
}
}
}
然后我们再看 patchVnode
中间相关的逻辑,先看下,前面提及的 key
在这的用处
function patchVnode (oldVnode, vnode) {
// 新旧节点完全一样,则直接 return
if (oldVnode === vnode) {
return
}
// 如果新旧节点都被标注静态节点,且节点的 key 相同。
// 则直接将老节点的 componentInstance 直接拿过来便OK了
if (
isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
}
接下来,我们看看 vnode 上面的文本内容是如何进行对比的
- 若 vnode 为非文本节点
const elm = vnode.elm = oldVnode.elm
const oldCh = oldVnode.children
const ch = vnode.children
if (isUndef(vnode.text)) {
// 如果 oldCh,ch 都存在且不相同,则执行 updateChildren 函数更新子节点
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch)
// 如果只有 ch 存在
} else if (isDef(ch)) {
// 老节点为文本节点,先将老节点的文本清空,然后将 ch 批量插入到节点 elm 下
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1)
// 如果只有 oldCh 存在,则直接清空老节点
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
// 如果 oldCh,ch 都不存在,且老节点为文本节点,则只将老节点文本清空
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
}
- 若 vnode 为文本节点,且新旧节点文本不同,则直接将设置为 vnode 的文本内容
if (isDef(vnode.text) && oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
iii. updateChildren
首先我们先看下方法中对新旧节点起始和结束索引的定义
function updateChildren (parentElm, oldCh, newCh) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
}
直接画张图来理解下
紧接着就是一个 while
循环让新旧节点起始和结束索引不断往中间靠拢
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)
若 oldStartVnode
或者 oldEndVnode
不存在,则往中间靠拢
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
}
接下来就是 oldStartVnode
,newStartVnode
,oldEndVnode
,newEndVnode
两两对比的四种情况了
// oldStartVnode 和 newStartVnode 为 sameVnode,进行 patchVnode
// oldStartIdx 和 newStartIdx 向后移动一位
else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
// oldEndVnode 和 newEndVnode 为 sameVnode,进行 patchVnode
// oldEndIdx 和 newEndIdx 向前移动一位
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
// oldStartVnode 和 newEndVnode 为 sameVnode,进行 patchVnode
// 将 oldStartVnode.elm 插入到 oldEndVnode.elm 节点后面
// oldStartIdx 向后移动一位,newEndIdx 向前移动一位
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode)
nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
// 同理,oldEndVnode 和 newStartVnode 为 sameVnode,进行 patchVnode
// 将 oldEndVnode.elm 插入到 oldStartVnode.elm 前面
// oldEndIdx 向前移动一位,newStartIdx 向后移动一位
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode)
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
}
用张图来总结上面的流程
当以上条件都不满足的情况,则进行其他操作。
在看其他操作前,我们先看一下函数 createKeyToOldIdx
,它的作用主要是 return
出 oldCh
中 key
和 index
唯一对应的 map
表,根据 key
,则能够很方便的找出相应 key
在数组中对应的索引
function createKeyToOldIdx (children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}
除此之外,这块还有另外一个辅助函数 findIdxInOld
,用来找出 newStartVnode
在 oldCh
数组中对应的索引
function findIdxInOld (node, oldCh, start, end) {
for (let i = start; i < end; i++) {
const c = oldCh[i]
if (isDef(c) && sameVnode(node, c)) return i
}
}
接下来我们看下不满足上面条件的具体处理
else {
// 如果 oldKeyToIdx 不存在,则将 oldCh 转换成 key 和 index 对应的 map 表
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// 如果 idxInOld 不存在,即老节点中不存在与 newStartVnode 对应 key 的节点,直接创建一个新节点
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
// 在 oldCh 找到了对应 key 的节点,且该节点与 newStartVnode 为 sameVnode,则进行 patchVnode
// 将 oldCh 该位置的节点清空掉,并在 parentElm 中将 vnodeToMove 插入到 oldStartVnode.elm 前面
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode)
oldCh[idxInOld] = undefined
nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// 找到了对应的节点,但是却属于不同的 element 元素,则创建一个新节点
createElm(newStartVnode, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
// newStartIdx 向后移动一位
newStartVnode = newCh[++newStartIdx]
}
经过这一系列的操作,则完成了节点之间的 diff
和 patch
操作,即完成了 oldVnode
向 newVnode
转换的操作。
文章到这里也要告一段落了,看到这里,相信大家已经对 vue
中的 vdom
这块也一定有了自己的理解了。
那么,我们再回到文章开头我们抛出的问题,大家知道为什么会出现这个问题了么?