第 7 章 渲染器的设计

本文深入探讨了渲染器在DOM平台上的作用,如何将虚拟DOM转换为真实DOM,并介绍了渲染器与响应式数据的结合。通过示例展示了如何使用自定义渲染器和响应系统实现动态内容的更新。同时,解释了挂载、打补丁和容器等关键概念,以及在多次渲染中如何处理旧DOM和新DOM的差异。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

7.1 渲染器与响应系统的结合

顾名思义,渲染器是用来执行渲染任务的。在浏览器平台上,用它来渲染器中的真实DOM元素。渲染器不仅能够渲染真实DOM元素,还是框架跨平台能力的关键。因此,在设计渲染器的时候一定要考虑好可自定义的能力。

本节,暂定将渲染器限定在DOM平台,下面的函数就是一个合格的渲染器

function renderer(doString, container) {
	container.innerHTML = doString
}

我们可以如下所示使用它:

renderer('<h1>hello</h1>', document,getElementById('app'))

如果页面中存在 id 为 app 的 DOM 元素,那么上面的代码就会将 <h1>hello</h1> 插入到该DOM元素内。

当然,我们不仅可以渲染静态你的字符串,还可以渲染动态拼接的 HTML内容,如下图所示:

let count = 1
renderer(`<h1>${count}</h1>`, document.getElementById('app'))

这样,最终渲染出来的内容将会是 <h1>hello</h1>。注意上面这段代码中的变量 count,如果它是一个响应式数据,会怎么样呢?这让我们联想到副作用函数和响应式数据。利用响应系统,我们可以让整个渲染过程自动化。

const count = ref(1)
effect(() => {
	renderer(`<h1>${count}</h1>`, document.getElementById('app'))
})
count.value++ 

在这段代码中,我们首先定义了一个响应式数据 count,它是一个 ref,然后在副作用函数内调用 renderer 函数执行渲染。副作用函数执行完毕后,会与响应式数据建立响应联系。当我们修改 count.value 的值时,副作用函数会重新执行,完成重新渲染。所以上面的代码运行完毕后,最终渲染到页面的内容是 <h1>2</h1>

这就是响应系统和渲染器之间的关系。我们利用响应系统的能力,自动调用渲染器完成页面的渲染和更新。这个过程与渲染器的具体实现无关没在上面给出的渲染器的视线中,仅仅设置了元素的 innerHTML 内容。

7.2 渲染器的基本概念

通常使用英文 renderer 来表达“渲染器”。千万不要把 renderer 和 render 弄混了,前者代表渲染,而后者时动词,表示“渲染”。渲染器的作用时把虚拟DOM渲染为特定平台的真实元素。在浏览器平台上,渲染器会把虚拟DOM渲染为真实DOM元素。

渲染器把虚拟DOM节点渲染为真实DOM节点的过程叫做挂载,通常用英文 mount 来表达。那么渲染器把真实DOM挂载到哪里?其实渲染器并不知道应该把真实DOM挂载到哪里。因此,渲染器通常需要接收一个挂载点作为参数,用来指定具体的挂载位置。这里的“挂载点”其实就是一个DOM元素,渲染器会把该DOM元素作为容器元素,并把内容渲染到其中,我们通常用英文 container 来表达容器

上文分别阐述了渲染器、虚拟DOM(或虚拟节点)、挂载以及容器等概念。为了便于理解,下面举例说明:

function createRenderer() {
	function render(vnode, container) {
		// ...
	}
	return render
}

如上面代码所示,其中 createRenderer 函数用来创建一个渲染器。调用函数会得到一个 render 函数,该 render 函数会以 container 为挂载点,将 vnode 渲染为真实DOM并添加到该挂载点下。

有了渲染器我们就可以用它来执行渲染任务了,如下面代码所示:

const renderer = createRenderer()
// 首次渲染
renderer.render(vnode, document.querySelector("#app"))

在上面这段代码中,我们首先调用 createRenderer 函数常见一个渲染器,接着调用渲染器的 renderer.render 函数执行渲染。当首次调用 renderer.render 函数时,只需要创建新的DOM元素即可,这个过程只涉及挂载。
而当多次在同一个 container 上调用 renderer.render 函数进行渲染时,渲染器除了要执行挂载动作之外,还要执行更新动作。例如:

const renderer = createRenderer()
// 首次渲染
renderer.render(oldVnode, document.querySelector('#app'))
// 第二次渲染
renderer.render(newVnode, document.querySelector('#app'))

如上面代码所示,由于首次渲染时已经把 oldVnode 渲染到 container 内了,所以当再次调用 renderer.render 函数并尝试渲染 newVnode时,就不能简单地执行挂载动作了。在这种情况下,渲染器会使用 newVnode 与上一次渲染的 oldVnode 进行比较,试图找到并更新变更点。这个过程叫做“打补丁”(或更新),英文通常用 patch 来表达。但实际上,挂载动作本身也可以看作一种特殊的打补丁,它的特殊之处在于旧的 vnode是不存在的。所以我们不必过于纠结“挂载”和“打补丁”这两个概念。代码示例如下:

function createRenderer() {
	function render() {
		if(vnode){
			// 新 vnode 存在,将其与旧 vnode 一起传递给 patch 函数进行打补丁
			patch(container._vnode, vnode, container)
		} else {
			if(container._vnode) {
				// 旧 vnode 存在,且新 vnode 不存在,说明是卸载(unmount)操作
				// 只需要将 container 内的 DOM 清空即可
				container.innerHTML = ''
			}
		}
		// 把 vnode 存储到 container._vnode 下,即后续渲染中的旧 vnode
		container._vnode = vnode
	}
	return {
		render
	}
}

上面这段代码给出了 render 函数的基本实现。我们可以配合下面的代码分析其执行流程。从而更好地理解 render 函数的实现思路。假设我们连续三次调用 renderer.render 函数来执行渲染:

const renderer = createRenderer()

// 首次渲染
renderer.render(vnode1, document.querySelector('#app'))
// 第二次渲染
renderer.render(vnode2, document.querySelector('#app'))
// 第三次渲染
renderer.render(null, document.querySelector('#app'))
  • 在首次渲染时,渲染器会将 vnode1 渲染为真实 DOM。渲染完成后,vnode1会存储到容器元素的 container._vnode属性中,它会在后续渲染中作为旧 vnode 使用。
  • 在第二次渲染时,旧 vnode 存在,此时渲染器会把 vnode2 作为新 vnode,并将新旧 vnode一同传递给 patch 函数进行打补丁。
  • 在第三次渲染时,新 vnode 的值为 null,即什么都渲染。但此时容器中渲染的是 vnode2 所描述的内容,所以渲染器需要清空容器。从上面的代码中可以看出,我们使用 container.innerHTML = ''来清空容器。需要注意的是,这样清空容器是有问题的,不过这里我们暂时使用它来达到目的。

另外在上面给出的代码中,我们注意到 patch 函数的签名,如下:

patch(container._vnode, vnode, container)

我们并没有给出 patch 的具体实现,但从上面的代码中,仍然可以窥探 patch 函数的部分细节,实际上, patch 函数是整个渲染器的核心入口,它承载了最重要的渲染逻辑。但这里要对它做一些初步的解释。patch 函数至少接受三个参数

function patch(n1, n2, container) {
	// ...
}
  • 第一个参数 n1:旧 vnode
  • 第二个参数 n2:新 vnode
  • 第三个参数 container:容器

在首次渲染时,容器元素的container._vnode 属性是不存在的,即 undefined。这意味着,在首次渲染时传递给 patch 函数的第一个参数 n1 也是 undefined。这时,patch 函数会执行挂载动作,它会忽略 n1,并直接将 n2 所描述的内容渲染到容器中。从这一点可以看出,patch 函数不仅可以用来完成打补丁,也可以用来执行挂载。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值