vue2.x虚拟DOM与DIFF算法

snabbdom

简介

  1. snabbdom:是瑞典语单词,单词原意为“速度”。是著名的虚拟 DOM 库,是 diff 算法的鼻祖,Vue 源码就是借鉴了 snabbdom
  2. 官方 Git:https://github.com/snabbdom/snabbdom

安装

  1. 在 git 上的 snabbdom 源码是用 TypeScript 写的,git 上并不提供编译好的 JavaScript 版本
  2. 如果要直接使用 build 出来的 JavaScript 版的 snabbdom 库,可以从 npm 上下载:npm i -S snabbdom
  3. 安装步骤:新建文件夹study-snabbdom =》在终端打开study-snabbdom文件夹 =》npm初始化:npm init =》一路回车 =》安装snabbdom:npm i -S snabbdom【终端不要关】
  4. 安装好了之后 打开node_modules\snabbdom文件夹 里面的src中放着TS代码 build文件夹中放着JS代码

测试环境搭建

  1. snabbdom 库是 DOM 库,当然不能在 nodejs 环境运行,所以我们需要搭建 webpack 和 webpack-dev-server 开发环境,好消息是不需要安装任何loader
  2. 这里需要注意,必须安装最新版 webpack@5,不能安装 webpack@4,这是因为 webpack@4 没有读取身份证(package.json)中 exports 的能力,建议大家使用这样的版本:
npm i -D webpack@5 webpack-cli@3 webpack-dev-server@3

(在终端继续输入以上代码安装测试环境)

  1. 在study-snabbdom文件夹中新建webpack.config.js文件 参考webpack官网 将以下代码书写到webpack.config.js文件中
// https://webpack.docschina.org/
const path = require('path')

module.exports = {
  // 入口
  entry: './src/index.js',
  // 出口
  output: {
    // 虚拟打包路径,就是说文件夹不会真正生成,而是在 8080 端口虚拟生成,不会真正的物理生成
    publicPath: 'xuni',
    // 打包出来的文件名 不会真正的物理生成 而是虚拟生成在端口里
    filename: 'bundle.js'
  },
  devServer: {
    // 端口号
    port: 8080,
    // 静态资源文件夹
    contentBase: 'www'
  }
}
  1. study-snabbdom文件夹中新建src文件夹 在该文件夹下新建index.js文件
    study-snabbdom文件夹中新建www文件夹 在该文件夹下新建index.html文件
    将package.json文件夹中的"scripts"的内容改为:
scripts": {
	"dev": "webpack-dev-server"
},
  1. 跑通snabbdom官方git首页的demo程序,即证明调试环境已经搭建成功 src/index.js 中的内容改为snabbdom官方git首页的Example中的内容:
import {
    init,
    classModule,
    propsModule,
    styleModule,
    eventListenersModule,
    h,
} from "snabbdom";

const patch = init([
    // Init patch function with chosen modules
    classModule, // makes it easy to toggle classes
    propsModule, // for setting properties on DOM elements
    styleModule, // handles styling on elements with support for animations
    eventListenersModule, // attaches event listeners
]);

const container = document.getElementById("container");

const vnode = h("div#container.two.classes", { on: { click: function (){} } }, [
    h("span", { style: { fontWeight: "bold" } }, "This is bold"),
    " and this is just normal text",
    h("a", { props: { href: "/foo" } }, "I'll take you places!"),
]);
// Patch into empty DOM element – this modifies the DOM as a side effect
patch(container, vnode);

const newVnode = h(
    "div#container.two.classes",
    { on: { click: function (){} } },
    [
        h(
            "span",
            { style: { fontWeight: "normal", fontStyle: "italic" } },
            "This is now italic type"
        ),
        " and this is still just normal text",
        h("a", { props: { href: "/bar" } }, "I'll take you places!"),
    ]
);
// Second `patch` invocation
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state

var container = document.getElementById('container')这行代码表明页面也定要有一个id为container的div 将www/index.html文件内容:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>静态资源文件夹</title>
</head>
<body>
    <button id="btn">按我改变DOM</button>
    <div id="container"></div>
<!--    引入打包文件-->
    <script src="xuni/bundle.js"></script>
</body>
</html>
  • 在终端输入:npm run dev进行打包
  • 访问:http://localhost:8080/ 和 http://127.0.0.1:8080/xuni/bundle.js, 可以看到 www/index.html 和 xuni/bundle.js 文件的内容

虚拟 DOM 和 h 函数

虚拟 DOM

  1. 虚拟DOM:用 JavaScript 对象描述 DOM 的层次结构。DOM 中的一切属性都在虚拟 DOM 中有对应的属性。
  • 真实DOM
<div class="box">
  <h3>我是一个标题</h3>
  <ul>
    <li>牛奶</li>
    <li>咖啡</li>
    <li>可乐</li>
  </ul>
</div>

  • 虚拟DOM
{
  "sel": "div",// 标签名放在sel中
  "data": {// 标签的属性放在这里面
    "class": { "box": true }// 标签的class
  },
  "children": [// 该标签下的子元素放在children中
    {
      "sel": "h3",
      "data": {},
      "text": "我是一个标题"// 标签的内容
    },
    {
      "sel": "ul",
      "data": {},
      "children": [
        { "sel": "li", "data": {}, "text": "牛奶" },
        { "sel": "li", "data": {}, "text": "咖啡" },
        { "sel": "li", "data": {}, "text": "可乐" }
      ]
    }
  ]
}
  1. diff 是发生在虚拟 DOM 上的:新虚拟 DOM 和老虚拟 DOM 进行 diff (精细化比较),算出应该如何最小量更新,最后反映到真实的 DOM 上

也就是说 新虚拟 DOM 和老虚拟 DOM 进行 diff算法比较 找出不同的地方 然后对真实的DOM进行局部操作

  • 老虚拟DOM
{
  "sel": "div",
  "data": {
    "class": { "box": true }
  },
  "children": [
    {
      "sel": "h3",
      "data": {},
      "text": "我是一个标题"
    },
    {
      "sel": "ul",
      "data": {},
      "children": [
        { "sel": "li", "data": {}, "text": "牛奶" },
        { "sel": "li", "data": {}, "text": "咖啡" },
        { "sel": "li", "data": {}, "text": "可乐" }
      ]
    }
  ]
}
  • diff 之后的新虚拟DOM
{
  "sel": "div",
  "data": {
    "class": { "box": true }
  },
  "children": [
    {
      "sel": "h3",
      "data": {},
      "text": "我是一个标题"
    },
    {
      "sel": "span",
      "data": {},
      "text": "我是一个新的span"// 新增
    },
    {
      "sel": "ul",
      "data": {},
      "children": [
        { "sel": "li", "data": {}, "text": "牛奶" },
        { "sel": "li", "data": {}, "text": "咖啡" },
        { "sel": "li", "data": {}, "text": "可乐" }{ "sel": "li", "data": {}, "text": "雪碧" }// 新增
      ]
    }
  ]
}
  1. DOM 如何变为虚拟 DOM,属于模板编译原理范畴,本次课不研究

h函数

  1. h 函数用来产生虚拟节点(vnode)
    比如这样调用 h 函数:h('a', { props: { href: 'http://www.atguigu.com' } }, '尚硅谷')
    将得到这样的虚拟节点:{ "sel": "a", "data": { "props": { "href": "http://www.atguigu.com" } }, "text": "尚硅谷" }
    它表示的真正的 DOM 节点:<a href="http://www.atguigu.com">尚硅谷</a>

h函数的第一个参数是字符串类型的 传入标签名字 会被添加到虚拟DOM的sel属性中 也可以传入‘标签名.class名#id名’或‘标签名.class名’或‘标签名#id名’
第二个参数是个对象 传入标签的属性 属性也是一个对象 第二个参数会被添加到虚拟DOM的data属性中 可省略
第三个参数是字符串 传入标签的内容 会被添加到虚拟DOM的text属性中 也可以是h函数或者h函数构成的数组 传递子元素
在这里插入图片描述

  1. 虚拟节点有哪些属性
{
  children: undefined, // 子元素,undefined表示没有子节点
  data: {}, // 属性样式等
  elm: undefined, // 该元素对应的真正的DOM节点,undefined表示它还没有上树
  key: undefined, // 节点唯一标识
  sel: 'div', // selector选择器 节点类型(现在它是一个div)
  text: '我是一个盒子' // 文字
}

基本使用

将src/index.js中的代码改成:

import {
    init,
    classModule,
    propsModule,
    styleModule,
    eventListenersModule,
    h,
} from "snabbdom";

const patch = init([// 这个后面会介绍 现在照着写
    // Init patch function with chosen modules
    classModule, // makes it easy to toggle classes
    propsModule, // for setting properties on DOM elements
    styleModule, // handles styling on elements with support for animations
    eventListenersModule, // attaches event listeners
]);

// 创建虚拟节点
const myVNode1 = h(
    'a',
    { props: { href: 'http://www.atguigu.com', target: '_blank' } },
    '尚硅谷'
)
const myVNode2 = h('div', { class: { box: true } }, '我是一个盒子')

// 让虚拟节点上树 让虚拟DOM渲染到真实DOM上
// 1.获取容器
const container = document.getElementById('container')
// 让虚拟DOM渲染到真实DOM上:patch(container, myVNode1) 一个容器只能让一个节点上树 下面两行代码只能存在一行
// patch(container, myVNode1)
patch(container, myVNode2)

嵌套使用

  1. h函数用来产生虚拟节点 所以可以嵌套使用 h 函数:
h('ul', {}, [
  h('li', {}, '牛奶'),
  h('li', {}, '咖啡'),
  h('li', {}, '可乐')
])

将得到这样的虚拟 DOM 树:

{
  "sel": "ul",
  "data": {},
  "children": [
    { "sel": "li", "data": {}, "text": "牛奶" },
    { "sel": "li", "data": {}, "text": "咖啡" },
    { "sel": "li", "data": {}, "text": "可乐" }
  ]
}
  1. 第二个参数是标签属性 没有可以省略 第三个参数是标签内容(h函数开头已经展示)或标签子元素 如果元素只有一个子元素 那么不用写数组[] 如果子元素大于1个 需要用数组[]包起来
import {
    init,
    classModule,
    propsModule,
    styleModule,
    eventListenersModule,
    h,
} from "snabbdom";

const patch = init([// 这个后面会介绍 现在照着写
    // Init patch function with chosen modules
    classModule, // makes it easy to toggle classes
    propsModule, // for setting properties on DOM elements
    styleModule, // handles styling on elements with support for animations
    eventListenersModule, // attaches event listeners
]);

// 创建虚拟节点
const myVNode3 = h('ul', [
  h('li', {}, '苹果'),
  h('li', '西瓜'),
  h('li', [h('div', [h('p', '嘻嘻'), h('p', '哈哈')])]),// 如果子元素大于1个 需要用数组[]包起来
  h('li', h('p', '火龙果'))// 如果只有一个子元素 那么不用写数组[]
])
// 让虚拟节点上树
const container = document.getElementById('container')
patch(container, myVNode3)

h函数源码

import { vnode, VNode, VNodeData } from "./vnode";
import * as is from "./is";

export type VNodes = VNode[];
export type VNodeChildElement = VNode | string | number | undefined | null;
export type ArrayOrElement<T> = T | T[];
export type VNodeChildren = ArrayOrElement<VNodeChildElement>;

function addNS(
  data: any,
  children: VNodes | undefined,
  sel: string | undefined
): void {
  data.ns = "http://www.w3.org/2000/svg";
  if (sel !== "foreignObject" && children !== undefined) {
    for (let i = 0; i < children.length; ++i) {
      const childData = children[i].data;
      if (childData !== undefined) {
        addNS(childData, children[i].children as VNodes, children[i].sel);
      }
    }
  }
}

export function h(sel: string): VNode;
export function h(sel: string, data: VNodeData | null): VNode;
export function h(sel: string, children: VNodeChildren): VNode;
export function h(
  sel: string,
  data: VNodeData | null,
  children: VNodeChildren
): VNode;
export function h(sel: any, b?: any, c?: any): VNode {
  let data: VNodeData = {};
  let children: any;
  let text: any;
  let i: number;
  if (c !== undefined) {
    if (b !== null) {
      data = b;
    }
    if (is.array(c)) {
      children = c;
    } else if (is.primitive(c)) {
      text = c;
    } else if (c && c.sel) {
      children = [c];
    }
  } else if (b !== undefined && b !== null) {
    if (is.array(b)) {
      children = b;
    } else if (is.primitive(b)) {
      text = b;
    } else if (b && b.sel) {
      children = [b];
    } else {
      data = b;
    }
  }
  if (children !== undefined) {
    for (i = 0; i < children.length; ++i) {
      if (is.primitive(children[i]))
        children[i] = vnode(
          undefined,
          undefined,
          undefined,
          children[i],
          undefined
        );
    }
  }
  if (
    sel[0] === "s" &&
    sel[1] === "v" &&
    sel[2] === "g" &&
    (sel.length === 3 || sel[3] === "." || sel[3] === "#")
  ) {
    addNS(data, children, sel);
  }
  // 返回的是一个vnode函数
  return vnode(sel, data, children, text, undefined);
}
  1. 返回一个vnode函数创造出一个虚拟节点 vnode函数源码:
export function vnode(
  sel: string | undefined,// 冒号后面是该参数类型
  data: any | undefined,
  children: Array<VNode | string> | undefined,
  text: string | undefined,
  elm: Element | Text | undefined
): VNode {
  const key = data === undefined ? undefined : data.key;
  return { sel, data, children, text, elm, key };
}

vnode函数就是把传入的参数返回成一个对象

手写h函数

  1. 假设h函数必须传入3个参数 本文不考虑h函数参数省略情况
  2. 将原来的src/index.js改名为index-1.js 新建index.js文件、mysnabbdom文件夹 在mysnabbdom文件夹下新建h.js和vnode.js文件
  3. vnode.js中的代码:
// 函数的功能非常简单,就是把传入的5个参数组合成对象返回
export default function (sel, data, children, text, elm) {
  return {
    sel,
    data,
    children,
    text,
    elm
  }
}
  1. mysnabbdom/h.js中我们自己写的源码:
import vnode from './vnode'
// 编写一个低配版的h函数,这个函数必须接受3个参数,缺一不可
// 相当于它的重置功能较弱
// 也就是说,调用的时候形态必须是下面的三种之一
/*
  形态1:h('div', {}, '文字')
  形态2:h('div', {}, [])
  形态3:h('div', {}, h())
*/
export default function (sel, data, c) {
  // 检查参数的个数
  if (arguments.length !== 3)
    throw new Error('对不起,h函数必须传入3个参数,我们是低配版h函数')
  // 检查参数 c 的类型
  if (typeof c === 'string' || typeof c === 'number') {
    // 说明现在调用h函数的是形态1
    return vnode(sel, data, undefined, c, undefined)
  } else if (Array.isArray(c)) {
    // 说明现在调用h函数的是形态2
    let children = []
    // 遍历c
    for (let i = 0; i < c.length; i++) {
      // 检查 c[i] 必须是个对象
      if (!(typeof c[i] === 'object' && c[i].hasOwnProperty('sel')))
        throw new Error('传入的数组参数中有项不是 h 函数')
      // 这里不用执行 c[i],因为你的调用语句中已经有了执行 此时只要收集好就行了
      children.push(c[i])
    }
    // 循环结束了,就说明children收集完毕了,此时可以返回虚拟节点了,它是有children属性的
    return vnode(sel, data, children, undefined, undefined)
  } else if (typeof c === 'object' && c.hasOwnProperty('sel')) {
  	// 虚拟节点一定有sel属性
    // 说明现在调用h函数的是形态3
    // 即,传入的c是唯一的children,不用执行c,因为调用的时候已经执行过了
    let children = [c]
    return vnode(sel, data, children, undefined, undefined)
  } else {
    throw new Error('传入的第三个参数类型不对')
  }
}

5.在src/index.js中:

import h from './mysnabbdom/h'
const myVNode1 = h('div', {}, [
  h('p', {}, '哈哈'),
  h('p', {}, '嘻嘻'),
  h('p', {}, '呵呵'),
  h('p', {}, [h('span', {}, 'aa'), h('span', {}, 'bb')]),
  h('p', {}, h('span', {}, 'A'))
])

const myVNode2 = h('ul', {}, [
  h('li', {}, '牛奶'),
  h('li', {}, '咖啡'),
  h('li', {}, [h('div', {}, [h('p', {}, '可口可乐'), h('p', {}, '百事可乐')])]),
  h('li', {}, h('p', {}, '雪碧'))
])
console.log(myVNode1, myVNode2)

diff 算法

  1. diff 算法确实是最小量更新,key 很重要,key 是这个节点的唯一标识,告诉 diff 算法,在更改前后它们是同一个 DOM 节点
  2. 只有是同一个虚拟节点才进行精细化比较,否则就是暴力删除旧的、插入新的。同一个虚拟节点:选择器相同且 key 相同。
  3. 只进行同层比较,不会进行跨层比较。即使是同一片虚拟节点,但是如果跨层了,那么 diff 算法也不会进行精细化比较。而是暴力删除旧的、然后插入新的。
  4. diff并不是那么“无微不至” 真的能提高效率吗:上面进行暴力删除的情况在实际vue开发中并不常见 比如根据条件改变选择器 条件为真时显示有序列表 为假时显示无序列表 比如你本来有4个div 点击了某个按钮后将4个div用一个selection标签包裹起来 原来的4格div和现在的4格div已经不在同一层了 这些情况在实际开发中不常见。所以这是合理的优化机制

diff 处理新旧节点不是同一个节点时

在这里插入图片描述

  1. 同一个节点:旧节点的key要和新节点的key相同;旧节点的选择器和新节点相同
// snabbdom 源码
function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
  return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel
}
  1. 创建节点时,所有子节点都是通过递归创建的
// snabbdom 源码
function createElm() {
  // ...
  if (is.array(children)) {
    for (i = 0; i < children.length; ++i) {
      const ch = children[i]
      if (ch != null) {
        api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue))
      }
    }
  } else if (is.primitive(vnode.text)) {
    api.appendChild(elm, api.createTextNode(vnode.text))
  }
  // ...
}

手写代码

  1. 将原来的index.js文件改名为index-2.js 在mysnabbdom文件夹下创建patch.js和createElement.js文件
  2. createElement.js中:
// 真正创建节点 将 vnode 创建为 DOM 是孤儿节点,不进行插入
export default function createElement(vnode) {
  // 创建一个 DOM 节点,这个节点现在还是孤儿节点
  let domNode = document.createElement(vnode.sel)
  // 有子节点还是有文本?
  if (
    vnode.text !== '' &&
    (vnode.children === undefined || vnode.children.length === 0)
  ) {
    // 它内部是文字
    domNode.innerText = vnode.text
  } else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
    // 它内部是子节点,就要递归创建节点
    for (let i = 0; i < vnode.children.length; i++) {
      // 得到当前这个 child
      let ch = vnode.children[i]
      // 创建DOM,一旦调用createElement意味着:创建出DOM了,并且它的elm属性指向了创建出的DOM,但是还没有上树,是一个孤儿节点
      let chDom = createElement(ch)
      // 上树
      domNode.appendChild(chDom)
    }
  }
  // 补充 elm 属性
  vnode.elm = domNode
  // 返回 elm,elm是一个纯dom对象
  return vnode.elm
}
  1. patch.js中:
import vnode from './vnode'
import createElement from './createElement'
export default function patch(oldVnode, newVnode) {
  // 判断传入的第一个参数,是DOM节点还是虚拟节点?
  if (oldVnode.sel === '' || oldVnode.sel === undefined) {
    // 传入的第一个参数是DOM节点,此时要包装为虚拟节点
    oldVnode = vnode(
      oldVnode.tagName.toLowerCase(),
      {},
      [],
      undefined,
      oldVnode
    )
  }
  console.log(oldVnode, newVnode)
  // 判断 oldVnode和newVnode 是不是同一个节点
  if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {
    console.log('是同一个节点')
  } else {
    console.log('不是同一个节点,暴力插入新的,删除旧的')
    let newVnodeElm = createElement(newVnode)
    if (oldVnode.elm && newVnodeElm) {
      // 插入到老节点之前
      oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm)
    }
    // 删除老节点
    oldVnode.elm.parentNode.removeChild(oldVnode.elm)
  }
}
  1. index.js中
import h from './mysnabbdom/h'
import patch from './mysnabbdom/patch'

const myVnode1 = h('ul', {}, [
  h('li', {}, '牛奶'),
  h('li', {}, '咖啡'),
  h('li', {}, [h('div', {}, [h('p', {}, '可口可乐'), h('p', {}, '百事可乐')])]),
  h('li', {}, h('p', {}, '雪碧'))
])
const container = document.getElementById('container')
patch(container, myVnode1)

const btn = document.getElementById('btn')
const myVnode2 = h('section', {}, [
  h('h1', {}, '我是新的h1'),
  h('h2', {}, '我是新的h2')
])
btn.onclick = function () {
  patch(myVnode1, myVnode2)
}

diff 处理新旧节点是同一个节点时

在这里插入图片描述
在这里插入图片描述

尝试书写 diff 更新子节点

diff 算法的子节点更新策略

  1. 四种命中查找/判断(经典的diff算法优化策略):
    ① 新前与旧前
    ② 新后与旧后
    ③ 新后与旧前(此种命中,涉及移动节点,那么旧前指向的节点,移动到旧后之后 也就是移动到所有旧节点之后)
    ④ 新前与旧后(此种命中,涉及移动节点,那么旧后指向的节点,移动到旧前之前 也就是移动到所有旧节点之前)
    新前:新的虚拟节点中所有没有处理的开头的节点
    新后:所有新的虚拟节点的子结点中没有处理的节点的最后一个节点
    在这里插入图片描述
    一个节点命中一种判断就不继续比较了
  2. 步骤:
    (1)首先 需要四个指针:旧前 旧后 新前 新后 四个指针指向如下图所示
    在这里插入图片描述
    比较新前与旧前是不是同一个节点 如果是 则新前不需要新增、删除 有可能需要更新 更新之后旧前、新前指向下一个节点 继续比较新前与旧前 直到遍历完节点 如果遍历完旧节点 新节点没有遍历完 则表明新结点中有要插入的节点 如果遍历完新节点 旧节点没有遍历完 则表明旧结点中有要删除的节点 。如何定义遍历完:新/旧前在新/旧后后面

在这里插入图片描述
(2)如果新前与旧前不是同一个节点 则比较新后与旧后是不是同一个节点 如果是 则进行更新 更新后新后 旧后指向上一个节点
(3)如果新后与旧后不是同一个节点 则比较新后与旧前是不是同一个节点 如果是 则将新后指向的节点移动到旧后之后
(4)如果新后与旧前不是同一个节点 则比较新前与旧后是不是同一个节点 如果是 则将新前指向的节点移动到旧前之前

  1. 例子
    (1)删除的情况:新前与旧前是同一个节点 更新节点后
    在这里插入图片描述
    新前++ 旧前++ 得到下面的图 新前与旧前是同一个节点 更新节点后
    在这里插入图片描述
    新前++ 旧前++ 得到下面的图 新前与旧前指向的节点不是同一个 此时比较新后与旧后 发现是同一个节点
    在这里插入图片描述
    新后与旧后指向上一个节点 得到下图 此时新节点循环完毕 如果老节点中还有剩余节点 则删除剩余节点 即删除新前到旧前范围内(包括两个指针指向的节点)的全部节点
    在这里插入图片描述
    (2)都没有命中的情况:用循环来寻找
    在这里插入图片描述
    先比较新前与旧前指针指向的是不是同一个节点 如果是 新前 旧前指向下一个节点 如下图所示
    在这里插入图片描述
    继续比较新前与旧前指针指向的是不是同一个节点 如果是 新前 旧前指向下一个节点 如下图所示
    在这里插入图片描述
    继续比较新前与旧前指针指向的是不是同一个节点 如果不是 比较新后 旧后指向的是不是同一个节点 如果不是 比较新后 旧前指向的是不是同一个节点 如果不是 比较新前 旧后是不是同一个节点 也不是 四种条件都没命中 用循环来寻找 此时遍历旧节点 看看有没有新前 新后范围内(包括新前 新后指向的节点)的节点 他先在旧节点中看有没有节点D 发现有 将旧前指向的节点D插入到旧前指针前面(老师这里没有画出来)然后将旧节点中的节点D设为undefined 然后新前指针下移 如下图 此时新前位于新后指针后面 循环结束
    在这里插入图片描述
    (3)复杂的情况1
    在这里插入图片描述
    先比较旧前 新前是不是指向同一个节点 不是的话 比较旧后 新后指向的是不是同一个节点 如果不是 比较旧前与新后指向的是不是同一个节点 依然不是 比较旧后与新前指向的是不是同一个节点 终于命中了 此时需要移动节点 将旧后指针指向的节点移动到旧前指针前面 并将旧后指针指向的节点设为undefined 然后旧后上移 新前下移 如下图
    在这里插入图片描述
    然后重新比较 比较旧前 新前是不是指向同一个节点 不是的话 比较旧后 新后指向的是不是同一个节点 如果不是 比较旧前与新后指向的是不是同一个节点 依然不是 比较旧后与新前指向的是不是同一个节点 如果不是 利用遍历来查找 在旧节点中遍历查找新前指向的节点 找到了 将旧节点中的C节点移动到旧前指针的前面 并将旧节点原来的C节点设为undefined 因为新前指针命中了 所以新前指针下移 如下图所示
    在这里插入图片描述
    然后重新比较 比较旧前 新前是不是指向同一个节点 不是的话 比较旧后 新后指向的是不是同一个节点 如果不是 比较旧前与新后指向的是不是同一个节点 依然不是 比较旧后与新前指向的是不是同一个节点 如果不是 利用遍历来查找 在旧节点中遍历查找新前指向的节点 找到了 将旧节点中的M节点移动到旧前指针的前面(插入到真实DOM中) 并将旧节点原来的M节点设为undefined(虚拟节点中设为undefined) 因为新前指针命中了 所以新前指针下移 结束循环 如下图所示
    在这里插入图片描述
    新节点循环完了 老节点中还有剩余节点 所以要将他们删除 将旧前与旧后范围内的所有节点都删除(包括旧前 旧后节点)
    (4)复杂的情况2
    在这里插入图片描述
    比较旧前 新前是不是指向同一个节点 不是的话 比较旧后 新后指向的是不是同一个节点 如果不是 比较旧前与新后指向的是不是同一个节点 是同一个节点 将旧前指向的节点设为undefined 并将新后指向的节点移动到所有旧节点后面 也就是旧后的后面 旧前下移 新后上移 如下图所示

在这里插入图片描述
重新比较 比较旧前 新前是不是指向同一个节点 不是的话 比较旧后 新后指向的是不是同一个节点 如果不是 比较旧前与新后指向的是不是同一个节点 是同一个节点 将旧前指向的节点设为undefined 并将新后指向的节点移动到所有旧节点后面 也就是旧后的后面 旧前下移 新后上移 如下图所示
在这里插入图片描述
重新比较 比较旧前 新前是不是指向同一个节点 不是的话 比较旧后 新后指向的是不是同一个节点 如果不是 比较旧前与新后指向的是不是同一个节点 是同一个节点 将旧前指向的节点设为undefined 并将新后指向的节点移动到所有旧节点后面 也就是旧后的后面 旧前下移 新后上移 如下图所示
在这里插入图片描述
重新比较 比较旧前 新前是不是指向同一个节点 不是的话 比较旧后 新后指向的是不是同一个节点 如果不是 比较旧前与新后指向的是不是同一个节点 是同一个节点 将旧前指向的节点设为undefined 并将新后指向的节点移动到所有旧节点后面 也就是旧后的后面 旧前下移 新后上移 如下图所示
在这里插入图片描述
重新比较 比较旧前 新前是不是指向同一个节点 不是的话 比较旧后 新后指向的是不是同一个节点 如果不是 比较旧前与新后指向的是不是同一个节点 是同一个节点 将旧前指向的节点设为undefined 并将新后指向的节点移动到所有旧节点后面 也就是旧后的后面 旧前下移 新后上移(下图没移动两个指针)如下图所示 同时结束循环
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值