从0到1实现一个虚拟DOM
web前端开发
共 7759字,需浏览 16分钟
· 2020-08-20
Virtual DOM 是真实 DOM 的映射
当虚拟 DOM 树中的某些节点改变时,会得到一个新的虚拟树。算法对这两棵树(新树和旧树)进行比较,找出差异,然后只需要在真实的 DOM 上做出相应的改变。
用 JS 对象模拟 DOM 树
<ul class="”list”">
<li>item 1li>
<li>item 2li>
ul>
{ type: ‘ul’, props: { ‘class’: ‘list’ }, children: [
{ type: ‘li’, props: {}, children: [‘item 1’] },
{ type: ‘li’, props: {}, children: [‘item 2’] }
] }
用如下对象表示 DOM 元素
{ type: ‘…’, props: { … }, children: [ … ] }
用普通 JS 字符串表示 DOM 文本节点
function h(type, props, …children) {
return { type, props, children };
}
h(‘ul’, { ‘class’: ‘list’ },
h(‘li’, {}, ‘item 1’),
h(‘li’, {}, ‘item 2’),
);
<ul className="”list”">
<li>item 1li>
<li>item 2li>
ul>
React.createElement(‘ul’, { className: ‘list’ },
React.createElement(‘li’, {}, ‘item 1’),
React.createElement(‘li’, {}, ‘item 2’),
);
h(...)
函数代替 React.createElement(…)
,那么我们也能使用 JSX 语法。其实,只需要在源文件头部加上这么一句注释:/** @jsx h */
<ul className="”list”">
<li>item 1li>
<li>item 2li>
ul>
h(...)
函数代替 React.createElement(…)
,然后 Babel 就开始编译。’/** @jsx h */ const a = (
<ul className="”list”">
<li>item 1li>
<li>item 2li>
ul>
);
const a = (
h(‘ul’, { className: ‘list’ },
h(‘li’, {}, ‘item 1’),
h(‘li’, {}, ‘item 2’),
);
);
“h”
执行时,它将返回普通 JS 对象-即我们的虚拟 DOM:const a = (
{ type: ‘ul’, props: { className: ‘list’ }, children: [
{ type: ‘li’, props: {}, children: [‘item 1’] },
{ type: ‘li’, props: {}, children: [‘item 2’] }
] }
);
从 Virtual DOM 映射到真实 DOM
使用以’
$
‘开头的变量表示真正的 DOM 节点(元素,文本节点),因此 \$parent 将会是一个真实的 DOM 元素虚拟 DOM 使用名为
node
的变量表示
createElement(…)
,它将获取一个虚拟 DOM 节点并返回一个真实的 DOM 节点。这里先不考虑 props
和 children
属性:function createElement(node) {
if (typeof node === ‘string’) {
return document.createTextNode(node);
}
return document.createElement(node.type);
}
{ type: ‘…’, props: { … }, children: [ … ] }
createElement
传入虚拟文本节点和虚拟元素节点——这是可行的。appendChild()
添加到我们的元素中:function createElement(node) {
if (typeof node === ‘string’) {
return document.createTextNode(node);
}
const $el = document.createElement(node.type);
node.children
.map(createElement)
.forEach($el.appendChild.bind($el));
return $el;
}
props
属性放到一边。待会再谈。我们不需要它们来理解虚拟 DOM 的基本概念,因为它们会增加复杂性。/** @jsx h */
function h(type, props, ...children) {
return { type, props, children };
}
function createElement(node) {
if (typeof node === "string") {
return document.createTextNode(node);
}
const $el = document.createElement(node.type);
node.children.map(createElement).forEach($el.appendChild.bind($el));
return $el;
}
const a = (
<ul class="list">
<li>item 1li>
<li>item 2li>
ul>
);
const $root = document.getElementById("root");
$root.appendChild(createElement(a));
比较两棵虚拟 DOM 树的差异
添加新节点,使用 appendChild(…) 方法添加节点
移除老节点,使用 removeChild(…) 方法移除老的节点
节点的替换,使用 replaceChild(…) 方法
$parent
、newNode 和 oldNode,其中 \$parent 是虚拟节点的一个实际 DOM 元素的父元素。现在来看看如何处理上面描述的所有情况。添加新节点
function updateElement($parent, newNode, oldNode) {
if (!oldNode) {
$parent.appendChild(createElement(newNode));
}
}
移除老节点
$parent.removeChild(…)
方法把变化映射到真实的 DOM 上。但前提是我们得知道我们的节点在父元素上的索引,我们才能通过 \$parent.childNodes[index] 得到该节点的引用。function updateElement($parent, newNode, oldNode, index = 0) {
if (!oldNode) {
$parent.appendChild(createElement(newNode));
} else if (!newNode) {
$parent.removeChild($parent.childNodes[index]);
}
}
节点的替换
function changed(node1, node2) {
return typeof node1 !== typeof node2 ||
typeof node1 === ‘string’ && node1 !== node2 ||
node1.type !== node2.type
}
function updateElement($parent, newNode, oldNode, index = 0) {
if (!oldNode) {
$parent.appendChild(createElement(newNode));
} else if (!newNode) {
$parent.removeChild($parent.childNodes[index]);
} else if (changed(newNode, oldNode)) {
$parent.replaceChild(createElement(newNode), $parent.childNodes[index]);
}
}
比较子节点
当节点是 DOM 元素时我们才需要比较( 文本节点没有子节点 )
我们需要传递当前的节点的引用作为父节点
我们应该一个一个的比较所有的子节点,即使它是
undefined
也没有关系,我们的函数也会正确处理它。最后是 index,它是子数组中子节点的 index
function updateElement($parent, newNode, oldNode, index = 0) {
if (!oldNode) {
$parent.appendChild(createElement(newNode));
} else if (!newNode) {
$parent.removeChild($parent.childNodes[index]);
} else if (changed(newNode, oldNode)) {
$parent.replaceChild(createElement(newNode), $parent.childNodes[index]);
} else if (newNode.type) {
const newLength = newNode.children.length;
const oldLength = oldNode.children.length;
for (let i = 0; i < newLength || i < oldLength; i++) {
updateElement(
$parent.childNodes[index],
newNode.children[i],
oldNode.children[i],
i
);
}
}
}
完整的代码
/*_ @jsx h_ /
function h(type, props, ...children) {
return { type, props, children };
}
function createElement(node) {
if (typeof node === "string") {
return document.createTextNode(node);
}
const $el = document.createElement(node.type);
node.children.map(createElement).forEach($el.appendChild.bind($el));
return $el;
}
function changed(node1, node2) {
return (
typeof node1 !== typeof node2 ||
(typeof node1 === "string" && node1 !== node2) ||
node1.type !== node2.type
);
}
function updateElement($parent, newNode, oldNode, index = 0) {
if (!oldNode) {
$parent.appendChild(createElement(newNode));
} else if (!newNode) {
$parent.removeChild($parent.childNodes[index]);
} else if (changed(newNode, oldNode)) {
$parent.replaceChild(createElement(newNode), $parent.childNodes[index]);
} else if (newNode.type) {
const newLength = newNode.children.length;
const oldLength = oldNode.children.length;
for (let i = 0; i < newLength || i < oldLength; i++) {
updateElement(
$parent.childNodes[index],
newNode.children[i],
oldNode.children[i],
i
);
}
}
}
// ---------------------------------------------------------------------
const a = (
<ul>
<li>item 1li>
<li>item 2li>
ul>
);
const b = (
<ul>
<li>item 1li>
<li>hello!li>
ul>
);
const $root = document.getElementById("root");
const $reload = document.getElementById("reload");
updateElement($root, a);
$reload.addEventListener("click", () => {
updateElement($root, b, a);
});
<button id="reload">RELOADbutton>
<div id="root">div>
#root {
border: 1px solid black;
padding: 10px;
margin: 30px 0 0 0;
}
总结
设置元素属性(props)并进行 diffing/updating
处理事件——向元素中添加事件监听
让虚拟 DOM 与组件一起工作,比如 React
获取对实际 DOM 节点的引用
使用带有库的虚拟 DOM,这些库可以直接改变真实的 DOM,比如 jQuery 及其插件
评论
一个朋友
一个朋友,在深圳奋斗7年,和女友在去年合力在龙华买了一套房,总价600万,首付3成。但就在昨天,他们崩溃了。深圳推出可售型人才住房,就在他们新房附近,同样面积,总价不到400万,售价近乎腰斩。他们想不明白,同样是深圳人,买房人为什么都要被当成炒房客对待?二手房冰封,卖不出,新房不断打着,像极了上世纪
嵌入式Linux
0
老爸嘲讽我了,写破代码一年就挣十几万,他在工地带50个工人,一个月光人头费就3万,让我滚回去跟他干!
点击上方 "大数据肌肉猿"关注, 星标一起成长点击下方链接,进入高质量学习交流群今日更新| 1052个转型案例分享-大数据交流群来自:网络,侵删有个网友的父亲是做工程的,天天就嘲笑他,说他天天写着破代码有啥用,一年就拿个十多万的死工资,然后告诉他自己在工地里面带了50个工人,一个月能抽三万
程序源代码
0
测试新人,如何快速上手一个陌生的系统!
大家好,我是狂师!作为刚入行不久的测试新人,面对一个陌生的系统时,可能会感到有些手足无措。面对一个全新的系统系统,如何快速上手并展开有效的测试工作是一个重要的挑战。本文将探讨测试新人如何通过一系列步骤和策略,快速熟悉并掌握新系统的测试要点,从而提高测试效率和质量。本文旨在为测试新手提供一份指导,帮助
测试开发技术
0
Windows格式化对话框是一个使用了30年的 “临时解决方案”
戴夫-普卢默(Dave Plummer)是微软的资深工程师,曾创造了任务管理器、Windows 弹球、原生 ZIP 支持(微软出钱买断该功能后,他用这笔钱购买了一辆红色克尔维特)等传奇。近日他在自己的 X 账户上分享了创建 "格式化" 对话框的故事 —— 称其是一个使用了长达 30 年的 “临时解决
开源Linux
0
OpenAI的Sora竟然造假:生成的AI大片只有1%的AI,剩下的99%是人工!
点蓝色字关注“机器学习算法工程师”设为星标,干货直达!啊?Sora火爆短片《气球人》,也“造假”了???背后艺术家团队的最新揭秘,可谓一石激起千层浪:原来,视频画面并非完全由AI生成,其中有大量视觉效果需要人类后期实现。be like:这下网友不干了,合着大家伙儿跟OpenAI玩真心,OpenAI背
机器学习算法工程师
1
15种时间序列预测方法总结(包含多种方法代码实现)
向AI转型的程序员都关注了这个号👇👇👇在这篇文章中,我们将深入探讨时间序列预测的基本概念和方法。我们将首先介绍单元预测和多元预测的概念,然后详细介绍各种深度学习和传统机器学习方法如何应用于时间序列预测,包括循环神经网络(RNN)、一维卷积神经网络(1D-CNN)、Transformer、自回归模型(
机器学习AI算法工程
0
从原理到实践:掌握DPDK内存池技术
前言:本文整理下之前的学习笔记,基于DPDK17.11版本源码分析。主要分析一下内存管理部分代码。一、概述内存管理是数据面开发套件(DPDK)的一个核心部分,以此为基础,DPDK的其他部分和用户应用得以发挥其最佳性能。本系列文章将详细介绍DPDK提供的各种内存管理的功能。但在此之前,有必要先谈一谈为
开源Linux
0
一个神奇的 Linux命令——type
转自:科学随想录在Linux系统中,了解命令的类型、位置和完整路径对于系统管理和开发非常重要。type命令是一个强大而实用的工具,能够帮助我们查看给定命令的类型、位置和完整路径。在本文中,我们将深入探索type命令的用法和功能,并提供详细的代码示例和输出,以帮助读者全面了解该命令。第一部分:type
开源Linux
0