vue中的虚拟dom和diff算法

Posted webchang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了vue中的虚拟dom和diff算法相关的知识,希望对你有一定的参考价值。

目录


1. 虚拟DOM

JS引擎执行JS代码是很快的,比直接操作真实DOM要快的多。数据改变 --> 虚拟DOM(计算变更)—> 操作真实DOM —> 视图更新

在jquery时代,数据改变 —> 操作真实DOM —> 视图更新,是直接操作真实的DOM。vue框架引入了虚拟DOM

什么是虚拟DOM?

  • 虚拟DOM就是使用JS对象模拟真实的DOM结构,用JavaScript对象描述DOM的层次结构。DOM中的一切属性都在虚拟DOM中有对应的属性。
  • vue当中虚拟DOM的实现是参考一个库,snabbdom

为什么要有虚拟DOM

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

  • 从真实DOM变成虚拟DOM属于模板编译的内容。diff算法研究最小量更新,并将虚拟DOM变成真实的DOM

snabbdom库

  • snabbdom是著名的虚拟DOM库,是diff算法的鼻祖,Vue源码借鉴了 snabbdom
  • 在git上的snabbdom源码是用Typescript写的,git上并不提供编译好的javascript版本。如果要直接使用build出来的JavaScript版的snabbdom库,可以从npm上下载: npm i snabbdom
  • snabbdom库是DOM库,不能在nodejs环境运行

h函数

h函数用来产生虚拟节点(vnode),由vnode组成的树就是虚拟DOM树

Vnode的优点

  1. 兼容性强,不受执行环境的影响。VNode因为是JS对象,不管Node还是浏览器,都可以统一操作,从而获得了服务端渲染、原生渲染、手写渲染函数等能力。
  2. 减少操作DOM,任何页面的变化,都只使用VNode进行操作对比,只需要在最后一步挂载更新DOM,不需要频繁操作DOM,从而提高页面性能。

虚拟节点有哪些属性

  • children: 值可能是undefined(是undefined表示没有子元素),也可能使数组
  • data:
  • elm: undefined。elm是undefined说明这个节点还没有在DOM树上
  • key: undefined
  • sel: “div”。选择器
  • text:文字描述

h函数可以产生虚拟节点,h函数可以嵌套使用,从而得到虚拟DOM树。第二个参数可以省略。第三个参数可以是文字、数组、或者h函数

patch函数,让虚拟结点上树

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

// 创建patch函数
let patch = init([classModule, propsModule, styleModule, eventListenersModule]);

// 创建一个虚拟节点,但是它还没有在DOM树上。要想把它放在DOM树上,需要patch函数
let vnode1 = h('a', props: href: 'http://www.baidu.com',target:'_blank', '百度一下')

let container = document.getElementById('container');
// 让虚拟结点上树,patch函数只能让一个虚拟结点上树。如果vnode2和vnode3要上树,需要把这个注释掉
patch(container, vnode1);

let vnode2 = h('div',class:"box":true,'我是一个盒子');

let vnode3 = h('ul',,[
    h('li','苹果'), // 第二个参数可以没有
    h('li','香蕉'), // 这里已经调用了h函数
    h('li',[
        h('p',,'桔子'),
        h('p',,'哈哈')
    ]),
    h('li','西瓜'),
    h('li',h('span','火龙果')) // 如果children只有一个子元素,第三个参数可以不要数组
]);

手写h函数

h函数有很多种用法,第二个和第三个参数都可以省略。h函数源码中对参数是否存在做了很多的判断,在这里 我们只实现有三个参数的h函数。

// vnode.js
export default function vnode(sel, data, children, text, elm) 
  return sel, data, children, text, elm;

import vnode from './vnode';

// h函数---------------------------------------------------------------
// 编写一个低配版本的h函数,这个函数必须接受3个参数,缺一不可。相当于它的重载功能较弱。
// 形态1 h('div', ,‘文字')
// 形态2 h('div', ,[h(),h()])
// 形态3 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)) 
    // 形态2
    let children = [];
    for (let i = 0; i < c.length; i++) 
      if (!(typeof c[i] === 'object' && c[i].hasOwnProperty('sel'))) 
        throw new Error('数组参数中有某一项不是h函数')
      
      // 这里不用执行c[i],因为你的调用语句中已经有了执行,此时只需要收集好就可以了
      children.push(c[i]);
    
    return vnode(sel, data, children, undefined, undefined);
   else if (typeof c === 'object' && c.hasOwnProperty('sel')) 
    // 形态3,第三个参数是h函数,而h函数会返回一个vnode对象(因为直接调用了)
    // 说明传入的c是children中唯一的元素
    let children = [c];
    return vnode(sel, data, children, undefined, undefined);
   else 
    throw new Error('传入的第三个参数类型不对')
  

// 测试
import h from './snabbdom/h';

console.log(h('div',,[
    h('p',,'苹果'),
    h('p',,'香蕉'),
    h('p',,'橘子')
]));
console.log('---------------------');
console.log(h('div',,h('p',,'橘子')));

2. Diff算法

体验diff算法

使用snabbdom库

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

// 创建patch函数
let patch = init([classModule, propsModule, styleModule, eventListenersModule]);

let vnode1 = h('ul', , [
  h('li', , 'A'),
  h('li', , 'B'),
  h('li', , 'C'),
  h('li', , 'D')
]);

let container = document.getElementById('container');
let btn = document.getElementById('btn');
patch(container, vnode1);

let vnode2 = h('ul', , [
  h('li', , 'A'),
  h('li', , 'B'),
  h('li', , 'C'),
  h('li', , 'D'),
  h('li', , 'E')
]);

// 点击按钮时,将vnode1变为vnode2
btn.addEventListener('click',function () 
  patch(vnode1,vnode2);
)

测试页面

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>首页</title>
</head>
<body>
<button id="btn">改变DOM</button>
<div id="container"></div>
</body>
</html>

点击按钮后,将虚拟结点vnode1替换为vnode2,两者的区别在于vnode2比vnode1在最后边多了一个li,效果如图所示,进行了最小量更新:

如果我们把新增的li添加到最上边再看一下效果:

let vnode2 = h('ul', , [
  h('li', , 'E'),
  h('li', , 'A'),
  h('li', , 'B'),
  h('li', , 'C'),
  h('li', , 'D')
]);


这次它在更新的时候是在最后插入了一个新的li,然后把原来的A改成了E,把B改成了A,把C改成了B,把D改成了C,最后一个li的文字是D。为什么呢?原因在于我们没有给这些结构加key(回想一下vue中key的作用),加上key再试一下:

let vnode1 = h('ul', , [
  h('li', key: 'A', 'A'),
  h('li', key: 'B', 'B'),
  h('li', key: 'C', 'C'),
  h('li', key: 'D', 'D')
]);

let vnode2 = h('ul', , [
  h('li', key: 'E', 'E'),
  h('li', key: 'A', 'A'),
  h('li', key: 'B', 'B'),
  h('li', key: 'C', 'C'),
  h('li', key: 'D', 'D')
]);


我们发现这次确实实现了最小量更新。key很重要。key是这个节点的唯一标识,告诉diff算法,在更改前后它们是同一个DOM节点。如果没有key,它会销毁旧的,重新创建新的。

diff简介

diff最小量更新算法。diff算法可以进行精细化比对,实现最小量更新。

只有是同一个虚拟节点,才进行精细化比较,否则就是暴力删除旧的、插入新的。延伸问题:如何定义是同一个虚拟节点?答:选择器相同且key相同

只进行同层比较,不会进行跨层比较。即使是同一片虚拟节点,但是跨层了,此时diff算法不进行精细化比较。而是暴力删除旧的、然后插入新的。只要是在同一层进行比较,比如调换顺序,添加删除节点,diff算法就可以进行最小量更新。

// div中有4个p标签
let vnode1 = h('div', , [
  h('p', key:'A', 'A'),
  h('p', key:'B', 'B'),
  h('p', key:'C', 'C'),
  h('p', key:'D', 'D')
]);

// div中又套了一个div,内层div中有4个p标签
let vnode2 = h('div', , h('div',,[
  h('p', key:'A', 'A'),
  h('p', key:'B', 'B'),
  h('p', key:'C', 'C'),
  h('p', key:'D', 'D'),
  h('p', key:'E', 'E'),
]));

let container = document.getElementById('container');
let btn = document.getElementById('btn');
patch(container, vnode1);

// 点击按钮时,将vnode1变为vnode2
btn.addEventListener('click', function () 
  patch(vnode1, vnode2);
)

实现diff算法

有点难,大家跟着这个视频学吧:https://www.bilibili.com/video/BV1v5411H7gZ

前端学习交流QQ群:862748629 点击加入

以上是关于vue中的虚拟dom和diff算法的主要内容,如果未能解决你的问题,请参考以下文章

vue中的虚拟dom和diff算法

从零实现Vue虚拟DOM和DOM-DIFF算法

Vue 虚拟DOM和Diff算法源码解析

你怎么理解vue中的diff算法?

Vuejs571- Vue 虚拟DOM和Diff算法源码解析

Vue中diff算法详解