博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
细谈 vue 核心- vdom 篇
阅读量:6887 次
发布时间:2019-06-27

本文共 20358 字,大约阅读时间需要 67 分钟。

hot3.png

很早之前,我曾写过一篇文章,分析并实现过一版简易的 vdom。想看的可以点击

聊聊为什么又想着写这么一篇文章,实在是项目里,不管自己还是同事,都或多或少会遇到这块的坑。所以这里当给小伙伴们再做一次总结吧,希望大伙看完,能对 vue 中的 vdom 有一个更好的认知。好了,接下来直接开始吧

一、抛出问题

在开始之前,我先抛出一个问题,大家可以先思考,然后再接着阅读后面的篇幅。先上下代码

输入筛选后效果图如下

然后我在换一个关键词进行搜索,结果就会出现以下展示的问题

我并没有进行选择,但是 select 选择框中展示的值却发生了变更。老司机可能一开始看代码,就知道问题所在了。其实把 option 里面的 key 绑定换一下就OK,换成如下的

{
{ item.label }}

那么问题来了,这样可以避免问题,但是为什么可以避免呢?其实,这块就牵扯到 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

开篇前,先讲一下 VNodevue 中的 vdom 其实就是一个 vnode 对象。

vdom 稍作了解的同学都应该知道,vdom 创建节点的核心首先就是创建一个对真实 dom 抽象的 js 对象树,然后通过一系列操作(后面我再谈具体什么操作)。该章节我们就只谈 vnode 的实现

1、constructor

首先,我们可以先看看, VNode 这个类对我们这些使用者暴露了哪些属性出来,挑一些我们常见的看

constructor (  tag?: string,  data?: VNodeData,  children?: ?Array
, 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

现在,我们举个例子,假如我需要解析下面文本

使用 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    }  ]}

然后我再看一个稍微复杂一点的例子

{
{ n }}

假如让大家使用 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
{ // 仅考虑 number 的情况 let ret: ?Array
, 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
) { // 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
; // 子节点 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
{ // 此处为 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

{
{ n }}

这块首先会被解析成 html 字符串

let html = `
{
{ n }}
`

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 = /^]+>/iconst comment = /^
', '"': '"', '&': '&', '
': '\n', ' ': '\t'}const encodedAttr = /&(?:lt|gt|quot|amp);/gconst 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.elmconst oldCh = oldVnode.childrenconst ch = vnode.childrenif (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]}

接下来就是 oldStartVnodenewStartVnodeoldEndVnodenewEndVnode 两两对比的四种情况了

// 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,它的作用主要是 returnoldChkeyindex 唯一对应的 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 ,用来找出 newStartVnodeoldCh 数组中对应的索引

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]}

经过这一系列的操作,则完成了节点之间的 diffpatch 操作,即完成了 oldVnodenewVnode 转换的操作。

文章到这里也要告一段落了,看到这里,相信大家已经对 vue 中的 vdom 这块也一定有了自己的理解了。 那么,我们再回到文章开头我们抛出的问题,大家知道为什么会出现这个问题了么?

emmm,如果想要继续沟通此问题,欢迎大家加群进行讨论,前端大杂烩:731175396。小伙伴们记得加群哦,哪怕一起来水群也是好的啊 ~ (注:群里单身漂亮妹纸真的很多哦,当然帅哥也很多,比如。。。me)

转载于:https://my.oschina.net/qiangdada/blog/3033832

你可能感兴趣的文章
SaltStack部署Redis主从实现
查看>>
利用冗余实现企业局域网的高可用性
查看>>
nginx 配置页面压缩
查看>>
磁盘和文件系统管理(二)
查看>>
WCF中有关Session的小实验
查看>>
C#设计模式(13)——代理模式(Proxy Pattern)
查看>>
如何在VIEW 5中配置日志数据库
查看>>
android的互联网开发 下
查看>>
JDBC连接属性
查看>>
随手摘录
查看>>
《树莓派实战秘籍》——2.5 技巧25更新固件和预构建二进制内核的简易方法...
查看>>
《Adobe Acrobat DC经典教程》—第1章1.4节Acrobat DC移动版app简介
查看>>
Java 8 vs. Scala:Part I
查看>>
《玩转3D打印》——1.3节3D打印的优势
查看>>
《FLUENT 14.0超级学习手册》——2.3 FLUENT 14.0的功能模块
查看>>
微软 Windows Phone 有望回归 或于一年后面世
查看>>
《SAS 统计分析与应用从入门到精通(第二版)》一1.2 SAS for Windows的安装和启动...
查看>>
深度学习论文阅读路线图
查看>>
《智能家居产品 从设计到运营》——2.3 智能设备互联的语言:通信协议
查看>>
如何自己注册域名?什么样的域名是好域名?
查看>>