vue虚拟dom和diff算法
vue的虚拟dom和diff算法
1.虚拟dom
虚拟dom,我的理解就是通过js对象的方式来具体化每一个节点,把dom树上面的每个节点都变为对象里的一个元素,元素的子元素变为子节点,节点上面的class、id、attribute等属性变为data内的值,然后通过dom上面的createElement、appendChild、insertBefore等方法进行生成dom树。
let VNode = {
sel:'div',
data:{
key:0,
props:{},
attrs:{},
class:{},
style:{},
fn:{}
},
text:'虚拟dom',
elm:'<div>虚拟dom</div>'
children:[
{
sel:'div',
data:{
key:0,
props:{},
attrs:{},
class:{},
style:{},
fn:{}
},
text:'虚拟dom children',
elm:'<div>虚拟dom children</div>'
children:[]
}
]
}
2.diff算法
看了diff算法后感觉写的真是巧妙,真正做到了最小量更新 。
diff是当父节点相同时用来对子节点进行最小量更新的算法。
diff算法采用四个指针:旧节点开始指针,旧节点结束指针,新节点开始指针,新节点结束指针;
(上方虚拟节点中的key就是为了在进行diff算法时判断是否是同一个节点便于最小量更新)
while(旧节点开始指针<=旧节点结束指针&&新节点开始指针<=新节点结束指针){
分为以下五种情况:(前四种情况)
「
当进行下面5种判断后可能会出现新节点[新节点开始指针] 旧节点[旧节点开始指针] 新节点[新节点结束指针] 旧节点[旧节点结束指针]为空值的情况,如果出现空值则代表当前节点已经处理过了,所以就需要将指针++或者--
if(旧节点[旧节点开始指针] ==null){
旧节点开始指针++
}else if(旧节点[旧节点结束指针]==null){
旧节点结束指针--
}else if(新节点[新节点开始指针] ==null){
新节点开始指针++
}else if(新节点[新节点结束指针] ==null){
新节点结束指针--
}
」
1、新节点[新节点开始指针] 对比 旧节点[旧节点开始指针]
如果符合此种情况,则代表新节点[新节点开始指针] 与 旧节点[旧节点开始指针] 为同一个节点,实行最小量更新,即只更新节点内的属性而不进行dom销毁创建操作,完成更新后 新节点开始指针++,旧节点开始指针++
2、新节点[新节点结束指针] 对比 旧节点[旧节点结束指针]
如果符合此种情况,则代表新节点[新节点结束指针] 与 旧节点[旧节点结束指针] 为同一个节点,实行最小量更新,即只更新节点内的属性而不进行dom销毁创建操作,完成更新后 新节点结束指针--,旧节点结束指针--
3、新节点[新节点结束指针] 对比 旧节点[旧节点开始指针]
如果符合此种情况,则代表新节点[新节点结束指针] 与 旧节点[旧节点开始指针] 为同一个节点,实行最小量更新,先更新节点内的属性,然后使用insertBefore将旧节点[旧节点开始指针] 移动到旧节点[旧节点结束指针] 之后,(注意:此处要移动到旧节点[旧节点结束节点] 后,而不是所有旧节点后,因为这里的旧节点结束指针是会变化的),
父节点.insertBefore(旧节点[旧节点开始指针].elm, 旧节点[旧节点结束指针].elm.nextSibling)
完成操作后 新节点结束指针--,旧节点开始指针++
4、新节点[新节点开始指针] 对比 旧节点[旧节点结束指针] 如果符合此种情况,则代表新节点[新节点开始指针] 与 旧节点[旧节点结束指针] 为同一个节点,实行最小量更新,先更新节点内的属性,然后使用insertBefore将旧节点[旧节点结束指针] 移动到旧节点[旧节点开始指针] 前,(注意:此处要移动到旧节点[旧节点开始指针] 前,而不是所有旧节点前,因为旧节点开始指针也是会发生变化的)
父节点.insertBefore(旧节点[旧节点结束指针].elm, 旧节点[旧节点开始指针].elm)
完成操作后,旧节点结束指针--,新节点开始指针++
5、遍历旧节点数组,生成一个以key为键,index为值的对象为旧节点keyIndexMap,然后查询新节点[新节点开始指针]中的key是否在旧节点keyIndexMap中存在;
如果不存在,则证明新节点[新节点开始指针]在旧节点列表中不存在,此时需要创建新节点[新节点开始指针]为真实dom,并将其插入至旧节点[旧节点开始指针]前(因为此时新节点[新节点开始指针]一定处于全部未处理的旧节点前)
父节点.insertBefore(创建dom(新节点[新节点开始指针]), 旧节点[旧节点开始指针].elm)
如果存在则先需要判断旧节点[旧节点keyIndexMap[新节点[新节点开始指针][key]]]与新节点[新节点开始指针]的sel(标签)是否相同:
如果相同则代表为同一个标签,则进行最小量更新,先更新节点内的属性,然后insertBefore将旧节点[旧节点keyIndexMap[新节点[新节点开始指针][key]]]移动到旧节点[旧节点开始指针] 前,然后将旧节点[旧节点keyIndexMap[新节点[新节点开始指针][key]]]设置为undefined,代表当前节点处理过了;
如果不同则代表不是同一个标签,则只创建新节点[新节点开始指针]的真实dom,然后将其插入到旧节点[旧节点开始节点]。
最后新节点开始指针++
}
当以上循环完成后可能还会出现没有处理到的节点,所以还需要再查找没有处理到的节点:
如果是新节点开始指针<=新节点结束指针,则代表新节点列表内还有没有处理的节点,没有处理的节点全部为新增节点,此时需要遍历新节点[新节点开始指针](包含)至新节点[新节点结束指针](包含)之间的节点,然后将其添加至新节点[新节点结束指针+1]之前(新节点[新节点结束指针+1]可能为空,新节点[新节点结束指针+1]为空时可添加到最后)
for (let i = 新节点开始节点; i <= 新节点结束节点; i++) {
//insertBefore可以自动识别空值,如果是空值,则插入到最后
父节点.insertBefore(创建dom(新节点[i]), 新节点[新节点结束节点-1]?.elm)
}
如果是旧节点开始指针<=旧节点结束指针,则代表旧节点内还有没有处理的节点,没有处理的节点全部为需要删除节点,此时需要遍历旧节点[旧节点开始指针](包含)至旧节点[旧节点结束指针](包含) 之间的节点,然后将其全部删除。
for (let i = 旧节点开始指针; i <= 旧节点结束指针; i++) {
旧节点[i] && (父节点.removeChild(旧节点[i].elm))
}
以上就是我对diff算法的理解,下面贴上代码(阉割版,部分情况没有考虑,旨在学习diff算法,可能会有bug):
//updateChildren文件
import { sameVnode } from './is'
import patchVnode from './patchVnode'
import createElement from './createElement'
export default functionupdateChildren (parentElm, oldCh, newCh) {
console.log('updateChildren')
console.log(parentElm, 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 = {}
// 开始循环
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// debugger
console.log('while')
if (oldStartVnode === undefined || oldCh[oldStartIdx] === undefined) {
oldStartVnode = oldCh[++oldStartIdx]
} else if (oldEndVnode === undefined || oldCh[oldEndIdx] === undefined) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (newStartVnode === undefined || newCh[newStartIdx] === undefined) {
newStartVnode = newCh[++newStartIdx]
} else if (newEndVnode === undefined || newCh[newEndIdx] === undefined) {
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
//新前和旧前是同一个节点
console.log('新前和旧前是同一个节点')
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {//旧后和新后是同一个节点
console.log('旧后和新后是同一个节点')
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {//新后和旧前是同一个节点
console.log('新后和旧前是同一个节点')
patchVnode(oldStartVnode, newEndVnode)
//当新后节点是旧前节点时,此时需要移动节点,移动旧前节点到旧后的后面
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling)
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {//旧后和新前是同一个节点
console.log('旧后和新前是同一个节点')
// 当旧后和新前是同一个节点时,此时需要移动旧后节点到旧前节点的前面
patchVnode(oldEndVnode, newStartVnode)
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
//前四种都没有命中
if (Object.keys(keyMap).length === 0) {
keyMap = {}
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
const key = oldCh[i].key
if (key) {
keyMap[key] = i
}
}
}
console.log(keyMap)
//寻找当前节点在keyMap中的位置
const idxInOld = keyMap[newStartVnode.key]
console.log(idxInOld)
if (!idxInOld) {
//新节点不在旧节点中
parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm)
} else {
// 新节点在旧节点中,需要移动
const elmToMove = oldCh[idxInOld]
if (elmToMove.sel !== newStartVnode.sel) {
parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm)
} else {
patchVnode(elmToMove, newStartVnode)
// 把这项设置为undefined,表示已经移动过了
oldCh[idxInOld] = undefined
parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm)
}
}
//指针向后移动
newStartVnode = newCh[++newStartIdx]
}
}
// 继续查询是否有剩余节点
if (newStartIdx <= newEndIdx) {
console.log('新节点还有剩余节点没有处理', newStartIdx, newEndIdx)
const before = newCh[newEndIdx + 1]?.elm
console.log(before)
for (let i = newStartIdx; i <= newEndIdx; i++) {
//insertBefore可以自动识别undefined,如果是undefined,则插入到最后
parentElm.insertBefore(createElement(newCh[i]), before)
}
} else if (oldStartIdx <= oldEndIdx) {
console.log('旧节点还有剩余节点没有处理', oldStartIdx, oldEndIdx)
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
oldCh[i] && (parentElm.removeChild(oldCh[i].elm))
}
}
}
let arr = [1, 1, 2, 35, 9, 2, 9]
arr.reduce((p, n) => {
return p ^ n
}, 0)
//is文件
export function sameVnode (vnode1, vnode2) {
return vnode1.sel === vnode2.sel && vnode1.key === vnode2.key;
}
//createElement文件
//真正创建dom
export default function createElement (vnode) {
let domNode = document.createElement(vnode.sel);
if (
vnode.text !== "" &&
(vnode.children === undefined || vnode.children.length === 0)
) {
domNode.innerText = vnode.text;
// 补充elm
} else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
for (let i = 0; i < vnode.children.length; i++) {
domNode.appendChild(createElement(vnode.children[i]));
}
}
vnode.elm = domNode;
return vnode.elm
}
//patchVnode文件
import createElement from './createElement'
import updateChildren from './updateChildren'
export default function patchVnode (oldVnode, newVnode) {
// console.log('patchVnode')
if (oldVnode === newVnode) return
if (newVnode.text && (!newVnode.children || newVnode.children.length === 0)) {//判断newVnode的text是否为空,且不等于oldVnode的text,如果满足以上条件,则更新text
oldVnode.text !== newVnode.text && (oldVnode.elm.innerText = newVnode.text);
} else {//newVnode的text为空,则判断newVnode的children是否为空,如果不为空,则更新children
// 新节点没有text属性
if (oldVnode.children && oldVnode.children.length > 0) {
// 老节点有children,新节点也有children
updateChildren(oldVnode.elm, oldVnode.children, newVnode.children);
} else {
// 老的没有children,新的有children
oldVnode.elm.innerHTML = '';
for (let i = 0; i < newVnode.children.length; i++) {
let dom = createElement(newVnode.children[i])
oldVnode.elm.appendChild(dom)
}
}
}
}
//patch文件
import vnode from "./vnode";
import createElement from "./createElement";
import patchVnode from './patchVnode'
import { sameVnode } from './is'
export default function (oldVnode, newVnode) {
// console.log(oldVnode, newVnode)
//判断传入的第一个参数,是dom节点还是vnode
if (oldVnode.sel === "" || oldVnode.sel === undefined) {
//传入的如果是dom节点需要包装为虚拟节点
oldVnode = vnode(
oldVnode.tagName.toLowerCase(),
{},
[],
undefined,
oldVnode,
);
}
// 判断oldVnode和newVnode是否是同一个节点
if (sameVnode(oldVnode, newVnode)) {
// console.log("是同一个节点");
patchVnode(oldVnode, newVnode);
} else {
// console.log("不是同一个节点");
let newVnodeElm = createElement(newVnode, oldVnode.elm);
if (oldVnode.elm.parentNode && newVnodeElm) {
oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);
}
oldVnode.elm.parentNode.removeChild(oldVnode.elm);
}
}
//VNode文件
export default function (sel, data, children, text, elm) {
const key = data.key
return {
sel,
data,
children,
text,
elm,
key,
}
}
//h文件
import vnode from './vnode.js'
//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') {
return vnode(sel, data, undefined, c, undefined)
} else if (Array.isArray(c)) {
let children = []
for (let i = 0; i < c.length; i++) {
if (!(typeof c[i] === 'object' && c[i].hasOwnProperty('sel'))) {
throw new Error('传入的数组参数中有项不是h函数')
}
children.push(c[i])
}
// 循环结束,children收集完毕
return vnode(sel, data, children, undefined, undefined)
} else if (typeof c === 'object' && c.hasOwnProperty('sel')) {
let children = [c]
return vnode(sel, data, children, undefined, undefined)
} else {
throw new Error('h()参数类型不正确')
}
}
vue虚拟dom和diff算法的更多相关文章
- 【React 7/100 】 虚拟DOM和Diff算法
虚拟DOM和Diff算法 React更新视图的思想是:只要state变化就重新渲染视图 特点:思路非常清晰 问题:组件中只有一个DOM元素需要更新时,也得把整个组件的内容重新渲染吗? 不是这样的 理想 ...
- 虚拟DOM与diff算法
虚拟DOM与diff算法 虚拟DOM 在DOM操作中哪怕我们的数据,发生了一丢丢的变化,也会被强制重建整预DOM树.这么做,涉及到很多元素的重绘和重排,导致性能浪费严重 只要实现按需更新页面上的元素即 ...
- 深入理解react中的虚拟DOM、diff算法
文章结构: React中的虚拟DOM是什么? 虚拟DOM的简单实现(diff算法) 虚拟DOM的内部工作原理 React中的虚拟DOM与Vue中的虚拟DOM比较 React中的虚拟DOM是什么? ...
- vue之虚拟DOM、diff算法
一.真实DOM和其解析流程? 浏览器渲染引擎工作流程都差不多,大致分为5步,创建DOM树——创建StyleRules——创建Render树——布局Layout——绘制Painting 第一步,用HTM ...
- 虚拟dom?diff算法?key?Vue原理的核心三问?打包教你搞定。
为什么需要虚拟DOM 先介绍浏览器加载一个HTML文件需要做哪些事,帮助我们理解为什么我们需要虚拟DOM.webkit引擎的处理流程,如下图所示: 所有浏览器的引擎工作流程都差不多,如上图大致分5步: ...
- 虚拟dom与diff算法 分析
好文集合: 深入浅出React(四):虚拟DOM Diff算法解析 全面理解虚拟DOM,实现虚拟DOM
- react中虚拟dom的diff算法
.state 数据 .jsx模板 .生成虚拟dom(虚拟DOM就是一个js对象,用它来描述真实DOM) ['div', {id:'abc'}, ['span', {}, 'hello world']] ...
- 【前端知识体系-JS相关】虚拟DOM和Diff算法
1.介绍一下vdom? virtual dom, 虚拟DOM 使用JS来模拟DOM结构 DOM变化的对比,放在JS层来做(图灵完备语言),提高效率 DOM操作非常昂贵(消耗性能) 2.Snabbdom ...
- 【React自制全家桶】二、分析React的虚拟DOM和Diff算法
一.React如何更新DOM内容: 1. 获取state 数据 2. 获取JSX模版 3. 通过数据 +模版结合,生成真实的DOM, 来显示,以下行代码为例(简称代码1) <div id= ...
随机推荐
- python的编译和解释
编译和解释 1.编译: 将源代码一次性转换成目标代码的过程 源代码 → 编辑器 →目标代码 →程序执行(同时程序输入)→结果输出 2.解释: 将源代码逐条转换成目标代码同时逐条运行的过程 源代码+程序 ...
- k8s,coredns内部测试node节点上的pod的calico是否正常的一个小技巧
最近由于master整个挂掉,导致相关一些基础服务瘫掉,修复中测试有些节点网络又出现不通的情况正常的启动相关一些服务后,测试一些节点,比较费劲,还有进入pod,以及还有可能涉及命名空间操作这里可以这样 ...
- SpringMVC基础原理
1.拦截所有请求到DispatcherServlet 2.去寻找映射器 3.根据处理器适配器处理业务,返回视图 4.视图解析器解析显示视图
- MySQL入门学习day3随笔2
用户创建及权限部分 1 create user Cra2iTeT identified by '123456'-- 创建用户 2 3 alter user 'Cra2iTeT'@'%' identif ...
- Java学习day40
跟着视频回顾了整个JavaSE的学习过程
- angular.js中指令compile与link原理剖析
在angularJs应用启动之前,它们是以HTML文本形式存在文本编辑器当中.应用启动会进行编译和链接,作用域会同HTML进行绑定.这个过程包含了两个阶段! 编译阶段 在编译的阶段,angularJs ...
- what 的页面制作
1. html结构 <!-- section: what we do --> <section id="what" class="bg-light py ...
- Bugku练习题---MISC---FileStoragedat
Bugku练习题---MISC---FileStoragedat flag:bugku{WeChatwithSteg0} 解题步骤: 1.观察题目,下载附件 2.下载后发现是一个后缀名为.dat的文件 ...
- XCTF练习题---MISC---stegano
XCTF练习题---MISC---stegano flag:flag{1nv151bl3m3554g3} 解题步骤: 1.观察题目,下载附件 2.打开发现是一张PDF图片,尝试转换word无果后,想到 ...
- SpringBoot中异常处理
一.背景 在我们编写程序的过程中,程序中可能随时发生各种异常,那么我们如何优雅的处理各种异常呢? 二.需求 1.拦截系统中部分异常,返回自定义的响应. 比如: 系统发生HttpRequestMetho ...