mini-vue 的设计
mini-vue 使用流程与结果预览:
<!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><button class="btn">执行patch</button><hr></hr><button class="refreshBtn">刷新页面</button></body><script src="./renderer.js"></script><script src="./mount.js"></script><script src="./patch.js"></script><script>/*** 思路:* 1. 通过 h 函数创建 vnode* 2. 通过 mount 函数挂载*/// 1. 生成 vnodeconst vnode = h("div",{ class: "youxiaobei", id: "oldId", del: "这是将被删除的" },[h("ul", null, [h("li", null, "我是一个小li"),h("li", null, "我是一个小li"),h("li", null, "我是一个小li"),h("li", null, "我是一个小li"),]),h("button", null, "这是将被保留的小小按钮"),h("h4", null, "以上内容都会被比较为不同然后删除,包括我")]);// 2. 挂载,生成真实 dom,添加到 container 容器中const container = document.getElementById("app");mount(vnode, container);// 3. 新节点 01 (最外层 tagName 不一样,直接都被替换了)const newVnode = h("h2", { class: "newNode", id: "newId" }, [h("button", null, "我是你后来加的小按钮"),]);const btn = document.querySelector('.btn')btn.addEventListener("click", () => {patch(vnode, newVnode);btn.disabled = true;},true);const refreshBtn = document.querySelector('.refreshBtn')refreshBtn.addEventListener("click", () => {location.reload()})</script><style>/* 新的节点背景色是红色的 */.newNode {background-color: red; }</style>
</html>
执行 patch 前:
执行后:
1. h 函数
h 函数也就是 render 函数,作用简单:返回一个 Vnode 虚拟节点,但很重要!
/*** h 函数* 功能:返回vnode** @param {String} tagName - 标签名* @param {Object | Null} props - 传递过来的参数* @param {Array | String} children - 子节点* @return {vnode} 虚拟节点*/
const h = (tagName, props, children) => {// 直接返回一个对象,里面包含vnode结构return {tagName,props,children,};
};
2. 响应式
考虑以下功能:
- 收集依赖某个数据的函数
- 当数据变化后,重新执行依赖此数据的函数
/*** 实现一个类* 构造一个 订阅列表 subscribers set 对象* 收集者 addEffect 添加影响的函数,往 subscribers 里面添加* 通知者 notifier 通知函数, 依次执行 subscribers 里面的函数*/
class Dep {constructor() {// 1.订阅列表 构造一个 subscribers set 对象this.subscribers = new Set();}// 2. 收集者 addEffect 添加影响的函数addEffect(effect) {this.subscribers.add(effect);}// 3. 通知者 notifiernotifier() {this.subscribers.forEach((effect) => {effect();});}
}const dep = new Dep();let count = 1;const addFun = function () {console.log(++count);
};addFun(); // 2// 收集订阅
dep.addEffect(addFun);// 当数据改变后
count = 100;// 通知者通知函数重执行
dep.notifier(); // 101
3. mount 函数
挂载 Vnode 为真实的 DOM 元素
/*** mount 函数* 功能:挂载 vnode 为 真实dom* 重点:递归调用处理子节点** @param {Object} vnode -虚拟节点* @param {elememt} container -需要被挂载节点*/
const mount = (vnode, container) => {// 1. 创建出真实元素, 给 vnode 添加 el 属性const el = (vnode.el = document.createElement(vnode.tagName));// 2. 处理 propsif (vnode.props) {for (const key in vnode.props) {const value = vnode.props[key];// 2.1 prop 是函数if (key.startsWith("on")) {el.addEventListener(key.slice(2).toLowerCase(), value);} else {// 2.2 prop 是字符串el.setAttribute(key, value);}}}// 3. 处理 childrenif (vnode.children) {// 3.1 如果 children 是字符串,直接设置文本内容if (typeof vnode.children === "string") {el.textContent = vnode.children;}// 3.2 如果 children 是数组,递归挂载每个子节点else {// 先拿到里面的每一个 vnodevnode.children.forEach((item) => {// 再把里面的vnode递归调用mount(item, el);});}}// 4. 挂载container.appendChild(el);
};
04. patch 函数
patch 对比节点数组,优化性能
/*** 节点比较* 调用时机:节点发生变化(数量,内容)* 功能:比较节点数组,尽可能减少 DOM 操作*//*** @param {Vnode} n1 - 旧节点* @param {Vnode} n2 - 新节点*/
const patch = (n1, n2) => {// 节点不相同,卸载旧节点,挂载新节点if (n1.tagName !== n2.tagName) {const parentElementNode = n1.el.parentElement;parentElementNode.removeChild(n1.el);mount(n2, parentElementNode);} else {// 1. 取出 element 并保存到 n2const el = (n2.el = n1.el);// 2. 处理 propsconst oldProps = n1.props || {};const newProps = n2.props || {};for (const key in newProps) {const oldValue = oldProps[key];const newValue = newProps[key];// 2.1 值不同才替换if (oldValue !== newValue) {if (key.startsWith("on")) {el.addEventListener(key.slice(2).toLowerCase(), newValue);} else {// 2.2 prop 是字符串el.setAttribute(key, newValue);}}}// 3. 删除旧的 propsfor (const key in oldProps) {// 如果旧 key 不在新的 props 里if (!(key in newProps)) {const oldValue = oldProps[key];if (key.startsWith("on")) {el.removeEventListener(key.slice(2).toLowerCase(), oldValue);} else {// 2.2 prop 是字符串el.removeAttribute(key, oldValue);}}}// 4. 处理 childrenconst oldChildren = n1.children;const newChildren = n2.children;// children 字符串if (typeof newChildren === "string") {// 4.1 如果新 children 是字符串,直接设置文本内容if (oldChildren !== newChildren) {el.textContent = newChildren;} else {el.innerHTML = newChildren;}} else {// 4.2 如果新 children 是数组,递归挂载每个子节点// 如果旧 children 的是字符串if (typeof oldChildren === "string") {el.innerHTML = "";// 遍历 childrennewChildren.forEach((item) => {mount(item, el);});} else {// 两个都是数组,开始 diff 算法// n1: [a,b,d]// n2: [b,a,c,f]/*** 没有 key*/if (!n1.props.key && !n2.props.key) {// 4.3.1 获取两个 vnode 数组的公共长度,比较相同的const commonLength = Math.min(oldChildren.length, newChildren.length);for (let i = 0; i < commonLength; i++) {patch(oldChildren[i], newChildren[i]);}// 4.3.2 新的长度多于旧的,挂载if (oldChildren.length < newChildren.length) {newChildren.slice(oldChildren.length).forEach((item) => {mount(item, el);});}// 4.3.3 旧的长度多于新的,卸载if (oldChildren.length > newChildren.length) {oldChildren.slice(newChildren.length).forEach((item) => {el.removeChild(item.el);});}} else {/*** 有 key*/// 4.4.1 根据 key 创建一个映射表,方便查找和比较const keyMap = {};oldChildren.forEach((child) => {if (child.props.key) {keyMap[child.props.key] = child;}});// 4.4.2 遍历新的 children 数组newChildren.forEach((newChild, index) => {const oldChild = keyMap[newChild.props.key];if (oldChild) {// 4.4.2.1 如果旧的 children 存在对应的 key,对比并更新子节点patch(oldChild, newChild);oldChildren[index] = oldChild; // 更新旧的 children 数组,方便后续删除处理} else {// 4.4.2.2 如果旧的 children 中没有对应的 key,说明是新增的节点,直接挂载mount(newChild, el, index);}});// 4.4.3 删除旧的 children 中没有对应的 key 的子节点oldChildren.forEach((oldChild) => {if (!oldChildren.find((child) => child.props.key === oldChild.props.key)) {el.removeChild(oldChild.el);}});}}}}
};