前言:
目前同学们对“jquerydom渲染后执行”可能比较讲究,兄弟们都想要剖析一些“jquerydom渲染后执行”的相关内容。那么小编在网络上网罗了一些有关“jquerydom渲染后执行””的相关内容,希望姐妹们能喜欢,各位老铁们快快来学习一下吧!vue采用的虚拟dom对比实现最小化视图更新,相比以前jquery操作真实dom,大大提高提高页面的渲染性能,提升用户体验。那为什么虚拟dom提高了性能,虚拟dom又是如何对比的?本文将带你一步步分析其工作原理。
一、虚拟DOM
虚拟DOM通常用英文virtual DOM来表达,有时会简写为vdom。虚拟DOM和真实DOM的结构一样,都是由一个个节点组成的树型结构。所以,我们常能听到“虚拟节点”这样的词,即virtual node,有时也会简写为vnode。
虚拟DOM的本质是JavaScript对象,并且最少包含标签名( type)、属性(attrs)和子元素对象( children)三个属性。不同的框架对这三个属性的命名会有点差别。总而言之,虚拟dom就是用js对象来描述dom节点。
const title = {
// 标签名称
type: "h1",
// 标签属性
props: {
onclick: handler,
},
// 子节点
children: [
{
tag: "span",
},
],
};
对应到Vue.js模板,其实就是:
<h1 @click="handler"><span></span></h1>
其实我们在Vue.js组件中手写渲染函数就是使用虚拟DOM来描述UI的,如以下代码所示:
import { h } from "vue";
export default {
render() {
return h("h1", { onclick: handler });// 虚拟DOM
},
};
此处h函数调用,也不是JavaScript对象,怎么是虚拟DOM呢?其实h函数的返回值就是一个对象,其作用是让我们编写虚拟DOM更加方便。如果把上面的h函数调用的代码改成JavaScript对象,就需要写更多的内容:
export default {
render() {
return {
// 标签名称
type: "h1",
// 标签属性
props: {
onclick: handler,
},
};
},
};
如果还有子节点,那么需要编写的内容就更多了,所以h函数就是一个辅助创建虚拟DOM的工具函数,仅此而已。
二、Diff算法
了解Diff算法
简单来说,当新旧vnode的子节点都是一组节点时,为了以最小的性能开销完成更新操作,需要比较两组子节点,用于比较的算法就叫作Diff算法。我们知道,操作DOM的性能开销通常比较大,而Diff算法就是为了解决这个问题诞生的。
Diff算法的过程就是Vue里面patch函数的调用,比较新旧节点,一边比较一边给真实的DOM打补丁。它的核心是参考 Snabbdom,通过新旧虚拟 DOM 对比(即 patch 过程),找出最小变化的地方转为进行 DOM 操作。
在页面首次渲染的时候会调用一次 patch 并创建新的 vnode,不会进行更深层次的比较。
然后是在组件中数据发生变化时,会触发 setter 然后通过 Notify 通知 Watcher,对应的 Watcher 会通知更新并执行更新函数,它会执行 render 函数获取新的虚拟 DOM,然后执行 patch 对比上次渲染结果的老的虚拟 DOM,并计算出最小的变化,然后再去根据这个最小的变化去更新真实的 DOM。
体验Diff算法
先通过第三方包snabbdom,先来感受一下Diff算法的强大之处。
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
//创建出patch函数
const patch = init([
classModule,
propsModule,
styleModule,
eventListenersModule,
]);
const myVnode1 = h("ul", {}, [
h("li", "A"),
h("li", "B"),
h("li", "C"),
h("li", "D"),
]);
// 得到盒子和按钮
const container = document.getElementById("container");
const btn = document.getElementById("btn");
// 第一次上树
patch(container, myVnode1);
// 新节点
const myVnode2 = h("ul", {}, [
h("li", "A"),
h("li", "B"),
h("li", "C"),
h("li", "D"),
h("li", "E"),
]);
btn.onclick = function () {
patch(myVnode1, myVnode2);
};
当我们点击改变DOM的时候,发现会新增一个 li标签 内容为 E,直接在 旧的虚拟DOM 上直接在后面添加一个节点,验证了是进行了 diff算法精细化的比较,以最小量进行更新。那么问题就来了,如果我在前面添加一个节点呢?是不是也是像在最后添加一样,直接在前面添加一个节点。我们不妨也来试一试看看效果:
const myVnode1 = h("ul", {}, [
h("li", "A"),
h("li", "B"),
h("li", "C"),
h("li", "D"),
]);
// 新节点
const myVnode2 = h("ul", {}, [
h("li", "Q"),
h("li", "T"),
h("li", "A"),
h("li", "B"),
h("li", "Z"),
h("li", "C"),
h("li", "D"),
h("li", "E"),
]);
结果跟我们想的不一样,你会发现,里面的文本内容全部发生了变化,也就是说将之前的 DOM 全部拆除,然后将新的DOM的重新渲染。这样是无法让人接受的,所以 Vue里使用 Diff 算法的时候都遵循深度优先,同层比较的策略做了一些优化,来计算出最小变化。
Diff算法的优化
1.同层比较(只比较同一层级,不跨级比较)
Diff 过程只会把同颜色框起来的同一层级的 DOM 进行比较,这样来简化比较次数。
2.深度优先
比对到相同的节点,会对两个节点的所有子节点都比较完成,才会回到同层的节点继续比对。
3.key唯一标识符
如果标签名相同,key 也相同,就会认为是相同节点,也不继续按这个树状结构做深度比较,比如我们写 v-for 的时候会比较 key,不写 key 就会报错,这也就是因为 Diff 算法需要比较 key。
三、key的作用
上述案例我们带上key再来看一下效果:
const myVnode1 = h("ul", {}, [
h("li", { key: "A" }, "A"),
h("li", { key: "B" }, "B"),
h("li", { key: "C" }, "C"),
h("li", { key: "D" }, "D"),
]);
// 新节点
const myVnode2 = h("ul", {}, [
h("li", { key: "Q" }, "Q"),
h("li", { key: "T" }, "T"),
h("li", { key: "A" }, "A"),
h("li", { key: "B" }, "B"),
h("li", { key: "Z" }, "Z"),
h("li", { key: "C" }, "C"),
h("li", { key: "D" }, "D"),
h("li", { key: "E" }, "E"),
]);
我们可以推出的结论一就是:key是当前节点的唯一标识,告诉 diff算法,在更改前后它们是同一个 DOM节点。
总结一下:
key 的作用主要是为了更高效的更新虚拟 DOM,因为它可以非常精确的找到相同节点,因此 patch 过程会非常高效。Vue 在 patch 过程中会判断两个节点是不是相同节点时,key 是一个必要条件。比如渲染列表时,如果不写 key,Vue 在比较的时候,就可能会导致频繁更新元素,使整个 patch 过程比较低效,影响性能。应该避免使用数组下标作为 key,因为 key 值不是唯一的话可能会导致上面图中表示的 bug,使 Vue 无法区分它他,还有比如在使用相同标签元素过渡切换的时候,就会导致只替换其内部属性而不会触发过渡效果。Vue 判断两个节点是否相同时主要判断两者的元素类型和 key 等,如果不设置 key,就可能永远认为这两个是相同节点,只能去做更新操作,就造成大量不必要的 DOM 更新操作,明显是不可取的。
四、Diff核心原理patch函数
path函数
patch函数 的主要作用就是:判断是否是同一个节点类型,是就在进行精细化对比,不是就进行暴力删除,插入新的。
我们在可以简单的画出patch函数现在的主要流程图如下:
// patch.js patch函数
import vnode from "./vnode";
import sameVnode from "./sameVnode";
import createElement from "./createElement";
export default function (oldVnode, newVnode) {
// 判断oldVnode是否是虚拟节点
if (oldVnode.type== "" || oldVnode.type== undefined) {
// console.log('不是虚拟节点');
// 创建虚拟DOM
oldVnode = emptyNodeAt(oldVnode);
}
// 判断是否是同一个节点
if (sameNode(oldVnode, newVnode)) {
console.log("是同一个节点");
} else {
// 暴力删除旧节点,插入新的节点
// 传入两个参数,创建的节点 插入到指定标杆的位置
createElement(newVnode, oldVnode.elm);
}
}
// 创建虚拟DOM
function emptyNodeAt(elm) {
return vnode(elm.tagName.toLowerCase(), {}, [], undefined, elm);
}
sameVnode函数
这个是用来判断是不是同一个节点的函数。
function sameVnode(a, b) {
return (
a.key === b.key && // key 是不是一样
a.asyncFactory === b.asyncFactory && // 是不是异步组件
((a.tag === b.tag && // 标签是不是一样
a.isComment === b.isComment && // 是不是注释节点
isDef(a.data) === isDef(b.data) && // 内容数据是不是一样
sameInputType(a, b)) || // 判断 input 的 type 是不是一样
(isTrue(a.isAsyncPlaceholder) && // 判断区分异步组件的占位符否存在
isUndef(b.asyncFactory.error)))
);
}
createElement函数
主要用来创建子节点的真实DOM
// createElement.js只负责创建真正节点
export default function createElement(vnode) {
// 创建上树的节点
let domNode = document.createElement(vnode.type)
// 判断有文本内容还是子节点
if (vnode.text != '' && (vnode.children == undefined || vnode.children.length == 0)) {
// 文本内容 直接赋值
domNode.innerText = vnode.text
// 上树 往body上添加节点
// insertBefore() 方法:可在已有的字节点前中插入一个新的子节点。相对于子节点来说的
} else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
// 有子节点
for(let i = 0; i < vnode.children.length; i++) {
// console.log(vnode.children[i]);
let ch = vnode.children[i]
// 进行递归 一旦调用createElement意味着 创建了DOM 并且elm属性指向了创建好的DOM
let chDom = createElement(ch)
// 添加节点 使用appendChild 因为遍历下一个之前 上一个真实DOM(这里的domVnode)已经生成了 所以可以使用appendChild
domNode.appendChild(chDom)
}
}
vnode.elm = domNode
return vnode.elm
}
Diff处理新旧节点是同一个节点时
上面的 patch函数 流程图中,我们已经处理了不同节点的时候,进行暴力删除旧的节点,然后插入新的节点,现在我们进行处理相同节点的时候,进行精细化的比较,继续完善 patch函数 的主流程图:
总结
在Javascript中,渲染 真实DOM 的开销是非常大的,比如我们修改了某个数据,如果直接渲染到 真实DOM,会引起整个 DOM树 的 回流和重绘。那么有没有可能实现只更新我们修改的那一小块DOM而不会引起整个DOM更新?此时我们就需要先根据 真实DOM 生成 虚拟DOM ,当 虚拟DOM 某个节点的数据改变后会生成一个 新的Vnode,然后 新的Vnode 和 旧的Vnodde 进行比较,发现有不一样的地方就直接修改到 真实DOM 上,然后使 旧的Vnode 的值变成 新的Vnode。
Diff算法的过程就是 patch函数 的调用,比较新旧节点,一边比较一边给 真实的DOM 打补丁。在采用 diff算法 比较新旧节点的时候,只会进行同层级的比较。在 patch方法 中,首先比较新旧虚拟节点是否是同一个节点,如果不是同一个节点,那么就会将旧的节点删除掉,插入新的虚拟节点,然后再使用 createElement函数 创建 真实DOM,渲染到真实的 DOM树。如果是同一个节点,使用 patchVnode函数 比较新旧节点,包括属性更新、文本更新、子节点更新,新旧节点均有子节点,则需要进行 diff算法,调用updateChildren方法,如果新节点没有文本内容而旧节点有文本内容,则需要将旧节点的文本删除,然后再增加子节点,如果新节点有文本内容,则直接替换旧节点的文本内容。
标签: #jquerydom渲染后执行