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 如何实现一个响应式系统的更多相关文章

  1. 使用Javascript来创建一个响应式的超酷360度全景图片查看幻灯效果

    360度的全景图片效果常常可以用到给客户做产品展示,今天这里我们推荐一个非常不错的来自Robert Pataki的360全景幻灯实现教程,这里教程中将使用javascript来打造一个超酷的全景幻灯实 ...

  2. vue原理探索--响应式系统

    Vue.js 是一款 MVVM 框架,数据模型仅仅是普通的 JavaScript 对象,但是对这些对象进行操作时,却能影响对应视图,它的核心实现就是「响应式系统」. 首先看一下 Object.defi ...

  3. Vue 及框架响应式系统原理

    个人bolg地址 全局概览 Vue运行内部运行机制 总览图: 初始化及挂载 在 new Vue()之后. Vue 会调用 _init 函数进行初始化,也就是这里的 init 过程,它会初始化生命周期. ...

  4. Vuejs - 深入浅出响应式系统

    Vue 最独特的特性之一,是其非侵入性的响应式系统.数据模型仅仅是普通的 Javascript 对象.而当你修改它们时,视图会进行更新.这使得状态管理非常简单直接,不过理解其工作原理同样非常重要,这样 ...

  5. Vue的响应式系统

    Vue的响应式系统 我们第一次使用Vue的时候,会感觉有些神奇,举个例子: <div id="app"> <div>价格:¥{{price}}</di ...

  6. 【js】vue 2.5.1 源码学习 (七) 初始化之 initState 响应式系统基本思路

    大体思路(六) 本节内容: 一.生命周期的钩子函数的实现 ==> callHook(vm , 'beforeCreate') beforeCreate 实例创建之后 事件数据还未创建 二.初始化 ...

  7. 前端必读:Vue响应式系统大PK

    转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 原文参考:https://www.sitepoint.com/vue-3-reactivity-system ...

  8. 使用jQuery开发一个响应式超酷整合RSS信息阅读杂志

    在线演示1 本地下载     申请达人,去除赞助商链接 如果大家喜欢阅读博客文章的话,可能都会使用RSS阅读器,今天这里我们将使用jQuery来开发一个响应式的RSS信息阅读应用,使用它你可以将你喜欢 ...

  9. AudioPlayer.js,一个响应式且支持触摸操作的jquery音频插件

    AudioPlayer.js是一个响应式.支持触摸操作的HTML5 的音乐播放器.本文是对其官网的说用说明文档得翻译,博主第一次翻译外文.不到之处还请谅解.之处. JS文件地址:http://osva ...

  10. 你是如何理解Vue的响应式系统的

    1.响应式系统简述: 任何一个 Vue Component 都有一个与之对应的 Watcher 实例. Vue 的 data 上的属性会被添加 getter 和 setter 属性. 当 Vue Co ...

随机推荐

  1. Bootstrap前端开发框架

    一 Bootstrap 简介 Bootstrap 来自 Twitter(推特),是目前最受欢迎的前端框架.Bootstrap 是基于 HTML.CSS 和 JAVASCRIPT 的,它简洁灵活,使得 ...

  2. golang 依赖控制反转(IoC) 改进版

    最近在开发基于golang下的cqrs框架 https://github.com/berkaroad/squat (陆续开发中,最近断了半年,懒了...).这个框架依赖ioc框架,因为之前写了一个io ...

  3. 三维模型3DTile格式轻量化压缩集群处理方法分析

    三维模型3DTile格式轻量化压缩集群处理方法分析 在地理信息系统中,由于三维模型的数据密度和文件体积较大,因此需要进行轻量化和压缩处理.这里我们将对使用集群处理方法来实现3D Tiles数据的轻量化 ...

  4. 三维模型OBJ格式轻量化压缩并行计算处理方法浅析

    三维模型OBJ格式轻量化压缩并行计算处理方法浅析 三维模型的轻量化是指通过一系列技术和算法来减小三维模型的文件大小,以提高模型在计算机中的加载.渲染和传输效率.并行计算是利用多个计算单元同时执行任务, ...

  5. 快速排序遇到的小bug

     测试环境 Ubuntu 18.04, gcc 8.4 复习一下快排算法,不料却得到了非预期的结果.示例代码如下 1 #include <stdio.h> 2 3 void mySwap( ...

  6. 基于proteus的数字电路设计

    基于proteus的数字电路设计 1.实验原理 proteus的数字电路仿真能力还是比较强大的.这里总结一下proteus的几个基本操作以备后用.大致包括74hc系列的使用.常用调试设备.仿真开关.器 ...

  7. 可视化学习:实现Canvas图片局部放大镜

    前言 最近我在可视化课程中学习了如何在Canvas中利用像素处理来实现滤镜效果,在这节课程的结尾留了一道局部放大镜的题目,提示我们用像素处理的方式去实现这个效果,最终实现随着鼠标移动将图片局部放大,本 ...

  8. KingbaseES 原生XML系列二 -- XML数据操作函数

    KingbaseES 原生XML系列二--XML数据操作函数(DELETEXML,APPENDCHILDXML,INSERTCHILDXML,INSERTCHILDXMLAFTER,INSERTCHI ...

  9. KingbaseES V8R6 集群运维案例 -- 自动故障转移失败处理

    KingbaseES V8R6版本 数据库自动故障转移失败(Automatic Database Failover Fails) 适用于: KingbaseES V8R6 版本. repmgr配置信息 ...

  10. 14 JavaScript神奇的windows

    14 神奇的windows window对象是一个很神奇的东西. 你可以把这东西理解成javascript的全局. 如果我们默认不用任何东西访问一个标识符. 那么默认认为是在用window对象. 例如 ...