基于 vue3源码 尝试 mini-vue 的实现

news2024/12/23 11:45:29

基于 vue3源码 尝试 mini-vue 的实现

预览:
gif01

1. 实现思路

  • 渲染系统模块
  • 响应式系统
  • mini-vue 程序入口

2. 渲染系统模块

2.1 初识 h 函数

以下是 vue 的模版语法代码:

<template>
	<div class='container'>hello mini-vue</div>
</template>

它并不是传统的 html 代码,而是通过 h 函数生成的虚拟 dom 节点,h 函数接收 3 个参数:

  • 第一个参数是标签名 (tag),此例中为 ‘div’
  • 第二个参数是标签的属性 (props), 此例中为 ‘container’
  • 第三个参数是子节点,可以是字符串、数组 (children), 此例中为 ‘hello mini-vue’
/**
 * 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,
    };
};

export default h;

2.2 创建一个 vnode

vnode 就是虚拟 dom 节点,创建方法很简单:

h(
    'div', { class: 'container' }, [
        h('h1', {}, `文本:${this.data.msg},可变数字:${this.data.count}`),
        h('button', {
            onclick: () =>{
                this.data.msg = 'hello miniVue',
                    this.data.count++
            }
        }, '点击试试'),
    ]
)

2.3 挂载真实 DOM

mount函数的主要功能是将虚拟节点(vnode)挂载到真实的DOM容器(container)中:

  1. 创建真实元素:

    • 使用 document.createElement 创建一个具有指定标签名的真实元素。
    • 将创建的元素保存在 vnode.el 属性中,以便后续的操作。
  2. 处理属性(props):

    • 遍历虚拟节点的 props 属性,分别处理函数类型的事件监听器和其他类型的属性。
    • 如果属性名以 “on” 开头,将其作为事件处理函数添加到元素上。
    • 否则,使用 setAttribute 方法设置元素的属性。
  3. 处理子节点(children):

    • 如果虚拟节点有子节点,分两种情况处理:
      • 如果子节点是字符串,直接设置元素的文本内容。
      • 如果子节点是数组,递归调用 mount 函数挂载每个子节点。
  4. 挂载到容器中:

    • 最后,使用 container.appendChild 将创建的真实元素挂载到指定的DOM容器中。

这个mount函数的重点在于递归调用,通过递归处理子节点,实现了对整个虚拟DOM树的挂载。

/**
 * mount 函数
 * 功能:挂载 vnode 为 真实dom
 * 重点:递归调用处理子节点
 *
 * @param {Object} vnode -虚拟节点
 * @param {elememt} container -需要被挂载节点
 */
const mount = (vnode, container) => {
  console.log(vnode);
  // 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 {
      console.log(vnode.children);
      // 先拿到里面的每一个 vnode
      vnode.children.forEach((item) => {
        // 再把里面的vnode递归调用
        mount(item, el);
      });
    }
  }
  // 4. 挂载
  container.appendChild(el);
};

export default mount;

3. 响应式系统

3.1 依赖收集与 proxy 劫持

主要包含两个功能:

  1. 观察者函数 (watchEffect):

    • 通过 createEffect 函数创建观察者函数,用于定义在数据变化时执行的逻辑。
    • 设置一个全局变量 activeEffect 作为当前活跃的观察者,以便在属性访问时收集依赖。
  2. 响应式对象创建函数 (reactive):

    • 使用 Proxy 对象对原始数据对象进行代理,以便捕获对对象属性的访问和修改。
    • 当访问对象属性时,通过 getDependencies 函数收集依赖关系,将当前观察者添加到依赖中。
    • 当设置对象属性时,通过 set 方法触发依赖更新,通知所有依赖的观察者执行。
/**
 * 此段响应式代码主要完成两个功能:
 * 1. 创建观察者函数
 * 2. 响应式的对象创建函数
 */

// 定义一个依赖管理类
class DependencyManager {
  constructor() {
    // 使用 Set 存储订阅者(观察者)
    this.subscribers = new Set();
  }

  // 添加订阅者
  addSubscriber(subscriber) {
    if (activeEffect) {
      this.subscribers.add(subscriber);
    }
  }

  // 通知所有订阅者执行
  notifySubscribers() {
    this.subscribers.forEach((subscriber) => subscriber());
  }
}

// 定义一个全局变量,表示当前活跃的观察者
let activeEffect = null;

// 定义一个函数,用于创建并执行观察者
function createEffect(effect) {
  // 将当前观察者设置为全局活跃观察者
  activeEffect = effect;

  // 执行观察者函数
  effect();

  // 执行完后将全局活跃观察者重置为 null
  activeEffect = null;
}

// 使用 WeakMap 存储目标对象与其对应的依赖映射关系
const targetDependenciesMap = new WeakMap();

// 获取指定目标对象和键值的依赖对象
function getDependencies(target, key) {
  let dependenciesMap = targetDependenciesMap.get(target);
  // 如果目标对象还没有对应的依赖映射关系,则创建一个
  if (!dependenciesMap) {
    dependenciesMap = new Map();
    targetDependenciesMap.set(target, dependenciesMap);
  }
  let dependency = dependenciesMap.get(key);
  // 如果键值还没有对应的依赖对象,则创建一个
  if (!dependency) {
    dependency = new DependencyManager();
    dependenciesMap.set(key, dependency);
  }
  return dependency;
}

// 创建响应式对象
function createReactiveObject(raw) {
  return new Proxy(raw, {
    // 当访问对象的属性时,收集依赖
    get(target, key) {
      const dependency = getDependencies(target, key);
      dependency.addSubscriber(activeEffect);
      return target[key];
    },
    // 当设置对象的属性时,触发依赖更新
    set(target, key, newValue) {
      const dependency = getDependencies(target, key);
      target[key] = newValue;
      dependency.notifySubscribers();
      return true;
    },
  });
}

// 导出观察者函数和响应式对象创建函数
export { createEffect as watchEffect, createReactiveObject as reactive };

3.2 diff 算法

patch函数的核心思想是通过比较两个虚拟节点(Vnode)来更新实际的DOM,以减少不必要的DOM操作,提高性能。它首先比较节点的标签名,如果不同则直接替换整个节点;然后比较节点的属性和事件,更新发生变化的部分;最后比较子节点,支持使用key进行优化,减少删除和添加的操作。这种差异比较的方式避免了不必要的DOM重新渲染,以更有效地实现视图的更新。这符合虚拟DOM的设计理念,通过最小化实际DOM的操作来提高性能。

/**
 * 节点比较
 * 调用时机:节点发生变化(数量,内容)
 * 功能:比较节点数组,尽可能减少 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) {
      if (key.startsWith("on")) {
        console.log(oldProps);
        const value = oldProps[key];
        console.log(key);
        el.removeEventListener(key.slice(2).toLowerCase(), value);
      }
      // 如果旧 key 不在新的 props 里
      if (!(key in newProps)) {
        el.removeAttribute(key);
      }
    }

    // 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" || typeof oldChildren === "number") {
        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);
            }
          });
        }
      }
    }
  }
};

export default patch;

4. 程序的入口

import h from "./renderer.js";
import mount from "./mount.js";
import patch from "./patch.js";
import { reactive, watchEffect } from "./reactive.js";

const createApp = (rootComponent) => {
  return {
    mount: (selector) => {
      let container = document.getElementById(selector);
      let isMounted = false;
      let oldVNode = null;
      watchEffect(function () {
        if (!isMounted) {
          console.log(rootComponent.renderTemplate());
          oldVNode = rootComponent.renderTemplate();
          mount(oldVNode, container);
          isMounted = true;
        } else {
          const newVNode = rootComponent.renderTemplate();
          patch(oldVNode, newVNode);
          oldVNode = newVNode;
        }
      });
    },
  };
};

export  {
  createApp,
  mount,
  h,
  reactive,
};

5. 测试运行

<!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>
</body>
<script type="module">
  import {
    reactive, h, createApp,
  } from './index.js'

  // 1.将被解析的 vue 组件
  let miniVueComponent = {
    data: reactive({
      msg: 'hello world',
      count: 1,
    }),
    renderTemplate:
      function () {
        return h(
          'div', { class: 'container' }, [
          h('h1', {}, `文本:${this.data.msg},可变数字:${this.data.count}`),
          h('button', {
            onclick: () =>{
            this.data.msg = 'hello miniVue',
            this.data.count++
          }
          }, '点击试试'),
        ]
        )
      }
    ,
    method: {
      sayHello: () => {
        if (window) {
          alert(`i say ${msg}`)
        }
      }
    }
  }

  // 2. 挂载,生成真实 dom,添加到 container 容器中
  createApp(miniVueComponent).mount('app');

</script>

</html>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1209286.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【STM32】FreeModbus 移植Modbus-RTU从机协议到STM32详细过程

背景 FreeModbus是一款开源的Modbus协议栈&#xff0c;但是只有从机开源&#xff0c;主机源码是需要收费的。 第一步&#xff1a;下载源码 打开.embedded-experts的官网&#xff0c;连接如下&#xff1a; https://www.embedded-experts.at/en/freemodbus-downloads/ 其中给出…

【PTQ】Cross-Layer Equalization跨层均衡-证明和实践详细解读

Cross-Layer Equalization跨层均衡 aimet解读 符合规则的模型结构 统一要求&#xff1a;单数据流&#xff0c;中间激活不流向其他地方概念说明&#xff1a; Conv: gruoups1的普通卷积&#xff0c;包括TransposedConv和ConvDepthwiseConv: 深度可分离卷积&#xff0c;groupsi…

Adobe家里的“3D“建模工 | Dimension

今天&#xff0c;我们来谈谈一款在Adobe系列中比肩C4D的高级3D软件的存在—— Dimension。 Adobe Dimension &#xff0c;其定位是一款与Photoshop以及Illustrator相搭配的3D绘图软件。 Adobe Dimensions与一般的3D绘图软件相较之下&#xff0c;在操作界面在功能上有点不大相同…

第四天课程 分布式搜索引擎1

分布式搜索引擎01 – elasticsearch基础 0.学习目标 1.初识elasticsearch 1.1.了解ES 1.1.1.elasticsearch的作用 elasticsearch是一款非常强大的开源搜索引擎&#xff0c;具备非常多强大功能&#xff0c;可以帮助我们从海量数据中快速找到需要的内容 例如&#xff1a; …

常见排序算法实现

&#x1f495;"每一天都是值得被热爱的"&#x1f495; 作者&#xff1a;Mylvzi 文章主要内容&#xff1a;常见排序算法实现 1.排序的概念 所谓排序&#xff0c;就是按照特定顺序重新排列序列的操作 排序的稳定性&#xff1a; 当一个序列中存在相同的元素时 排序过…

Swift制作打包framework

新建framework项目 设置生成fat包&#xff0c;包括模拟器x86_64和arm64 Buliding Settings -> Architectures -> Build Active Architecture Only 设置为NO 设置打包环境&#xff0c;选择release edit Scheme -> run -> Build configuration 设置为 Release 设置…

专题解读|Graph Fairness代表性工作介绍

1. 图上的公平性问题 图在现实世界中无处不在&#xff0c;例如知识图谱&#xff0c;社交网络和生物网络。近年来&#xff0c;图神经网络( graph neural networks&#xff0c;GNNs ) 在图结构数据建模方面表现出了强大的能力。一般地&#xff0c;GNNs采用消息传递机制&#xff…

什么是应用集成?应用集成快速指南

什么是应用集成&#xff1f; 想象一下&#xff0c;在剧院观看音乐剧&#xff0c;没有人站在正确的地方&#xff0c;每个人都在互相交谈&#xff0c;或者有漫长而尴尬的沉默&#xff0c;管弦乐队的音乐家们在错误的时刻演奏&#xff0c;完全是混乱的&#xff0c;就会很难看。 业…

房产中介租房小程序系统开发搭建:详细指南教你如何构建

随着微信小程序的日益普及&#xff0c;越来越多的企业和个人开始尝试开发自己的小程序。以下是制作一个房地产微信小程序的详细教程&#xff0c;希望对大家有所帮助。 一、注册登录乔拓云平台&#xff0c;进入后台 首先&#xff0c;需要注册并登录乔拓云平台&#xff0c;该平台…

Centos上删除文件及目录的命令积累

01-如果我想删除Centos上当前目录下的文件 test06-2023-11-14-01.sql 该怎么操作&#xff1f; 答&#xff1a;如果你想删除CentOS上当前目录下的文件 test06-2023-11-14-01.sql&#xff0c;可以使用 rm 命令。以下是删除文件的基本语法&#xff1a; rm test06-2023-11-14-01.s…

sql查询查看数据库空间使用情况

SELECT UPPER(F.TABLESPACE_NAME) "表空间名", D.TOT_GROOTTE_MB "表空间大小(M)", D.TOT_GROOTTE_MB - F.TOTAL_BYTES "已使用空间(M)", TO_CHAR(ROUND((D.TOT_GROOTTE_MB - F.TOTAL_BYTES) / D.TOT_GROOTTE_MB * 100,2),990.99) || % "使…

Nexus的Maven私有仓库搭建

Nexus的maven私有仓库搭建 一、了解 maven仓库设置 默认设置 其中&#xff1a; maven-central: 预定义的代理Maven Central仓库&#xff0c;它包含了大量的开源Java依赖包。maven-public: 存储库是一个组合存储库&#xff0c;它包含了maven-releases和maven-snapshots存储库…

【机器学习基础】机器学习的模型评估(评估方法及性能度量原理及主要公式)

&#x1f680;个人主页&#xff1a;为梦而生~ 关注我一起学习吧&#xff01; &#x1f4a1;专栏&#xff1a;机器学习 欢迎订阅&#xff01;后面的内容会越来越有意思~ &#x1f4a1;往期推荐&#xff1a; 【机器学习基础】机器学习入门&#xff08;1&#xff09; 【机器学习基…

CM211-1 MC022主板输入刷Armbian

咋一看以为是NAND的存储&#xff0c;经过各方搜索&#xff0c;发现BWCMMQ511G08G存储芯片是狭义的NAND&#xff0c;支持emmc协议&#xff0c;故而做尝试。 烧写步骤 1.下载Armbian镜像 Armbian_23.11.0_amlogic_s905l3-cm211_lunar_6.1.60_server_2023.11.01.img.gz 2.将镜像…

Leetcode—4.寻找两个正序数组的中位数【困难】

2023每日刷题&#xff08;二十九&#xff09; Leetcode—4.寻找两个正序数组的中位数 直接法实现代码 int mid, mid1, mid2; bool findmid(int n, int k, int x) {if(n % 2 1) {if(k n / 2) {mid x;return true;}} else {if(k n / 2 - 1) {mid1 x;} else if(k n / 2) {…

程序员,你的护城河挖好了吗?

程序员的护城河 在遥远的古代&#xff0c;护城河是一种防御工事&#xff0c;通常用于保护城市或城堡免受外部攻击。它是由人工挖掘或天然形成的河流、壕沟或城墙等&#xff0c;可以作为防御屏障&#xff0c;阻止敌人的进入。 而对于程序员而言&#xff0c;“护城河”是一种比喻…

Java之SpringCloud Alibaba【九】【Spring Cloud微服务Skywalking】

Java之SpringCloud Alibaba【一】【Nacos一篇文章精通系列】跳转Java之SpringCloud Alibaba【二】【微服务调用组件Feign】跳转Java之SpringCloud Alibaba【三】【微服务Nacos-config配置中心】跳转Java之SpringCloud Alibaba【四】【微服务 Sentinel服务熔断】跳转Java之Sprin…

【milkv】2、mpu6050驱动添加及测试

前言 本章介绍mpu6050的驱动添加以及测试。 其中驱动没有采用sdk提供的驱动&#xff0c;一方面需要配置irq&#xff0c;另一方面可以学习下如何通过ko方式添加驱动。 一、参考文章 驱动及测试文件编译流程&#xff1a; https://community.milkv.io/t/risc-v-milk-v-lsm6ds…

Semantic Kernel 学习笔记2

本来想白瞟免费Bing Search API如下&#xff0c;但是报错无法链接利用免费的必应 Bing 自定义搜索打造站内全文搜索_bing_subscription_key-CSDN博客 改成按照官方推荐申请&#xff0c;并在.env文件中添加BING_API_KEY""字段。 1. 打开https://www.microsoft.com/en-…

Quarkus 替代 SpringBoot

1 概述2 SpringBoot3 Quarkus4 比较5 调查结果6 从 Spring 转换到 Quarkus7 我是 Spring 开发者&#xff0c;为什么要选Quarkus&#xff1f;8 Spring 开发者可以活用哪些现有知识&#xff1f;9 对Spring开发者有额外的好处吗&#xff1f;10 Spring开发者如何开始学习Quarkus&am…