第七章 渲染器的设计
在Vue.js框架很多功能依赖渲染器实现,也是框架性能的核心,能够直接影响框架性能。对此,Vue.js3的渲染器通过快捷路径更新的方式,利用编译器提供的信息提升性能。
回顾: 初识渲染器
渲染器就是把虚拟DOM渲染为真实DOM
实现思路:
- 创建元素
- 为元素添加属性和事件
- 处理children
以上是简单的创建节点的过程,但渲染器的精髓在更新节点的阶段。
1. 渲染器与响应系统的结合
渲染器是用来执行渲染任务的,可以在浏览器平台来渲染真实DOM元素,它还能实现框架跨平台(如canvas平台)能力。
利用响应系统,自动调用渲染器完成页面的渲染和更新。副作用函数执行完毕后,会与响应式数据建立响应联系。修改count.value的值时,副作用函数会重新执行,完成重新渲染。所以上面的代码运行完毕后,最终渲染到页面的内容是<h1>2</h1>
从本章开始,将使用@vue/reactivity包提供的响应式API进行讲解。
2. 渲染器的基本概念
- 渲染器(renderer): 渲染器的作用是把虚拟DOM渲染为特定平台的真实元素。在浏览器上,渲染器会把DOM渲染为真实DOM元素。注意与render的区别。
- 虚拟DOM(vdom):虚拟DOM和真实DOM的结构一样,都是由一个个节点组成的树型结构。
- 虚拟节点(vnode):虚拟DOM是树型结构,其中任何一个vnode节点都可以是一棵子树,因此vdom和vnode可以替换使用。本书统一使用vnode
- 挂载(mount):渲染器把虚拟DOM节点渲染成真实DOM节点的过程叫做挂载。
- 容器(container):渲染器通常需要一个挂载点作为参数,用来指定具体的挂载位置。
代码:
createRenderer函数用来创建一个渲染器。调用createRenderer函数会得到一个render函数,该render函数会以container为挂载点,将vnode渲染为真实DOM并添加到该挂载点下。
- 渲染器和渲染:渲染器是更加宽泛的概念,其包含了渲染。渲染器不仅可以用来渲染,还可以用来激活已有的DOM元素。render函数只是其中一部分。
- 挂载、更新(打补丁patch)和卸载(unmount)
挂载: 首先用createRenderer函数创建一个渲染器,然后用渲染器renderer.render函数执行渲染任务。首次调用renderer.render函数时,只需要创建新的DOM元素即可,这个过程只涉及挂载。
更新: 多次在同一个container上调用renderer.render函数进行渲染时,除了要挂载动作外,还要执行更新动作。在这种情况下,就要渲染器就要将newVnode和上一次渲染的oldVnode进行比较,试图找到更新变更点。这个过程叫“打补丁”(或更新)。
实际上,挂载动作本身也可以看成是一种特殊的打补丁,其特殊之处在于旧的vnode是不存在的。
卸载: 第三次渲染是,新vnode 的值为null,即什么都不渲染。渲染器需要清空容器,暂且使用container.innerHTML = '' 来清空容器。
综上:render函数的基本实现。
patch的函数签名:
实际上,patch函数时整个渲染器的核心入口,承载了最重要的渲染逻辑。
3. 自定义渲染器
渲染器不仅能够把虚拟DOM渲染为浏览器平台上的真实DOM,通过将渲染器设计为可配置的“通用”渲染器,即可实现渲染到任意目标平台上。
通过抽象,将浏览器特定的API抽离,这样就可以使得渲染器的核心不依赖于浏览器。在此基础上,我们再为那些被抽离的API提供可配置的接口,即可实现渲染器的跨平台能力。
先从渲染一个普通的<h1>
标签开始,可以用如下vnode对象来描述一个<h1>
标签
上面的vnode对象,使用type属性来描述vnode类型,当type属性时字符串类型值时,可以认为其描述的是普通标签,并使用该type属性的字符串值作为标签的名称。这样我们可以使用render函数来渲染,如下面代码所示:
为了完成渲染工作,需要补充patch函数
n1代表旧vnode,n2代表新vnode。当n1不存在时,意味着没有旧vnode,此时只需要执行挂载即可。这时调用mountElement完成挂载,其实现如下:
上面的代码实现了挂载一个普通标签元素的工作,但是我们的目的是设计一个不依赖于浏览器平台的通用渲染器,而mountElement函数内调用了大量依赖于浏览器的api,所以第一步要做的就是将这些浏览器特有的api抽离。具体可以将这些操作DOM的API作为配置项,该配置项可以作为createRenderer的参数,如下面代码
这里直接把用于操作DOM的API封装为一个对象,并将其传递给createRenderer函数。这样在mountElement等函数内就可以通过配置项来取得操作DOM的API了
接着从配置项取出得到的API重新实现mountElement函数:
如上面代码所示,重构后的mountElement 函数在功能上没有任何变化。不同的是,它不再直接依赖于浏览器特有的API了。这意味着,只要传入不同的配置项,就能够完成非浏览器环境下的渲染工作。
接下来我们可以实现一个用来打印渲染器操作流程的自定义渲染器,如下面代码所示(7.3自定义渲染器.html可演示)
总结:自定义渲染器就是通过抽象的手段,让核心代码不再依赖平台特有的 API,再通过支持个性化配置的能力来实现跨平台。