彻底澄清“Virtual DOM 飞快”的神话。
留神:原文发表于 2018-12-27,随着框架一直演进,局部内容可能已不实用。
近年来,如果你有应用过 JavaScript 框架,那么你可能据说过“Virtual DOM 飞快”,甚至认为比实在的 DOM 还要快。
令人震惊的是,这种说法居然深入人心。
有人曾问我 Svelte 不应用 Virtual DOM,它为何更快?看来当初是时候认真探讨一下。
什么是 Virtual DOM?
在泛滥框架中,你通常是应用 render()
函数来构建应用程序 UI 的,就像下方这个简略的 React
组件:
function HelloMessage(props) {
return (
<div className="greeting">
Hello {props.name}
</div>
);
}
不必 JSX,你一样能够做同样的事件……
function HelloMessage(props) {
return React.createElement(
'div',
{className: 'greeting'},
'Hello',
props.name
);
}
……后者正是前者的宗本道源,其后果天然统一:代表的是页面渲染的对象。
这个对象就是 Virtual DOM。
一旦程序更新了状态(例如 name
属性被批改),便会创立新的对象。
框架要做的工作是比照新旧对象之间的差别,找出须要进行从新渲染的局部,并将其利用到实在的 DOM 中。
这种观点是如何开始的?
对于 Virtual DOM 性能的误会,能够追溯到 React 正式公布那会。
在 2013 年,React 前团队核心成员 Pete Hunt 在《从新思考最佳实际》的演讲中提到:
这的确是快如闪电,次要是因为大多数 DOM 操作慢如蜗牛,DOM 有很多性能上的开销,大多数 DOM 操作往往会掉链子。
截图来自 JSConf EU 2013《从新思考最佳实际》
然而 —— 慢着!
Virtual DOM 只是实在 DOM 操作精益求精的补充而已。
它之所以快,是因为拿性能更差的框架做比照(在 2013 年,能够欺侮的抉择有很多!),另一种抉择是做一些别人不屑去做的事件:
onEveryStateChange(() => {document.body.innerHTML = renderMyApp();
});
Pete 很快就廓清……
React 不是魔法。
就像你能够应用 C 进入汇编程序并击溃 C 编译器一样,你能够进入原生 DOM 操作和 DOM API 调用,并在机会来长期击溃 React。
然而,应用 C、Java 或者 JavaScript,能够将性能晋升一个数量级,你不用放心……平台的细节。
应用 React,你能够构建应用程序时无需顾及性能问题,它自身就很快。
…… 但这还是没有挠到痒处。
那么……Virtual DOM 慢吗?
并不尽然。
如果可能防患于未然,那的确“Virtual DOM 飞快”。
React 最后的承诺是,你能够在每次状态扭转时,主动从新渲染你的整个利用,且不必放心性能。
不敢苟同。
果真如此,那就不须要像 shouldComponentUpdate
这样的优化了(这是一个用于通知 React 何时能够平安地跳过一个组件的办法)。
就算用了 shouldComponentUpdate
,一次性更新整个利用的 Virtual DOM 也大费周折。
前不久 React 团队引入一种叫 React Fiber 的货色,它能够将更新划分成较小的块。
这意味着(除了其余事项外)更新不会长时间阻塞主线程,只管它不会缩小工作总量或总体耗时。
开销从何而来?
不言而喻,DOM 差别比拟(diffing)并非毫无代价。
这必须先将新的 Virtual DOM 与旧的差别(快照)进行比拟,而后能力对实在 DOM 利用更改。
就拿后面的 HelloMessage
为例,假如 name
属性从“world”更改为“everybody”:
- 两个快照都蕴含一个元素,在这种状况下,它都是
<div>
,这意味着咱们能够放弃雷同的 DOM 节点。 - 咱们枚举
<div>
旧的和新的所有属性,以查看是否须要更改、增加或者删除任何属性。在这两种状况下,咱们都有一个个性,就是它的值为“greeting
”的类名
。 - 扫描元素外部,咱们看到文本曾经更改了,因而咱们须要更新理论的 DOM。
在上述三步里,只有第 3 步在该示例中有价值,因为程序的根本构造是没有扭转的,这其实在绝大多数的更新中都是如此。
如果咱们间接跳到第 3 步,效率就高得多了:
if (changed.name) {text.data = name;}
(这简直就是 Svelte 生成的更新代码了。与传统的 UI 框架有所不同,Svelte 是一个编译器,它能够在构建时便知悉程序中可能产生的变动,而非运行时。)
不止差别比拟一个方面
React 和其余 Virtual DOM 框架应用的 diffing 算法速度都很快。
换而言之,组件自身的开销更大。
例如你不太可能会写出这样的代码……
function StrawManComponent(props) {const value = expensivelyCalculateValue(props.foo);
return (<p>the value is {value}</p>
);
}
如果这么干,无论 props.foo
是否曾经更改,你可能会粗心地在每次更新时不小心从新计算了 value
。
不过,对于进行不必要的计算和调配,更广泛的是上面这种形式:
function MoreRealisticComponent(props) {const [selected, setSelected] = useState(null);
return (
<div>
<p>Selected {selected ? selected.name : 'nothing'}</p>
<ul>
{props.items.map(item =>
<li>
<button onClick={() => setSelected(item)}>
{item.name}
</button>
</li>
)}
</ul>
</div>
);
}
在这里,咱们每次状态更改时,都要生成一个新的虚构的 <li>
元素数组,每个元素都有本人内联的事件处理程序,而不管 props.items
是否产生了变动。
除非你对性能有所不满,否则就不会对其进行优化。这么干毫无意义,它足够快了。
然而你想晓得怎么会更快吗?那是在浪费时间。
其实,默认就做一些不必要的计算(即便是微不足道的),其危险之处在于你的利用最终会温水煮青蛙般死掉,因为没有显著的瓶颈值得去优化。
React Hooks 会使状况变本加厉,后果可想而知。
Svelte 专门设计用来避免你陷入这种窘境。
那些框架为何还要用 Virtual DOM?
要害是要了解:Virtual DOM 不是一种个性,而是一种伎俩。
它要达到的目标是反对申明式的、状态驱动的 UI 开发。
Virtual DOM 很有价值,因为它容许你在构建应用程序时无需思考状态转换,而且性能通常_曾经足够好了_。
这意味着更少的错误代码,更多的工夫花在创造性的工作上,而不是在单调乏味的中央折腾。
但事实证明,咱们能够在不应用 Virtual DOM 的状况下实现相似的编程模型 —— 这正是 Svelte 的用武之地。
< The End >
– 窗明几净,静候时日变迁 –