Vue.js 源码(5)—— 虚拟 DOM

499 阅读4分钟

这是我参与更文挑战的第5天,活动详情查看: 更文挑战

前言

虚拟 DOM 现在大部分同学都已经很熟悉了,所以本文只是作为笔记记录下。

什么是虚拟 DOM

刚学前端的时候,还不知道虚拟 DOM。虽说 React 在2013年就开源了,但我 2015 年刚踏足前端领域,使用的还是 jQuery 来更改视图。当然当时也有一些比较厉害的框架,如热门的 Backbone 之类的 MVC 框架。直到 2016 年下半年,才开始接触 Vue.js。因为有 Vue.js 的开发使用经验,17年对着文档很快就入门了 React。

早期使用 jQuery 来更改视图,属于命令式操作 DOM。现在,主流的三大框架 Vue.js、Angular 和 React 都是声明式操作 DOM。我们通过描述状态和 DOM 之间的映射关系是怎样的,就可以将状态渲染成视图。我们通过更新 ViewModel 就能实现更新 View 视图。

任何应用都有状态,并不是使用了框架才有状态。程序中使用的变量都是状态。

通常程序在运行时,状态会不断发生变化。引起状态变化的原因有很多,有可能是用户点击了某个按钮,也可能是某个 Ajax 请求,这些行为都是异步发生的。每当状态发生变化时,都需要重新渲染。那么,究竟是什么状态发生了变化,我们又需要去更新哪些 Dom?

比较粗暴的方式是,直接使用最新的状态,生成新的 DOM,把旧的整个替换掉。可想而知,这样做的代价是比较大的,那有没有更好的方式呢?

当然是有的。

目前,主流的框架都自己的一套解决方案。之前我们也讲过,在 Angular 中是脏检查,在 React 中是虚拟 DOM,在 Vue.js 1.0 中是细粒度的依赖绑定。所以,虚拟 DOM 并不是一颗银弹。

虚拟 DOM 的解决方式是通过状态生成一个棵虚拟节点树,使用它来渲染视图。

为什么要引入虚拟 DOM

Vue.js 在1.0时,就能实现 MVVM 模式,为什么又要在 2.0 中引入虚拟 DOM呢?

我们不能知其然而不知其所以然。这个问题就像 “为什么 React 要引入 hooks?” 一样。

Vue.js 1.0中,当状态发生变化时,它能知道是哪个 dom 使用了这个状态,直接就可以更新这些 dom。但是这样做的代价就是,每个使用到状态的节点,都需要实例化一个 watcher,以便状态发生变化时,直接更新相应的节点。当应用程序比较复杂时,这种方式对内存的消耗就会很大。

所以,Vue.js 在 2.0 引入了虚拟 Dom,将 Watcher 放到组件上。无论组件内有多少个节点使用了某个状态,都只有一个 watcher 在观察着这个状态的变化。

Vue.js 中的虚拟 DOM

我们可以使用 template,也可以使用 render。前者,会在编译阶段编译成渲染函数(render),执行渲染函数可以得到一个虚拟节点树,然后就能渲染视图了。

graph LR
    t[模板]:::font --> r[渲染函数]:::font
    subgraph 虚拟 DOM
        v[vnode]:::font --> view[视图]:::font
    end
    r --> v
    
    classDef font  fill:#007fff,color: #fff

模板转成视图的过程

我们可以使用虚拟节点树来渲染视图。当状态发生变化时,产生一个新的虚拟节点树,这时我们该怎么更新视图?

直接使用新的虚拟节点树来生成新的视图自然是可以的,但不是最佳方式。Vue.js 使用 patch 来比较新旧两个vnode,在 diff 的过程中,直接更新相应的 DOM。

graph LR
    v[vnode]
    subgraph Patch
        nv[vnode] --diff--> ov[oldVnode] --> nv
    end
    v --> nv
    
    ov --更新--> view[视图]

总结

  1. 虚拟 DOM 是将状态映射成视图的众多解决方案中的一种
  2. vue.js 2.0 引入了虚拟 DOM,将粒度由细粒度调整为中粒度,减少了一定的内存开销。变化侦测不再是细化到某个节点,而是提高到组件级别,大大减少了 watcher 的数量。
  3. Vue.js 中虚拟 DOM所做的事是,对比新旧 vnode,然后操作 dom 来更新视图。