7. 渲染器的设计
7.1 渲染器与响应系统的结合
渲染器不仅能够渲染真实 DOM 元素,它还是框架跨平台能力的关键
js
// 最简单的渲染器
function renderer(domString, container) {
container.innerHTML = domString;
}
js
// 结合副作用函数和响应式数据
const count = ref(1);
effect(() => {
renderer(`<h1>${count.value}</h1>`, document.getElementById("app"));
});
count.value++;
配合 @vue/reactivity
包使用渲染器
html
<script src="https://unpkg.com/@vue/reactivity@3.0.5/dist/reactivity.global.js"></script>
js
const { effect, ref } = VueReactivity;
function renderer(domString, container) {
container.innerHTML = domString;
}
const count = ref(1);
effect(() => {
renderer(`<h1>${count.value}</h1>`, document.getElementById("app"));
});
count.value++;
7.2 渲染器的基本概念
- 通常使用英文
renderer
来表达“渲染器“,而render
表示“渲染“,在浏览器平台上,渲染器会把虚拟 DOM 渲染为真实 DOM 元素。 - 把虚拟 DOM 节点渲染为真实 DOM 节点的过程叫作挂载,通常用英文
mount
来表达 - 渲染器通常需要接收一个挂载点(容器)作为参数,用来指定具体的挂载位置,通常用英文
container
来表达容器 - 多次在同一个
container
上调用renderer.render
函数进行渲染时,渲染器除了要执行挂载动作外,还要执行更新动作
js
function createRenderer() {
function render(vnode, container) {
// ...
}
// 渲染器与渲染是不同的。渲染器是更加宽泛的概念,它包含渲染。渲染器不仅可以用来渲染,
//还可以用来激活已有的 DOM 元素。这个过程通常发生在同构渲染的情况下
function hydrate(vnode, container) {
// ...
}
return {
render,
hydrate,
};
}
const renderer = createRenderer();
// 首次渲染
renderer.render(oldVNode, document.querySelector("#app"));
// 第二次渲染
renderer.render(newVNode, document.querySelector("#app"));
- 挂载动作本身也可以看作一种特殊的打补丁,它的特殊之处在于旧的 vnode 是不存在的
js
function createRenderer() {
function render(vnode, container) {
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,
};
}
7.3 自定义渲染器
js
function createRenderer() {
function mountElement(vnode, container) {
// 创建 DOM 元素
const el = document.createElement(vnode.type);
// 处理子节点,如果子节点是字符串,代表元素具有文本节点
if (typeof vnode.children === "string") {
// 因此只需要设置元素的 textContent 属性即可
el.textContent = vnode.children;
}
// 将元素添加到容器中
container.appendChild(el);
}
function patch(n1, n2, container) {
// 在这里编写渲染逻辑
// 如果 n1 不存在,意味着挂载,则调用 mountElement 函数完成挂载
if (!n1) {
mountElement(n2, container);
} else {
// n1 存在,意味着打补丁,暂时省略
}
}
function render(vnode, container) {
if (vnode) {
patch(container._vnode, vnode, container);
} else {
if (container._vnode) {
container.innerHTML = "";
}
}
container._vnode = vnode;
}
return {
render,
};
}
const vnode = {
type: "h1",
children: "hello",
};
// 创建一个渲染器
const renderer = createRenderer();
// 调用 render 函数渲染该 vnode
renderer.render(vnode, document.querySelector("#app"));
为了实现一个跨平台的通用渲染器,还需要将操作 DOM 的 API 作为配置项
js
function createRenderer(options) {
// 通过 options 得到操作 DOM 的 API
const { createElement, insert, setElementText } = options;
// 在这个作用域内定义的函数都可以访问那些 API
function mountElement(vnode, container) {
// ...
// 调用 createElement 函数创建元素
const el = createElement(vnode.type);
if (typeof vnode.children === "string") {
// 调用 setElementText 设置元素的文本节点
setElementText(el, vnode.children);
}
// 调用 insert 函数将元素插入到容器内
insert(el, container);
}
function patch(n1, n2, container) {
// ...
}
function render(vnode, container) {
// ...
}
return {
render,
};
}
// 在创建 renderer 时传入配置项
const renderer = createRenderer({
// 用于创建元素
createElement(tag) {
return document.createElement(tag);
},
// 用于设置元素的文本节点
setElementText(el, text) {
el.textContent = text;
},
// 用于在给定的 parent 下添加指定元素
insert(el, parent, anchor = null) {
parent.insertBefore(el, anchor);
},
});