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. 记录--Cesium+Vue实战教程——地图导航

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 项目效果 我们今天要实现的是一个路径规划的功能,有两个输入框.输入起点终点,然后查询,得到规划的路径,效果如下: 我们会用到以下库: Ax ...

  2. WPF 组件间通信 MVVM 进行解耦

    假设有这样一个需求,有这样一个聊天界面,主界面是选项卡,其一选项卡内部是真正的聊天列表和聊天界面,我们需要实时的在主界面显示未读消息的数量 假设我们已经有方法可以拿到未读消息的数量,那么如何在主界面的 ...

  3. Go | 浅谈包管理模式

    任何一门编程语言都离不开对各种工具包的使用,工具包的管理就显得异常重要了.Go 的包管理方式是逐渐演进的,本文介绍Go语言的两种包管理模式. GOPATH模式引包(不推荐) 在 1.5 版本之前,所有 ...

  4. elementui树形表格分页

    效果图 如果你刚好需求中需要如上效果那么只需要吧代码复制过去直接用即可,注意写在nextTick中 前提是vue加elementui 代码如下 /**    *  树形表格分页    * @param ...

  5. IDEA (任意 JetBrains IDE)拆分先前 commit

    最近在合并上游代码,遇到了一个问题:某个 commit 杂糅了几个不同的特性修改,这可能会导致 rebase 上游代码时需要再对该 commit 进行额外的代码冲突处理 解决方法:合并上游分支前,拆分 ...

  6. KingbaseESV8R6使用pageinspect插件观察空值

    前言 在KingbaseES元组头数据中,有一个t_bits数组,用于存储空值位图.当元组中没有null值的时候,t_bits是空的,当元组有null值的列时,t_bits使用一个bit来表示列是否为 ...

  7. 关于Java 线程的运行状态

    首先需要说明的是,所指状态为JVM线程状态,而非操作系统线程状态.同一时间,一个线程只会存在于一种状态. 线程状态,enum State: 1.NEW 已创建,未运行. 2.RUNNABLE 线程于J ...

  8. 运维排查 | Systemd 之服务停止后状态为 failed

    哈喽大家好,我是咸鱼. 我们知道 CentOS 7 之后,Systemd 代替了原来的 SystemV 来管理服务,相比 SystemV ,Systemd 能够很好地解决各个服务间的依赖关系,还能让所 ...

  9. #二分图匹配#UVA1194 Machine Schedule

    题目 有两台机器 \(A,B\) 分别有 \(n,m\) 种模式. 现在有 \(k\) 个任务.对于每个任务 \(i\) ,给定两个整数 \(a_i\) 和 \(b_i\)​, 表示如果该任务在 \( ...

  10. 小师妹学JavaIO之:try with和它的底层原理

    目录 简介 IO关闭的问题 使用try with resource try with resource的原理 自定义resource 总结 简介 小师妹是个java初学者,最近正在学习使用java I ...