虚拟DOM和实现diff算法,从了解到深入

2021-08-0409:05:54WEB前端开发Comments1,452 views字数 24412阅读模式
虚拟DOMdiff 算法 ,大家有的时候就会经常听到,那么它们是什么实现的呢,这是小浪我在学习的 虚拟DOMdiff 的时候总结,在这里就来带大家来深入了解 virtual DOMdiff 算法,从 snabbdom 的基础使用 ,到自己实现一个丐版 snabbdom,自己实现 h函数(创建虚拟DOM) patch函数(通过比较新旧虚拟DOM更新视图),这里我也画了几个动图 来帮助大家理解 diff 的四种优化策略。

 文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

1.介绍

Virtual DOM 简单的介绍文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

JavaScript按照DOM的结构来创建的虚拟树型结构对象,是对DOM的抽象,比DOM更加轻量型文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

为啥要使用Virtual DOM文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

  • 当然是前端优化方面,避免频繁操作DOM,频繁操作DOM会可能让浏览器回流和重绘,性能也会非常低,还有就是手动操作 DOM 还是比较麻烦的,要考虑浏览器兼容性问题,当前jQuery等库简化了 DOM操作,但是项目复杂了,DOM操作还是会变得复杂,数据操作也变得复杂
  • 并不是所有情况使用虚拟DOM 都提高性能,是针对在复杂的的项目使用。如果简单的操作,使用虚拟DOM,要创建虚拟DOM对象等等一系列操作,还不如普通的DOM 操作
  • 虚拟DOM 可以实现跨平台渲染,服务器渲染 、小程序、原生应用都使用了虚拟DOM
  • 使用虚拟DOM改变了当前的状态不需要立即的去更新DOM 而且更新的内容进行更新,对于没有改变的内容不做任何操作,通过前后两次差异进行比较
  • 虚拟 DOM 可以维护程序的状态,跟踪上一次的状态

2.snabbdom 介绍

首先来介绍下 snabbdom文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

我们要了解虚拟DOM ,那么就先了解它的始祖,也就是 snabbdom文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

snabbdom 是一个开源的项目,Vue 里面的 虚拟DOM 当初是借鉴了 snabbdom,我们可以通过了解snabbdom 的虚拟DOM 来理解 Vue 的虚拟DOM,Vue 的源码太多,snabbdom 比较简洁,所以用它来展开 虚拟 DOM 的研究文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

通过npm 进行安装文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

npm install snabbdom
复制代码

1.snabbdom简单使用

下面来写个简单的例子使用下 snabbdom文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

<body>
  <div id="app"></div>
  <script src="./js/test.js"></script>
</body>
复制代码

写个 test.js 进行使用文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

/* test.js */

// 导入 snabbdom
import { h, init, thunk } from 'snabbdom'
// init() 方法返回一个 patch 函数 用来比较两个虚拟DOM 的差异 然后更新到真实的DOM里
// 这里暂时传入一个空数组 []
let patch = init([])
// h 方法是用来创建 Virtual DOM
// 第一个参数是 虚拟DOM 标签
// 第二个参数是 虚拟DOM 的数据
// 第三个参数是 虚拟DOM 的子虚拟DOM
// 它有好几种传参方式 h函数做了重载 这里就 用上面的传参
// 而且可以进行嵌套使用
let vnode = h('div#box', '测试', [
  h('ul.list', [
    h('li', '我是一个li'),
    h('li', '我是一个li'),
    h('li', '我是一个li'),
  ]),
])
// 获取到 html 的 div#app
let app = document.querySelector('#app')
// 用来比较两个虚拟DOM 的差异 然后更新到真实的DOM里
let oldNode = patch(app, vnode)
// 再来模拟一个异步请求
setTimeout(() => {
  let vNode = h('div#box', '重新获取了数据', [
    h('ul.list', [
      h('li', '我是一个li'),
      h('li', '通过path判断了差异性'),
      h('li', '更新了数据'),
    ]),
  ])
  // 再来进行比较差异判断是否更新
  patch(oldNode, vNode)
}, 3000)

复制代码

虚拟DOM和实现diff算法,从了解到深入文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

可以看见把 虚拟DOM更新到了 真实DOM ,直接 把之前的 div#app 给替换更新了文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

虚拟DOM和实现diff算法,从了解到深入文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

过了3秒进行对比虚拟DOM 的 差异来添加到真实DOM ,这里改变了第二个和第三个 li 用h函数渲染成虚拟DOMoldNode 不一样所以进行了对比更新文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

2.介绍下 snabbdom中的模块

几个模块 这里简单过一下文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

模块名简介
attributesDOM 自定义属性,包括两个布尔值 checked selected,通过setAttribute() 设置
props是DOM 的 property属性,通过 element[attr] = value 设置
datasetdata- 开头的属性 data-src...
style行内样式
eventListeners用来注册和移除事件

有了上面的介绍,那我们就来简单的使用一下文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

/* module_test.js */

// 第一步当然是先导入 snabbdom 的 init() h()
import { h, init } from 'snabbdom'

// 导入模块
import attr from 'snabbdom/modules/attributes'
import style from 'snabbdom/modules/style'
import eventListeners from 'snabbdom/modules/eventlisteners'

// init()注册模块 返回值是 patch 函数用来比较 两个虚拟DOM 差异 然后添加到 真实DOM
let patch = init([attr, style, eventListeners])

// 使用 h() 渲染一个虚拟DOM
let vnode = h(
  'div#app',
  {
    // 自定义属性
    attrs: {
      myattr: '我是自定义属性',
    },
    // 行内样式
    style: {
      fontSize: '29px',
      color: 'skyblue',
    },
    // 事件绑定
    on: {
      click: clickHandler,
    },
  },
  '我是内容'
)

// 点击处理方法
function clickHandler() {
  // 拿到当前 DOM
  let elm = this.elm
  elm.style.color = 'red'
  elm.textContent = '我被点击了'
}

// 获取到 div#app
let app = document.querySelector('#app')

// patch 比较差异 ,然后添加到真实DOM 中
patch(app, vnode)

复制代码

然后再 html 中引入文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

<body>
  <div id="app"></div>
  <script src="./js/module_test.js"></script>
  <script></script>
</body>
复制代码

来看看效果文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

虚拟DOM和实现diff算法,从了解到深入文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

可以看见的是 自定义属性 ,行内样式 ,点击事件都被 h() 渲染出来了文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

上面的使用都简单地过了一遍,那么我们就来看看 snabbdom 中的源码吧文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

3.虚拟DOM 例子

说了这么久的 h() 函数和 虚拟DOM 那么 渲染出来的 虚拟DOM 是什么样呢文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

真实DOM 结构文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

<div class="container">
  <p>哈哈</p>
  <ul class="list">
    <li>1</li>
    <li>2</li>
  </ul>
</div>
复制代码

转为为 虚拟DOM 之后的结构文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

{ 
  // 选择器
  "sel": "div",
  // 数据
  "data": {
    "class": { "container": true }
  },
  // DOM
  "elm": undefined,
  // 和 Vue :key 一样是一种优化
  "key": undefined,
  // 子节点
  "children": [
    {
      "elm": undefined,
      "key": undefined,
      "sel": "p",
      "data": { "text": "哈哈" }
    },
    {
      "elm": undefined,
      "key": undefined,
      "sel": "ul",
      "data": {
        "class": { "list": true }
      },
      "children": [
        {
          "elm": undefined,
          "key": undefined,
          "sel": "li",
          "data": {
            "text": "1"
          },
          "children": undefined
        },
        {
          "elm": undefined,
          "key": undefined,
          "sel": "li",
          "data": {
            "text": "1"
          },
          "children": undefined
        }
      ]
    }
  ]
}

复制代码

在之前提到的 snabbdompatch方法文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

就是对 新的虚拟DOM老的虚拟DOM 进行diff(精细化比较),找出最小量更新 是在虚拟DOM 比较文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

不可能把所有的 DOM 都拆掉 然后全部重新渲染文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

4.h 函数

在上面我们体验了虚拟DOM的使用 ,那么我们现在来实现一个 丐版的 snabbdom文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

h 函数在介绍下文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

snabbdom 我们也使用了多次的 h 函数,主要作用是创建 虚拟节点文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

snabbdom 使用 TS 编写, 所以 h 函数中做了 方法重载 使用起来灵活文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

下面是 snabbdomh 函数,可以看出 参数的有好几种方式文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

export declare function h(sel: string): VNode;
export declare function h(sel: string, data: VNodeData): VNode;
export declare function h(sel: string, children: VNodeChildren): VNode;
export declare function h(sel: string, data: VNodeData, children: VNodeChildren): VNode;
复制代码

实现 vnode 函数文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

在写 h 函数之前 先实现 vnode 函数,vnode 函数要在 h 中使用, 其实这个 vnode 函数实现功能非常简单 在 TS 里面规定了很多类型,不过我这里和之后都是 用 JS 去写文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

/* vnode.js */

/**
 * 把传入的 参数 作为 对象返回
 * @param {string} sel 选择器
 * @param {object} data 数据
 * @param {array} children 子节点
 * @param {string} text 文本
 * @param {dom} elm DOM
 * @returns object
 */
export default function (sel, data, children, text, elm) {
  return { sel, data, children, text, elm }
}

复制代码

实现简易 h 函数文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

这里写的 h 函数 只实现主要功能,没有实现重载,直接实现 3个 参数的 h 函数文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

/* h.js */

// 导入 vnode
import vnode from './vnode'

// 导出 h 方法
// 这里就实现简单3个参数 参数写死
/**
 *
 * @param {string} a sel
 * @param {object} b data
 * @param {any} c 是子节点 可以是文本,数组
 */
export default function h(a, b, c) {
  // 先判断是否有三个参数
  if (arguments.length < 3) throw new Error('请检查参数个数')
  // 第三个参数有不确定性 进行判断
  // 1.第三个参数是文本节点
  if (typeof c === 'string' || typeof c === 'number') {
    // 调用 vnode 这直接传 text 进去
    // 返回值 {sel,data,children,text,elm} 再返回出去
    return vnode(a, b, undefined, c, undefined)
  } // 2.第三个参数是数组 [h(),h()] [h(),text] 这些情况
  else if (Array.isArray(c)) {
    // 然而 数组里必须是 h() 函数
    // children 用收集返回结果
    let children = []
    // 先判断里面是否全是 h()执行完的返回结果 是的话添加到 chilren 里
    for (let i = 0; i < c.length; i++) {
      // h() 的返回结果 是{} 而且 包含 sel
      if (!(typeof c[i] === 'object' && c[i].sel))
        throw new Error('第三个参数为数组时只能传递 h() 函数')
      // 满足条件进行push [{sel,data,children,text,elm},{sel,data,children,text,elm}]
      children.push(c[i])
    }
    // 调用 vnode 返回 {sel,data,children,text,elm} 再返回
    return vnode(a, b, children, undefined, undefined)
  } // 3.第三个参数直接就是函数 返回的是 {sel,data,children,text,elm}
  else if (typeof c === 'object' && c.sel) {
    // 这个时候在 使用h()的时候 c = {sel,data,children,text,elm} 直接放入children
    let children = [c]
    // 调用 vnode 返回 {sel,data,children,text,elm} 再返回
    return vnode(a, b, children, undefined, undefined)
  }
}

复制代码

是不是很简单呢,他说起来也不是递归,像是一种嵌套,不断地收集 {sel,data,children,text,elm}文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

chirldren 里面再套 {sel,data,children,text,elm}文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

举个例子文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

/* index.js */

import h from './my-snabbdom/h'

let vnode = h('div', {}, 
  h('ul', {}, [
    h('li', {}, '我是一个li'),
    h('li', {}, '我是一个li'),
    h('li', {}, '我是一个li'),
  ),
])
console.log(vnode)

复制代码
<body>
  <div id="container"></div>
  <script src="/virtualdir/bundle.js"></script>
</body>
复制代码

虚拟DOM和实现diff算法,从了解到深入文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

OK,写的 h 函数没有问题,生成了虚拟DOM 树,生成了虚拟 DOM,我们之后 就会用的到文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

简单说下流程吧文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

大家都知道js 函数执行,当然是先执行最里面的 函数文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

  • 1.h('li', {}, '我是一个li')第一个执行 返回的 {sel,data,children,text,elm} 连续三个 li 都是这个
  • 2.接着就是 h('ul', {}, []) 进入到了第二个判断是否为数组,然后 把每一项 进行判断是否对象 和 有sel 属性,然后添加到 children 里面又返回了出去 {sel,data,children,text,elm}
  • 3.第三就是执行 h('div', {},h()) 了, 第三个参数 直接是 h()函数 = {sel,data,children,text,elm} ,他的 children 把他用 [ ] 包起来再返回给 vnode

5.patch 函数

简介文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

snabbdom 中我们 通过 init() 返回了一个 patch 函数,通过 patch 进行吧比较两个 虚拟 DOM 然后添加的 真实的 DOM 树上,中间比较就是我们等下要说的 diff文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

先来了解下 patch里面做了什么文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

虚拟DOM和实现diff算法,从了解到深入文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

按照上面的流程我们来写个简单的 patch文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

1.patch

先写个sameVnode文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

用来对比两个虚拟DOMkeysel文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

/* sameVnode.js */

/**
 * 判断两个虚拟节点是否是同一节点
 * @param {vnode} vnode1 虚拟节点1
 * @param {vnode} vnode2 虚拟节点2
 * @returns boolean
 */
export default function sameVnode(vnode1, vnode2) {
  return (
    (vnode1.data ? vnode1.data.key : undefined) ===
      (vnode2.data ? vnode2.data.key : undefined) && vnode1.sel === vnode2.sel
  )
}

复制代码

写个基础的patch文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

/* patch.js */

// 导入 vnode
import vnode from './vnode'


// 导出 patch
/**
 *
 * @param {vnode/DOM} oldVnode
 * @param {vnode} newVnode
 */
export default function patch(oldVnode, newVnode) {
  // 1.判断oldVnode 是否为虚拟 DOM 这里判断是否有 sel
  if (!oldVnode.sel) {
    // 转为虚拟DOM
    oldVnode = emptyNodeAt(oldVnode)
  }
  // 判断 oldVnode 和 newVnode 是否为同一虚拟节点
  // 通过 key 和 sel 进行判断
  if (sameVnode(oldVnode, newVnode)) {
    // 是同一个虚拟节点 调用我们写的 patchVnode.js 中的方法
    ...
  } else {
    // 不是同一虚拟个节点 直接暴力拆掉老节点,换上新的节点
    ...
  }
  newVnode.elm = oldVnode.elm

  // 返回newVnode作为 旧的虚拟节点
  return newVnode
}

/**
 * 转为 虚拟 DOM
 * @param {DOM} elm DOM节点
 * @returns {object}
 */
function emptyNodeAt(elm) {
  // 把 sel 和 elm 传入 vnode 并返回
  // 这里主要选择器给转小写返回vnode
  // 这里功能做的简陋,没有去解析 # .
  // data 也可以传 ID 和 class
  return vnode(elm.tagName.toLowerCase(), undefined, undefined, undefined, elm)
}

复制代码

现在要处理是否是 同一个虚拟节点的问题文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

2.createElm

先来处理不是同一个虚拟节点文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

处理这个我们得去写个 创建节点的方法 这里就放到 createElm.js 中完成文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

/* createElm.js */

/**
 * 创建元素
 * @param {vnode} vnode 要创建的节点
 */
export default function createElm(vnode) {
  // 拿出 新创建的 vnode 中的 sel
  let node = document.createElement(vnode.sel)
  // 存在子节点
  // 子节点是文本
  if (
    vnode.text !== '' &&
    (vnode.children === undefined || vnode.children.length === 0)
  ) {
    // 直接添加文字到 node 中
    node.textContent = vnode.text

    // 子节点是数组
  } else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
    let children = vnode.children
    // 遍历数组
    for (let i = 0; i < children.length; i++) {
      // 获取到每一个数组中的 子节点
      let ch = children[i]
      // 递归的方式 创建节点
      let chDom = createElm(ch)
      // 把子节点添加到 自己身上
      node.appendChild(chDom)
    }
  }
  // 更新vnode 中的 elm
  vnode.elm = node
  // 返回 DOM
  return node
}

复制代码

上面的 createElm 就是使用了递归的方式去创建子节点 ,然后我们就去 patch 中 具体的调用这个 创建节点的方法文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

/* patch.js */

// 导入 vnode createELm
import vnode from './vnode'
import createElm from './createElm'


// 导出 patch
/**
 *
 * @param {vnode/DOM} oldVnode
 * @param {vnode} newVnode
 */
export default function patch(oldVnode, newVnode) {
  // 1.判断oldVnode 是否为虚拟 DOM 这里判断是否有 sel
  if (!oldVnode.sel) {
    // 转为虚拟DOM
    oldVnode = emptyNodeAt(oldVnode)
  }
  // 判断 oldVnode 和 newVnode 是否为同一虚拟节点
  // 通过 key 和 sel 进行判断
  if (sameVnode(oldVnode, newVnode)) {
    // 是同一个虚拟节点 调用我们写的 patchVnode.js 中的方法
    ...
  } else {
    // 不是同一虚拟个节点 直接暴力拆掉老节点,换上新的节点
    // 这里通过 createElm 递归 转为 真实的 DOM 节点
    let newNode = createElm(newVnode)
    // 旧节点的父节点
    if (oldVnode.elm.parentNode) {
      let parentNode = oldVnode.elm.parentNode
      // 添加节点到真实的DOM 上
      parentNode.insertBefore(newNode, oldVnode.elm)
      // 删除旧节点
      parentNode.removeChild(oldVnode.elm)
    }
  }
  newVnode.elm = oldVnode.elm
  return newVnode
}
...
}

复制代码

在递归添加子节点 到了最后我们在 patch 添加到 真实的 DOM 中,移除之前的老节点文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

写到这里了来试试 不同节点 是否真的添加文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

/* index.js */

import h from './my-snabbdom/h'
import patch from './my-snabbdom/patch'


let app = document.querySelector('#app')

let vnode = h('ul', {}, [
  h('li', {}, '我是一个li'),
  h('li', {}, [
    h('p', {}, '我是一个p'),
    h('p', {}, '我是一个p'),
    h('p', {}, '我是一个p'),
  ]),
  h('li', {}, '我是一个li'),
])


let oldVnode = patch(app, vnode)

复制代码
<body>
  <div id="app">hellow</div>
  <script src="/virtualdir/bundle.js"></script>
</body>
复制代码

虚拟DOM和实现diff算法,从了解到深入文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

div#app 给替换了,并且成功替换文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

3.patchVnode

我们现在来实现同一个虚拟 DOM 的处理文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

在 patchVnode 中文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

步骤都是按照 之前那个流程图进行编写,我们把比较两个相同的 虚拟 DOM 代码写在 patchVnode.js文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

在比较 两个相同的虚拟节点分支 有好几种情况文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

/* patchVnode.js */

// 导入 vnode createELm
import createElm from './createElm'

/**
 *
 * @param {vnode} oldVnode 老的虚拟节点
 * @param {vnode} newVnode 新的虚拟节点
 * @returns
 */
// 对比同一个虚拟节点
export default function patchVnode(oldVnode, newVnode) {
  // 1.判断是否相同对象
  console.log('同一个虚拟节点')
  if (oldVnode === newVnode) return
  // 2.判断newVnode上有没有text
  // 这里为啥不考虑 oldVnode呢,因为 newVnode有text说明就没children
  if (newVnode.text && !newVnode.children) {
    // 判断是text否相同
    if (oldVnode.text !== newVnode.text) {
      console.log('文字不相同')
      // 不相同就直接把 newVnode中text 给 elm.textContent
      oldVnode.elm.textContent = newVnode.text
    }
  } else {
    // 3.判断oldVnode有children, 这个时候newVnode 没有text但是有 children
    if (oldVnode.children) {
      ...这里新旧节点都存在children 这里要使用 updateChildren 下面进行实现
    } else {
      console.log('old没有children,new有children')
      // oldVnode没有 children ,newVnode 有children
      // 这个时候oldVnode 只有text 我们把 newVnode 的children拿过来
      // 先清空 oldVnode 中text
      oldVnode.elm.innerHTML = ''
      // 遍历 newVnode 中的 children
      let newChildren = newVnode.children
      for (let i = 0; i < newChildren.length; i++) {
        // 通过递归拿到了 newVnode 子节点
        let node = createElm(newChildren[i])
        // 添加到 oldVnode.elm 中
        oldVnode.elm.appendChild(node)
      }
    }
  }
}

复制代码

按照流程图进行编码,现在要处理 newVnodeoldVnode 都存在 children 的情况了文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

在这里我们要进行精细化比较 也就是我们经常说的 diff文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

4.diff

经常听到的 diff(精细化比较) ,那我们先来了解下文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

diff四种优化策略文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

在这里要使用 4 个指针,从1-4的顺序来开始命中优化策略,命中一个,指针进行移动(新前和旧前向下移动,新后和旧后向上移动),没有命中,就使用下一个策略,如果四个策略都没有命中,只能靠循环来找文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

命中:两个节点 selkey 一样文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

  1. 新前与旧前
  2. 新后与旧后
  3. 新后与旧前
  4. 新前与旧后

先来说下新增的情况文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

四种策略都是在 循环里面执行文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

while(旧前<=旧后&&新前<=新后){
  ...
}
复制代码

虚拟DOM和实现diff算法,从了解到深入文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

可以看出 旧子节点 先循环完毕,那么说明了新的子节点有需要 新增的 子节点文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

新前新后 的 节点 就是需要新增的字节文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

删除的情况1文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

虚拟DOM和实现diff算法,从了解到深入文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

这里新子节点 先循环完毕说明 旧子节点有需要删除的节点文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

删除的情况2文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

当我们删除多个,而且 4种策略都没有满足,我们得通过 while 循环 旧子节点 找到 新子节点需要寻找节点并标记为 undefined 虚拟节点是 undefined实际上在 DOM已经把它移动了 ,旧前旧后 之间的节点就是需要删除的节点文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

虚拟DOM和实现diff算法,从了解到深入文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

复杂情况1文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

当触发了 第四种 策略,这里就需要移动节点了,旧后指向的节点(在虚拟节点标为 undefined),实际把 新前 指向的节点 在DOM 中 移动到旧前之前文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

虚拟DOM和实现diff算法,从了解到深入文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

复杂情况2文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

当触发了 第三种 策略,这里也需要移动节点了,旧前 指向的节点(在虚拟节点标为 undefined),实际把 新后 指向的节点 在DOM 中 移动到旧后之后文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

虚拟DOM和实现diff算法,从了解到深入文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

注意几个点 :文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

  • h('li',{key:'A'} : "A"}) 比如这其中的 key 是这个节点的唯一的标识
  • 它的存在是在告诉 diff ,在更改前后它们是同一个DOM节点。
  • 只有是同一个虚拟节点,才进行精细化比较,否则就是暴力删除旧的、插入新的
  • 同一虚拟节点 不仅要 key 相同而且要 选择器相同也就是上面的 h() 函数创建的 虚拟节点 对象里的 sel
  • 只进行同层比较,不会进行跨层比较

5.updateChildren

看了上面对于 diff 的介绍,不知道我画的图 演示清楚了没,然后我们接着继续来完成 patchVnode文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

我们得写个 updateChildren 来进行精细化比较文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

这个文件就是 diff 算法的核心,我们用来比较 oldVnodenewVnode 都存在 children 的情况文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

这里有点绕,注释都写了,请耐心观看,流程就是按照 diff 的四种策略来写,还要处理没有命中的情况文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

/* updateChilren.js */

// 导入 createElm patchVnode sameVnode
import createElm from './createElm'
import patchVnode from './patchVnode'
import sameVnode from './sameVnode'

// 导出 updateChildren
/**
 *
 * @param {dom} parentElm 父节点
 * @param {array} oldCh 旧子节点
 * @param {array} newCh 新子节点
 */
export default function updateChildren(parentElm, oldCh, newCh) {
  // 下面先来定义一下之前讲过的 diff 的几个指针 和 指针指向的 节点
  // 旧前 和 新前
  let oldStartIdx = 0,
    newStartIdx = 0
  let oldEndIdx = oldCh.length - 1 //旧后
  let newEndIdx = newCh.length - 1 //新后
  let oldStartVnode = oldCh[0] //旧前 节点
  let oldEndVnode = oldCh[oldEndIdx] //旧后节点
  let newStartVnode = newCh[0] //新前节点
  let newEndVnode = newCh[newEndIdx] //新后节点
  let keyMap = null //用来做缓存
  // 写循环条件
  while (newStartIdx <= newEndIdx && oldStartIdx <= oldEndIdx) {
    console.log('---进入diff---')

    // 下面按照 diff 的4种策略来写 这里面还得调用 pathVnode
    // patchVnode 和 updateChildren 是互相调用的关系,不过这可不是死循环
    // 指针走完后就不调用了

    // 这一段都是为了忽视我们加过 undefined 节点,这些节点实际上已经移动了
    if (oldCh[oldStartIdx] == undefined) {
      oldStartVnode = oldCh[++oldStartIdx]
    } else if (oldCh[oldEndIdx] == undefined) {
      oldEndVnode = oldCh[--oldEndIdx]
    } else if (newCh[newStartIdx] == undefined) {
      newStartVnode = newCh[++newStartIdx]
    } else if (newCh[newEndIdx] == undefined) {
      newEndVnode = newCh[--newEndIdx]
    }
    // 忽视了所有的 undefined 我们这里来 判断四种diff优化策略
    // 1.新前 和 旧前
    else if (sameVnode(oldStartVnode, newStartVnode)) {
      console.log('1命中')
      // 调用 patchVnode 对比两个节点的 对象 文本 children
      patchVnode(oldStartVnode, newStartVnode)
      // 指针移动
      newStartVnode = newCh[++newStartIdx]
      oldStartVnode = oldCh[++oldStartIdx]
    } // 2.新后 和 旧后
    else if (sameVnode(oldEndVnode, newEndVnode)) {
      console.log('2命中')
      // 调用 patchVnode 对比两个节点的 对象 文本 children
      patchVnode(oldEndVnode, newEndVnode)
      // 指针移动
      newEndVnode = newCh[--newEndIdx]
      oldEndVnode = oldCh[--oldEndIdx]
    } // 3.新后 和 旧前
    else if (sameVnode(oldStartVnode, newEndVnode)) {
      console.log('3命中')
      // 调用 patchVnode 对比两个节点的 对象 文本 children
      patchVnode(oldStartVnode, newEndVnode)
      // 策略3是需要移动节点的 把旧前节点 移动到 旧后 之后
      // insertBefore 如果参照节点为空,就插入到最后 和 appendChild一样
      parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling)
      // 指针移动
      newEndVnode = newCh[--newEndIdx]
      oldStartVnode = oldCh[++oldStartIdx]
    }
    // 4.新前 和 旧后
    else if (sameVnode(oldEndVnode, newStartVnode)) {
      console.log('4命中')
      // 调用 patchVnode 对比两个节点的 对象 文本 children
      patchVnode(oldEndVnode, newStartVnode)
      // 策略4是也需要移动节点的 把旧后节点 移动到 旧前 之前
      parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm)
      // 指针移动
      newStartVnode = newCh[++newStartIdx]
      oldEndVnode = oldCh[--oldEndIdx]
    } else {
      console.log('diff四种优化策略都没命中')
      // 当四种策略都没有命中
      // keyMap 为缓存,这样就不用每次都遍历老对象
      if (!keyMap) {
        // 初始化 keyMap
        keyMap = {}
        // 从oldStartIdx到oldEndIdx进行遍历
        for (let i = oldStartIdx; i < oldEndIdx; i++) {
          // 拿个每个子对象 的 key
          const key = oldCh[i].data.key
          // 如果 key 不为 undefined 添加到缓存中
          if (!key) keyMap[key] = i
        }
      }

      // 判断当前项是否存在 keyMap 中 ,当前项时 新前(newStartVnode)
      let idInOld = keyMap[newStartIdx.data]
        ? keyMap[newStartIdx.data.key]
        : undefined

      // 存在的话就是移动操作
      if (idInOld) {
        console.log('移动节点')
        // 从 老子节点 取出要移动的项
        let moveElm = oldCh[idInOld]
        // 调用 patchVnode 进行对比 修改
        patchVnode(moveElm, newStartVnode)
        // 将这一项设置为 undefined
        oldCh[idInOld] = undefined
        // 移动 节点 ,对于存在的节点使用 insertBefore移动
        // 移动的 旧前 之前 ,因为 旧前 与 旧后 之间的要被删除
        parentElm.insertBefore(moveElm.elm, oldStartVnode.elm)
      } else {
        console.log('添加新节点')
        // 不存在就是要新增的项
        // 添加的节点还是虚拟节点要通过 createElm 进行创建 DOM
        // 同样添加到 旧前 之前
        parentElm.insertBefore(createElm(newStartVnode), oldStartVnode.elm)
      }

      // 处理完上面的添加和移动 我们要 新前 指针继续向下走
      newStartVnode = newCh[++newStartIdx]
    }
  }
  // 我们添加和删除操作还没做呢
  // 首先来完成添加操作 新前 和 新后 中间是否还存在节点
  if (newStartIdx <= newEndIdx) {
    console.log('进入添加剩余节点')
    // 这是一个标识
    // let beforeFlag = oldCh[oldEndIdx + 1] ? oldCh[oldEndIdx + 1].elm : null
    let beforeFlag = newCh[newEndIdx + 1] ? newCh[newEndIdx + 1] : null
    // new 里面还有剩余节点 遍历添加
    for (let i = newStartIdx; i <= newEndIdx; i++) {
      // newCh里面的子节点还需要 从虚拟DOM 转为 DOM
      parentElm.insertBefore(createElm(newCh[i]), beforeFlag)
    }
  } else if (oldStartIdx <= oldEndIdx) {
    console.log('进入删除多余节点')
    // old 里面还有剩余 节点 ,旧前 和 旧后 之间的节点需要删除
    for (let i = oldStartIdx; i <= oldEndIdx; i++) {
      // 删除 剩余节点之前 先判断下是否存在
      if (oldCh[i].elm) parentElm.removeChild(oldCh[i].elm)
    }
  }
}

复制代码

到了这里我们基本写都完成了, h 函数 创建 虚拟 DOM , patch 比较 虚拟DOM 进行更新视图文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

6.我们来测试一下写的

其实在写代码的时候就在不断的调试。。。现在随便测试几个文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

1.代码

html文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

<body>
  <button class="btn">策略3</button>
  <button class="btn">复杂</button>
  <button class="btn">删除</button>
  <button class="btn">复杂</button>
  <button class="btn">复杂</button>
  <ul id="app">
    hellow
  </ul>

  <script src="/virtualdir/bundle.js"></script>
</body>
复制代码

index.js文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

/* index.js */

import h from './my-snabbdom/h'
import patch from './my-snabbdom/patch'

let app = document.querySelector('#app')

let vnode = h('ul', {}, [
  h('li', { key: 'A' }, 'A'),
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'C' }, 'C'),
  h('li', { key: 'D' }, 'D'),
  h('li', { key: 'E' }, 'E'),
])

let oldVnode = patch(app, vnode)

let vnode2 = h('ul', {}, [
  h('li', { key: 'E' }, 'E'),
  h('li', { key: 'D' }, 'D'),
  h('li', { key: 'C' }, 'C'),
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'A' }, 'A'),
])
let vnode3 = h('ul', {}, [
  h('li', { key: 'E' }, 'E'),
  h('li', { key: 'D' }, 'D'),
  h('li', { key: 'C' }, 'C'),
  h('li', { key: 'A' }, 'A'),
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'K' }, 'K'),
])
let vnode4 = h('ul', {}, [
  h('li', { key: 'A' }, 'A'),
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'C' }, 'C'),
])
let vnode5 = h('ul', {}, [
  h('li', { key: 'E' }, 'E'),
  h('li', { key: 'C' }, 'C'),
  h('li', { key: 'V' }, 'V'),
])
let vnode6 = h('ul', {}, [
  h('li', { key: 'A' }, 'A'),
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'C' }, 'C'),
  h('li', { key: 'D' }, 'D'),
  h(
    'li',
    { key: 'E' },
    h('ul', {}, [
      h('li', { key: 'A' }, 'A'),
      h('li', { key: 'B' }, 'B'),
      h('li', { key: 'C' }, 'C'),
      h('li', { key: 'D' }, 'D'),
      h('li', { key: 'E' }, h('div', { key: 'R' }, 'R')),
    ])
  ),
])
let vnodeList = [vnode2, vnode3, vnode4, vnode5, vnode6]
let btn = document.querySelectorAll('.btn')
for (let i = 0; i < btn.length; i++) {
  btn[i].onclick = () => {
    patch(vnode, vnodeList[i])
  }
}
复制代码

2.演示

策略3文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

虚拟DOM和实现diff算法,从了解到深入文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

复杂文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

虚拟DOM和实现diff算法,从了解到深入文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

删除文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

虚拟DOM和实现diff算法,从了解到深入文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

复杂文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

虚拟DOM和实现diff算法,从了解到深入文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

复杂(这里是简单 。。)文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

虚拟DOM和实现diff算法,从了解到深入文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

7.结语

注释我都写了喔,大家可以对照 我上面画的图不清楚可以反复耐心的看哈文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

如果看的话没什么感觉,大家可以自己动手写写,下面我会贴出所有的代码文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

代码同样也放在 github 文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

完整代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

h.js文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

/* h.js */

// 导入 vnode
import vnode from './vnode'

// 导出 h 方法
// 这里就实现简单3个参数 参数写死
/**
 *
 * @param {string} a sel
 * @param {object} b data
 * @param {any} c 是子节点 可以是文本,数组
 */
export default function h(a, b, c) {
  // 先判断是否有三个参数
  if (arguments.length < 3) throw new Error('请检查参数个数')
  // 第三个参数有不确定性 进行判断
  // 1.第三个参数是文本节点
  if (typeof c === 'string' || typeof c === 'number') {
    // 调用 vnode 这直接传 text 进去
    // 返回值 {sel,data,children,text,elm} 再返回出去
    return vnode(a, b, undefined, c, undefined)
  } // 2.第三个参数是数组 [h(),h()] [h(),text] 这些情况
  else if (Array.isArray(c)) {
    // 然而 数组里必须是 h() 函数
    // children 用收集返回结果
    let children = []
    // 先判断里面是否全是 h()执行完的返回结果 是的话添加到 chilren 里
    for (let i = 0; i < c.length; i++) {
      // h() 的返回结果 是{} 而且 包含 sel
      if (!(typeof c[i] === 'object' && c[i].sel))
        throw new Error('第三个参数为数组时只能传递 h() 函数')
      // 满足条件进行push [{sel,data,children,text,elm},{sel,data,children,text,elm}]
      children.push(c[i])
    }
    // 调用 vnode 返回 {sel,data,children,text,elm} 再返回
    return vnode(a, b, children, undefined, undefined)
  } // 3.第三个参数直接就是函数 返回的是 {sel,data,children,text,elm}
  else if (typeof c === 'object' && c.sel) {
    // 这个时候在 使用h()的时候 c = {sel,data,children,text,elm} 直接放入children
    let children = [c]
    // 调用 vnode 返回 {sel,data,children,text,elm} 再返回
    return vnode(a, b, children, undefined, undefined)
  }
}

复制代码

patch.js文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

/* patch.js */

// 导入 vnode createELm patchVnode sameVnode.js
import vnode from './vnode'
import createElm from './createElm'
import patchVnode from './patchVnode'
import sameVnode from './sameVnode'

// 导出 patch
/**
 *
 * @param {vnode/DOM} oldVnode
 * @param {vnode} newVnode
 */
export default function patch(oldVnode, newVnode) {
  // 1.判断oldVnode 是否为虚拟 DOM 这里判断是否有 sel
  if (!oldVnode.sel) {
    // 转为虚拟DOM
    oldVnode = emptyNodeAt(oldVnode)
  }
  // 判断 oldVnode 和 newVnode 是否为同一虚拟节点
  // 通过 key 和 sel 进行判断
  if (sameVnode(oldVnode, newVnode)) {
    // 是同一个虚拟节点 调用我们写的 patchVnode.js 中的方法
    patchVnode(oldVnode, newVnode)
  } else {
    // 不是同一虚拟个节点 直接暴力拆掉老节点,换上新的节点
    // 这里通过 createElm 递归 转为 真实的 DOM 节点
    let newNode = createElm(newVnode)
    // 旧节点的父节点
    if (oldVnode.elm.parentNode) {
      let parentNode = oldVnode.elm.parentNode
      // 添加节点到真实的DOM 上
      parentNode.insertBefore(newNode, oldVnode.elm)
      // 删除旧节点
      parentNode.removeChild(oldVnode.elm)
    }
  }
  newVnode.elm = oldVnode.elm
  // console.log(newVnode.elm)

  // 返回newVnode作为 旧的虚拟节点
  return newVnode
}

/**
 * 转为 虚拟 DOM
 * @param {DOM} elm DOM节点
 * @returns {object}
 */
function emptyNodeAt(elm) {
  // 把 sel 和 elm 传入 vnode 并返回
  // 这里主要选择器给转小写返回vnode
  // 这里功能做的简陋,没有去解析 # .
  // data 也可以传 ID 和 class
  return vnode(elm.tagName.toLowerCase(), undefined, undefined, undefined, elm)
}

复制代码

createElm.js文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

/* createElm.js */

/**
 * 创建元素
 * @param {vnode} vnode 要创建的节点
 */
export default function createElm(vnode) {
  // 拿出 新创建的 vnode 中的 sel
  let node = document.createElement(vnode.sel)
  // 存在子节点
  // 子节点是文本
  if (
    vnode.text !== '' &&
    (vnode.children === undefined || vnode.children.length === 0)
  ) {
    // 直接添加文字到 node 中
    node.textContent = vnode.text
    // 子节点是数组
  } else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
    let children = vnode.children
    // 遍历数组
    for (let i = 0; i < children.length; i++) {
      // 获取到每一个数组中的 子节点
      let ch = children[i]
      // 递归的方式 创建节点
      let chDom = createElm(ch)
      // 把子节点添加到 自己身上
      node.appendChild(chDom)
    }
  }
  // 更新vnode 中的 elm
  vnode.elm = node
  // 返回 DOM
  return node
}

复制代码

vnode.js文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

/* vnode.js */

/**
 * 把传入的 参数 作为 对象返回
 * @param {string} sel 选择器
 * @param {object} data 数据
 * @param {array} children 子节点
 * @param {string} text 文本
 * @param {dom} elm DOM
 * @returns
 */
export default function (sel, data, children, text, elm) {
  return { sel, data, children, text, elm }
}

复制代码

patchVnode.js文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

/* patchVnode.js */

// 导入 vnode createELm patchVnode updateChildren
import createElm from './createElm'
import updateChildren from './updateChildren'
/**
 *
 * @param {vnode} oldVnode 老的虚拟节点
 * @param {vnode} newVnode 新的虚拟节点
 * @returns
 */
// 对比同一个虚拟节点
export default function patchVnode(oldVnode, newVnode) {
  // 1.判断是否相同对象
  // console.log('同一个虚拟节点')
  if (oldVnode === newVnode) return
  // 2.判断newVnode上有没有text
  // 这里为啥不考虑 oldVnode呢,因为 newVnode有text说明就没children
  if (newVnode.text && !newVnode.children) {
    // 判断是text否相同
    if (oldVnode.text !== newVnode.text) {
      console.log('文字不相同')
      // 不相同就直接把 newVnode中text 给 elm.textContent
      oldVnode.elm.textContent = newVnode.text
    }
  } else {
    // 3.判断oldVnode有children, 这个时候newVnode 没有text但是有 children
    if (oldVnode.children) {
      updateChildren(oldVnode.elm, oldVnode.children, newVnode.children)
    } else {
      console.log('old没有children,new有children')
      // oldVnode没有 children ,newVnode 有children
      // 这个时候oldVnode 只有text 我们把 newVnode 的children拿过来
      // 先清空 oldVnode 中text
      oldVnode.elm.innerHTML = ''
      // 遍历 newVnode 中的 children
      let newChildren = newVnode.children
      for (let i = 0; i < newChildren.length; i++) {
        // 通过递归拿到了 newVnode 子节点
        let node = createElm(newChildren[i])
        // 添加到 oldVnode.elm 中
        oldVnode.elm.appendChild(node)
      }
    }
  }
}

复制代码

sameVnode.js文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

/* sameVnode.js */

/**
 * 判断两个虚拟节点是否是同一节点
 * @param {vnode} vnode1 虚拟节点1
 * @param {vnode} vnode2 虚拟节点2
 * @returns boolean
 */
export default function sameVnode(vnode1, vnode2) {
  return (
    (vnode1.data ? vnode1.data.key : undefined) ===
      (vnode2.data ? vnode2.data.key : undefined) && vnode1.sel === vnode2.sel
  )
}

复制代码

updateChildren.js文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

/* updateChilren.js */

// 导入 createElm patchVnode sameVnode
import createElm from './createElm'
import patchVnode from './patchVnode'
import sameVnode from './sameVnode'

// 导出 updateChildren
/**
 *
 * @param {dom} parentElm 父节点
 * @param {array} oldCh 旧子节点
 * @param {array} newCh 新子节点
 */
export default function updateChildren(parentElm, oldCh, newCh) {
  // 下面先来定义一下之前讲过的 diff 的几个指针 和 指针指向的 节点
  // 旧前 和 新前
  let oldStartIdx = 0,
    newStartIdx = 0
  let oldEndIdx = oldCh.length - 1 //旧后
  let newEndIdx = newCh.length - 1 //新后
  let oldStartVnode = oldCh[0] //旧前 节点
  let oldEndVnode = oldCh[oldEndIdx] //旧后节点
  let newStartVnode = newCh[0] //新前节点
  let newEndVnode = newCh[newEndIdx] //新后节点
  let keyMap = null //用来做缓存
  // 写循环条件
  while (newStartIdx <= newEndIdx && oldStartIdx <= oldEndIdx) {
    console.log('---进入diff---')

    // 下面按照 diff 的4种策略来写 这里面还得调用 pathVnode
    // patchVnode 和 updateChildren 是互相调用的关系,不过这可不是死循环
    // 指针走完后就不调用了

    // 这一段都是为了忽视我们加过 undefined 节点,这些节点实际上已经移动了
    if (oldCh[oldStartIdx] == undefined) {
      oldStartVnode = oldCh[++oldStartIdx]
    } else if (oldCh[oldEndIdx] == undefined) {
      oldEndVnode = oldCh[--oldEndIdx]
    } else if (newCh[newStartIdx] == undefined) {
      newStartVnode = newCh[++newStartIdx]
    } else if (newCh[newEndIdx] == undefined) {
      newEndVnode = newCh[--newEndIdx]
    }
    // 忽视了所有的 undefined 我们这里来 判断四种diff优化策略
    // 1.新前 和 旧前
    else if (sameVnode(oldStartVnode, newStartVnode)) {
      console.log('1命中')
      // 调用 patchVnode 对比两个节点的 对象 文本 children
      patchVnode(oldStartVnode, newStartVnode)
      // 指针移动
      newStartVnode = newCh[++newStartIdx]
      oldStartVnode = oldCh[++oldStartIdx]
    } // 2.新后 和 旧后
    else if (sameVnode(oldEndVnode, newEndVnode)) {
      console.log('2命中')
      // 调用 patchVnode 对比两个节点的 对象 文本 children
      patchVnode(oldEndVnode, newEndVnode)
      // 指针移动
      newEndVnode = newCh[--newEndIdx]
      oldEndVnode = oldCh[--oldEndIdx]
    } // 3.新后 和 旧前
    else if (sameVnode(oldStartVnode, newEndVnode)) {
      console.log('3命中')
      // 调用 patchVnode 对比两个节点的 对象 文本 children
      patchVnode(oldStartVnode, newEndVnode)
      // 策略3是需要移动节点的 把旧前节点 移动到 旧后 之后
      // insertBefore 如果参照节点为空,就插入到最后 和 appendChild一样
      parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling)
      // 指针移动
      newEndVnode = newCh[--newEndIdx]
      oldStartVnode = oldCh[++oldStartIdx]
    }
    // 4.新前 和 旧后
    else if (sameVnode(oldEndVnode, newStartVnode)) {
      console.log('4命中')
      // 调用 patchVnode 对比两个节点的 对象 文本 children
      patchVnode(oldEndVnode, newStartVnode)
      // 策略4是也需要移动节点的 把旧后节点 移动到 旧前 之前
      parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm)
      // 指针移动
      newStartVnode = newCh[++newStartIdx]
      oldEndVnode = oldCh[--oldEndIdx]
    } else {
      console.log('diff四种优化策略都没命中')
      // 当四种策略都没有命中
      // keyMap 为缓存,这样就不用每次都遍历老对象
      if (!keyMap) {
        // 初始化 keyMap
        keyMap = {}
        // 从oldStartIdx到oldEndIdx进行遍历
        for (let i = oldStartIdx; i < oldEndIdx; i++) {
          // 拿个每个子对象 的 key
          const key = oldCh[i].data.key
          // 如果 key 不为 undefined 添加到缓存中
          if (!key) keyMap[key] = i
        }
      }

      // 判断当前项是否存在 keyMap 中 ,当前项时 新前(newStartVnode)
      let idInOld = keyMap[newStartIdx.data]
        ? keyMap[newStartIdx.data.key]
        : undefined

      // 存在的话就是移动操作
      if (idInOld) {
        console.log('移动节点')
        // 从 老子节点 取出要移动的项
        let moveElm = oldCh[idInOld]
        // 调用 patchVnode 进行对比 修改
        patchVnode(moveElm, newStartVnode)
        // 将这一项设置为 undefined
        oldCh[idInOld] = undefined
        // 移动 节点 ,对于存在的节点使用 insertBefore移动
        // 移动的 旧前 之前 ,因为 旧前 与 旧后 之间的要被删除
        parentElm.insertBefore(moveElm.elm, oldStartVnode.elm)
      } else {
        console.log('添加新节点')
        // 不存在就是要新增的项
        // 添加的节点还是虚拟节点要通过 createElm 进行创建 DOM
        // 同样添加到 旧前 之前
        parentElm.insertBefore(createElm(newStartVnode), oldStartVnode.elm)
      }

      // 处理完上面的添加和移动 我们要 新前 指针继续向下走
      newStartVnode = newCh[++newStartIdx]
    }
  }
  // 我们添加和删除操作还没做呢
  // 首先来完成添加操作 新前 和 新后 中间是否还存在节点
  if (newStartIdx <= newEndIdx) {
    console.log('进入添加剩余节点')
    // 这是一个标识
    // let beforeFlag = oldCh[oldEndIdx + 1] ? oldCh[oldEndIdx + 1].elm : null
    let beforeFlag = newCh[newEndIdx + 1] ? newCh[newEndIdx + 1] : null
    // new 里面还有剩余节点 遍历添加
    for (let i = newStartIdx; i <= newEndIdx; i++) {
      // newCh里面的子节点还需要 从虚拟DOM 转为 DOM
      parentElm.insertBefore(createElm(newCh[i]), beforeFlag)
    }
  } else if (oldStartIdx <= oldEndIdx) {
    console.log('进入删除多余节点')
    // old 里面还有剩余 节点 ,旧前 和 旧后 之间的节点需要删除
    for (let i = oldStartIdx; i <= oldEndIdx; i++) {
      // 删除 剩余节点之前 先判断下是否存在
      if (oldCh[i].elm) parentElm.removeChild(oldCh[i].elm)
    }
  }
}

作者:小浪努力学前端
链接:https://juejin.cn/post/6990582632270528525
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

 文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21759.html

  • 本站内容整理自互联网,仅提供信息存储空间服务,以方便学习之用。如对文章、图片、字体等版权有疑问,请在下方留言,管理员看到后,将第一时间进行处理。
  • 转载请务必保留本文链接:https://www.cainiaoxueyuan.com/gcs/21759.html

Comment

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定