欢迎关注我的公众号睿Talk
,获取我最新的文章:
一、前言
目前最流行的两大前端框架,React和Vue,都不约而同的借助Virtual DOM技术提高页面的渲染效率。那么,什么是Virtual DOM?它是通过什么方式去提升页面渲染效率的呢?本系列文章会详细讲解Virtual DOM的创建过程,并实现一个简单的Diff算法来更新页面。本文的内容脱离于任何的前端框架,只讲最纯粹的Virtual DOM。敲单词太累了,下文Virtual DOM一律用VD表示。
这是VD系列文章的第三篇,以下是本系列其它文章的传送门:
你不知道的Virtual DOM(一):Virtual Dom介绍
你不知道的Virtual DOM(二):Virtual Dom的更新
你不知道的Virtual DOM(三):Virtual Dom更新优化
你不知道的Virtual DOM(四):key的作用
你不知道的Virtual DOM(五):自定义组件
你不知道的Virtual DOM(六):事件处理&异步更新
本文基于本系列文章的第二篇,对VD的比较过程进行优化。
二、优化一:省略patch对象,直接更新dom
在上一个版本的代码里,我们是通过在diff过程中生成patch对象,然后在利用这个对象更新dom。
function tick(element) {
if (state.num > 20) {
clearTimeout(timer);
return;
}
const newVDom = view();
// 生成差异对象
const patchObj = diff(preVDom, newVDom);
preVDom = newVDom;
// 给dom打个补丁
patch(element, patchObj);
}
实际上这步是多余的。既然在diff的时候就已经知道要如何操作dom了,那为什么不直接在diff里面更新呢?先来回顾下之前的diff代码:
function diff(oldVDom, newVDom) {
// 新建node
if (oldVDom == undefined) {
return {
type: nodePatchTypes.CREATE,
vdom: newVDom
}
}
// 删除node
if (newVDom == undefined) {
return {
type: nodePatchTypes.REMOVE
}
}
// 替换node
if (
typeof oldVDom !== typeof newVDom ||
((typeof oldVDom === 'string' || typeof oldVDom === 'number') && oldVDom !== newVDom) ||
oldVDom.tag !== newVDom.tag
) {
return {
type: nodePatchTypes.REPLACE,
vdom: newVDom
}
}
// 更新node
if (oldVDom.tag) {
// 比较props的变化
const propsDiff = diffProps(oldVDom, newVDom);
// 比较children的变化
const childrenDiff = diffChildren(oldVDom, newVDom);
// 如果props或者children有变化,才需要更新
if (propsDiff.length > 0 || childrenDiff.some( patchObj => (patchObj !== undefined) )) {
return {
type: nodePatchTypes.UPDATE,
props: propsDiff,
children: childrenDiff
}
}
}
}
diff最终返回的对象是这个数据结构:
{
type,
vdom,
props: [{
type,
key,
value
}]
children
}
现在,我们把生成对象的步骤省略掉,直接操作dom。这时候我们需要将父元素,还有子元素的索引传进来(原patch的逻辑):
function diff(oldVDom, newVDom, parent, index=0) {
// 新建node
if (oldVDom == undefined) {
parent.appendChild(createElement(newVDom));
}
const element = parent.childNodes[index];
// 删除node
if (newVDom == undefined) {
parent.removeChild(element);
}
// 替换node
if (
typeof oldVDom !== typeof newVDom ||
((typeof oldVDom === 'string' || typeof oldVDom === 'number') && oldVDom !== newVDom) ||
oldVDom.tag !== newVDom.tag
) {
parent.replaceChild(createElement(newVDom), element);
}
// 更新node
if (oldVDom.tag) {
// 比较props的变化
diffProps(oldVDom, newVDom, element);
// 比较children的变化
diffChildren(oldVDom, newVDom, element);
}
}
function diffProps(oldVDom, newVDom) {
const allProps = {...oldVDom.props, ...newVDom.props};
// 获取新旧所有属性名后,再逐一判断新旧属性值
Object.keys(allProps).forEach((key) => {
const oldValue = oldVDom.props[key];
const newValue = newVDom.props[key];
// 删除属性
if (newValue == undefined) {
element.removeAttribute(key);
}
// 更新属性
else if (oldValue == undefined || oldValue !== newValue) {
element.setAttribute(key, newValue);
}
}
)
}
function diffChildren(oldVDom, newVDom, parent) {
// 获取子元素最大长度
const childLength = Math.max(oldVDom.children.length, newVDom.children.length);
// 遍历并diff子元素
for (let i = 0; i < childLength; i++) {
diff(oldVDom.children[i], newVDom.children[i], parent, i);
}
}
本质上来说,这次的优化是将patch的逻辑整合进diff的过程中了。经过这次优化,JS计算的时间快了那么几毫秒。虽然性能的提升不大,但代码比原来的少了80多行,降低了逻辑复杂度,优化的效果还是不错的。
三、优化二:VD与真实dom融合
在之前的版本里面,diff操作针对的是新旧2个VD。既然真实的dom已经根据之前的VD渲染出来了,有没办法用当前的dom跟新的VD做比较呢?
答案是肯定的,只需要按需获取dom中不同的属性就可以了。比如,当比较tag的时候,使用的是nodeType和tagName,比较文本的时候用的是nodeValue。
function tick(element) {
if (state.num > 20) {
clearTimeout(timer);
return;
}
const newVDom = view();
// 比较并更新节点
diff(newVDom, element);
// diff(preVDom, newVDom, element);
// preVDom = newVDom;
}
function diff(newVDom, parent, index=0) {
const element = parent.childNodes[index];
// 新建node
if (element == undefined) {
parent.appendChild(createElement(newVDom));
return;
}
// 删除node
if (newVDom == undefined) {
parent.removeChild(element);
return;
}
// 替换node
if (!isSameType(element, newVDom)) {
parent.replaceChild(createElement(newVDom), element);
return;
}
// 更新node
if (element.nodeType === Node.ELEMENT_NODE) {
// 比较props的变化
diffProps(newVDom, element);
// 比较children的变化
diffChildren(newVDom, element);
}
}
// 比较元素类型是否相同
function isSameType(element, newVDom) {
const elmType = element.nodeType;
const vdomType = typeof newVDom;
// 当dom元素是文本节点的情况
if (elmType === Node.TEXT_NODE &&
(vdomType === 'string' || vdomType === 'number') &&
element.nodeValue == newVDom
) {
return true;
}
// 当dom元素是普通节点的情况
if (elmType === Node.ELEMENT_NODE && element.tagName.toLowerCase() == newVDom.tag) {
return true;
}
return false;
}
为了方便属性的比较,提高效率,我们将VD的props存在dom元素的__preprops_
字段中:
const ATTR_KEY = '__preprops_';
// 创建dom元素
function createElement(vdom) {
// 如果vdom是字符串或者数字类型,则创建文本节点,比如“Hello World”
if (typeof vdom === 'string' || typeof vdom === 'number') {
return doc.createTextNode(vdom);
}
const {tag, props, children} = vdom;
// 1. 创建元素
const element = doc.createElement(tag);
// 2. 属性赋值
setProps(element, props);
// 3. 创建子元素
children.map(createElement)
.forEach(element.appendChild.bind(element));
return element;
}
// 属性赋值
function setProps(element, props) {
// 属性赋值
element[ATTR_KEY] = props;
for (let key in props) {
element.setAttribute(key, props[key]);
}
}
进行属性比较的时候再取出来:
// 比较props的变化
function diffProps(newVDom, element) {
let newProps = {...element[ATTR_KEY]};
const allProps = {...newProps, ...newVDom.props};
// 获取新旧所有属性名后,再逐一判断新旧属性值
Object.keys(allProps).forEach((key) => {
const oldValue = newProps[key];
const newValue = newVDom.props[key];
// 删除属性
if (newValue == undefined) {
element.removeAttribute(key);
delete newProps[key];
}
// 更新属性
else if (oldValue == undefined || oldValue !== newValue) {
element.setAttribute(key, newValue);
newProps[key] = newValue;
}
}
)
// 属性重新赋值
element[ATTR_KEY] = newProps;
}
通过这种方式,我们不再需要用变量preVDom
将上一次生成的VD存下来,而是直接跟真实的dom进行比较,灵活性更强。
四、总结
本文基于上一个版本的代码,简化了页面渲染的过程(省略patch对象),同时提供了更灵活的VD比较方法(直接跟dom比较),可用性越来越强了。基于当前这个版本的代码还能做怎样的优化呢,请看下一篇的内容:你不知道的Virtual DOM(四):key的作用。
P.S.: 想看完整代码见这里,如果有必要建一个仓库的话请留言给我:代码