虚拟DOM 和 diff 算法

虚拟DOM 和 diff 算法

  • 研究1:虚拟DOM如何被渲染函数(h函数)产生? — 我们手写h函数
  • 研究2:diff 算法原理?— 我们要手写diff 算法
  • 研究3:虚拟DOM 如何通过diff 变成真正的DOM的—事实上,虚拟DOM变成真正的,是涵盖在diff算法里面的

项目创建

  1. 创建文件夹:vnode
  2. 使用npm init 管理vnode 文件
  3. 安装 snabbdom npm i -S snabbdom
  4. 安装webpack 相关包 npm i -D webpack@5 webpack-cli@3 webpack-dev-server@3
  5. 新增 webpack.config.js 文件 并设置相关配置
const path = require('path')
module.exports = {
    //入口
    entry: './src/index.js',
    output: {
        publicPath: 'xuni',
        filename: 'bundle.js'
    },
    devServer: {
        port: '8080',
        contentBase: 'www'
    }
}
  1. 新增 src 文件夹和www文件夹

在这里插入图片描述

  1. 修改package.json

在这里插入图片描述

修改为

"scripts": {
    "dev": "webpack-dev-server"
  },
  1. 在www/index.html 文件中引入bundle.js
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

</head>

<body>
    <div>你好</div>
    <script src="/xuni/bundle.js"></script>
</body>

</html>

snabbdom简介

虚拟DOM

虚拟DOM:用javascript对象描述DOM 的层次结构。DOM 中的一切都在虚拟DOM 中有对应的属性

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

diff 算法是发生在虚拟DOM 上的,新虚拟DOM 和老虚拟DOM进行diff(精细化比较),算出应该如何最小量更新,最后反映到真正的DOM 上。

在这里插入图片描述

h函数用来产生虚拟节点(vnode)

一个虚拟节点有哪些属性

{
    children:undefind, //子元素
    data:{},//属性、数据
    elm:undefind,//虚拟DOM的真正DOM节点,如果是undefind表示虚拟节点还没有上树
    key:undefind,//虚拟节点的唯一标识
    sel:'div',//标签名
    text:'我是一个盒子'//文本
}

h函数的基本使用

  1. 如何调用h函数:
h('a',{props:{href:'http://www.baidu.com'}},'百度')
  1. 将得到这样的虚拟节点
{"sel":"a","data":{props:{href:'http://www.baidu.com'}},"text":"百度"}
  1. 它表示的真正DOM节点:
<a href="http://www.baidu.com">百度</a>

h函数是可以嵌套使用的,从而得到虚拟DOM树

  • 比如这样嵌套使用h函数
h('url',{},[
    h('li',{},'牛奶'),
    h('li',{},'咖啡'),
    h('li',{},'可乐'),
])
  • 得到这样的虚拟DOM树
{
    "sel":"url",
    "data":{},
    "children":[
        {'sel':'li','text':'牛奶'},
        {'sel':'li','text':'咖啡'},
        {'sel':'li','text':'可乐'},
    ]
   
}

手写h函数

创建vNode.js文件

 // 函数的功能十分简单,就是把传入的5个参数组合成对象返回
export default function (sel, data, children, text, elm) {
    return {
        sel, data, children, text, elm
    }
}

创建h.js文件

import vNode from './vNode'
/**
1. 编写低配版的h函数,这个函数必须接受3个参数,缺一不可
2.相当于它的重载功能较弱
3.也就是说,调用的形态必须是下面三种之一
   情况1: h('div',{},'文字')
   情况2: h('div',{},[])
   情况3: h('div',{},h())
*/

/**
* 
* @param {*} sel  选择器
* @param {*} data 属性
* @param {*} c 不确定
*/
export default function (sel, data, c) {
   //首先检查参数个数是否为三个
   if (arguments.length !== 3) throw new Error('对不起,h函数必须传3个参数,我们是低配版h函数');
   //检查参数c的类型

   if (typeof (c) === 'string' || typeof (c) === 'number') {
       //1. c的类型为字符串类型或者数字类型
       return vNode(sel, data, undefined, c, undefined)
   } else if (Array.isArray(c)) {
       let children = []
       //2. c的类型是数组
       //遍历c 
       for (let i = 0; i < c.length; i++) {
           // 判断 c数组对象中的每一项是不是一个h函数; 第一:h函数返回的肯定是一个对象,第二:h函数对象中肯定有sel属性
           if (!c[i] instanceof Object && c[i].hasOwnProperty('sel')) throw new Error('传入的数组对象中有项不是h函数');
           //这里不需要c[i],因为你的测试语句中已经有了执行
           //此时只需要收集即可(如果是一个h函数,则向children 添加c[i]) 
           children.push(c[i])
       }
       return vNode(sel, data, children, undefined, undefined)
   } else if (c instanceof Object && c.hasOwnProperty('sel')) {
       //2. c的类型是一个h函数 第一:h函数返回的肯定是一个对象,第二:h函数对象中肯定有sel属性
       return vNode(sel, data, [c], undefined, undefined)
   } else {
       throw new Error('对不起,h函数传入的第三个参数类型不对');
   }
}

diff 算法的心得

  • 最小量更新太厉害啦!真的是最小量更新!当然,key很重要。key是这个节点的唯一标识,告诉diff算法,在更改前后它们是同一个DOM节点

  • 只有是同一个虚拟节点,才进行精细化比较,否则就是暴力删除旧的、插入新的。

    ​ 延伸问题:如何定义是同一个虚拟节点?

    ​ 答:选择器相同且key相同

  • 只进行同层比较,不会进行跨层比较。即使是同一片虚拟节点,但是跨层了,对不起,精细化比较不diff你。而是暴力删除旧的、然后插入新的。

diff 并不是那么的‘无微不至’啊!真的影响效率吗?

答:上面的操作,在实际vue开发中,基本不会遇见,所以这是合理的优化机制

diff 算法流程图

在这里插入图片描述

  1. 如何定义新老虚拟DOM是 ‘同一个节点’?

    新老节点的key相同并且,sel(选择器)相同

在这里插入图片描述

  1. 创建节点时,所有子节点需要递归创建的

在这里插入图片描述

diff算法的子节点更新策略

四种命中查找

  1. 新前与旧前
  2. 新后与旧后
  3. 新后与旧前(此种情况发生,要移动节点,移动新前指向的这个节点到老节点的旧后的后面)
  4. 新前与旧后(此种情况发生,要移动节点,移动新前指向的这个节点到老节点的旧前的前面)

命中一种就不再进行命中判断

如果老节点比新节点先遍历完,说明是老节点后面需要增加新节点里没有被遍历的子节点

如果新节点比老节点先遍历完,说明是老节点需要删除新节点里没有被遍历的子节点

面试题: 如何理解 虚拟DOM 和 diff 算法

虚拟DOM 其实就是 一个JS 对象,我们用它来描述真实的DOM。虚拟DOM 和diff 算法配合使用,可以优化浏览器渲染的性能。但是更重要的一点虚拟DOM可以用来做跨端应用。

diff 算法 底层其实是调用了patch 方法,patch 方法需要传递两个参数,第一个参数是老节点,第二个参数是新节点。当调用patch方法是,我们会首先判断老节点是虚拟DOM还是真实DOM。如果是真实DOM,我会会把这个真实DOM 转换成虚拟DOM。 因为diff算法比较本来就是比较新的虚拟DOM和老的虚拟DOM的差别。然后我会再比较老的虚拟DOM 和 新的虚拟DOM是不是同一个节点的,也就是比较它们的sel(选择器)和key 是否相同。如果不相同,我们则暴力删除旧的,添加新的。如果相同,我们就要进行精细化比较。diff算法的子节点更新策略就是判断四个命中。分别是新前与后前,新旧与后旧,新旧与旧前,新前与后旧。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值