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 响应式系统内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 微信小程序仿抖音短视频切换效果的实例代码

    微信小程序仿抖音短视频切换效果的实例代码

    这篇文章主要介绍了微信小程序仿抖音短视频切换效果,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-06-06
  • Layui实现多条件查询的示例代码

    Layui实现多条件查询的示例代码

    本文主要介绍了Layui实现多条件查询,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-12-12
  • img标签中onerror用法

    img标签中onerror用法

    可是这会引起再因网络原因或其他原因使图片不能正常加载的话 这样就是再次调用onerror,基于微软的一套核心的浏览器就会认为这是死循环
    2009-08-08
  • javascript+css实现进度条效果

    javascript+css实现进度条效果

    这篇文章主要为大家详细介绍了javascript+css实现进度条效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-03-03
  • 从QQ网站中提取的纯JS省市区三级联动菜单

    从QQ网站中提取的纯JS省市区三级联动菜单

    在浏览网页过程中发现QQ自己的JS省市区三级联动。所以研究了一下,就将其提取出来了。他的界面如下,喜欢的朋友可以学习下
    2013-12-12
  • Javascript 获取滚动条位置等信息的函数

    Javascript 获取滚动条位置等信息的函数

    有时为了准确定位一个元素,我们需要获取滚动条的位置,这种需求经常出现在 tooltip 和 拖放等应用中,其实这个技术很简单,关键是要考虑浏览器的兼容性,本文就是介绍这一问题的解决方法。
    2009-09-09
  • JavaScript设计模式之外观模式介绍

    JavaScript设计模式之外观模式介绍

    这篇文章主要介绍了JavaScript设计模式之外观模式介绍,外观模式是用于由于子系统或程序组成较复杂而提供的一个高层界面接口,使用客户端更容易访问底层的程序或系统接口,需要的朋友可以参考下
    2014-12-12
  • 小程序短信验证码页面实现demo

    小程序短信验证码页面实现demo

    这篇文章主要为大家介绍了小程序短信验证码页实现demo,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-11-11
  • JS插件plupload.js实现多图上传并显示进度条

    JS插件plupload.js实现多图上传并显示进度条

    这篇文章主要为大家详细介绍了PHP结合plupload.js JS插件实现多图上传并显示进度条加删除实例,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-11-11
  • 一起来了解JavaScript面向对象

    一起来了解JavaScript面向对象

    本篇文章主要介绍了Javascript的面向对象,小编觉得这篇文章内容很不错,需要的朋友可以看下,希望能够给你带来帮助
    2021-09-09

最新评论