从头构建一个简单的虚拟 DOM
上周,我在曼彻斯特 Web Meetup #4上做了一个现场编程演讲。演讲期间,我在不到一个小时的时间里从零开始构建了一个虚拟 DOM。这是迄今为止我做过的技术难度最高的演讲。
我的演讲视频已上传至此处。这篇文章基本上是我演讲的打字稿,旨在澄清一些我在演讲中没来得及提及的额外内容。建议在阅读本文之前先观看视频,这样更容易理解。
这是我在演讲中编写的代码的github repo和codesandbox 。
附注
- 本文将在所有变量前添加
$
- 当提及真实领域时,例如$div
,,$el
$app
v
- 当提到虚拟 DOM 时,例如vDiv
,,vEl
vApp
- 本文将以实际演讲的形式呈现,逐步添加代码。每个部分都会有一个 codesandbox 链接来展示进度。
- 这篇文章很长,可能需要半个多小时才能读完。请确保你有足够的时间阅读。或者考虑先看视频。
- 如果您发现任何错误,请随时指出!
概述
背景:什么是虚拟 DOM?
虚拟 DOM 通常指的是代表实际DOM的普通对象。
文档对象模型 (DOM) 是 HTML 文档的编程接口。
例如,当您执行以下操作时:
const $app = document.getElementById('app');
您将获得页面上的 DOM <div id="app"></div>
。此 DOM 将提供一些编程接口供您控制。例如:
$app.innerHTML = 'Hello world';
为了制作一个简单的对象来表示$app
,我们可以写如下内容:
const vApp = {
tagName: 'div',
attrs: {
id: 'app',
},
};
谈话中没有提到
虚拟 DOM 的外观没有严格的规定。你可以用tagLabel
代替tagName
,或者props
用 代替attrs
。只要它代表了 DOM,它就是“虚拟 DOM”。
虚拟 DOM 不需要任何编程接口。这使得它们比实际的 DOM更轻量。
不过,请记住,由于 DOM 是浏览器的基本元素,大多数浏览器肯定都对其进行了认真的优化。因此,实际的 DOM 可能并不像许多人声称的那样慢。
设置
我们首先创建并进入我们的项目目录。
$ mkdir /tmp/vdommm
$ cd /tmp/vdommm
然后我们将启动 git repo,.gitignore
使用gitignorer创建文件并启动 npm。
$ git init
$ gitignore init node
$ npm init -y
让我们进行初始提交。
$ git add -A
$ git commit -am ':tada: initial commit'
接下来,安装Parcel Bundler,一款真正的零配置打包工具。它开箱即用,支持各种文件格式。它一直是我在现场编程演讲中首选的打包工具。
$ npm install parcel-bundler
(有趣的事实:您不再需要通过--save
了。)
在安装的同时,让我们在项目中创建一些文件。
src/index.html
<html>
<head>
<title>hello world</title>
</head>
<body>
Hello world
<script src="./main.js"></script>
</body>
</html>
src/main.js
const vApp = {
tagName: 'div',
attrs: {
id: 'app',
},
};
console.log(vApp);
包.json
{
...
"scripts": {
"dev": "parcel src/index.html", // add this script
}
...
}
现在您可以通过执行以下操作来生成开发服务器:
$ npm run dev
> vdommm@0.0.1 dev /private/tmp/vdommm
> parcel src/index.html
Server running at http://localhost:1234
Built in 959ms.
访问http://localhost:1234,你应该会在页面上看到 hello world,并在控制台中看到我们定义的虚拟 DOM。如果看到了,就说明设置正确!
createElement(标签名称,选项)
大多数虚拟 DOM 实现都会有一个叫做createElement
function 的函数,通常简称为h
。这些函数只会返回一个“虚拟元素”。所以,让我们来实现它。
src/vdom/createElement.js
export default (tagName, opts) => {
return {
tagName,
attrs: opts.attrs,
children: opts.children,
};
};
利用对象解构,我们可以像这样写上述内容:
src/vdom/createElement.js
export default (tagName, { attrs, children }) => {
return {
tagName,
attrs,
children,
};
};
我们还应该允许创建没有任何选项的元素,因此让我们为选项设置一些默认值。
src/vdom/createElement.js
export default (tagName, { attrs = {}, children = [] } = {}) => {
return {
tagName,
attrs,
children,
};
};
回想一下我们之前创建的虚拟 DOM:
src/main.js
const vApp = {
tagName: 'div',
attrs: {
id: 'app',
},
};
console.log(vApp);
现在可以写成:
src/main.js
import createElement from './vdom/createElement';
const vApp = createElement('div', {
attrs: {
id: 'app',
},
});
console.log(vApp);
回到浏览器,你应该会看到和我们之前定义的相同的虚拟 DOM。让我们在div
giphy 的 sourcing 下添加一张图片:
src/main.js
import createElement from './vdom/createElement';
const vApp = createElement('div', {
attrs: {
id: 'app',
},
children: [
createElement('img', {
attrs: {
src: 'https://media.giphy.com/media/cuPm4p4pClZVC/giphy.gif',
},
}),
],
});
console.log(vApp);
返回浏览器,您应该会看到更新的虚拟 DOM。
谈话中没有提到
对象字面量(例如{ a: 3 }
)自动继承自Object
。这意味着通过对象字面量创建的对象将具有在 、 等中定义Object.prototype
的hasOwnProperty
方法toString
。
我们可以使用 来让我们的虚拟 DOM 更“纯粹”一些Object.create(null)
。这将创建一个真正的普通对象,它不是继承自 ,Object
而是null
继承自 。
src/vdom/createElement.js
export default (tagName, { attrs, children }) => {
const vElem = Object.create(null);
Object.assign(vElem, {
tagName,
attrs,
children,
});
return vElem;
};
渲染(vNode)
渲染虚拟元素
现在我们有了一个可以生成虚拟 DOM 的函数。接下来,我们需要一个方法将虚拟 DOM 转换为真实 DOM。让我们定义一个函数,render (vNode)
该函数接受一个虚拟节点并返回相应的 DOM。
src/vdom/render.js
const render = (vNode) => {
// create the element
// e.g. <div></div>
const $el = document.createElement(vNode.tagName);
// add all attributs as specified in vNode.attrs
// e.g. <div id="app"></div>
for (const [k, v] of Object.entries(vNode.attrs)) {
$el.setAttribute(k, v);
}
// append all children as specified in vNode.children
// e.g. <div id="app"><img></div>
for (const child of vNode.children) {
$el.appendChild(render(child));
}
return $el;
};
export default render;
上面的代码应该很容易理解。如果您有任何需要,我很乐意提供更多解释。
ElementNode 和 TextNode
在真实的 DOM 中,有8 种类型的节点。在本文中,我们只讨论两种类型:
ElementNode
,例如<div>
和<img>
TextNode
、纯文本
我们的虚拟元素结构{ tagName, attrs, children }
仅代表ElementNode
DOM 中的 。因此,我们还需要一些 的表示TextNode
。我们只需使用String
来表示TextNode
。
为了演示这一点,让我们向当前虚拟 DOM 添加一些文本。
src/main.js
import createElement from './vdom/createElement';
const vApp = createElement('div', {
attrs: {
id: 'app',
},
children: [
'Hello world', // represents TextNode
createElement('img', {
attrs: {
src: 'https://media.giphy.com/media/cuPm4p4pClZVC/giphy.gif',
},
}), // represents ElementNode
],
}); // represents ElementNode
console.log(vApp);
扩展渲染以支持 TextNode
正如我提到的,我们正在考虑两种类型的节点。当前的节点render (vNode)
仅渲染ElementNode
。因此,让我们扩展render
它,使其TextNode
也支持 的渲染。
我们首先要根据现有函数的功能重命名renderElem
它。我还将添加对象解构,使代码看起来更美观。
src/vdom/render.js
const renderElem = ({ tagName, attrs, children}) => {
// create the element
// e.g. <div></div>
const $el = document.createElement(tagName);
// add all attributs as specified in vNode.attrs
// e.g. <div id="app"></div>
for (const [k, v] of Object.entries(attrs)) {
$el.setAttribute(k, v);
}
// append all children as specified in vNode.children
// e.g. <div id="app"><img></div>
for (const child of children) {
$el.appendChild(render(child));
}
return $el;
};
export default render;
让我们重新定义render (vNode)
。我们只需要检查 是否vNode
是String
。如果是,那么我们可以使用document.createTextNode(string)
来渲染textNode
。否则,只需调用renderElem(vNode)
。
src/vdom/render.js
const renderElem = ({ tagName, attrs, children}) => {
// create the element
// e.g. <div></div>
const $el = document.createElement(tagName);
// add all attributs as specified in vNode.attrs
// e.g. <div id="app"></div>
for (const [k, v] of Object.entries(attrs)) {
$el.setAttribute(k, v);
}
// append all children as specified in vNode.children
// e.g. <div id="app"><img></div>
for (const child of children) {
$el.appendChild(render(child));
}
return $el;
};
const render = (vNode) => {
if (typeof vNode === 'string') {
return document.createTextNode(vNode);
}
// we assume everything else to be a virtual element
return renderElem(vNode);
};
export default render;
现在我们的render (vNode)
函数能够渲染两种类型的虚拟节点:
createElement
虚拟元素 - 使用我们的函数创建- 虚拟文本 - 用字符串表示
渲染我们的vApp
!
现在让我们尝试渲染我们的vApp
和console.log
它!
src/main.js
import createElement from './vdom/createElement';
import render from './vdom/render';
const vApp = createElement('div', {
attrs: {
id: 'app',
},
children: [
'Hello world',
createElement('img', {
attrs: {
src: 'https://media.giphy.com/media/cuPm4p4pClZVC/giphy.gif',
},
}),
],
});
const $app = render(vApp);
console.log($app);
转到浏览器,您将看到控制台显示以下内容的 DOM:
<div id="app">
Hello world
<img src="https://media.giphy.com/media/cuPm4p4pClZVC/giphy.gif">
</div>
安装 ($node, $target)
现在,我们可以创建虚拟 DOM 并将其渲染为真实 DOM。接下来,我们需要将真实 DOM 放到页面上。
让我们首先为我们的应用程序创建一个挂载点。我将用替换Hello world
上的。src/index.html
<div id="app"></div>
src/index.html
<html>
<head>
<title>hello world</title>
</head>
<body>
<div id="app"></div>
<script src="./main.js"></script>
</body>
</html>
我们现在要做的是div
用我们渲染的替换掉这个空的$app
。如果我们忽略 IE 和 Safari,这真的超级简单。我们直接使用 就可以了ChildNode.replaceWith
。
让我们定义mount ($node, $target)
。此函数将简单地替换$target
为$node
并返回$node
。
src/vdom/mount.js
export default ($node, $target) => {
$target.replaceWith($node);
return $node;
};
现在在我们的main.js中只需将我们的安装$app
到空的 div 上。
src/main.js
import createElement from './vdom/createElement';
import render from './vdom/render';
import mount from './vdom/mount';
const vApp = createElement('div', {
attrs: {
id: 'app',
},
children: [
'Hello world',
createElement('img', {
attrs: {
src: 'https://media.giphy.com/media/cuPm4p4pClZVC/giphy.gif',
},
}),
],
});
const $app = render(vApp);
mount($app, document.getElementById('app'));
我们的应用程序现在将显示在页面上,我们应该在页面上看到一只猫。
让我们的应用程序更有趣
现在让我们让我们的应用程序更有趣一些。我们将我们的代码包装vApp
在一个名为的函数中createVApp
。它将接收一个count
参数,然后它vApp
会使用它。
src/main.js
import createElement from './vdom/createElement';
import render from './vdom/render';
import mount from './vdom/mount';
const createVApp = count => createElement('div', {
attrs: {
id: 'app',
dataCount: count, // we use the count here
},
children: [
'The current count is: ',
String(count), // and here
createElement('img', {
attrs: {
src: 'https://media.giphy.com/media/cuPm4p4pClZVC/giphy.gif',
},
}),
],
});
let count = 0;
const vApp = createVApp(count);
const $app = render(vApp);
mount($app, document.getElementById('app'));
然后,我们将setInterval
每秒增加计数并在页面上再次创建、渲染和安装我们的应用程序。
src/main.js
import createElement from './vdom/createElement';
import render from './vdom/render';
import mount from './vdom/mount';
const createVApp = count => createElement('div', {
attrs: {
id: 'app',
dataCount: count, // we use the count here
},
children: [
'The current count is: ',
String(count), // and here
createElement('img', {
attrs: {
src: 'https://media.giphy.com/media/cuPm4p4pClZVC/giphy.gif',
},
}),
],
});
let count = 0;
const vApp = createVApp(count);
const $app = render(vApp);
let $rootEl = mount($app, document.getElementById('app'));
setInterval(() => {
count++;
$rootEl = mount(render(createVApp(count)), $rootEl);
}, 1000);
注意,我曾经$rootEl
跟踪过根元素。这样它就mount
知道在哪里挂载我们的新应用了。
如果我们现在回到浏览器,我们应该看到计数每秒增加 1,并且运行正常!
现在,我们能够以声明式的方式创建应用程序了。该应用程序的渲染方式可预测,并且非常易于理解。如果您了解 JQuery 的实现方式,您就会明白这种方法是多么简洁明了。
但是,每秒重新渲染整个应用程序存在一些问题:
- 真实 DOM 比虚拟 DOM 重得多。将整个应用程序渲染到真实 DOM 上可能会非常昂贵。
- 元素将丢失其状态。例如,
<input>
每当应用程序重新挂载到页面时,它们将失去焦点。点击此处查看现场演示。
我们将在下一节解决这些问题。
差异(旧VTree,新VTree)
假设我们有一个函数diff (oldVTree, newVTree)
,它计算两个虚拟树之间的差异;返回一个patch
函数,该函数接受的真实 DOMoldVTree
并对真实 DOM 执行适当的操作,使真实 DOM 看起来像newVTree
。
如果我们有这个diff
函数,那么我们可以将间隔重写为:
src/main.js
import createElement from './vdom/createElement';
import render from './vdom/render';
import mount from './vdom/mount';
import diff from './vdom/diff';
const createVApp = count => createElement('div', {
attrs: {
id: 'app',
dataCount: count, // we use the count here
},
children: [
'The current count is: ',
String(count), // and here
createElement('img', {
attrs: {
src: 'https://media.giphy.com/media/cuPm4p4pClZVC/giphy.gif',
},
}),
],
});
let count = 0;
let vApp = createVApp(count);
const $app = render(vApp);
let $rootEl = mount($app, document.getElementById('app'));
setInterval(() => {
count++;
const vNewApp = createVApp(count)
const patch = diff(vApp, vNewApp);
// we might replace the whole $rootEl,
// so we want the patch will return the new $rootEl
$rootEl = patch($rootEl);
vApp = vNewApp;
}, 1000);
那么让我们尝试实现它diff (oldVTree, newVTree)
。让我们从一些简单的案例开始:
newVTree
是undefined
- 我们可以简单地删除
$node
传递到 then 的部分patch
!
- 我们可以简单地删除
- 它们都是 TextNode(字符串)
- 如果它们是相同的字符串,则不执行任何操作。
- 如果不是,请替换
$node
为render(newVTree)
。
- 其中一个树是 TextNode,另一个是 ElementNode
- 那么,它们显然不是同一个东西,那么我们就
$node
用 来代替render(newVTree)
。
- 那么,它们显然不是同一个东西,那么我们就
oldVTree.tagName !== newVTree.tagName
- 我们假设在这种情况下,新树和旧树是完全不同的。
- 我们不会尝试找出两棵树之间的差异,而是将 替换
$node
为render(newVTree)
。 - 这个假设在 React 中也存在。(来源)
- > 两种不同类型的元素将产生不同的树。
src/vdom/diff.js
import render from './render';
const diff = (oldVTree, newVTree) => {
// let's assume oldVTree is not undefined!
if (newVTree === undefined) {
return $node => {
$node.remove();
// the patch should return the new root node.
// since there is none in this case,
// we will just return undefined.
return undefined;
}
}
if (typeof oldVTree === 'string' ||
typeof newVTree === 'string') {
if (oldVTree !== newVTree) {
// could be 2 cases:
// 1. both trees are string and they have different values
// 2. one of the trees is text node and
// the other one is elem node
// Either case, we will just render(newVTree)!
return $node => {
const $newNode = render(newVTree);
$node.replaceWith($newNode);
return $newNode;
};
} else {
// this means that both trees are string
// and they have the same values
return $node => $node;
}
}
if (oldVTree.tagName !== newVTree.tagName) {
// we assume that they are totally different and
// will not attempt to find the differences.
// simply render the newVTree and mount it.
return $node => {
const $newNode = render(newVTree);
$node.replaceWith($newNode);
return $newNode;
};
}
// (A)
};
export default diff;
如果代码达到(A)
,则意味着以下内容:
oldVTree
和newVTree
都是虚拟元素。- 他们有相同的
tagName
。 - 他们可能有不同的
attrs
和children
。
我们将实现两个函数来分别处理属性和子项,即diffAttrs (oldAttrs, newAttrs)
和diffChildren (oldVChildren, newVChildren)
,它们将分别返回一个补丁。由于我们目前知道不会替换,因此在应用两个补丁后,$node
我们可以安全地返回。$node
src/vdom/diff.js
import render from './render';
const diffAttrs = (oldAttrs, newAttrs) => {
return $node => {
return $node;
};
};
const diffChildren = (oldVChildren, newVChildren) => {
return $node => {
return $node;
};
};
const diff = (oldVTree, newVTree) => {
// let's assume oldVTree is not undefined!
if (newVTree === undefined) {
return $node => {
$node.remove();
// the patch should return the new root node.
// since there is none in this case,
// we will just return undefined.
return undefined;
}
}
if (typeof oldVTree === 'string' ||
typeof newVTree === 'string') {
if (oldVTree !== newVTree) {
// could be 2 cases:
// 1. both trees are string and they have different values
// 2. one of the trees is text node and
// the other one is elem node
// Either case, we will just render(newVTree)!
return $node => {
const $newNode = render(newVTree);
$node.replaceWith($newNode);
return $newNode;
};
} else {
// this means that both trees are string
// and they have the same values
return $node => $node;
}
}
if (oldVTree.tagName !== newVTree.tagName) {
// we assume that they are totally different and
// will not attempt to find the differences.
// simply render the newVTree and mount it.
return $node => {
const $newNode = render(newVTree);
$node.replaceWith($newNode);
return $newNode;
};
}
const patchAttrs = diffAttrs(oldVTree.attrs, newVTree.attrs);
const patchChildren = diffChildren(oldVTree.children, newVTree.children);
return $node => {
patchAttrs($node);
patchChildren($node);
return $node;
};
};
export default diff;
diffAttrs(旧Attrs,newAttrs)
我们先来看一下diffAttrs
。其实很简单。我们知道要把所有东西都设置到 中newAttrs
。设置完成后,我们只需要检查 中的所有键oldAttrs
,确保它们也都存在于 中newAttrs
。如果不存在,就删除它们。
const diffAttrs = (oldAttrs, newAttrs) => {
const patches = [];
// setting newAttrs
for (const [k, v] of Object.entries(newAttrs)) {
patches.push($node => {
$node.setAttribute(k, v);
return $node;
});
}
// removing attrs
for (const k in oldAttrs) {
if (!(k in newAttrs)) {
patches.push($node => {
$node.removeAttribute(k);
return $node;
});
}
}
return $node => {
for (const patch of patches) {
patch($node);
}
return $node;
};
};
注意我们如何创建包装器补丁并循环应用patches
它们。
diffChildren(旧VChildren,新VChildren)
儿童的情况会稍微复杂一些。我们可以考虑三种情况:
oldVChildren.length === newVChildren.length
- 我们可以做从
diff(oldVChildren[i], newVChildren[i])
哪里到哪里。i
0
oldVChildren.length - 1
- 我们可以做从
oldVChildren.length > newVChildren.length
- 我们还可以执行从到 的
diff(oldVChildren[i], newVChildren[i])
操作。i
0
oldVChildren.length - 1
newVChildren[j]
将undefined
用于j >= newVChildren.length
- 但这没关系,因为我们
diff
可以处理diff(vNode, undefined)
!
- 我们还可以执行从到 的
oldVChildren.length < newVChildren.length
- 我们还可以执行从到 的
diff(oldVChildren[i], newVChildren[i])
操作。i
0
oldVChildren.length - 1
- 此循环将为每个已经存在的子项创建补丁
- 我们只需要创建剩余的额外子项,即
newVChildren.slice(oldVChildren.length)
。
- 我们还可以执行从到 的
总而言之,oldVChildren
无论如何,我们都会循环并调用diff(oldVChildren[i], newVChildren[i])
。
然后我们将渲染额外的子项(如果有),并将它们附加到$node
。
const diffChildren = (oldVChildren, newVChildren) => {
const childPatches = [];
oldVChildren.forEach((oldVChild, i) => {
childPatches.push(diff(oldVChild, newVChildren[i]));
});
const additionalPatches = [];
for (const additionalVChild of newVChildren.slice(oldVChildren.length)) {
additionalPatches.push($node => {
$node.appendChild(render(newVChildren));
return $node;
});
}
return $parent => {
// since childPatches are expecting the $child, not $parent,
// we cannot just loop through them and call patch($parent)
$parent.childNodes.forEach(($child, i) => {
childPatches[i]($child);
});
for (const patch of additionalPatches) {
patch($parent);
}
return $parent;
};
};
我认为如果我们使用该函数的话会更优雅一些zip
。
import render from './render';
const zip = (xs, ys) => {
const zipped = [];
for (let i = 0; i < Math.min(xs.length, ys.length); i++) {
zipped.push([xs[i], ys[i]]);
}
return zipped;
};
const diffChildren = (oldVChildren, newVChildren) => {
const childPatches = [];
oldVChildren.forEach((oldVChild, i) => {
childPatches.push(diff(oldVChild, newVChildren[i]));
});
const additionalPatches = [];
for (const additionalVChild of newVChildren.slice(oldVChildren.length)) {
additionalPatches.push($node => {
$node.appendChild(render(additionalVChild));
return $node;
});
}
return $parent => {
// since childPatches are expecting the $child, not $parent,
// we cannot just loop through them and call patch($parent)
for (const [patch, $child] of zip(childPatches, $parent.childNodes)) {
patch($child);
}
for (const patch of additionalPatches) {
patch($parent);
}
return $parent;
};
};
完成 diff.js
src/vdom/diff.js
import render from './render';
const zip = (xs, ys) => {
const zipped = [];
for (let i = 0; i < Math.min(xs.length, ys.length); i++) {
zipped.push([xs[i], ys[i]]);
}
return zipped;
};
const diffAttrs = (oldAttrs, newAttrs) => {
const patches = [];
// setting newAttrs
for (const [k, v] of Object.entries(newAttrs)) {
patches.push($node => {
$node.setAttribute(k, v);
return $node;
});
}
// removing attrs
for (const k in oldAttrs) {
if (!(k in newAttrs)) {
patches.push($node => {
$node.removeAttribute(k);
return $node;
});
}
}
return $node => {
for (const patch of patches) {
patch($node);
}
return $node;
};
};
const diffChildren = (oldVChildren, newVChildren) => {
const childPatches = [];
oldVChildren.forEach((oldVChild, i) => {
childPatches.push(diff(oldVChild, newVChildren[i]));
});
const additionalPatches = [];
for (const additionalVChild of newVChildren.slice(oldVChildren.length)) {
additionalPatches.push($node => {
$node.appendChild(render(additionalVChild));
return $node;
});
}
return $parent => {
// since childPatches are expecting the $child, not $parent,
// we cannot just loop through them and call patch($parent)
for (const [patch, $child] of zip(childPatches, $parent.childNodes)) {
patch($child);
}
for (const patch of additionalPatches) {
patch($parent);
}
return $parent;
};
};
const diff = (oldVTree, newVTree) => {
// let's assume oldVTree is not undefined!
if (newVTree === undefined) {
return $node => {
$node.remove();
// the patch should return the new root node.
// since there is none in this case,
// we will just return undefined.
return undefined;
}
}
if (typeof oldVTree === 'string' ||
typeof newVTree === 'string') {
if (oldVTree !== newVTree) {
// could be 2 cases:
// 1. both trees are string and they have different values
// 2. one of the trees is text node and
// the other one is elem node
// Either case, we will just render(newVTree)!
return $node => {
const $newNode = render(newVTree);
$node.replaceWith($newNode);
return $newNode;
};
} else {
// this means that both trees are string
// and they have the same values
return $node => $node;
}
}
if (oldVTree.tagName !== newVTree.tagName) {
// we assume that they are totally different and
// will not attempt to find the differences.
// simply render the newVTree and mount it.
return $node => {
const $newNode = render(newVTree);
$node.replaceWith($newNode);
return $newNode;
};
}
const patchAttrs = diffAttrs(oldVTree.attrs, newVTree.attrs);
const patchChildren = diffChildren(oldVTree.children, newVTree.children);
return $node => {
patchAttrs($node);
patchChildren($node);
return $node;
};
};
export default diff;
使我们的应用程序更加复杂
我们当前的应用并没有充分利用虚拟 DOM 的强大功能。为了展示虚拟 DOM 的强大,让我们将应用变得更复杂一些:
src/main.js
import createElement from './vdom/createElement';
import render from './vdom/render';
import mount from './vdom/mount';
import diff from './vdom/diff';
const createVApp = count => createElement('div', {
attrs: {
id: 'app',
dataCount: count, // we use the count here
},
children: [
'The current count is: ',
String(count), // and here
...Array.from({ length: count }, () => createElement('img', {
attrs: {
src: 'https://media.giphy.com/media/cuPm4p4pClZVC/giphy.gif',
},
})),
],
});
let vApp = createVApp(0);
const $app = render(vApp);
let $rootEl = mount($app, document.getElementById('app'));
setInterval(() => {
const n = Math.floor(Math.random() * 10);
const vNewApp = createVApp(n);
const patch = diff(vApp, vNewApp);
// we might replace the whole $rootEl,
// so we want the patch will return the new $rootEl
$rootEl = patch($rootEl);
vApp = vNewApp;
}, 1000);
我们的应用现在会生成一个n
0 到 9 之间的随机数,并n
在页面上显示猫咪照片。如果你打开开发者工具,你会看到我们是如何<img>
根据 智能地插入和移除的n
。
谢谢
如果你读到这里,非常感谢你花时间读完这篇文章。这篇文章真的很长!如果你真的读完了,请留言。爱你!
文章来源:https://dev.to/ycmjason/building-a-simple-virtual-dom-from-scratch-3d05