基本概念
了解渲染器所涉及的基本概念,有助于更好的理解框架 API 的设计。
渲染器 & 渲染
通常使用名词 renderer
来表示 "渲染器",使用动词 render
来表示 "渲染"。渲染器 的作用是把虚拟 DOM
渲染 为特定平台上的真实元素,例如,在浏览器平台上,渲染器会把 虚拟 DOM
渲染为 真实 DOM
元素。
虚拟 DOM & 虚拟节点
虚拟 DOM
通常使用英文 virtual DOM
表示,简写为 vdom
。虚拟 DOM
和 真实 DOM
结构是一样的,都是由一个个节点组成的树形结构,而 虚拟节点
使用 virtual node
来表示,简写为 vnode
。虚拟 DOM
是树形结构,其中的任何一个节点 vnode
都可以代表一颗子树,因此 vnode
和 vdom
是可以替换使用的。
挂载
渲染器
把 虚拟 DOM
节点渲染为 真实 DOM
节点的过程叫作 挂载,英文表示为 mount
,例如 在 Vue.js 组件中的 mounted
钩子就会在挂载完成时触发,这就意味着可以在这个钩子中访问到 真实 DOM
元素。
通过一下代码来辅助理解:
function createRenderer(){
fucntion render(vnode, container){
...
}
fucntion hydrate(){
...
}
// 返回渲染函数和 createApp
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
其中 createRenderer
函数用来创建一个渲染器,调用 createRenderer
函数后会得到一个 render
函数,这个 render
函数会以 container
为挂载点,将 vnode
渲染为真实 DOM 并进行挂载。
为什么需要 createRenderer 函数?
渲染器
和 渲染
是不同的,渲染器
是更加宽泛的概念,它包含了 渲染
,渲染器不仅可以用来渲染,还可以用来激活已有的 DOM 元素
,这通常发生在 同构渲染
的情况下。可以看到,当创建渲染器时,渲染器除了包含 render
函数外,还包含了 hydrate
函数,专门用于处理服务端渲染。
// 创建对应平台的渲染器
cosnt renderer = createRenderer()
// 首次渲染,进行挂载
renderer.render(vnode, document.querySelector('#app'))
// 后续渲染,进行更新
renderer.render(newVnode, document.querySelector('#app'))
render 函数的实现思路
为了便于理解,先看下面的 render
部分的伪代码:
function createRenderer() {
function render(vnode, container) {
if (vnode) {
// 新的 vnode 存在,将其与旧的 vnode 一起传递给 patch 函数,进行补丁(挂载 或 更新)
patch(container._vnode, vnode, container)
} else {
if(container._vnode){
// 新的 vnode 不存在,旧的 vnode 存在,说明当前属于 unmount 操作
// 这里简单的通过 container.innerHTML 将 container 的内容清空
container.innerHTML = ''
}
}
// 将新的 vnode 存储到 container._vnode 中,即后续渲染中旧的 vnode
container._vnode = vnode
}
return {
render,
}
}
假设连续使用三次 renderer.render
函数执行渲染,如下:
// 容器元素
const app = document.querySelector("#app");
// 创建渲染器
const renderer = createRenderer();
// 首次渲染
renderer.render(vnode1, app);
// 第二次渲染
renderer.render(vnode2, app);
// 第三次渲染
renderer.render(null, app);
- 首次渲染时,会将
vnode1
渲染为真实 DOM
,渲染完成后,vnode1
会被存储到container._vnode
中,作为后续渲染中的 旧vnode
使用 - 第二次渲染时,旧
vnode
存在,此时会把vnode2
作为 新vndoe
,并将 新旧vnode
传递给patch
函数进行补丁 - 第三次渲染时,新
vnode
节点为null
,即什么都不渲染,但此时容器中渲染的是vnode2
的内容,所以渲染器需要清空容器,当然直接通过innerHTML = ''
清空的方式是有问题的,这里只是用于表示达到清空的目的
上面的三次渲染分别对应着:挂载、更新、卸载 的过程,patch
函数是整个渲染器的核心入口,它包含了重要的渲染逻辑,其中 patch
函数的各个参数:
function patch(n1, n2, container){...}
n1
代表 旧vnode
节点n2
代表 新vnode
节点container
代表真实的容器元素
自定义渲染器
渲染器不仅应该能够把 虚拟 DOM
渲染为浏览器平台上的 真实 DOM
,也应该能实现在渲染到任意平台上,这就意味需要将渲染器中浏览器特定的 API
进行抽象,这样就可以使得渲染器的核心不依赖于浏览器。在此基础上,再为那些抽离 API 提供可配置的接口,既可实现渲染器跨平台的能力。
抽离和平台强相关的 API
首先针对 patch
函数进行一个简单的实现,并且通过 mountElement
完成挂载操作,如下:
// 渲染器
function createRenderer() {
// mountElement
function mountElement(vnode, container) {
// 创建 dom 元素
const el = document.createElement(vnode.type)
// 若子节点是字符串,则代表是文本内容
if (typeof vnode.children === 'string') {
el.textContext = el.children;
}
// 将子元素添加到容器中
container.appendChild(el)
}
// patch
function patch(n1, n2, container) {
if (!n1) {
mountElement(n2, container)
}
}
// 渲染函数
function render(vnode, container) {
if (vnode) {
patch(container._vnode, vnode, container)
} else {
if (container._vnode) {
container.innerHTML = ''
}
}
container._vnode = vnode
}
return {
render,
}
}
通过上述内容,我们的目标是设计一个不依赖于浏览器平台的通用渲染器,但是在 mountElement
函数内调用了大量依赖于浏览器的 API(如:createElement、appendChild、textContext)
,因此第一步就是将这些依赖于浏览器的 API
进行抽离。
可以在创建渲染器时通过传入对应的配置项,如下:
// 在创建 renderer 时传入配置项
const renderer = createRenderer({
// 用于创建元素
createdElement(tag) {
return document.createElement(tag)
},
// 用于设置元素的文本节点
setElementText(el, text) {
el.textContent = text
},
// 用于在给定的 parent 下添加指定元素
insert(el, parent, anchor = null) {
parent.insertBefore(anchor, el)
}
})
于是在渲染器内就可以通过配置项 options
对获取对应操作 DOM 的 API 了:
// 渲染器
function createRenderer(options) {
// 通过配置项获取操作 DOM 的 API
const {
createElement,
setElementText,
insert
} = options;
// mountElement
function mountElement(vnode, container) {
// 创建 dom 元素
const el = createElement(vnode.type)
// 若子节点是字符串,则代表是文本内容
if (typeof vnode.children === 'string') {
setElementText(el.children);
}
// 将子元素添加到容器中
insert(el, container)
}
// patch
function patch(n1, n2, container) {
if (!n1) {
mountElement(n2, container)
}
}
// 渲染函数
function render(vnode, container) {
if (vnode) {
patch(container._vnode, vnode, container)
} else {
if (container._vnode) {
container.innerHTML = ''
}
}
container._vnode = vnode
}
return {
render,
}
}
重构后的代码,已经不再直接依赖于浏览器特有的 API
了,并且通过传入不同的配置项,就能够完成非浏览器环境下的渲染工作。
最后
有了对渲染器最基本的了解,在结合 Vue.js
源码的学习会有更深刻的理解,为什么 vue.js
要如此设计其 API
。