跳至主要內容

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 中实现。