很早以前,我曾写过一篇文章,分析并实现过一版简易的 vdom
。想看的能够点击 传送门javascript
聊聊为何又想着写这么一篇文章,实在是项目里,无论本身仍是同事,都或多或少会遇到这块的坑。因此这里当给小伙伴们再作一次总结吧,但愿大伙看完,能对 vue
中的 vdom
有一个更好的认知。好了,接下来直接开始吧html
在开始以前,我先抛出一个问题,你们能够先思考,而后再接着阅读后面的篇幅。先上下代码前端
<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>
复制代码
输入筛选后效果图以下vue
而后我在换一个关键词进行搜索,结果就会出现如下展现的问题java
我并无进行选择,可是 select 选择框中展现的值却发生了变动。老司机可能一开始看代码,就知道问题所在了。其实把 option 里面的 key
绑定换一下就OK,换成以下的node
<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 再捡起来一次web
开始以前,看几个下文中常常出现的 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
}
复制代码
开篇前,先讲一下 VNode ,vue
中的 vdom
其实就是一个 vnode
对象。express
对 vdom
稍做了解的同窗都应该知道,vdom
建立节点的核心首先就是建立一个对真实 dom 抽象的 js 对象树,而后经过一系列操做(后面我再谈具体什么操做)。该章节咱们就只谈 vnode
的实现数组
首先,咱们能够先看看, 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 的惟一标识
}
复制代码
如今,咱们举个例子,假如我须要解析下面文本
<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
}
]
}
// 依次循环
]
复制代码
咱们看完了 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
在看 vue 中 createElement 的实现前,咱们先看看同文件下私有方法 _createElement
的实现。其中是对 tag 具体的一些逻辑断定
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
复制代码
if (!tag) {
return createEmptyVNode()
}
复制代码
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
复制代码
createComponent()
建立一个 Component 对象vnode = createComponent(tag, data, context, children)
复制代码
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)
复制代码
这里咱们先总体看下,挂载在 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。具体代码此处忽略
}
}
复制代码
到目前为止,咱们看到的 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 第二个参数
};
复制代码
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 字符串数组
}
}
复制代码
看了上面这么多,对 vue 不太了解的一些小伙伴可能会以为有些晕,这里直接举一个 v-for
渲染的例子给你们来理解。
<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>`
复制代码
拿到 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
复制代码
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 和 patch 环节了,想一想仍是很鸡冻呢。
看进行具体 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
}
复制代码
提示:上面咱们列出来的 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 typefunction 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
}
复制代码
谈到这,先挪(盗)用下我之前文章中相关的两张图
看过我之前文章的小伙伴都应该知道,我以前文章中关于 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 上面的文本内容是如何进行对比的
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, '')
}
}
复制代码
if (isDef(vnode.text) && oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
复制代码
首先咱们先看下方法中对新旧节点起始和结束索引的定义
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
这块也必定有了本身的理解了。 那么,咱们再回到文章开头咱们抛出的问题,你们知道为何会出现这个问题了么?
emmm,若是想要继续沟通此问题,欢迎你们加群进行讨论,前端大杂烩:731175396。小伙伴们记得加群哦,哪怕一块儿来水群也是好的啊 ~ (注:群里单身漂亮妹纸真的不少哦,固然帅哥也不少,好比。。。me)
我的准备从新捡回本身的公众号了,以后每周保证一篇高质量好文,感兴趣的小伙伴能够关注一波。