likes
comments
collection
share

又被“面试官”问到什么是虚拟Dom,你要如何应对?

作者站长头像
站长
· 阅读数 4

又被“面试官”问到什么是虚拟Dom,你要如何应对?

大家好,我是梦兽编程。欢迎回来与梦兽编程一起刷《那些被问烂的前端面试题》的系列。

如果你喜欢看梦兽编程的[面试官]集合可以订阅《那些被问烂的前端面试题》,梦兽编程也期待大家关注我的个人网站。

加入梦兽编程微信群,公众号回复111即可加入交流群与梦兽进行交流。

什么是虚拟Dom

互联网市场行情好的时候相信大家找工作,有些企业会问你什么是虚拟Dom。其实这个问题很多当时面试官也没怎么去了解。

那时候中小型企业问这个问题,或许面试官都不知道为什么会快。主要只是想看看你有没有去了解新鲜事物,大多数都是踩着热点重构或者实战跳槽。

只要你回答Vnode数组对象维护的结构树和跨平台的等等优点就可以了。这也是非常正常的毕竟熟悉浏览器渲染机制的程序员已经是上古年代了。追溯到jquery那一批程序员才知道其中的奥秘。

React使用VNode,而不使用HTMLNode的还有另一个原因,主要是因为React使用虚拟dom拥有更好的跨平台功能。在React Native、React Server、浏览器客户端分别适配不同平台Runtime代码就可以一套代码走天下。

如果直接操作HTMLNode,那跨平台估计具备点挑战。当然也有一些比较新的框架SolidJS是直接在编译的过程把每个HTMLNode都记录起来,达到减少获取Dom元素的性能开销。但是如果页面元素SolidJS元素过多估计也快不了多少一直更新操作一个数组导致SolidJS一直操作元素,也会存在问题。

function ListComponent() {
  const items = useState([]);

  return (
    <ul>
      {items.map((item) => (
        // 每次渲染都会创建一个新的li元素
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

SolidJs不使用Vnode导致它在跨平台的能力也非常的弱哦,想想Tarojs早期在编译时进行适配每个小程序端的场景,问题可是多多的。

但是怎么说呢(实际情况下又有多少项目是真正"大"?)。但目前看了Vnode是性能与维护之间减少浏览器重绘最优解,达到了一个微妙的零界点。

Vnode是如何提升性能

上面提及到Vnode其实是使用了一课数组嵌套关系的对象树进行管理你的页面。

<ul>
  <li>name1</li>
  <li>name2</li>
</ul>

// 对应 
[
	{
		type: 'ui',
		children: [
			{
				type: 'li'
				children: []
			}
		]
	}
]

梦兽编程都Linux和Rust比较熟一点,首先谷歌浏览器其实是一个C++的应用。如果赶兴趣可以去看看它的代码github.com/chromium/ch…。当然你也可以类似Tauri基于webkit2gtk的扩展包自己写渲染层也不是不可以,其实最终还是会调用谷歌的系统lib 扩展 ==libwebkit2gtk-4.0-dev==。

重绘

当DOM元素的样式发生变化,但不影响其布局时,浏览器需要重新绘制这些元素。这个过程称为重绘。虽然重绘本身可能很快,但如果频繁发生,尤其是在复杂的应用中,它会导致性能下降。

回流

当DOM元素的布局发生变化时(例如,元素的尺寸、位置或内容发生变化),浏览器需要重新计算所有受影响元素的布局。这个过程称为回流。回流是一个计算密集型的操作,因为它涉及到元素的尺寸、位置和父子关系的计算。频繁的回流会导致应用响应速度变慢。

上古前端是如何解决这个问题?重渲染的由来

我们来看看这一段代码

// 这是我们的渲染函数,每次都会重新创建整个列表
function renderTodos() {
  const listElement = document.getElementById('todo-list');
  listElement.innerHTML = ''; // 清空当前列表
  todos.forEach(todo => {
    const listItem = document.createElement('li');
    listItem.textContent = todo.text;
    // ...其他DOM操作,比如添加勾选按钮等
    listElement.appendChild(listItem);
  });
}

上面提到问题的关键是HTMLNode的属性和布局发生变化的时候都会导致浏览器重新计算重新渲染,在数量大时候会导致cpu密集型计算。问题的关键在HTMLNode,我们只要把问题的根源处理掉不就解决问题了吗?所以我们直接把root元素的innerHTML直接清空,然后在进行插入不就完美解决重绘与回流的问题了吗?

var _d1 = new Date();
for (var index = 0; index < 10000; index++) {
  var button = document.createElement('button');
  button.innerText = '按钮' + index;
  document.getElementById('box').appendChild(button);
}
var _d2 = new Date();
console.log(_d2 - _d1); //40ms

// 优化后的代码,使用字符串拼接避免重绘和回流。这也是早期使用模版引擎的理由
var _dom = '';
for (var index = 0; index < 10000; index++) {
  _dom += '<button>按钮' + index + '</button>';
}
document.getElementById('box').innerHTML = _dom; 
console.log(_d2 - _d1); //16ms

如果我们直接操作Dom浏览器都经历什么?

在介绍之前,我们都已经达成一个共识,js运行肯定会比c++运行慢。由来这个共识之后,我们在看看下面的代码:

const ele =  document.getElementById('todo-list');
ele.style.backgroundColor = 'red';
ele.style.style.width = '500px';

JavaScript引擎首先解析这段代码,当JavaScript代码请求修改DOM元素时,JavaScript引擎会通过浏览器C++层提供的API与DOM进行交互。这可能包括创建、修改或删除DOM节点。

浏览器C++层包含了样式计算引擎,它会根据CSS规则计算元素的最终样式。在这段代码中,当ele元素的样式被修改时,样式计算引擎会被调用来重新计算该元素的样式。

然后C++这边需要对Gui进行重绘,才能把内容给我们展示出来。

直接操作Dom元素的性能来源于Js和C++之间的通讯存在一点的损耗,频繁操作量大成山。另外js查找Dom元素的性能也比C++慢许多

Vnode 转换与Diff算法也是存在消耗

我们需要那一个Vnode转换成HTMLNode这个过程,其实也是非常耗性能的。加上每次更新的时候还需要对props状态进行对比才能进行渲染。为什么还有这么多人会说Vnode比HTMLNode快呢?主要的原因还是来源这些消耗都没有重绘和回流带来的耗时上。

当然我只是说你写的页面元素足够多情况下,如果元素比较少其实还不如直接操作Dom元素性能高,但是维护性要自己把控哦。

Diff进一步减少你更新Dom带来的重绘和回流的性能开销。只要你回答上这一点几乎就已经可以换一个问题了不会继续问,在问就是问引擎了...

总结

Vnode其实就类似js与dom直接的一层缓存,优化了以下内容:

  1. 减少使用JS获取Dom元素的操作,也可以理解减少与C++程序之间的通讯操作吧。
  2. 复杂Dom操作场景下减少的浏览器重绘与回流带来的耗时。
  3. 简化了重绘与回流的统一处理,并不是所有前端都知道重绘与回流。我敢保证肯定很多前端都没听说过重绘和回流
  4. 开发者可以专注于应用逻辑,而不必担心底层的DOM操作细节。

如果你的开发的页面需要经常更新网页上的内容,那么Vnode是一个不错的选择。如果不是那么直接操作HTMLNode会更快哦。