mini-vue 的设计

mini-vue 的设计

mini-vue 使用流程与结果预览:

html 复制代码
<!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. 生成 vnode
    const 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 虚拟节点,但很重要!

js 复制代码
/**
 * 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. 响应式

考虑以下功能:

  1. 收集依赖某个数据的函数
  2. 当数据变化后,重新执行依赖此数据的函数
js 复制代码
/**
 * 实现一个类
 * 构造一个 订阅列表 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. 通知者 notifier

  notifier() {
    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 元素

js 复制代码
/**
 * 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. 处理 props
  if (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. 处理 children
  if (vnode.children) {
    // 3.1 如果 children 是字符串,直接设置文本内容
    if (typeof vnode.children === "string") {
      el.textContent = vnode.children;
    }
    // 3.2 如果 children 是数组,递归挂载每个子节点
    else {
      // 先拿到里面的每一个 vnode
      vnode.children.forEach((item) => {
        // 再把里面的vnode递归调用
        mount(item, el);
      });
    }
  }
  // 4. 挂载
  container.appendChild(el);
};

04. patch 函数

patch 对比节点数组,优化性能

js 复制代码
/**
 * 节点比较
 * 调用时机:节点发生变化(数量,内容)
 * 功能:比较节点数组,尽可能减少 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 并保存到 n2
    const el = (n2.el = n1.el);

    // 2. 处理 props
    const 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. 删除旧的 props
    for (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. 处理 children
    const 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 = "";
        // 遍历 children
        newChildren.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);
            }
          });
        }
      }
    }
  }
};
相关推荐
桂月二二4 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
CodeClimb5 小时前
【华为OD-E卷 - 第k个排列 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
沈梦研5 小时前
【Vscode】Vscode不能执行vue脚本的原因及解决方法
ide·vue.js·vscode
hunter2062065 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb5 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角5 小时前
CSS 颜色
前端·css
轻口味6 小时前
Vue.js 组件之间的通信模式
vue.js
浪浪山小白兔6 小时前
HTML5 新表单属性详解
前端·html·html5
lee5767 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm
2401_897579657 小时前
AI赋能Flutter开发:ScriptEcho助你高效构建跨端应用
前端·人工智能·flutter