diff算法笔记

本文介绍了如何利用snabbdom搭建测试环境,讲解了虚拟DOM的概念,h函数的应用,以及diff算法在更新DOM时的作用。通过实例演示了低配版h函数和vnode的实现,重点强调了key在diff算法中的关键作用,以及如何确保节点唯一性以提高渲染效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

搭建测试环境snabbdom:
snabbdom 是著名的虚拟 DOM 库,是 diff 算法的鼻祖,Vue 源码就是借鉴了 snabbdom

  • 创建一个全英文路径下的空文档,在vscode的命令行先初始化文档:npm init
  • 搭建webpack 和 webpack-dev-server 开发环境,必须安装最新版 webpack@5,不能安装 webpack@4,这是因为 webpack@4 没有读取身份证(package.json)中 exports 的能力,建议大家使用这样的版本: npm i -D webpack@5 webpack-cli@3 webpack-dev-server@3
  • 安装snabbdom,要求本地安装npm i -S snabbdom
  • 相关入口出口文件配置,端口隐藏文件设置,详细见邵山欢老师的视频讲解。
  1. 何为虚拟DOM
    虚拟DOM就是普通的js对象,其可以用来描述DOM对象,但是由于不是真正的DOM对象,因此人们把它叫做虚拟DOM。
  2. h函数产生虚拟DOM
  3. diff用在哪
    diff是发生在虚拟DOM上的,是新的虚拟DOM和老的虚拟DOM进行diff(精细化比较),最后反映到真实DOM中。
    譬如真实DOM和虚拟DOM如下:
    真实DOM:
<div class="box">
  <h3>我是一个标题</h3>
  <ul>
    <li>牛奶</li>
    <li>咖啡</li>
    <li>可乐</li>
  </ul>
</div>

虚拟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": "可乐" }
      ]
    }
  ]
}
  1. 手写h函数,产生虚拟DOM
    比如这样调用h函数:
h('a',{props:{href:'http://baidu.com'}},'百度')

将得到下面的虚拟节点:

{"sel":"a","data":{props:{href:'http://baidu.com'}},"text":"百度"}

它其实表示的是真正的DOM:

<a href="http://baidu.com">百度</a>

一个虚拟DOM有哪些属性:

{	children:子元素
	data:{} 属性、样式
	elm:对应的真正节点,是否上树
	key:节点的唯一标识,服务于最小量
	sel:选择器
	text:内容
  1. 在index.js中创建一个虚拟节点
var myVnode1 = h('a', { props: { href: 'http://baidu.com' } }, '百度');
console.log(myVnode1);

输出结果:
在这里插入图片描述
elm为underfined,说明此时虚拟节点未上树,考虑创建patch函数,渲染页面

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

const patch = init([
        classModule,
        propsModule,
        styleModule,
        eventListenersModule
    ])
const container = document.getElementById('container');
patch(container, myVnode1);

虚拟节点已经创建:
在这里插入图片描述
再写一个h函数:

const myVnode3 = h('ul', [h('li', '香蕉'), h('li', '苹果'),
    h('li', [h('ol', [h('li', '背景'), h('li', '荆州')])]), h('li', '橘子')
]);

在这里插入图片描述
综上:h函数有多种写法:

h('div')
h('div','文字')
h('div',[])

h('div',h())
h('div',{},[])
h('div',{},h())

我们这里着手写后三种形式的h函数:
src 目录下新建 mysnabbdom 目录
新建2个文件
src/mysnabbdom/h.jssrc/mysnabbdom/vnode.js
h.js

import vnode from "./vnode.js";
// console.log('div', 2, 3, 4, 5);
//编写一个低配版的h函数,必须接受3个参数
//调用的形态必须是三个之一
// h('div','文字')
// h('div',{},[])
// h('div',{},h())
export default function (sel, data, c) {
  //检查参数个数
  if (arguments.length !== 3) {
    throw new Error("对不起,我们是低配版h函数");
  }
  //检查c的类型
  if (typeof c == "string" || typeof c == "number") {
    //调用形态为1
    return vnode(sel, data, undefined, c, undefined);
  } else if (Array.isArray(c)) {
    let children = [];
    //形态2 遍历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收集结束
    return vnode(sel, data, children, undefined, undefined);
  } else if (typeof c == "object" && c.hasOwnProperty("sel")) {
    //形态3 说明传入的c是唯一的children
    let children = [c];
    return vnode(sel, data, children, undefined, undefined);
  } else {
    throw new Error("传入的第3个参数类型不对");
  }
}

vnode.js

export default function(sel, data, children, text, elm) {
    return {
        sel,
        data,
        children,
        text,
        elm
    }
}

上述写完后便可以识别低配版的h函数,比如给了这样一个myVnode1(形态二,第二个判断条件)

import h from "./mysnabbdom/h.js";
var myVnode1 = h("div", {}, [
  h("div", { class: "box" }, "哈哈哈"),
  h("div", {}, "嘻嘻嘻"),
  h("div", {}, "嘻嘻嘻"),
]);
console.log(myVnode1);

在这里插入图片描述
diff算法:
点击按钮,在后面新增一个节点ooo,其实前面的都没变,这是在已经选然后的html中改变li的值,当点击按钮时,前面的都没变。代码中的 patch 函数所做的工作其实是在原有 vnode1 基础上新增了一个节点。

const vnode1 = h("ul", {}, [
  h("li", { key: "a" }, "AA"),
  h("li", { key: "b" }, "bb"),
  h("li", { key: "c" }, "CC"),
  h("li", { key: "d" }, "DD"),
  h("li", { key: "d" }, "HH"),
]);
const container = document.getElementById("container");
patch(container, vnode1);

const vnode2 = h("ul", {}, [
  h("li", { key: "a" }, "AA"),
  h("li", { key: "b" }, "bb"),
  h("li", { key: "c" }, "CC"),
  h("li", {}, "ooo"),
  h("li", { key: "d" }, "DD"),
  h("li", { key: "d" }, "HH"),
]);
const btn = document.getElementById("btn");
//点击按钮时,vnode1变为2
btn.onclick = function () {
  patch(vnode1, vnode2);
};

在这里插入图片描述
在这里插入图片描述
注意:这里的key非常关键,key是这里的唯一标识,告诉diff算法,更改前后,他们是同一个DOM节点;

  1. 只要是同一个虚拟节点,就可进行精细化比较,否则就是暴力删除旧的,添加新的,那么如何定义一个虚拟节点呢,选择器相同且key相同。
  2. 只进行统计比较,不进行跨层比较,即使是同一虚拟节点,一旦跨层,则不会diff。
  3. 给节点加了唯一标识 key 之后页面渲染效率会大大提高的原因

如何证明上述是否正确呢?,我们将vnode2改为ol,已经不是一个选择器

const vnode1 = h("ul", {}, [
  h("li", { key: "a" }, "AA"),
  h("li", { key: "b" }, "bb"),
  h("li", { key: "c" }, "CC"),
  h("li", { key: "d" }, "DD"),
  h("li", { key: "d" }, "HH"),
]);
const container = document.getElementById("container");
patch(container, vnode1);

const vnode2 = h("ol", {}, [
  h("li", { key: "a" }, "AA"),
  h("li", { key: "b" }, "bb"),
  h("li", { key: "c" }, "CC"),
  h("li", {}, "ooo"),
  h("li", { key: "d" }, "DD"),
  h("li", { key: "d" }, "HH"),
]);
const btn = document.getElementById("btn");
//点击按钮时,vnode1变为2
btn.onclick = function () {
  patch(vnode1, vnode2);
};

我们仍然在浏览器中改变每个li的内容,点击按钮,都变了。
在这里插入图片描述
在这里插入图片描述

  • 代码示意
    diff 处理新旧节点不是同一个节点时:
    代码如下:
if (sameVnode(oldVnode, vnode)) {
      patchVnode(oldVnode, vnode, insertedVnodeQueue);
    } else {
      elm = oldVnode.elm!;
      parent = api.parentNode(elm) as Node;

      createElm(vnode, insertedVnodeQueue);

      if (parent !== null) {
        api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
        removeVnodes(parent, [oldVnode], 0, 0);
      }
    }

可简化为以下流程图:
在这里插入图片描述
如何定义同一个节点:源码如下,比较新旧节点的key,且选择器相同;

function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
  const isSameKey = vnode1.key === vnode2.key;
  const isSameIs = vnode1.data?.is === vnode2.data?.is;
  const isSameSel = vnode1.sel === vnode2.sel;
  return isSameSel && isSameKey && isSameIs;
}

创建子节点时,递归调用新节点:

elm = oldVnode.elm!;
      parent = api.parentNode(elm) as Node;

      createElm(vnode, insertedVnodeQueue);

      if (parent !== null) {
        api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
        removeVnodes(parent, [oldVnode], 0, 0);
      }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值