阅读 426

虚拟 DOM 和 diff 算法 -01

本文是关于虚拟 DOM 和 diff 算法的学习笔记,目的在于更好的理解 Vue 的底层原理,篇幅较长,故而拆分为几篇,今后将陆续更新

简介

  • diff:精细化对比,最小量更新(diff 算法是发生在虚拟 DOM 上的)
  • 虚拟 DOM:用 js 对象描述 DOM 的层次结构。DOM 中的一切属性都在虚拟 DOM 中有对应的属性

比如有如下真实 DOM

<div class="box">
  <h3>标题</h3>
  <ul>
    <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": "东风破" }
      ]
    }
  ]
}
复制代码

snabbdom

著名的虚拟 DOM 库,是 diff 算法的鼻祖,vue 就借鉴了 snabbdom。在 github 上的 snabbdom 的源码是用 ts 写的,要是直接使用 build 出来的 js 版的 snabbdom 库,可以从 npm 上下载。记录本篇笔记时安装的 snabbdom 的版本是 ^3.0.1

h 函数

作用

  • 产生虚拟节点(vnode)

比如调用 h 函数:h("a", { props: { href: "/foo" } }, "I'll take you places!") 得到的虚拟节点:

{ 
  sel: "a", 
  data: { props: { href: "/foo" } }, 
  children: undefined, 
  text: "I'll take you places!", 
  elm: undefined, // 挂载到真实 DOM 树上了才有值
  key: undefined
}
复制代码

它表示的真正的 DOM 节点: <a href="/bar">I'll take you places!</a>

  • h 函数可以嵌套使用,得到虚拟 DOM 树
h("div", { class: { haha: true } }, [
  h("div", "子元素一"),
  h("div", "子元素二")
])
复制代码

得到的虚拟 DOM 树:

image.png

手写实现

在源码中 h 函数的参数是很灵活的,可以传 1~3 个参数,第 2 个参数可以不传,第 3 个参数的类型还可以变,既可以是文本、数字,也可以是数组,还可以是 h 函数的执行。这就实现了函数的重载。为了减少用在细枝末节上的时间,我们手写实现的 h 函数,将规定只能传递 3 个参数,并且如果第 3 个参数为数组,数组里的元素只能是 h 函数。

  1. 先准备 vnode 函数

vnode 函数的功能很简单,就是把传入的值组合成一个对象返回,这个对象有 5 个属性( key 属性下面会讲):sel, data, children, text, elm,它们的值为传入的 sel, data, children, text, elm。

// vnode 函数
export default (sel, data, children, text, elm) => {
  return { sel, data, children, text, elm } // 注意这是 es6 写法
}
复制代码
  1. h 函数

注意,传入 h 函数的第 3个参数,如果里面有 h 函数,其实是 h 函数的执行,执行结果是 vnode 函数的返回,是一个拥有 sel, data, children, text, elm 属性的对象

// h 函数
import vnode from './vnode'
export default function(sel, data, c) { // 这里用 function 声明是因为箭头函数不支持 arguments
  if (arguments.length !== 3) {
    throw Error('请传入3个参数')
  }
  // 如果第 3 个参数为字符串或数字,则返回的值中,children 为 undefined,text 就是 c
  if (typeof c === ('string' || 'number')) { // 注意 'string' || 'number' 要用括号包起来
    return vnode(sel, data, undefined, c, undefined)
  } else if (Array.isArray(c) && c.length) { // 如果第 3 个参数为数组,且不为空
    const children = []
    c.forEach(item => {
      // 遍历数组,判断元素是不是对象,并且包含 sel 属性
      if (!(typeof item === 'object' && item.hasOwnProperty('sel'))) 
        throw Error('传入数组的元素不是 h 函数') // 这里 if 后面只有一条语句,省略了{}
      // 如过通过了 if 检测,则把 item 加入到 children 即可,因为数组元素必然是 h 函数的执行,
      // 而 h 数的执行结果是 vnode 函数返回的对象,必然包含 sel 属性
      children.push(item)
    })
    return vnode(sel, data, children, undefined, undefined)
  } else if (typeof c === 'object' && c.hasOwnProperty('sel')) { // 如果第 3 个参数为 h 函数的执行
    // 直接把 h 函数的执行结果对象放入数组中作为 children 的值传给 vnode
    return vnode(sel, data, [c], undefined, undefined)
  } else {
    throw Error('第 3 个参数不正确')
  }
}
复制代码
  1. 查看结果

在 index.js 文件引入 h 函数,并传递正确的参数

// index.js
import h from './h.js'
const vnode = h('div', {}, [h("div", {}, "子元素一"), h("div", {}, "子元素二")])
console.log(vnode)
复制代码

One More Thing

snabbdom 的 v2.1.0 版本,package.json 还有配置 "exports",如下图:

image (1).png
这样我们在项目中引入相关文件就可以用别名引入,比如

image (2).png
这是属于 webpack5 的新特性,但练习时下载的 snabbdom 版本为 v3.0.1,已经取消了 package.json 的 "exports" 配置

感谢.gif

点赞.png

文章分类
前端
文章标签