文章目录
前言
突然发现草稿箱里还躺着这篇早在暑假就完成的文章,抓紧发了,内容是之前编辑的,如有错误,麻烦告知。😁
一、snabbdom简介
-
snabbdom是瑞典语单词,单词原意“速度”;
-
snabbdom是著名的虚拟DOM库,是diff算法的鼻祖,Vue源码借鉴了snabbdom;
-
在git上的snabbdom源码是用TypeScript写的,git上并不提供编译好的 JavaScript版本;
-
如果要直接使用build出来的JavaScript版的snabbdom库,可以从npm上下载:
npm i snabbdom
- 视频里的老师说:学习库底层时,建议大家阅读原汁原味的TS代码,最好带有库作者原注释, 这样对你的源码阅读能力会有很大的提升。
二、snabbdom测试环境搭建
- 新建文件夹,创建package.json
npm init
- 安装 snabbdom:
npm i snabbdom
可以看到build文件夹下有 js 和 ts 文件,而src文件夹中只有 ts 文件:
- 搭建webpack 和webpack-dev-server开发环境:
注意:必须安装最新版webpack@5,不能安装webpack@4,因为 webpack4没有读取身份证中exports的能力。
npm i -D webpack@5 webpack-cli@3 webpack-dev-server@3
4. 新建 src 文件夹,在该文件夹创建 index.js 文件,比如输入 alert(‘111’) :
alert('111');
- 新建webpack.config.js文件,参考webpack官网进行配置:
// 从https://www.webpackjs.com/官网照着配置
const path = require('path');
module.exports = {
// 入口
entry: './src/index.js',
// 出口
output: {
// 虚拟打包路径,就是说文件夹不会真正生成,而是在8080端口虚拟生成
publicPath: 'xuni',
// 打包出来的文件名,不会真正的物理生成
filename: 'bundle.js'
},
devServer: {
// 端口号
port: 8080,
// 静态资源文件夹
contentBase: 'www'
}
};
-
新建 www 文件夹,在该文件夹中创建 index.html 文件,并引入 bundle.js 文件:
-
将 package.json 文件中的 scripts 改成:
"dev": "webpack-dev-server"
- 运行,并打开 http://localhost:8080/ 网址:
npm run dev
而虚拟打包的文件在 http://localhost:8080/xuni/bundle.js网址,没有被真正的生成:
- 跑通snabbdom官方git首页的demo程序,即可证明调试环境已经搭建成功。步骤如下:
官方git网址
将该官方git首页的下述代码复制粘贴到src文件夹的index.js文件中:
将下图所示框内代码进行更改:
更改为:
在 www 文件夹中的 index.html文件中添加下述代码,如下图所示:
<div id="container"></div>
最后刷新网页,得如下所示结果:
说明跑通了snabbdom官方git首页的demo程序,即可证明调试环境已经搭建成功。
三、虚拟DOM和h函数
1. 什么是虚拟 DOM
用JavaScript对象描述DOM的层次结构,DOM中的一切属性都在虚拟DOM中有对应的属性。
2. diff是发生在虚拟DOM上的
新虚拟DOM和老虚拟DOM进行diff(精细化比较),算出应该如何最小量更新,最后反映到真正的DOM上。diff是发生在虚拟DOM上的diff。
3. DOM如何变为虚拟DOM,属于模板编译原理范畴。
4. h函数用来产生虚拟节点(vnode)
5. 一个虚拟节点有哪些属性
6. 创建虚拟节点
import { h } from 'snabbdom/h'
//创建虚拟节点
const myVnode1 = h('a',{props: { href:'https://www.baidu.com/' } },'百度');
console.log(myVnode1);
7. h函数可以嵌套使用,从而得到虚拟DOM树
import { h } from 'snabbdom/h'
const myVnode2 = h('ul',{},[
h('li',{},'牛奶'),
h('li',{},'咖啡'),
h('li',{},'可乐')
]);
console.log(myVnode2);
8. 虚拟节点上树
import { init } from 'snabbdom/init'
import { classModule } from 'snabbdom/modules/class'
import { propsModule } from 'snabbdom/modules/props'
import { styleModule } from 'snabbdom/modules/style'
import { eventListenersModule } from 'snabbdom/modules/eventlisteners'
import { h } from 'snabbdom/h'
//创建出patch函数
const patch = init([classModule,propsModule,styleModule,eventListenersModule]);
//创建虚拟节点
const myVnode1 = h('a',{
props: {
href:'https://www.baidu.com/',
target:'_blank'
}
},'百度');
console.log(myVnode1);
//让虚拟节点上树
const container = document.getElementById('container');
patch(container,myVnode1);
点击链接,即可打开新标签页。
四、手写h函数
例如,编写一个低配版本的h函数,这个函数必须接受3个参数,缺一不可,相当于它的重载功能较弱。也就是说,调用的时候形态必须是下面的三种之一:
形态① h(‘div’, {}, ‘文字’)
形态② h(‘div’, {}, [])
形态③ h(‘div’, {}, h())
新建 mysnabbdom 文件夹,在该文件夹创建 h.js 文件和 vnode.js 文件:
vnode.js文件代码:
//把传入的5个参数组合成对象返回
export default function(sel,data,children,text,elm){
const key = data.key;
return {
sel,data,children,text,elm,key
}
}
h.js文件代码:
import vnode from './vnode.js'
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函数是形态①
return vnode(sel,data,undefined,c,undefined);
}else if(Array.isArray(c)){
// 说明现在调用h函数是形态②
let children = [];
// 遍历c,收集children
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')){
// 说明现在调用h函数是形态③
// 即,传入的c是唯一的children。不用执行c,因为测试语句中已经执行了c。
let children = [c];
return vnode(sel, data, children, undefined, undefined);
}else{
throw new Error('传入的第三个参数类型不对');
}
}
index.js文件代码:
import h from './mysnabbdom/h.js';
//创建虚拟节点
const myVnode1 = h('a',{
props: {
href:'https://www.baidu.com/',
target:'_blank'
}
},'百度');
console.log(myVnode1);
const myVnode2 = h('ul',{},[
h('li',{},'牛奶'),
h('li',{},'咖啡'),
h('li',{},'可乐')
]);
console.log(myVnode2);
五、感受diff算法
举例:
index.js文件代码:
import { init } from 'snabbdom/init';
import { classModule } from 'snabbdom/modules/class';
import { propsModule } from 'snabbdom/modules/props';
import { styleModule } from 'snabbdom/modules/style';
import { eventListenersModule } from 'snabbdom/modules/eventlisteners';
import { h } from 'snabbdom/h';
// 得到盒子和按钮
const container = document.getElementById('container');
const btn = document.getElementById('btn');
// 创建出patch函数
const patch = init([classModule, propsModule, styleModule, eventListenersModule]);
const vnode1 = h('ul', {}, [
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'D' }, 'D')
]);
patch(container, vnode1);
const vnode2 = h('ul', {}, [
h('li', { key: 'D' }, 'D'),
h('li', { key: 'A' }, 'A'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'B' }, 'B')
]);
// 点击按钮时,将vnode1变为vnode2
btn.onclick = function () {
patch(vnode1, vnode2);
};
index.html文件代码:
<button id="btn">按我改变DOM</button>
<div id="container"></div>
<script src="/xuni/bundle.js"></script>
刷新后运行结果如下:
例如我在浏览器 中将 li 标签中的 A 改成 111:
然后点击按钮,可以看到不是暴力删除旧的、插入新的而是在原来的基础上进行微改。这是因为前后均是同一虚拟节点(选择器相同且key相同),会进行精细化比较。
结论:
- key很重要,key是这个节点的唯一标识,告诉diff算法,在更改前后它们是同一个DOM节点。
- 只有是同一个虚拟节点,才进行精细化比较,否则就是暴力删除旧的、插入新的。 如何定义是同一个虚拟节点:选择器相同且key相同。
- 只进行同层比较,不会进行跨层比较。即使是同一片虚拟节点,但是跨层了,不会进行精细化比较,而是暴力删除旧的、然后插入新的。在实际Vue开发中,基本不会遇见跨层比较,所以这是合理的优化机制。同层比较示意图如下:
六、如何定义是否是“同一个节点”
旧节点的key要和新节点的key相同,且,旧节点的选择器要和新节点的选择器相同。
// snabbdom 源码
function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel
}
七、创建节点时,所有子节点需要递归创建
// 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))
}
// ...
}
八、diff 处理新旧节点不是同一个节点时
1、手写第一次上树时(新节点内部没有子节点,只有文本)
这里是简单的一次上树,考虑的是: 在新旧节点不是同一个节点时,新节点内部没有子节点,只有文本的情况。
www文件夹下的index.html代码:
<div id="container"></div>
<script src="/xuni/bundle.js"></script>
src文件夹的index.js文件代码:
import h from './mysnabbdom/h.js';
import patch from './mysnabbdom/patch.js';
const myVnode1 = h('h1', {}, '你好');
const container = document.getElementById('container');
// 第一次上树
patch(container, myVnode1);
mysnabbdom文件夹下的 h.js 和 vnode.js文件同上述,新建 createElement.js 文件,代码如下:
// 真正创建节点。将vnode创建为DOM,是孤儿节点,不进行插入,插入在patch进行
export default function createElement(vnode) {
console.log('目的是把虚拟节点', vnode,'真正变成DOM,并添加elm属性');
// 创建一个DOM节点,这个节点现在还是孤儿节点
let domNode = document.createElement(vnode.sel);
// 有子节点还是有文本?
if (vnode.text != '' && (vnode.children == undefined || vnode.children.length == 0)) {
// 它内部是文字
domNode.innerText = vnode.text;
// 补充elm属性
vnode.elm = domNode;
} else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
// 它内部是子节点,就要递归创建节点
}
// 返回elm,elm属性是一个纯DOM对象
return vnode.elm;
};
在该文件夹下继续新建 patch.js 文件,代码如下:
import vnode from './vnode.js';
import createElement from './createElement.js';
export default function patch(oldVnode, newVnode) {
// 判断传入的第一个参数,是DOM节点还是虚拟节点?
if (oldVnode.sel == '' || oldVnode.sel == undefined) {
// 传入的第一个参数是DOM节点,此时要包装为虚拟节点
oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
}
// 判断oldVnode和newVnode是不是同一个节点
if (oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) {
console.log('是同一个节点');
//
//
//
} else {
console.log('不是同一个节点,暴力插入新的,删除旧的');
let newVnodeElm = createElement(newVnode);
// 插入到老节点之前
oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);
}
};
运行结果:
2、手写递归创建子节点(新节点内部有子节点)
这里考虑了: 在新旧节点不是同一个节点时,新节点内部有子节点的情况。
mysnabbdom文件夹下:
createElement.js 文件代码:
// 真正创建节点。将vnode创建为DOM,是孤儿节点,不进行插入,插入在patch进行
export default function createElement(vnode) {
// console.log('目的是把虚拟节点', vnode,'真正变成DOM,并添加elm属性');
// 创建一个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++) {
// 得到当前这个children
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;
};
patch.js 文件代码:
import vnode from './vnode.js';
import createElement from './createElement.js';
export default function patch(oldVnode, newVnode) {
// 判断传入的第一个参数,是DOM节点还是虚拟节点?
if (oldVnode.sel == '' || oldVnode.sel == undefined) {
// 传入的第一个参数是DOM节点,此时要包装为虚拟节点
oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
}
// 判断oldVnode和newVnode是不是同一个节点
if (oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) {
console.log('是同一个节点');
//
//
//
} else {
console.log('不是同一个节点,暴力插入新的,删除旧的');
let newVnodeElm = createElement(newVnode);
// 插入到老节点之前
if (oldVnode.elm.parentNode && newVnodeElm) {
oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);
}
// 删除老节点
oldVnode.elm.parentNode.removeChild(oldVnode.elm);
}
};
www文件夹下的index.html代码:
<button id="btn">按我改变DOM</button>
<div id="container"></div>
<script src="/xuni/bundle.js"></script>
src文件夹的index.js文件代码:
import h from './mysnabbdom/h.js';
import patch from './mysnabbdom/patch.js';
const myVnode1 = h('ul', {}, [
h('li', {}, 'A'),
h('li', {}, 'B'),
h('li', {}, 'C'),
h('li', {}, 'D'),
h('li', {}, 'E')
]);
// 得到盒子和按钮
const container = document.getElementById('container');
const btn = document.getElementById('btn');
// 第一次上树
patch(container, myVnode1);
// 新节点
const myVnode2 = h('ul', {}, [
h('h1', {}, '我是新的h1'),
h('h1', {}, '我是新的h2'),
]);
btn.onclick = function () {
patch(myVnode1, myVnode2);
}
运行结果:
按钮点击前:
按钮点击后:
九、手写diff处理新旧节点是同一个节点时
1、手写新旧节点 text 的不同情况
在 mysnabbdom 文件夹新建 patchVnode.js 文件,代码如下:
import createElement from "./createElement";
// 对比同一个虚拟节点
export default function patchVnode(oldVnode, newVnode) {
// 判断新旧vnode是否是同一个对象
if (oldVnode === newVnode) return;
// 判断新vnode有没有text属性
if (newVnode.text != undefined && (newVnode.children == undefined || newVnode.children.length == 0)) {
// 新vnode有text属性
console.log('新vnode有text属性');
if (newVnode.text != oldVnode.text) {
// 如果新虚拟节点中的text和老的虚拟节点的text不同,那么直接让新的text写入老的elm中即可。如果老的elm中是children,那么也会立即消失掉。
oldVnode.elm.innerText = newVnode.text;
}
} else {
// 新vnode没有text属性,有children
console.log('新vnode没有text属性');
// 判断老的有没有children
if (oldVnode.children != undefined && oldVnode.children.length > 0) {
// 老的有children,新的也有children,此时就是最复杂的情况。
//
//
//
} else {
// 老的没有children,新的有children
// 清空老的节点的内容
oldVnode.elm.innerHTML = '';
// 遍历新的vnode的子节点,创建DOM,上树
for (let i = 0; i < newVnode.children.length; i++) {
let dom = createElement(newVnode.children[i]);
oldVnode.elm.appendChild(dom);
}
}
}
}
patch.js 文件改变位置如下:
2、手写diff更新子节点
(1)经典的diff算法优化策略
何为新前 新后 旧前 旧后:
- 新前:所有新的虚拟节点中 子节点当中 所有未处理的节点中 开头的节点
- 新后:所有新的虚拟节点当中 子节点当中没有处理的节点中 最后一个节点
- 旧前 旧后 同理。
- 为四个指针,分别指向新前 新后 旧前 旧后
四种命中查找:
①新前与旧前
新前与旧前进行对比,对比新前与旧前是不是同一个节点,如果是同一个节点 说明不是新增 不是删除 是更新,执行更新操作。如果完全相同,新前 与 旧前 指针下移,继续比较,如果不命中①,判断是否命中②,依次类推。
②新后与旧后
新前 与 旧前 在指针下移到某位置时对比发现 不再是同一节点,新后 与 旧后 两指针 比较是否是同一节点,如果是同一节点, 指针上移,继续比较,如果不命中②,判断是否命中③,依次类推。
③新后与旧前
此种发生涉及移动节点,将新前指向的节点,移动到旧后之后。
如果命中③,移动之后,新后指针上移,旧前指针下移。
由上图可知,新后 与 旧前 命中,此时在虚拟节点中该节点被标注为 undefined,在真实DOM中被插入在 旧后 的 后面。然后 新后 指针上移,旧前 指针下移。
④新前与旧后
此种发生涉及移动节点,将新前指向的节点,移动到旧前之前
如果命中④,移动之后,新前指针上下移,旧后指针上移。
由上图可知,新前 与 旧后 命中,此时在虚拟节点中该节点被标注为 undefined,在真实DOM中被插入在旧前的前面。然后 新前指针上下移,旧后指针上移。
- 经典的diff算法优化策略 命中一种 就不再进行命中判断。
- 如果都没有命中,就需要用循环来寻找,循环旧子节点。
- 如果是旧节点先循环完毕,说明新节点中有要插入的节点。
- 如果是新节点先循环完毕,如果老节点中还有剩余节点(旧前和新后指针中间的节点),说明他们是要被删除的节点。h(‘li’,{key:‘D’},‘D’)
(2)手写实现
在 mysnabbdom 文件夹下新增 updateChildren.js 文件,代码如下:
import patchVnode from './patchVnode.js';
import createElement from './createElement.js';
// 判断是否是同一个虚拟节点
function checkSameVnode(a, b) {
return a.sel == b.sel && a.key == b.key;
};
export default function updateChildren(parentElm, oldCh, newCh) {
console.log('我是updateChildren');
console.log(oldCh, newCh);
// 旧前
let oldStartIdx = 0;
// 新前
let newStartIdx = 0;
// 旧后
let oldEndIdx = oldCh.length - 1;
// 新后
let newEndIdx = newCh.length - 1;
// 旧前节点
let oldStartVnode = oldCh[0];
// 旧后节点
let oldEndVnode = oldCh[oldEndIdx];
// 新前节点
let newStartVnode = newCh[0];
// 新后节点
let newEndVnode = newCh[newEndIdx];
let keyMap = null;
// 开始大while了
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
console.log('★');
// 首先不是判断①②③④命中,而是要略过已经加undefined标记的东西
if (oldStartVnode == null || oldCh[oldStartIdx] == undefined) {
oldStartVnode = oldCh[++oldStartIdx];
} else if (oldEndVnode == null || oldCh[oldEndIdx] == undefined) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (newStartVnode == null || newCh[newStartIdx] == undefined) {
newStartVnode = newCh[++newStartIdx];
} else if (newEndVnode == null || newCh[newEndIdx] == undefined) {
newEndVnode = newCh[--newEndIdx];
} else if (checkSameVnode(oldStartVnode, newStartVnode)) {
// 新前和旧前
console.log('①新前和旧前命中');
patchVnode(oldStartVnode, newStartVnode);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (checkSameVnode(oldEndVnode, newEndVnode)) {
// 新后和旧后
console.log('②新后和旧后命中');
patchVnode(oldEndVnode, newEndVnode);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (checkSameVnode(oldStartVnode, newEndVnode)) {
// 新后和旧前
console.log('③新后和旧前命中');
patchVnode(oldStartVnode, newEndVnode);
// 当③新后与旧前命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧后的后面
// 如何移动节点?只要你插入一个已经在DOM树上的节点,它就会被移动
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (checkSameVnode(oldEndVnode, newStartVnode)) {
// 新前和旧后
console.log('④新前和旧后命中');
patchVnode(oldEndVnode, newStartVnode);
// 当④新前和旧后命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧前的前面
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
// 如何移动节点?只要你插入一个已经在DOM树上的节点,它就会被移动
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
// 四种命中都没有命中
// 制作keyMap一个映射对象,这样就不用每次都遍历老对象了。
if (!keyMap) {
keyMap = {};
// 从oldStartIdx开始,到oldEndIdx结束,创建keyMap映射对象
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
const key = oldCh[i].key;
if (key != undefined) {
keyMap[key] = i;
}
}
}
console.log(keyMap);
// 寻找当前这项(newStartIdx)这项在keyMap中的映射的位置序号
const idxInOld = keyMap[newStartVnode.key];
console.log(idxInOld);
if (idxInOld == undefined) {
// 判断,如果idxInOld是undefined表示它是全新的项
// 被加入的项(就是newStartVnode这项)现不是真正的DOM节点
parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm);
} else {
// 如果不是undefined,不是全新的项,而是要移动
const elmToMove = oldCh[idxInOld];
patchVnode(elmToMove, newStartVnode);
// 把这项设置为undefined,表示我已经处理完这项了
oldCh[idxInOld] = undefined;
// 移动,调用insertBefore也可以实现移动。
parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm);
}
// 指针下移,只移动新的头
newStartVnode = newCh[++newStartIdx];
}
}
// 继续看看有没有剩余的。循环结束了start还是比old小
if (newStartIdx <= newEndIdx) {
console.log('new还有剩余节点没有处理,要加项。要把所有剩余的节点,都要插入到oldStartIdx之前');
// 遍历新的newCh,添加到老的没有处理的之前
for (let i = newStartIdx; i <= newEndIdx; i++) {
// insertBefore方法可以自动识别null,如果是null就会自动排到队尾去。和appendChild是一致了。
// newCh[i]现在还没有真正的DOM,所以要调用createElement()函数变为DOM
parentElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIdx].elm);
}
} else if (oldStartIdx <= oldEndIdx) {
console.log('old还有剩余节点没有处理,要删除项');
// 批量删除oldStart和oldEnd指针之间的项
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
if (oldCh[i]) {
parentElm.removeChild(oldCh[i].elm);
}
}
}
};
在 patchVnode.js 文件的红框位置写入以下代码:
到此,该部分内容就学完了,以上是根据 Vue源码探秘之虚拟DOM和diff算法 视频内容整理的,希望帮到自己的同时能帮到大家😁。
这部分内容之前寒假有看过,也整理了这篇博客的一部分,但是不太懂,所以一直在草稿箱躺着。最近突然想整理一下,但是,现在整理好发出来了并不代表是学会了,而是学废了😵脑袋晕晕的。