JavaScript 实现一个响应式系统的解决方案

 更新时间:2024年04月25日 09:15:32   作者:jiang_xin_yu  
这篇文章主要介绍了JavaScript 实现一个响应式系统的解决方案,本次示例使用Proxy实现数据监控,结合实例代码给大家介绍的非常详细,需要的朋友可以参考下

第一阶段目标

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

第一阶段问题

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

第一阶段问题的解决方案

  • 我们可用参考现有的响应式系统(vue)

    vue2 是通过 Object.defineProperty实现数据变化的监控,详细查看 Vue2官网

    vue3 是通过Proxy实现数据变化的监控,详细查看 Vue3官网

  • 本次示例使用Proxy实现数据监控,Proxy详细信息查看官网
  • 根据解决方案,需要改变第一阶段目标为-> Proxy对象变化重新运行依赖数据的过程
  • 问题变更->如何知道Proxy发生了变化
  • 问题变更->如何知道哪些函数依赖了哪些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);
  };
}

第二阶段目标

  • 在合适的时机触发合适的函数

第二阶段问题

  • 在什么时间触发函数
  • 到达触发时间时,应该触发什么函数

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

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

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

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

{
  //对象
  "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 dragable的Move对象

    javascript dragable的Move对象

    一个dragable的Move对象,大家可以运行下,测试看下效果。
    2009-08-08
  • js简单的分页器插件代码实例

    js简单的分页器插件代码实例

    这篇文章主要介绍了js简单的分页器插件代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-09-09
  • JavaScript实现div的鼠标拖拽效果

    JavaScript实现div的鼠标拖拽效果

    这篇文章主要为大家详细介绍了JavaScript实现div的鼠标拖拽效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-11-11
  • 微信小程序中换行空格(多个空格)写法详解

    微信小程序中换行空格(多个空格)写法详解

    这篇文章主要介绍了微信小程序中换行空格(多个空格)写法详解,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下
    2018-07-07
  • 解决input输入框仅支持输入数字及两位小数点的限制

    解决input输入框仅支持输入数字及两位小数点的限制

    这篇文章主要为大家介绍了解决input输入框仅支持输入数字及两位小数点的限制技巧示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-11-11
  • 使用Promise进行异步处理的操作步骤

    使用Promise进行异步处理的操作步骤

    在JavaScript中,异步操作是非常常见的,如网络请求、文件操作、定时任务等,Promise是一种用于管理异步操作的解决方案,它使得异步代码变得更易读、易于组合和错误处理更加集中,本文将详细介绍如何使用Promise进行错误处理,需要的朋友可以参考下
    2025-03-03
  • for of 和 for in 的区别介绍

    for of 和 for in 的区别介绍

    这篇文章主要介绍了for of 和 for in 的区别,for of 和 for in都是用来遍历的属性,本文重点介绍下for of 和 for in 的区别,需要的朋友可以参考下
    2022-12-12
  • 详解如何编写一个Typescript的类型声明文件

    详解如何编写一个Typescript的类型声明文件

    我们知道TypeScript根据类型声明进行类型检查,但有些情况可能没有类型声明,这个时候就需要我们自己写一个,下面小编就来和大家聊聊如果写一个Typescript的类型声明文件呢
    2023-06-06
  • webpack-cli在webpack打包中的作用小结

    webpack-cli在webpack打包中的作用小结

    webpack 是打包代码时依赖的核心内容,而 webpack-cli 是一个用来在命令行中运行 webpack 的工具,那么webpack-cli在webpack打包中的作用是什么,本文就详细的介绍一下,感兴趣的可以了解一下
    2022-04-04
  • javascript 数字格式化输出的实现代码

    javascript 数字格式化输出的实现代码

    这篇文章主要是对javascript中数字格式化输出的实现代码进行了介绍,需要的朋友可以过来参考下,希望对大家有所帮助
    2013-12-12

最新评论