细谈 vue 核心- vdom 篇

很早以前,我曾写过一篇文章,分析并实现过一版简易的 vdom。想看的能够点击 传送门javascript

聊聊为何又想着写这么一篇文章,实在是项目里,无论本身仍是同事,都或多或少会遇到这块的坑。因此这里当给小伙伴们再作一次总结吧,但愿大伙看完,能对 vue 中的 vdom 有一个更好的认知。好了,接下来直接开始吧html

1、抛出问题

在开始以前,我先抛出一个问题,你们能够先思考,而后再接着阅读后面的篇幅。先上下代码前端

<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
}
复制代码

2、class VNode

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

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

一、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 的惟一标识
}
复制代码

二、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
      }
    ]
  }
  // 依次循环
]
复制代码

三、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

3、render

一、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)
复制代码

二、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 字符串数组
  }
}
复制代码

三、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 = {
  '&lt;': '<',
  '&gt;': '>',
  '&quot;': '"',
  '&amp;': '&',
  '&#10;': '\n',
  '&#9;': '\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 这块的逻辑了。

4、diff and patch

哎呀,终于到 diff 和 patch 环节了,想一想仍是很鸡冻呢。

一、一些 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
}
复制代码

二、一些 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
}
复制代码

三、节点 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]
}
复制代码

接下来就是 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)

我的准备从新捡回本身的公众号了,以后每周保证一篇高质量好文,感兴趣的小伙伴能够关注一波。

相关文章
相关标签/搜索