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);
            }
          });
        }
      }
    }
  }
};
相关推荐
迷雾漫步者26 分钟前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-1 小时前
验证码机制
前端·后端
燃先生._.2 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖3 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235243 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240254 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar4 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人4 小时前
前端知识补充—CSS
前端·css
GISer_Jing5 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试