Vue 渲染器实现原理
Vue 组件渲染管线
Vue 组件渲染分为以下几个主要过程:
编译:Vue 模板被编译为渲染函数:即用来返回虚拟 DOM 树的函数。这一步骤可以通过构建步骤提前完成,也可以通过使用运行时编译器即时完成。
挂载:运行时渲染器调用渲染函数,遍历返回的虚拟 DOM 树,并基于它创建实际的 DOM 节点。这一步会作为响应式副作用执行,因此它会追踪其中所用到的所有响应式依赖。
更新:当一个依赖发生变化后,副作用会重新运行,这时候会创建一个更新后的虚拟 DOM 树。运行时渲染器遍历这棵新树,将它与旧树进行比较,然后将必要的更新应用到真实 DOM 上去。
最简模型
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue Renderer</title>
<style>
.button {
display: block;
width: 90%;
height: 30px;
margin: 0 auto;
background-color: red;
color: #fff;
border: none;
cursor: pointer;
}
</style>
</head>
<body>
<div id="app"></div>
<script src="https://unpkg.com/@vue/reactivity@3.3.4/dist/reactivity.global.js"></script>
<script>
const Text = Symbol.for('Text');
const Comment = Symbol.for('Comment');
/**
* 渲染器实现
*/
function createRenderer({ createElement, setElementText, insert, createText, setText, patchProp }) {
// 挂载元素
function mountElement(vnode, container) {
// 创建真实 DOM 元素
const el = createElement(vnode.type);
// 保存真实 DOM 元素引用
vnode.el = el;
// 处理子节点
if (typeof vnode.children === 'string') {
setElementText(el, vnode.children);
} else if (Array.isArray(vnode.children)) {
vnode.children.forEach(child => {
mountElement(child, el);
});
}
// 处理属性
Object.keys(vnode.props || {}).forEach(key => {
patchProp(el, key, null, vnode.props[key]);
});
// 挂载
insert(el, container);
}
// 更新元素
function patchElement(n1, n2) {
console.log(n1, n2);
const el = n2.el = n1.el;
// 更新属性
const oldProps = n1.props;
const newProps = n2.props;
Object.keys(newProps).forEach(key => {
if (newProps[key] !== oldProps[key]) {
patchProp(el, key, oldProps[key], newProps[key]);
}
});
Object.keys(oldProps).forEach(key => {
if (!(key in newProps)) {
patchProp(el, key, oldProps[key], null);
}
})
// 更新子节点
patchChildren(n1, n2, el);
}
// 更新子节点(没有子节点、文本子节点、一组子节点)
function patchChildren(n1, n2, el) {
// 判断新子节点是否是文本节点
if (typeof n2.children === 'string') {
if (Array.isArray(n1.children)) {
n1.children.forEach((child) => {
unmount(child);
});
}
setElementText(el, n2.children);
} else if (Array.isArray(n2.children)) {
// 新子节点是一组子节点,旧子节点也是一组子节点,则需要 Diff 新旧子节点
if (Array.isArray(n1.children)) {
// @TODO vnode diff
} else {
// 旧子节点是文本节点或者不存在,则清空容器,直接挂载新节点
setElementText(container, '');
n2.children.forEach((child) => {
mountElement(child, container);
});
}
} else {
if (Array.isArray(n1.children)) {
n1.children.forEach((child) => {
unmount(child);
});
} else {
setElementText(container, '');
}
}
}
// 挂载或更新
function patch(n1, n2, container) {
// 如果 n1 存在,则对比 n1 和 n2 的类型;如果类型不一致,则卸载 n1
if (n1 && n1.type !== n2.type) {
unmount(n1);
n1 = null;
}
const { type } = n2;
if (typeof type === 'string') {
// 标签元素节点
if (!n1) {
mountElement(n2, container);
} else {
patchElement(n1, n2);
}
} else if (type === Text) {
// 文本节点
if (!n1) {
const el = n2.el = createText(n2.children);
insert(el, container);
} else {
const el = n2.el = n1.el;
if (n1.children !== n2.children) {
setText(n2.children);
}
}
} else if (type === Comment) {
// 注释节点
if (!n1) {
const el = n2.el = createComment(n2.children);
insert(el, container);
} else {
if (n1.children !== n2.children) {
setText(n2.children);
}
}
}
}
// 卸载
function unmount(vnode) {
const parent = vnode.el.parentNode;
if (parent) {
parent.removeChild(vnode.el);
}
}
// 渲染
function render(vnode, container) {
if (vnode) {
// vnode 挂载或打补丁
patch(container._vnode, vnode, container)
} else {
// 卸载 vnode
if (container._vnode) {
unmount(container._vnode)
}
}
// 保存本次渲染的 vnode
container._vnode = vnode;
}
return {
render
}
}
/**
* 测试代码
*/
const { render } = createRenderer({
createElement(tag) {
console.log('createElement: ', tag);
return document.createElement(tag);
},
setElementText(el, text) {
console.log('setElementText: ', el, text);
el.textContent = text;
},
insert(el, parent) {
console.log('insert: ', el, parent);
parent.appendChild(el);
},
createText(text) {
console.log('createText: ', text);
return document.createTextNode(text);
},
setText(el, text) {
console.log('setText: ', el, text);
el.nodeValue = text;
},
createComment(text) {
console.log('createComment: ', text);
return document.createComment(text);
},
patchProp(el, key, prevValue, nextValue) {
console.log('patchProp: ', el, key, prevValue, nextValue);
if (/^on/.test(key)) {
const invokers = el._invokers || (el._invokers = {});
let invoker = invokers[key];
const name = key.slice(2).toLowerCase();
if (nextValue) {
if (!invoker) {
invoker = (e) => {
if (e.timeStamp < invoker.attached) {
return;
}
if (Array.isArray(invoker.value)) {
invoker.value.forEach(fn => fn(e));
} else {
invoker.value(e);
}
};
invoker.value = nextValue;
invoker.attached = performance.now();
el.addEventListener(name, invoker);
el._invokers[key] = invoker;
} else {
invoker.value = nextValue;
}
} else if (invoker) {
el.removeEventListener(name, invoker);
}
} else if (key === 'class') {
el.className = nextValue || '';
} else if (key in el) {
// disabled、form 等属性值需要特殊处理
el[key] = nextValue;
} else {
el.setAttribute(key, nextValue);
}
}
});
const { effect, ref } = VueReactivity;
const enable = ref(false);
effect(() => {
const vnode = {
type: 'div',
props: enable.value ? {
onClick: () => {
console.log('div click event');
}
} : {},
children: [
{
type: 'button',
props: {
type: 'button',
class: 'button',
onClick: () => {
enable.value = true;
console.log('button click event');
},
onMouseover: () => {
console.log('button hover event');
}
},
children: '点击一下'
}
]
};
const container = document.getElementById('app');
render(vnode, container);
});
</script>
</body>
</html>