1.虚拟DOM的优势
传统开发:
HTML ==> DOM树 ==> 浏览器计算渲染
当框架纷纷引入虚拟DOM对真实DOM进行抽象成VNode(虚拟节点)后
对于其diff和clone等操作会变得非常简单,方便更好的表达操作逻辑
最重要是方便实现跨平台,你可以将VNode节点渲染成任意想要的节点,如渲染在canvas、WebGL、SSR、Native(iOS、Android)上,当然我们也能开发属于自己的渲染器(renderer),在其他的平台上渲染
Vue虚拟DOM的渲染过程 :
模板template ==> 渲染函数render function ==> 虚拟节点VNode ==> 真实元素 ==> 浏览器展示
2.三大核心系统
Vue的源码实际包含三大核心:
- Compiler模块:编译模板系统
- Runtime模块:也可以称之为Renderer模块,真正渲染的模块
- Reactivity模块:响应式系统
编译系统: 模板template ==> 渲染函数render function
渲染系统: 渲染函数render function ==> 虚拟节点VNode ==> 真实元素 ===> 浏览器展示
响应式系统:VNode变更 ==> diff算法 ==> 新的VNode ==> 真实元素 ==> 浏览器展示
3.实现Mini-Vue
我们实现一个简洁版的Mini-Vue框架,该Vue包括三个模块
- 渲染系统模块
- 可响应式系统模块
- 应用程序入口模块
1.渲染系统模块
主要包含三个功能
- h函数,用于返回一个VNode对象
- mount函数,用于将VNode挂载到DOM上
- patch函数,用于对两个VNode进行对比,决定如何处理新的VNode
h函数实现
mount函数
- 根据tag,创建HTML元素,并且存储 到vnode的el中
- 处理props属性:如果以on开头,那么监听事件,普通属性直接通过 setAttribute 添加即可
- 处理子节点,如果是字符串节点,那么直接设置 textContent,如果是数组节点,那么遍历调用 mount 函 数
patch函数
patch函数的实现,分为两种情况
当n1和n2是不同类型的节点:
- 找到n1的el父节点,删除原来的n1节点的el
- 挂载n2节点到n1的el父节点上
n1和n2节点是相同的节点:
- 处理props的情况
- 对比新旧节点props不同后将新的props全部挂载到el上
- 判断旧节点的props是否不需要在新节点上,如果不需要,那么删除对应的属性
- 处理children的情况
- 如果新节点是一个字符串类型,那么直接调用 el.textContent = newChildren
- 如果新节点不同一个字符串类型
- 旧节点是一个字符串类型
- 将el的textContent设置为空字符串
- 旧节点是一个字符串类型,那么直接遍历新节点,挂载到el上
- 旧节点也是一个数组类型
- 取出数组的最小长度
- 遍历所有的节点,新节点和旧节点进行path操作
- 如果新节点的length更长,那么剩余的新节点进行挂载操作
- 如果旧节点的length更长,那么剩余的旧节点进行卸载操作
对tag和props的处理:
对children的处理:
2.可响应式系统模块
依赖收集系统
响应式系统Vue2实现
之前只要当我们调用dep.notify所有的watchEffect里面的回调函数会重新执行
响应式系统Vue3实现
3.应用程序入口模块
createApp用于创建一个app对象
该app对象有一个mount方法,可以将根组件挂载到某一个dom元素上
4.Mini-Vue大功告成
mini-vue/index.js
function createApp(rootComponent) {
return {
mount(selector) {
const container = document.querySelector(selector);
let isMounted = false;
let oldVNode = null;
watchEffect(function () {
if (!isMounted) {
oldVNode = rootComponent.render();
mount(oldVNode, container);
isMounted = true;
} else {
const newVNode = rootComponent.render();
patch(oldVNode, newVNode);
oldVNode = newVNode;
}
});
},
};
}
mini-vue/reactive.js
class Dep {
constructor() {
this.subscribers = new Set();
}
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect);
}
}
notify() {
this.subscribers.forEach((effect) => {
effect();
});
}
}
let activeEffect = null;
function watchEffect(effect) {
activeEffect = effect;
effect();
activeEffect = null;
}
const targetMap = new WeakMap();
function getDep(target, key) {
// 1.根据对象(target)取出对应的Map对象
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 2.取出具体的dep对象
let dep = depsMap.get(key);
if (!dep) {
dep = new Dep();
depsMap.set(key, dep);
}
return dep;
}
// vue3对raw进行数据劫持
function reactive(raw) {
return new Proxy(raw, {
get(target, key) {
const dep = getDep(target, key);
dep.depend();
return target[key];
},
set(target, key, newValue) {
const dep = getDep(target, key);
target[key] = newValue;
dep.notify();
},
});
}
mini-vue/renderer.js
const h = (tag, props, children) => {
// vnode -> javascript对象 {}
return {
tag,
props,
children,
};
};
// vnode -> element
const mount = (vnode, container) => {
// 1.创建出真实的原生, 并且在vnode上保留el
const el = (vnode.el = document.createElement(vnode.tag));
// 2.处理props
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);
}
}
}
// 3.处理children
if (vnode.children) {
if (typeof vnode.children === "string") {
el.textContent = vnode.children;
} else {
vnode.children.forEach((item) => {
mount(item, el);
});
}
}
// 4.将el挂载到container上
container.appendChild(el);
};
// n1旧节点 n2新节点
const patch = (n1, n2) => {
// 如果元素标签不一样,直接删除重建
if (n1.tag !== n2.tag) {
const n1ElParent = n1.el.parentElement;
n1ElParent.removeChild(n1.el);
mount(n2, n1ElParent);
} else {
// 1.取出element对象, 并且在n2中进行保存
const el = (n2.el = n1.el);
// 2.处理props
const oldProps = n1.props || {};
const newProps = n2.props || {};
// 2.1.获取所有的newProps添加到el
for (const key in newProps) {
const oldValue = oldProps[key];
const newValue = newProps[key];
if (newValue !== oldValue) {
if (key.startsWith("on")) {
// 对事件监听的判断
el.addEventListener(key.slice(2).toLowerCase(), newValue);
} else {
el.setAttribute(key, newValue);
}
}
}
// 2.2.删除旧的props
for (const key in oldProps) {
if (key.startsWith("on")) {
// 对事件监听的判断
const value = oldProps[key];
el.removeEventListener(key.slice(2).toLowerCase(), value);
}
if (!(key in newProps)) {
el.removeAttribute(key);
}
}
// 3.处理children
const oldChildren = n1.children || [];
const newChidlren = n2.children || [];
if (typeof newChidlren === "string") {
// 情况一: newChildren本身是一个string
// 边界情况 (edge case)
if (typeof oldChildren === "string") {
if (newChidlren !== oldChildren) {
el.textContent = newChidlren;
}
} else {
el.innerHTML = newChidlren;
}
} else {
// 情况二: newChildren本身是一个数组
if (typeof oldChildren === "string") {
el.innerHTML = "";
newChidlren.forEach((item) => {
mount(item, el);
});
} else {
// oldChildren: [v1, v2, v3, v8, v9]
// newChildren: [v1, v5, v6]
// 1.前面有相同节点的元素进行patch操作
const commonLength = Math.min(oldChildren.length, newChidlren.length);
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChidlren[i]);
}
// 2.newChildren.length > oldChildren.length
if (newChidlren.length > oldChildren.length) {
newChidlren.slice(oldChildren.length).forEach((item) => {
mount(item, el);
});
}
// 3.newChildren.length < oldChildren.length
if (newChidlren.length < oldChildren.length) {
oldChildren.slice(newChidlren.length).forEach((item) => {
el.removeChild(item.el);
});
}
}
}
}
};
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./renderer.js"></script>
<script src="./reactive.js"></script>
<script src="./index.js"></script>
<script>
// 1.创建根组件
const App = {
data: reactive({
counter: 0,
}),
render() {
return h("div", null, [
h("h2", null, `当前计数: ${this.data.counter}`),
h(
"button",
{
onClick: () => {
this.data.counter++;
console.log(this.data.counter);
},
},
"+1"
),
]);
},
};
// 2.挂载根组件
const app = createApp(App);
app.mount("#app");
</script>
</body>
</html>