JavaScript 如何实现一个响应式系统
JavaScript 如何实现一个响应式系统
第一阶段目标
- 数据变化重新运行依赖数据的过程
第一阶段问题
- 如何知道数据发生了变化
- 如何知道哪些过程依赖了哪些数据
第一阶段问题的解决方案
- 我们可用参考现有的响应式系统(vue)
- 本次示例使用
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来创建一个响应式的超酷360度全景图片查看幻灯效果
360度的全景图片效果常常可以用到给客户做产品展示,今天这里我们推荐一个非常不错的来自Robert Pataki的360全景幻灯实现教程,这里教程中将使用javascript来打造一个超酷的全景幻灯实 ...
- vue原理探索--响应式系统
Vue.js 是一款 MVVM 框架,数据模型仅仅是普通的 JavaScript 对象,但是对这些对象进行操作时,却能影响对应视图,它的核心实现就是「响应式系统」. 首先看一下 Object.defi ...
- Vue 及框架响应式系统原理
个人bolg地址 全局概览 Vue运行内部运行机制 总览图: 初始化及挂载 在 new Vue()之后. Vue 会调用 _init 函数进行初始化,也就是这里的 init 过程,它会初始化生命周期. ...
- Vuejs - 深入浅出响应式系统
Vue 最独特的特性之一,是其非侵入性的响应式系统.数据模型仅仅是普通的 Javascript 对象.而当你修改它们时,视图会进行更新.这使得状态管理非常简单直接,不过理解其工作原理同样非常重要,这样 ...
- Vue的响应式系统
Vue的响应式系统 我们第一次使用Vue的时候,会感觉有些神奇,举个例子: <div id="app"> <div>价格:¥{{price}}</di ...
- 【js】vue 2.5.1 源码学习 (七) 初始化之 initState 响应式系统基本思路
大体思路(六) 本节内容: 一.生命周期的钩子函数的实现 ==> callHook(vm , 'beforeCreate') beforeCreate 实例创建之后 事件数据还未创建 二.初始化 ...
- 前端必读:Vue响应式系统大PK
转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 原文参考:https://www.sitepoint.com/vue-3-reactivity-system ...
- 使用jQuery开发一个响应式超酷整合RSS信息阅读杂志
在线演示1 本地下载 申请达人,去除赞助商链接 如果大家喜欢阅读博客文章的话,可能都会使用RSS阅读器,今天这里我们将使用jQuery来开发一个响应式的RSS信息阅读应用,使用它你可以将你喜欢 ...
- AudioPlayer.js,一个响应式且支持触摸操作的jquery音频插件
AudioPlayer.js是一个响应式.支持触摸操作的HTML5 的音乐播放器.本文是对其官网的说用说明文档得翻译,博主第一次翻译外文.不到之处还请谅解.之处. JS文件地址:http://osva ...
- 你是如何理解Vue的响应式系统的
1.响应式系统简述: 任何一个 Vue Component 都有一个与之对应的 Watcher 实例. Vue 的 data 上的属性会被添加 getter 和 setter 属性. 当 Vue Co ...
随机推荐
- 关于云XR介绍,以及5G时代云化XR的发展机遇
XR技术进入全面沉浸化时代 基于云化XR技术将大幅降低XR终端设备的计算负荷和能耗,摆脱线缆的束缚,XR终端设备将变得更轻.更沉浸.更智能.更有利于商业化. 网络XR终端能力的提升,将推动XR技术进入 ...
- 记录-记一次不规范使用key引发的惨案
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 前言 平时在使用v-for的时候,一般会要求传入key,有没有像我一样的小伙伴,为了省心,直接传索引index,貌似也没有遇到过什么问题, ...
- 阿里云服务器安装mysql后本地连接失败
阿里云服务器安装mysql后本地连接失败 一.问题描述 在阿里云安装mysql后,想在本地电脑用可视化工具连接mysql,但是提示连接失败 错误如图所示: 二.问题分析 1.检查3306端口 首先,检 ...
- 面试官:只知道v-model是:modelValue和@onUpdate语法糖,那你可以走了
前言 我们每天都在用v-model,并且大家都知道在vue3中v-model是:modelValue和@update:modelValue的语法糖.那你知道v-model指令是如何变成组件上的mode ...
- Gaussian YOLOv3 : 对bbox预测值进行高斯建模输出不确定性,效果拔群 | ICCV 2019
在自动驾驶中,检测模型的速度和准确率都很重要,出于这个原因,论文提出Gaussian YOLOv3.该算法在保持实时性的情况下,通过高斯建模.损失函数重建来学习bbox预测值的不确定性,从而提高准确率 ...
- Makefile 简单学习
一.Makefile 简介 Makefile 是一种常用于编译的脚本语言.它可以更好更方便的管理你的项目的代码编译,节约编译时间(没改动的文件不编译).注意 Makefile 文件命令必须是 Make ...
- Flume入门操作
十一.Flume 1)开启Flume的监控端口 bin/flume-ng agent -c conf/ -n a1 -f job/flume-netcat-logger.conf -Dflume.ro ...
- MySQL插入更新删除数据
数据插入 插入完整的行 INSERT INTO customers VALUES(NULL, 'Pep E. LaPew', '100 Main Street', 'Los Angeles', 'CA ...
- 开启新时代,承接新使命,开放原子开源大赛OpenHarmony创新赛正式启航!
开放原子开源大赛OpenHarmony创新赛,正式启动啦! "OpenHarmony创新赛"是开放原子全球开源大赛下开设的创新赛道,面向企业.个人.高校师生等广大开发者,聚焦O ...
- JDK 19新特性 & JDK 多版本安装切换配置
新的JDK 19包含如下7个新的特性: 转自:JDK19中比较重要的新特性-电子发烧友网 JEP 405: Record Patterns(Record模式) JEP 422: Linux/RISC- ...