8. 实现 runtime-dom
8. 实现 runtime-dom
1.渲染器的作用
渲染器的作用是把虚拟 DOM 渲染为特定平台上的真实元素。在浏览器中,渲染器会把虚拟 DOM 渲染成真实 DOM 元素。
import {
createRenderer,
h,
render
} from '/node_modules/@vue/runtime-dom/dist/runtime-dom.esm-browser.js';
const renderer = createRenderer({
createElement(element) {
return document.createElement(element);
},
setElementText(el, text) {
el.innerHTML = text;
},
insert(el, container) {
container.appendChild(el);
}
});
// 自定义渲染器
renderer.render(h('h1', 'hello world'), document.getElementById('app'));
// 内置渲染器
render(h('h1', 'hello world'), document.getElementById('app'));
2.创建 runtime-dom 包
runtime-dom 针对浏览器运行时,包括 DOM API 、属性、事件处理等
runtime-dom/package.json
{
"name": "@vue/runtime-dom",
"main": "index.js",
"module": "dist/runtime-dom.esm-bundler.js",
"unpkg": "dist/runtime-dom.global.js",
"buildOptions": {
"name": "VueRuntimeDOM",
"formats": ["esm-bundler", "cjs", "global"]
}
}
pnpm install @vue/shared@workspace --filter @vue/runtime-dom
3.实现节点常用操作
runtime-dom/src/nodeOps
这里存放常见 DOM 操作 API,不同运行时提供的具体实现不一样,最终将操作方法传递到runtime-core
中,所以runtime-core
不需要关心平台相关代码~
export const nodeOps = {
insert: (child, parent, anchor) => {
// 添加节点
parent.insertBefore(child, anchor || null);
},
remove: (child) => {
// 节点删除
const parent = child.parentNode;
if (parent) {
parent.removeChild(child);
}
},
createElement: (tag) => document.createElement(tag), // 创建节点
createText: (text) => document.createTextNode(text), // 创建文本
setText: (node, text) => (node.nodeValue = text), // 设置文本节点内容
setElementText: (el, text) => (el.textContent = text), // 设置文本元素中的内容
parentNode: (node) => node.parentNode, // 父亲节点
nextSibling: (node) => node.nextSibling, // 下一个节点
querySelector: (selector) => document.querySelector(selector) // 搜索元素
};
4.比对属性方法
export const patchProp = (el, key, prevValue, nextValue) => {
if (key === 'class') {
patchClass(el, nextValue);
} else if (key === 'style') {
patchStyle(el, prevValue, nextValue);
} else if (/^on[^a-z]/.test(key)) {
patchEvent(el, key, nextValue);
} else {
patchAttr(el, key, nextValue);
}
};
操作类名
function patchClass(el, value) {
// 根据最新值设置类名
if (value == null) {
el.removeAttribute('class');
} else {
el.className = value;
}
}
操作样式
function patchStyle(el, prev, next) {
// 更新style
const style = el.style;
for (const key in next) {
// 用最新的直接覆盖
style[key] = next[key];
}
if (prev) {
for (const key in prev) {
// 老的有新的没有删除
if (next[key] == null) {
style[key] = null;
}
}
}
}
操作事件
function createInvoker(initialValue) {
const invoker = (e) => invoker.value(e);
invoker.value = initialValue;
return invoker;
}
function patchEvent(el, rawName, nextValue) {
// 更新事件
const invokers = el._vei || (el._vei = {});
const exisitingInvoker = invokers[rawName]; // 是否缓存过
if (nextValue && exisitingInvoker) {
exisitingInvoker.value = nextValue;
} else {
const name = rawName.slice(2).toLowerCase(); // 转化事件是小写的
if (nextValue) {
// 缓存函数
const invoker = (invokers[rawName] = createInvoker(nextValue));
el.addEventListener(name, invoker);
} else if (exisitingInvoker) {
el.removeEventListener(name, exisitingInvoker);
invokers[rawName] = undefined;
}
}
}
在绑定事件的时候,绑定一个伪造的事件处理函数 invoker,把真正的事件处理函数设置为 invoker.value 属性的值
操作属性
function patchAttr(el, key, value) {
// 更新属性
if (value == null) {
el.removeAttribute(key);
} else {
el.setAttribute(key, value);
}
}
5.创建渲染器
最终我们在
index.js
中引入写好的方法,渲染选项就准备好了。 稍后将虚拟 DOM 转化成真实 DOM 会调用这些方法
import { nodeOps } from './nodeOps';
import { patchProp } from './patchProp';
// 准备好所有渲染时所需要的的属性
const renderOptions = Object.assign({ patchProp }, nodeOps);
export function render(vnode, container) {
return createRenderer(renderOptions).render(vnode, container);
}
createRenderer 接受渲染所需的方法,h 方法为创建虚拟节点的方法。这两个方法和平台无关,所以我们将这两个方法在 runtime-core 中实现。