虚拟 DOM 详解
介绍
虚拟 DOM 是 React 首次出现时的主要差异化因素之一。与之前的框架相比,这是一个巨大的优势,而较新的库也开始采用相同的方法(例如 Vue.js)。
尽管这一概念在过去几年中受到了广泛关注,但围绕这一主题仍然存在一些问题。它在幕后是如何工作的?为什么它被认为比直接 DOM 操作更快?它与脏模型检查有何关系?
它试图解决什么问题?
当你处理客户端应用程序时,你很快就会面临一个问题:DOM 操作成本高昂。如果你的应用程序很大或非常动态,操作 DOM 元素所花费的时间会迅速增加,你会遇到性能瓶颈。
这个问题的答案显而易见,除非绝对必要,否则避免操作元素。Angular 所使用的方法称为脏模型检查, Angular可以说是推广 SPA(单页应用程序)概念的框架。
示例模型:
{
subject: 'World'
}
示例模板:
<div>
<div id="header">
<h1>Hello, {{model.subject}}!</h1>
<p>How are you today?</p>
</div>
</div>
通过这种方法,框架可以监视所有模型。如果模型发生变化,它会插入/执行相应的模板,直接操作 DOM。如果模型没有变化,它就不会触及 DOM。
现在,这是一个聪明的解决方案。但是,它仍然存在问题。当模型的更改不一定转化为模板的更改时,或者更糟糕的是,当您的模型和模板非常复杂时,主要问题之一就会变得非常明显。在上面的示例中,该p标签永远不会改变。每次您的模型被视为脏污时,它仍会更新- 您的模板和实际 DOM 之间没有任何东西,因此每次都会修改整个内容。
这个问题的一个简单解决方案是:在模板和 DOM 之间添加一个层!
什么是虚拟 DOM?
基本上,它是为您的页面创建的实际元素的内存表示。
让我们回到之前的 HTML:
<div>
<div id="header">
<h1>Hello, {{state.subject}}!</h1>
<p>How are you today?</p>
</div>
</div>
渲染后,你的虚拟 DOM 可以表示如下:
{
tag: 'div',
children: [
{
tag: 'div',
attributes: {
id: 'header'
},
children: [
{
tag: 'h1',
children: 'Hello, World!'
},
{
tag: 'p',
children: 'How are you today?'
}
]
}
]
}
现在,假设我们的状态发生了变化 - state.subject现在是Mom。新的表示形式将是:
{
tag: 'div',
children: [
{
tag: 'div',
attributes: {
id: 'header'
},
children: [
{
tag: 'h1',
children: 'Hello, Mom!'
},
{
tag: 'p',
children: 'How are you today?'
}
]
}
]
}
现在我们可以比较这两棵树,并确定只有h1发生了变化。然后我们精确地更新该单个元素 - 无需操纵整个元素。
让我们让事情变得更有趣一些——我们将编写我们自己的虚拟 DOM 库的简单实现!
代码
因为我们希望事情尽可能简单,所以我们根本不要担心边缘情况 - 我们将提供足够的功能来抽象我们之前的 Hello World 示例。
基础组件
我们将编写几个基本组件:div、p和h1。为了尽可能保持简单,我们将强制每个节点包含一个id,以便我们稍后可以轻松快速地找到实际的 DOM 元素。
/*
* Helper to create DOM abstraction
*/
const makeComponent = tag => (attributes, children) => {
if (!attributes || !attributes.id) {
throw new Error('Component needs an id');
}
return {
tag,
attributes,
children,
};
};
const div = makeComponent('div');
const p = makeComponent('p');
const h1 = makeComponent('h1');
现在,我们已将函数div、p和h1纳入范围。如果您熟悉函数式编程,您会将其识别为部分应用。如果您不熟悉,您可以将这些函数视为语法糖 - 您不必在每次需要组件时都提供标签参数。
更复杂的组件
现在我们有了一些基本元素,我们可以开始编写更复杂的组件了。这里我们先介绍一下状态的概念。
再次强调,因为我们想保持简单,所以我们不会深入讨论状态管理。我们假设状态在其他地方被跟踪/管理。
/*
* app component - creates a slightly more complex component out of our base elements
*/
const app = state => div({ id: 'main' }, [
div({ id: 'header' }, [
h1({ id: 'title' }, `Hello, ${state.subject}!`)
]),
div({ id: 'content' }, [
p({ id: 'static1' }, 'This is a static component'),
p({ id: 'static2' }, 'It should never have to be re-created'),
]),
]);
如您所见,我们刚刚表示了与之前的 HTML 模板类似的内容 - 但这次是用 JavaScript 表示的。这是 JSX 背后的基本本质。在 HTML 式语法之下,它最终被转换为 JavaScript 函数调用 - 这与我们此处的简单实现并没有太大区别。
简而言之,该“组件”是一个简单的函数,它接受一个状态(类似于我们前面提到的模型)并返回一个虚拟 DOM 树。假设我们的状态如下所示:
{
subject: 'World'
}
那么我们的 DOM 树应该是这样的:
{
"tag": "div",
"attributes": {
"id": "main"
},
"children": [
{
"tag": "div",
"attributes": {
"id": "header"
},
"children": [
{
"tag": "h1",
"attributes": {
"id": "title"
},
"children": "Hello, World!"
}
]
},
{
"tag": "div",
"attributes": {
"id": "content"
},
"children": [
{
"tag": "p",
"attributes": {
"id": "static1"
},
"children": "This is a static component"
},
{
"tag": "p",
"attributes": {
"id": "static2"
},
"children": "It should never have to be re-created"
}
]
}
]
}
渲染虚拟 DOM
你不会想到我们会就此止步吧?
再次强调,为了与本指南的主题保持一致,我们不要构建任何太复杂的东西。我们只会编写足够的代码来覆盖我们的简单应用程序。
代码如下:
/*
* Sets element attributes
* element: a DOM element
* attributes: object in the format { attributeName: attributeValue }
*/
const setAttributes = (element, attributes) => {
return Object
.keys(attributes)
.forEach(a => element.setAttribute(a, attributes[a]));
};
/*
* Renders a virtual DOM node (and its children)
*/
const renderNode = ({ tag, children = '', attributes = {} }) => {
// Let's start by creating the actual DOM element and setting attributes
const el = document.createElement(tag);
setAttributes(el, attributes);
if ((typeof children) === 'string') {
// If our "children" property is a string, just set the innerHTML in our element
el.innerHTML = children;
} else {
// If it's not a string, then we're dealing with an array. Render each child and then run the `appendChild` command from this element
children.map(renderNode).forEach(el.appendChild.bind(el));
}
// We finally have the node and its children - return it
return el;
};
正如您所见,这并不是超级复杂,也没有涵盖很多边缘情况 - 但对我们来说已经足够了。
我们现在可以通过运行以下脚本来查看它的实际运行(假设我们的 HTML 包含一个 id 为#root的元素):
const virtualDOMTree = app({ subject: 'World' });
const rootEl = document.querySelector('#root');
rootEl.appendChild(renderNode(virtualDOMTree));
处理变更
到目前为止,我们已经创建了一个 DOM 抽象层 - 现在让我们来处理我们的diff。
第一步是获取两个节点并检查它们是否不同。我们使用以下代码:
/*
* Runs a shallow comparison between 2 objects
*/
const areObjectsDifferent = (a, b) => {
// Set of all unique keys (quick and dirty way of doing it)
const allKeys = Array.from(new Set([...Object.keys(a), ...Object.keys(b)]));
// Return true if one or more elements are different
return allKeys.some(k => a[k] !== b[k]);
};
/*
* Diff 2 nodes
* Returns true if different, false if equal
*/
const areNodesDifferent = (a, b) => {
// If at least one of the nodes doesn't exist, we'll consider them different.
// A
免责声明:本内容来源于第三方作者授权、网友推荐或互联网整理,旨在为广大用户提供学习与参考之用。所有文本和图片版权归原创网站或作者本人所有,其观点并不代表本站立场。如有任何版权侵犯或转载不当之情况,请与我们取得联系,我们将尽快进行相关处理与修改。感谢您的理解与支持!
请先 登录后发表评论 ~