本文为系列文章:
手写虚拟DOM(一)—— VirtualDOM介绍
手写虚拟DOM(二)—— VirtualDOM Diff
手写虚拟DOM(三)—— Diff算法优化
手写虚拟DOM(四)—— 进一步提升Diff效率之关键字Key
手写虚拟DOM(五)—— 自定义组件
手写虚拟DOM(六)—— 事件处理
手写虚拟DOM(七)—— 异步更新
一、前言
本文继续上一节的Virtual DOM Diff,来聊一聊,有哪些可优化的点:
- 我们通过diff获取了新、老 Virtual DOM的差异,然后再对现有DOM进行打补丁;
既然,我们都找出了差异,何不直接修改DOM,还要再多此一举呢? - 我们每次都会保存上一次的Virtual DOM,与新的Virtual DOM比较,再转成真实的dom树;
那我们何不将Virtual DOM与真实dom树直接关联,diff差异,然后直接更新呢?
二、优化
2.1、优化diff,去掉patch
// 改造 render
function render(container) {
const vdom = view();
if (!vdomPre) {
container.appendChild(createElement(vdom));
} else {
diff(vdomPre, vdom, container); <====== 直接传入 root container
}
vdomPre = vdom;
setTimeout(() => {
state.number += 1;
render(container);
}, 3000);
}
// 改造 diff
function diff(pre, post, parent, cid = 0) {
const len = parent.childNodes.length;
const child = cid >= len ? undefined : parent.childNodes[cid];
// 原vdom没有,新vdom有,则表明是新增节点
if (pre === undefined) {
parent.appendChild(createElement(post));
return;
}
// 原vdom有,新vdom没有,则表明是移除节点
if (post === undefined) {
parent.removeChild(child);
return
}
// 新老节点(类型不同 or tag不同 or 内容不同),则表明是替换节点
if (typeof pre !== typeof post || pre.tag !== post.tag ||
(typeof pre === 'string' || typeof pre === 'number') && pre !== post) {
parent.replaceChild(createElement(post), child);
return;
}
// 至此,只有可能是当前vdom的自身props变化 or 其children发生变化
if (pre.tag) {
diffProps(pre.props, post.props, child);
diffChildren(pre, post, child);
}
}
// 改造 diffProps
function diffProps(preProps, postProps, element) {
// 合并所有props的键值(后者替换前者)
const all = {...preProps, ...postProps};
// 遍历props的所有键值
Object.keys(all).forEach(key => {
const ov = preProps[key];
const nv = postProps[key];
// 新vdom没有该属性
if (nv === undefined) {
element.removeAttribute(key);
}
// 老vdom没有该属性,or 该属性值与新vdom的属性值不一致
if (ov === undefined || ov !== nv) {
element.setAttribute(key, nv);
}
});
}
// 改造 diffChildren
function diffChildren(pre, post, element) {
// 子元素最大长度
const len = Math.max(pre.children.length, post.children.length);
// 依次遍历并diff子元素
for (let i = 0; i < len; i ++) {
diff(pre.children[i], post.children[i], element, i);
}
}
在diff、diffProps中,直接操作dom或者属性。
2.2、关联真实dom树,直接更新
// 修改 render
function render(container) {
diff(view(), container);
setTimeout(() => {
state.number += 1;
render(container);
}, 3000);
}
// 修改 setProps
function setProps(element, props) {
for (let k in props) {
if (props.hasOwnProperty(k)) {
element.setAttribute(k, props[k]);
}
}
// 保存当前的属性,之后用于新VirtualDOM的属性比较
element['props'] = props; <===== 重点是这里
}
// 修改 diff
function diff(vdom, parent, cid = 0) {
const len = parent.childNodes.length;
// child是当前真实dom!
const child = cid >= len ? undefined : parent.childNodes[cid];
// 原dom没有,新vdom有,则表明是新增节点
if (child === undefined) {
parent.appendChild(createElement(vdom));
return;
}
// 原dom有,新vdom没有,则表明是移除节点
if (vdom === undefined) {
parent.removeChild(child);
return
}
// 新老节点(类型不同 or tag不同 or 内容不同),则表明是替换节点
if (!isEqual(vdom, child)) {
parent.replaceChild(createElement(vdom), child);
return;
}
// 至此,只有可能是当前vdom的自身props变化 or 其children发生变化
if (child.nodeType === Node.ELEMENT_NODE) {
diffProps(vdom.props, child);
diffChildren(vdom, child);
}
}
这里,用到了一个新的比较方法判断是否需要Replace节点:
function isEqual(vdom, element) {
const elType = element.nodeType;
const vdomType = typeof vdom;
// 检查dom元素是文本节点的情况
if (elType === Node.TEXT_NODE &&
(vdomType === 'string' || vdomType === 'number') &&
element.nodeValue === vdom) {
return true;
}
// 检查dom元素是普通节点的情况
if (elType === Node.ELEMENT_NODE && element.tagName.toLowerCase() === vdom.tag.toLowerCase()) {
return true;
}
return false;
}
// 修改 diffProps
function diffProps(props, element) {
// 合并所有props的键值(后者替换前者)
const all = {...element['props'], ...props}; <===== 合并直接dom中的props与新的VirtualDOM的props
const newProps = {};
// 遍历props的所有键值
Object.keys(all).forEach(key => {
const ov = element['props'][key];
const nv = props[key];
// 新vdom没有该属性
if (nv === undefined) {
element.removeAttribute(key);
return;
}
// 老vdom没有该属性,or 该属性值与新vdom的属性值不一致
if (ov === undefined || ov !== nv) {
element.setAttribute(key, nv);
}
newProps[key] = all[key];
});
element['props'] = newProps; // 保存最新的属性
}
// 修改 diffChildren
function diffChildren(vdom, element) {
// 子元素最大长度
const len = Math.max(element.childNodes.length, vdom.children.length);
// 依次遍历并diff子元素
for (let i = 0; i < len; i ++) {
diff(vdom.children[i], element, i);
}
}
三、总结
本文基于上一个版本的代码,简化了页面渲染的过程(省略patch对象),
同时提供了更灵活的VD比较方法(直接跟dom比较),可用性越来越强了。
项目源码:
https://github.com/qingye/VirtualDOM-Study/tree/master/VirtualDOM-Study-03
网友评论