实现自定义渲染函数 createRenderer
,这是为了能兼容 canvas 等的 API 的使用。
官方给的说法是:
创建一个自定义渲染器。通过提供平台特定的节点创建以及更改 API,你可以在非 DOM 环境中也享受到 Vue 核心运行时的特性。
回顾上几节笔记,关于 DOM 渲染的功能都放到了 renderer.ts
这个模块中。
DOM 渲染到页面,经过的步骤有:
- 创建新标签
- 为标签注册事件、添加属性
- 孩子节点若为文本,直接设置标签内容为该文本,否则遍历
children
递归渲染 DOM - 把新创建的标签添加到页面
以上除了递归 Array 类型的 children vnode 外,其余的都是操作 DOM。
我们在 DOM 环境下有一套渲染标签的 API,而在非 DOM 环境下也有不同的 API,那为了使得可以兼容不同平台的渲染,可以把原本写死的 DOM API 逻辑抽离出来,内部抛出对应的 API 让用户去自定义实现页面的渲染。
createRenderer
的基本实现思路:
-
把原本写在
renderer.ts
的方法,用一个函数包裹着,该函数为createRenderer
-
options
作为参数,用户自定义渲染的 API 作为 options 的一员,传入给createRenderer
实现 -
内部抽离自定义渲染的逻辑,改为使用
options
的 API 抽象实现 -
重构 createApp.ts, 原本的
createApp
修改为闭包,即作为新的函数createAppAPI
的返回值函数。把render
函数作为createAppAPI
参数。renderer.ts 模块中,createRenderer
抛出createApp
成员。ts// runtime-core/createApp.ts import { createVNode } from './vnode'; export function createAppAPI(render) { return function createApp(rootComponent) { return { mount(rootContainer) { const vnode = createVNode(rootComponent); render(vnode, rootContainer); } } } }
ts// renderer.ts import { createAppAPI } from './createApp.ts'; export function createRenderer(options) { function render() { ... } return { createApp: createAppAPI(render) } }
options
API:
createElement
- 创建新标签patchProp
- 添加新标签insert
- 移除标签setElementText
- 设置文本
ts
// runtime-core/renderer.ts
import { createAppAPI } from './createApp';
export function createRender(options) {
const {
createElement: hostCreateElement, // 创建新标签 API
patchProp: hostPatchProp, // 处理属性 API
insert: hostInsert, // 添加新标签 API
remove: hostRemove, // 移除标签 API
setElementText: hostSetElementText, // 设置文本 API
} = options;
function render(vnode, container) {
...
}
function patch(vnode, container, parentComponent) {
...
}
function processElement(vnode, container, parentComponent) {
mountElement(vnode, container, parentComponent);
}
// 重构,把原本的 DOM 操作使用 API 来替代
function mountElement(vnode, container, parentComponent) {
// 1. 创建标签
const el = (vnode.el = hostCreateElement(vnode.type));
// 2. 处理 标签内容
const { children, shapeFlag } = vnode;
// children 是字符串
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// el.textContent = children;
hostSetElementText(ell, children);
// children 是数组
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
...
}
const { props } = vnode;
// 3. 处理标签的 props 属性
// 循环 props
for (const key in props) {
const val = props[key];
// 处理注册事件和 props
hostPatchProp(el, key, null, val);
}
// 4. 把 el 添加到 container
hostInsert(el, container);
}
function mountChildren(children, container, parentComponent) {
...
}
function processComponent(vnode, container, parentComponent) {
...
}
function mountComponent(initialVNode, container, parentComponent) {
...
}
function processFragment(vnode container, parentComponent) {
...
}
function mountFragment(vnode, container, parentComponent) {
...
}
function processText(vnode, container) {
...
}
function setupRenderEffect(instance, initialVNode, container) {
...
}
return {
createApp: createAppAPI(render),
}
}
ts
// runtime-core/index.ts
export { createRenderer } from './renderer';
vue 内部还是需要实现 DOM 环境的渲染的,所以新建 runtime-dom/index.ts
,把刚才的具体实现移到这个模块来。
这时我们就可以引入 createRenderer
来实现一下了。
ts
// runtime-dom/index.ts
import { createRenderer } from '../runtime-core';
function createElement(type) {
return document.createElement(type);
}
function patchProp(el, key, prevVal, nextVal) {
const isOn = (key: string) => /^on[A-Z]/.test(key);
if (isOn(key)) {
const event = key.slice(2).toLowerCase();
el.addEventListener(event, nextVal);
} else {
if(nextVal === undefined || nextVal === null) {
el.removeAttribute(key);
} else {
el.setAttribute(key, nextVal);
}
}
}
function insert(child, parent) {
parent.append(el);
}
function remove(child) {
const parent = child.parentNode;
if(parent) {
parent.removeChild(child);
}
}
function setElementText(el, text) {
el.textContent = text;
}
// 使用刚才写好的 createRender API
const renderer: any = createRenderer({
createElement,
patchProp,
insert,
remove,
setElementText,
});
// 在这里抛出 createApp 给外部使用
export function createApp(...args) {
return renderer.createApp(...args)
}
export * from '../runtime-core';
为什么在最后写 export * from '../runtime-core';
呢?
这是遵循了 vue3 的模块依赖关系有序性。
因为 runtime-dom
依赖于 runtime-core
,所以最终在末尾添加导出 runtime-core
的声明。
这样,我们在 src/index.ts
这个总入口,导出:
ts
export * from './runtime-dom';
这样,在打包后,便向用户抛出了所有在 runtime-core/index
和 runtime-dom/index
中导出的成员。