Vue3源码学习
真实的DOM渲染
我们传统的前端开发中,我们是编写自己的HTML,最终被渲染到浏览器上的,那么它是什么样的过程呢?
虚拟DOM的优势
目前框架都会引入虚拟DOM来对真实的DOM进行抽象,这样做有很多的好处:
首先是可以对真实的元素节点进行抽象,抽象成VNode(虚拟节点),这样方便后续对其进行各种操作:
- p因为对于直接操作DOM来说是有很多的限制的,比如diff、clone等等,但是使用JavaScript编程语言来操作这 些,就变得非常的简单;
- 我们可以使用JavaScript来表达非常多的逻辑,而对于DOM本身来说是非常不方便的;
其次是方便实现跨平台,包括你可以将VNode节点渲染成任意你想要的节点
- 如渲染在canvas、WebGL、SSR、Native(iOS、Android)上;
- 并且Vue允许你开发属于自己的渲染器(renderer),在其他的平台上渲染;
虚拟DOM的渲染过程
三大核心系统
事实上Vue的源码包含三大核心:
- Compiler模块:编译模板系统;
- Runtime模块:也可以称之为Renderer模块,真正渲染的模块;
- Reactivity模块:响应式系统;
三大系统协同工作
三个系统之间如何协同工作呢:
实现Mini-Vue
这里我们实现一个简洁版的Mini-Vue框架,该Vue包括三个模块:
- 渲染系统模块;
- 可响应式系统模块;
- 应用程序入口模块;
渲染系统实现
渲染系统,该模块主要包含三个功能:
- 功能一:h函数,用于返回一个VNode对象;
- 功能二:mount函数,用于将VNode挂载到DOM上;
- 功能三:patch函数,用于对两个VNode进行对比,决定如何处理新的VNode;
h函数 – 生成VNode
h函数的实现:直接返回一个VNode对象即可
const h = (tag, props, children) => {
return {
tag,
props,
children,
}
}
Mount函数 – 挂载VNode
mount函数的实现:
const mount = (vnode, container) => {
const el = vnode.el = document.createElement(vnode.tag);
if (vnode.props) {
for (const key in vnode.props) {
const value = vnode.props[key];
if (key.startsWith("on")) {
el.addEventListener(key.slice(2).toLowerCase(), value);
} else {
el.setAttribute(key, value);
}
}
}
if (vnode.children) {
if (typeof vnode.children === "string") {
el.innerHTML = vnode.children;
} else if (vnode.children instanceof Array) {
vnode.children.forEach(childVnode => {
mount(childVnode, el);
})
}
}
if (!container) {
throw new Error('container is must be exist');
}
else if (typeof container === "string") {
document.querySelector(container).appendChild(el);
} else {
container.appendChild(el);
}
}
Patch函数 – 对比两个VNode
patch函数的实现,分为两种情况:
-
n1和n2是不同类型的节点:
- 找到n1的el父节点,删除原来的n1节点的el;
- 挂载n2节点到n1的el父节点上;
-
n1和n2节点是相同的节点:
- 处理props的情况
- 先将新节点的props全部挂载到el上;
- 判断旧节点的props是否不需要在新节点上,如果不需要,那么删除对应的属性;
- 处理children的情况
- 如果新节点是一个字符串类型,那么直接调用 el.textContent = newChildren;
- 如果新节点不同一个字符串类型:
- 旧节点是一个字符串类型
- 将el的textContent设置为空字符串;
- 就节点是一个字符串类型,那么直接遍历新节点,挂载到el上;
- 旧节点也是一个数组类型
- 取出数组的最小长度;
- 遍历所有的节点,新节点和旧节点进行path操作;
- 如果新节点的length更长,那么剩余的新节点进行挂载操作;
- 如果旧节点的length更长,那么剩余的旧节点进行卸载操作;
const patch = (n1, n2) => {
if (n1.tag !== n2.tag) {
const parent = n1.el.parentElement;
parent.removeChild(n1.el);
mount(n2, parent);
} else {
const el = n2.el = n1.el;
const oldProps = n1.props || {};
const newProps = n2.props || {};
for (const key in newProps) {
const oldValue = oldProps[key];
const newValue = newProps[key];
if (oldValue !== newValue) {
if (key.startsWith("on")) {
el.addEventListener(key.slice(2).toLowerCase(), newValue);
} else {
el.setAttribute(key, newValue);
}
}
}
for (const key in oldProps) {
if (!(key in newProps)) {
if (key.startsWith("on")) {
el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key]);
} else {
el.removeAttribute(key);
}
}
}
const oldChildren = n1.children || [];
const newChildren = n2.children || [];
if (typeof newChildren === "string") {
if (typeof oldChildren === "string") {
if (newChildren !== oldChildren)
el.textContent = newChildren;
} else {
el.innerHTML = newChildren;
}
}
else {
if (typeof oldChildren === "string") {
el.innerHTML = "";
newChildren.forEach(child => {
mount(child, el);
})
} else {
const commonLength = Math.min(oldChildren.length, newChildren.length);
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChildren[i]);
}
if (oldChildren.length > commonLength) {
oldChildren.slice(commonLength).forEach(child => {
el.removeChild(child.el);
})
} else if (newChildren > commonLength) {
newChildren.slice(commonLength).forEach(child => {
mount(child, el);
})
}
}
}
}
}
测试
代码如下:
<!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>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./renderer.js"></script>
<script>
const vnode = h("div", { class: 'mao', name: '111' }, [
h("h2", {}, "你好!!!!"),
h("h2", null, "你好!当前计数:100"),
h("button", { onClick: function () { } }, "+1")
]);
mount(vnode, "#app")
const vnode2 = h("div", { class: 'codermao', data: "mao" }, [
h("h2", {}, "你好!!!!"),
h("h2", { style: "color:red" }, "哈哈哈")
]);
setTimeout(() => { patch(vnode, vnode2); }, 3000);
</script>
</body>
</html>
const h = (tag, props, children) => {
return {
tag,
props,
children,
}
}
const mount = (vnode, container) => {
const el = vnode.el = document.createElement(vnode.tag);
if (vnode.props) {
for (const key in vnode.props) {
const value = vnode.props[key];
if (key.startsWith("on")) {
el.addEventListener(key.slice(2).toLowerCase(), value);
} else {
el.setAttribute(key, value);
}
}
}
if (vnode.children) {
if (typeof vnode.children === "string") {
el.innerHTML = vnode.children;
} else if (vnode.children instanceof Array) {
vnode.children.forEach(childVnode => {
mount(childVnode, el);
})
}
}
if (!container) {
throw new Error('container is must be exist');
}
else if (typeof container === "string") {
document.querySelector(container).appendChild(el);
} else {
container.appendChild(el);
}
}
const patch = (n1, n2) => {
if (n1.tag !== n2.tag) {
const parent = n1.el.parentElement;
parent.removeChild(n1.el);
mount(n2, parent);
} else {
const el = n2.el = n1.el;
const oldProps = n1.props || {};
const newProps = n2.props || {};
for (const key in newProps) {
const oldValue = oldProps[key];
const newValue = newProps[key];
if (oldValue !== newValue) {
if (key.startsWith("on")) {
el.addEventListener(key.slice(2).toLowerCase(), newValue);
} else {
el.setAttribute(key, newValue);
}
}
}
for (const key in oldProps) {
if (!(key in newProps)) {
if (key.startsWith("on")) {
el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key]);
} else {
el.removeAttribute(key);
}
}
}
const oldChildren = n1.children || [];
const newChildren = n2.children || [];
if (typeof newChildren === "string") {
if (typeof oldChildren === "string") {
if (newChildren !== oldChildren)
el.textContent = newChildren;
} else {
el.innerHTML = newChildren;
}
}
else {
if (typeof oldChildren === "string") {
el.innerHTML = "";
newChildren.forEach(child => {
mount(child, el);
})
} else {
const commonLength = Math.min(oldChildren.length, newChildren.length);
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChildren[i]);
}
if (oldChildren.length > commonLength) {
oldChildren.slice(commonLength).forEach(child => {
el.removeChild(child.el);
})
} else if (newChildren > commonLength) {
newChildren.slice(commonLength).forEach(child => {
mount(child, el);
})
}
}
}
}
}
效果
|