JavaScript 如何实现一个响应式系统

javascript · 浏览次数 : 0

小编点评

**第一阶段目标** * 实现对象数据变化的监控。 * 通过 `Object.defineProperty` 方法监控属性的读取和设置。 **如何知道哪些函数依赖了哪些 Proxy 对象?** * 在函数收集依赖时,将函数添加到一个数组中。 * 当数据变化时,遍历数组,根据函数的名称获取其依赖的 Proxy 对象。 **第二阶段目标** * 当数据发生变化时触发相应的函数。 * 通过建立一个映射关系,记录函数依赖的 Proxy 对象。 * 当数据变化时,遍历映射关系,并根据函数名称获取其依赖的 Proxy 对象。 * 调用依赖对象的 `get` 方法获取数据。 **代码示例** ```javascript // 1. 创建一个 Proxy 对象监控对象 const proxy = new Proxy(object, { get(target, key) { // 触发依赖对象的 get 方法 }, set(target, key, value) { // 触发依赖对象的 set 方法 } }); // 2. 获取依赖对象的数组 const handleDependencies = proxy.getOwnPropertyNames(); // 3. 当数据变化时触发相应的函数 const handle = handleDependencies[0]; proxy[handle] = function() { // 处理数据变化 }; ```

正文

JavaScript 如何实现一个响应式系统

第一阶段目标

  1. 数据变化重新运行依赖数据的过程

第一阶段问题

  1. 如何知道数据发生了变化
  2. 如何知道哪些过程依赖了哪些数据

第一阶段问题的解决方案

  1. 我们可用参考现有的响应式系统(vue)
    1. vue2 是通过 Object.defineProperty实现数据变化的监控,详细查看 Vue2官网
    2. vue3 是通过Proxy实现数据变化的监控,详细查看 Vue3官网
  2. 本次示例使用Proxy实现数据监控,Proxy详细信息查看官网
  3. 根据解决方案,需要改变第一阶段目标为-> Proxy对象变化重新运行依赖数据的过程
  4. 问题变更->如何知道Proxy发生了变化
  5. 问题变更->如何知道哪些函数依赖了哪些Proxy

如何知道 Proxy 对象发生了变化,示例代码

//这里传入一个对象,返回一个Proxy对象,对Proxy对象的属性的读取和修改会触发内部的get,set方法
function relyOnCore(obj) {
  if (typeof obj !== "object" || obj === null) {
    return obj;
  }
  return new Proxy(obj, {
    get(target, key, receiver) {
      return target[key];
    },
    set(target, key, value, receiver) {
      //这里需要返回是否修改成功的Boolean值
      return Reflect.set(target, key, value);
    },
  });
}

数据监控初步完成,但是这里只监控了属性的读取和设置,还有很多操作没有监控,以及数据的 this 指向,我们需要完善它

//完善后的代码
export function relyOnCore(obj) {
  if (typeof obj !== "object" || obj === null) {
    return obj;
  }
  return new Proxy(obj, {
    get(target, key, receiver) {
      if (typeof target[key] === "object" && target[key] !== null) {
        //当读取的值是一个对象,需要重新代理这个对象
        return relyOnCore(target[key]);
      }
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      return Reflect.set(target, key, value, receiver);
    },
    ownKeys(target) {
      return Reflect.ownKeys(target);
    },
    getOwnPropertyDescriptor(target, key) {
      return Reflect.getOwnPropertyDescriptor(target, key);
    },
    has(target, p) {
      return Reflect.has(target, p);
    },
    deleteProperty(target, key) {
      return Reflect.deleteProperty(target, key);
    },
    defineProperty(target, key, attributes) {
      return Reflect.defineProperty(target, key, attributes);
    },
  });
}

如何知道哪些函数依赖了哪些 Proxy 对象

问题:依赖 Proxy 对象的函数要如何收集

在收集依赖 Proxy 对象的函数的时候出现了一个问题: 无法知道数据在什么环境使用的,拿不到对应的函数

解决方案

既然是因为无法知道函数的执行环境导致的无法找到对应函数,那么我们只需要给函数一个固定的运行环境就可以知道函数依赖了哪些数据。

示例

//定义一个变量
export let currentFn;

export function trackFn(fn) {
  return function FnTrackEnv() {
    currentFn = FnTrackEnv;
    fn();
    currentFn = null;
  };
}

自此,我们的函数调用期间 Proxy 对象监听到的数据读取在 currentFn 函数内部发生的。

同样,我们的目标从最开始的 数据变化重新运行依赖数据的过程 -> Proxy 对象变化重新运行依赖收集完成的函数

完善函数调用环境

直接给全局变量赋值,在函数嵌套调用的情况下,这个依赖收集会出现问题

let obj1 = relyOnCore({ a: 1, b: 2, c: { d: 3 } });
function fn1() {
  let a = obj1.a;
  function fn2() {
    let b = obj1.b;
  }
  //这里的c会无法收集依赖
  let c = obj1.c;
}

我们修改一下函数收集

export const FnStack = [];
export function trackFn(fn) {
  return function FnTrackEnv() {
    FnStack.push(FnTrackEnv);
    fn();
    FnStack.pop(FnTrackEnv);
  };
}

第二阶段目标

  1. 在合适的时机触发合适的函数

第二阶段问题

  1. 在什么时间触发函数
  2. 到达触发时间时,应该触发什么函数

第一个问题:在什么时间触发函数

必然是在修改数据完成之后触发函数

第二个问题:应该触发什么函数

当操作会改变函数读取的信息的时候,需要重新运行函数。因此,我们需要建立一个映射关系

{
  //对象
  "obj": {
    //属性
    "key": {
      //对属性的操作
      "handle": ["fn"] //对应的函数
    }
  }
}

在数据改变的时候,我们只需要根据映射关系,循环运行 handle 内的函数

数据读取和函数建立联系

我们可以创建一个函数用于建立这种联系

export function track(object, handle, key, fn) {}

这个函数接收 4 个参数,object(对象),handle(对数据的操作类型) key(操作了对象的什么属性),fn(需要关联的函数)

我们现在来创建映射关系

export const ObjMap = new WeakMap();
export const handleType = {
  GET: "GET",
  SET: "SET",
  Delete: "Delete",
  Define: "Define",
  Has: "Has",
  getOwnPropertyDescriptor: "getOwnPropertyDescriptor",
  ownKeys: "ownKeys",
};

export function track(object, handle, key, fn) {
  setObjMap(object, key, handle, fn);
}

function setObjMap(obj, key, handle, fn) {
  if (!ObjMap.has(obj)) {
    ObjMap.set(obj, new Map());
  }
  setKeyMap(obj, key, handle, fn);
}

const setKeyMap = (obj, key, handle, fn) => {
  let keyMap = ObjMap.get(obj);
  if (!keyMap.has(key)) {
    keyMap.set(key, new Map());
  }
  setHandle(obj, key, handle, fn);
};

const setHandle = (obj, key, handle, fn) => {
  let keyMap = ObjMap.get(obj);
  let handleMap = keyMap.get(key);
  if (!handleMap.has(handle)) {
    handleMap.set(handle, new Set());
  }
  setFn(obj, key, handle, fn);
};
const setFn = (obj, key, handle, fn) => {
  let keyMap = ObjMap.get(obj);
  let handleMap = keyMap.get(key);
  let fnSet = handleMap.get(handle);
  fnSet.add(fn);
};

现在已经实现了数据和函数之间的关联只需要在读取数据时调用这个方法去收集依赖就可以,代码如下:

export function relyOnCore(obj) {
  if (typeof obj !== "object" || obj === null) {
    return obj;
  }
  return new Proxy(obj, {
    get(target, key, receiver) {
      track(target, handleType.GET, key, FnStack[FnStack.length - 1]);
      if (typeof target[key] === "object" && target[key] !== null) {
        return relyOnCore(target[key]);
      }
      return Reflect.get(target, key, receiver);
    },
    //....这里省略剩余代码
  });
}

接下来我们需要建立数据改变->影响哪些数据的读取之间的关联

export const TriggerToTrackMap = new Map([
  [handleType.SET, [handleType.GET, handleType.getOwnPropertyDescriptor]],
  [
    handleType.Delete,
    [
      handleType.GET,
      handleType.ownKeys,
      handleType.Has,
      handleType.getOwnPropertyDescriptor,
    ],
  ],
  [handleType.Define, [handleType.ownKeys, handleType.Has]],
]);

建立这样关联后,我们只需要在数据变动的时候,根据映射关系去寻找需要重新运行的函数就可以实现响应式。

export function trigger(object, handle, key) {
  let keyMap = ObjMap.get(object);
  if (!keyMap) {
    return;
  }
  let handleMap = keyMap.get(key);
  if (!handleMap) {
    return;
  }
  let TriggerToTrack = TriggerToTrackMap.get(handle);
  let fnSet = new Set();
  TriggerToTrack.forEach((handle) => {
    let fnSetChiren = handleMap.get(handle);
    if (fnSetChiren) {
      fnSetChiren.forEach((fn) => {
        if (fn) {
          fnSet.add(fn);
        }
      });
    }
  });
  fnSet.forEach((fn) => {
    fn();
  });
}

总结

以上简易的实现了响应式系统,只是粗略的介绍了如何实现,会存在一些 bug

与JavaScript 如何实现一个响应式系统相似的内容:

JavaScript 如何实现一个响应式系统

JavaScript 如何实现一个响应式系统 第一阶段目标 数据变化重新运行依赖数据的过程 第一阶段问题 如何知道数据发生了变化 如何知道哪些过程依赖了哪些数据 第一阶段问题的解决方案 我们可用参考现有的响应式系统(vue) vue2 是通过 Object.defineProperty实现数据变化的

如何使用Node.js、TypeScript和Express实现RESTful API服务

Node.js是一个基于 Chrome V8 引擎的 JavaScript 运行环境。Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。Express是一个保持最小规模的灵活的 Node.js Web应用程序开发框架,为Web和移动应用程序提供一组强大的功能。使用Node

使用 Promise.withResolvers() 来简化你将函数 Promise 化的实现~~

引言 在JavaScript编程中,Promise 是一种处理异步操作的常用机制。Promise 对象代表了一个尚未完成但预期将来会完成的操作的结果。在本文中,我们将探讨如何通过使用 ES2024 的 Promise.withResolvers API 来优化我们的 Promise 实现。 现有实现

如何使用JavaScript实现在线Excel附件的上传与下载?

前言 在本地使用Excel时,经常会有需要在Excel中添加一些附件文件的需求,例如在Excel中附带一些Word,CAD图等等。同样的,类比到Web端,现在很多人用的在线Excel是否也可以像本地一样实现附件文件的操作呢?答案是肯定的,不过和本地不同的是,Web端不会直接打开附件,而是使用超链接单

如何移除事件监听器

在运行时清理你的代码是构建高效、可预测的应用程序,没有商量余地的部分。在JavaScript中,实现这一目标的方法之一是很好地管理事件监听器,尤其是当不再需要时移除它们。 有好几种方法可以做到这件事情,每种都有自己的一套权衡方法,使其在某些情况下更合适。我们将介绍几种最常用的策略,以及当你试图决定哪

如何在低代码平台中引用 JavaScript ?

引言 在当今快速发展的数字化时代,企业对业务应用的需求日益复杂且多元。低代码开发平台作为一个创新的解决方案,以直观易用的设计理念,打破了传统的编程壁垒,让非技术人员也能轻松构建功能完备的Web应用程序,无需深入编码。这一特性极大地简化了应用开发流程,加速了业务需求转化为实际应用的速度,为企业带来了前

从零开始使用 Astro 的实用指南

在这个实用的Astro指南中,我将指导你完成设置过程,并告诉你如何构造你的文件。你将学习如何添加页面、交互式组件,甚至是markdown文章。我还会告诉你如何从服务器上获取数据,创建布局,并使用vanilla JavaScript和其他框架添加互动性。准备好享受一些动手的乐趣,因为我们将一起创建一个

如何用JavaScripte和HTML 实现一整套的考试答题卡和成绩表

相信在学校的你都有这样的体验,临近考试,要疯狂的“背诵”否则成绩单就要挂零,因为答题卡全部涂抹都是错的。 那么毕业多年的你,没有了考试,有没有一丝怀念涂答题卡的时候,有没有好奇这个答题卡到底如何制作,成绩表到底如何为每位同学定制动态生成的。 这些都要归功于“报表”工具 学校每年都会打印很多的学生成绩

实用教程丨如何将实时数据显示在前端电子表格中(一)

本文由葡萄城技术团队于博客园原创并首发转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具、解决方案和服务,赋能开发者。 前言 数据(包括股票、天气和体育比分)在不断更新为新信息时最为有用。比较通用的 JavaScript 电子表格组件,可以轻松地使用、显示并通过数据绑定提供实时数据更新。

JavaScript 如何验证 URL

前言 当开发者需要为不同目的以不同形式处理URL时,比如说浏览器历史导航,锚点目标,查询参数等等,我们经常会借助于JavaScript。然而,它的频繁使用促使攻击者利用其漏洞。这种被利用的风险是我们必须在我们的JavaScript应用程序中实现URL验证的原因。 URL验证检查URL是否遵循正确的U