Vue3响应系统的作用与实现

news/2024/9/14 2:05:26/ 标签: vue.js, 前端, javascript

 副作用函数的执行会直接或间接影响其他函数的执行。一个副作用函数中读取了某个对象的属性,当该属性的值发生改变后,副作用函数自动重新执行,这个对象就是响应式数据。

1 响应式系统的实现

拦截对象的读取和设置操作。当读取某个属性值时,把副作用函数存储到一个“桶”里,而设置该属性值时,则将这个副作用函数从“桶”中取出病执行。

/*** 响应式系统基本原理:Proxy 拦截设置及读取操作,读取属性时将副作用* 函数存于桶,设置属性时将副作用函数从桶中取出并执行*/
let obj = {name: '',tag: false,count: 0,num1: 0,num2: 0}let bucket = new Set()let proxyObj = new Proxy(obj,{get(target, p, receiver) {bucket.add(fun)return target[p]},set(target, p, newValue, receiver) {target[p] = newValuebucket.forEach(fn => fn())}
})function fun() {console.log(proxyObj.name)
}fun() // 触发执行,空字符串
proxyObj.name = "hello" // hello
proxyObj.name = "js" // js

1.1 桶的结构

存储副作用函数的“桶”,应该为不同的对象、及其属性存储对应的副函数集。存储的容器为WeakMap。

/*** 用WeakMap 作为副作用函数的容器,改进响应式系统,支持不同的* 响应式对象及其属性都能响应式执行*/
let obj = {name: '',tag: false,count: 0,num1: 0,num2: 0}let bucketMap = new WeakMap()
let activeFun // 用于指示当前需要注册的副作用函数let proxyObj = new Proxy(obj,{get(target, p, receiver) {track(target,p)return target[p]},set(target, p, newValue, receiver) {target[p] = newValuetrigger(target,p)}
})function track(target,p) { // 跟踪函数if (activeFun) {let map = bucketMap[target]if (!map) map = bucketMap[target] = new Map()let set = map[p]if (!set) set = map[p] = new Set()set.add(activeFun)}
}function trigger(target,p) { // 触发函数let map = bucketMap[target]if (map) {let set = map[p]set && set.forEach(fn => fn())}
}function effect(fn) { // 用于注册副作用函数let tempFun = () => {activeFun = fnfn()activeFun = null}tempFun()
}effect(() => {console.log(proxyObj.name,proxyObj.tag)
})
effect(() => {console.log("name2",proxyObj.name)
})
console.log("------------------------------------")
proxyObj.name = "hello"
console.log("------------")
proxyObj.tag = false
console.log("------------")
proxyObj.name = "js";
console.log("------------")

1.2 分支切换

分支切换是指,函数内部存在一个三元表达式,根据某个字段的值会执行不同的代码分支。当该字段的值发生变化时,代码执行的分支会跟着变化。

例如:console.log(proxyObj.tag ? proxyObj.name : "false");

按照上面的代码,当name或tag的值被设置时,都会触发副作用函数。但是,在副作用函数中,当tag为false时,name的值是不会被显示的,这意味着,当tag为false时,无论name被设置多少次,都不希望执行这个副作用函数。

解决方案:当该副作用函数被触发时,删除属性与该函数的关系。在副作用函数执行时再重新创建关系。

function track(target,p) { // 跟踪函数if (activeFun) {let map = bucketMap[target]if (!map) map = bucketMap[target] = new Map()let set = map[p]if (!set) set = map[p] = new Set()set.add(activeFun)activeFun.funSetList.push(set) }
}function effect(fn) { // 用于注册副作用函数let tempFun = () => {cleanup(tempFun)activeFun = tempFunfn()activeFun = null}tempFun.funSetList = []tempFun()
}function cleanup(fn) {fn.funSetList.forEach(set => {set.delete(fn)})fn.funSetList = []
}

1.3 嵌套的effect

组件在渲染时,会执行effect函数来注册副作用函数,而父组件在渲染时,不仅会执行其本身的effect函数,还会自行其子组件的effect,这是就发生了嵌套的effect的调用。即如下:

effect(() => {effect(() => {console.log(proxyObj.count)})console.log(proxyObj.tag);
})

当修改tga 属性时,父组件的副作用函数并不会执行。

解决方案:创建一个注册的副作用函数指示栈。副作用函数执行前,将函数压入到栈中,执行完后则弹出该函数。

let activeFunStack = []
let registerFunSet = new Set() // 防止函数多次被注册function effect(fn) { // 用于注册副作用函数if (!registerFunSet.has(fn)) {registerFunSet.add(fn)let tempFun = () => {cleanup(tempFun)activeFun = tempFunactiveFunStack.push(activeFun)fn()activeFunStack.pop()activeFun = activeFunStack.length < 1 ? null : activeFunStack[activeFunStack.length - 1]}tempFun.funSetList = []tempFun()}
}effect(() => {effect(sonFun)console.log(proxyObj.tag);
})function sonFun() {console.log(proxyObj.count)
}console.log("------------------------------------")
proxyObj.tag = false
proxyObj.count = 1

1.4 避免无限递归

在一个副作用函数设置及读取同一个属性时,上面代码中,会发生无限递归对情况。这是因为,当设置属性值时,会触发副作用函数执行,而副作用函数中又会设置该属性值…

解决方案:在触发时,不执行当前正在被注册的副作用函数。

function trigger(target,p) { // 触发函数let map = bucketMap[target]if (map) {let set = map[p]if (set) {let tempSet = new Set(set)tempSet.forEach(fn => {if (activeFun !== fn) fn()})}}
}effect(() => {console.log(proxyObj.count++);
})
console.log("------------------------------------")
proxyObj.count++;

1.5 调度执行

可调度,是指当动作触发副作用函数重复执行时,有能力决定副作用函数执行的时机、次数以及方式。

1.5.1 微任务

宏任务

通常是由宿主环境(浏览器)提供的。包括但不限于:script(整体代码)、setTimeout、setInterval、I/O、UI渲染。

微任务

由JS引擎(如V8)提供的。它们在当前宏任务之后,下一个宏任务之前执行。常见的微任务:Promose.then()

微任务通常用于执行需要尽快完成的异步操作。

过多使用微任务可能会导致主线程被阻塞,影响页面的响应。

表 宏任务和微任务两种类型的队列

执行顺序:

  1. 宏任务队列:从宏任务队列中取出一个任务执行。
  2. 执行宏任务:执行宏任务中的所有同步代码。
  3. 微任务队列:在执行完宏任务中的所有同步代码后,会查看并清空微任务队列中的所有任务。
  4. 渲染UI:微任务队列清空后,浏览器会进行UI渲染(如果需要)。
  5. 循环:重复步骤1~4,直到宏任务队列和微任务队列都为空。
function trigger(target,p) { // 触发函数let map = bucketMap[target]if (map) {let set = map[p]if (set) {let tempSet = new Set(set)tempSet.forEach(fn => {if (activeFun !== fn) {if (fn.options.scheduler) {fn.options.scheduler(fn)} else {fn()}}})}}
}function effect(fn,options = {}) { // 用于注册副作用函数if (!registerFunSet.has(fn)) {registerFunSet.add(fn)let tempFun = () => {cleanup(tempFun)activeFun = tempFunactiveFunStack.push(activeFun)fn()activeFunStack.pop()activeFun = activeFunStack.length < 1 ? null : activeFunStack[activeFunStack.length - 1]}tempFun.options = optionstempFun.funSetList = []tempFun()}
}const jobQueue = new Set()
const promise = Promise.resolve()
let isFlushing = falsefunction flushJob() {if (!isFlushing) {isFlushing = truepromise.then(() => {jobQueue.forEach(fn => fn())}).finally(() => {isFlushing = false})}
}effect(() => {console.log(proxyObj.count);
},{scheduler(fn) {jobQueue.add(fn)flushJob()}
})
console.log("------------------------------------")
proxyObj.count++;
proxyObj.count++;

1.6 计算属性computed 与 lazy

计算属性,只有当相关依赖发生改变时,计算属性才会重新求值。否则,就是多次访问计算属性,也会立即返回之前的计算结果,不需要再次执行函数。

function effect(fn,options = {}) { // 用于注册副作用函数if (!registerFunSet.has(fn)) {registerFunSet.add(fn)let tempFun = () => {cleanup(tempFun)activeFun = tempFunactiveFunStack.push(activeFun)let res = fn()activeFunStack.pop()activeFun = activeFunStack.length < 1 ? null : activeFunStack[activeFunStack.length - 1]return res}tempFun.options = optionstempFun.funSetList = []if (!options.lazy) {tempFun()}return tempFun}
}function computed(fn) {let valuelet dirty = truelet tempFun = () => { trigger(obj,"value") }let effectFn = effect(fn,{lazy: true,scheduler() {if (!dirty) {dirty = truejobQueue.add(tempFun)flushJob()}}})let obj = {get value() {if (dirty) {value = effectFn()dirty = false}track(obj,"value")return value}}return obj
}let computedRes = computed(() => proxyObj.num1 + proxyObj.num2)effect(()=> {console.log("computedRes1",computedRes.value)
})proxyObj.num1 = 2
proxyObj.num2 = 3

1.7 watch 的实现原理

Vue 的 watch,可以监听对象、对象的某个属性。可以对对象进行深层次监听。当属性值改变时,会触发监听的回调函数。

function watch(source,callBack) {let newValue,oldValuelet getterif (typeof source === "function") {getter = source} else {getter = () => traverse(source)}const job = () => {newValue = effectFun()callBack(newValue,oldValue)if (typeof newValue === "object") {oldValue = {...newValue}} else {oldValue = newValue}}let effectFun = effect(getter,{scheduler() {job()}})let tempRes = effectFun()if (typeof tempRes === "object") {oldValue = {...tempRes}} else {oldValue = tempRes}
}function traverse(value) {if (typeof value != "object" || value === null) returnfor (const k in value) traverse(value[k])return value
}watch(proxyObj,(newValue,oldValue) => {console.log("proxyObj",newValue,oldValue)
})watch(proxyObj.name,(newValue,oldValue) => {console.log("name",newValue,oldValue)
})proxyObj.count = 1
proxyObj.name = "hello"

1.8 过期的副作用

竞态问题指的是两个或多个操作几乎同时发生,并且结果依赖于它们发生的顺序,但顺序又是不确定的。 在单线程JS环境中(浏览器),我们通常不会遇到竞态问题,但是,随着Web API的引入(如异步操作,Promises,async/aswait,Web Workers等),导致JS代码中仍然可以出现竞态问题。

watch(() => proxyObj.count,(newValue,oldValue) => {Promise.resolve().then(() => {setTimeout(() => {console.log(newValue)},newValue * 1000)})
})proxyObj.count = 5
setTimeout(()=> {proxyObj.count = 2
},500)

解决方案:在第二次触发时,将前一次的触发状态设置为过期,只有状态非过期,产生的结果才有效。

function watch(source,callBack) {let newValue,oldValuelet getterif (typeof source === "function") {getter = source} else {getter = () => traverse(source)}let cleanuplet cleanFun = (fn) => {cleanup = fn}const job = () => {if (cleanup) cleanup()newValue = effectFun()callBack(newValue,oldValue,cleanFun)if (typeof newValue === "object") {oldValue = {...newValue}} else {oldValue = newValue}}let effectFun = effect(getter,{scheduler() {job()}})let tempRes = effectFun()if (typeof tempRes === "object") {oldValue = {...tempRes}} else {oldValue = tempRes}
}watch(() => proxyObj.count,(newValue,oldValue,cleanFun) => {let expire = falsecleanFun(() => {expire = true})Promise.resolve().then(() => {setTimeout(() => {if (!expire) console.log(newValue)},newValue * 1000)})
})

http://www.ppmy.cn/news/1475606.html

相关文章

澳门建筑插画:成都亚恒丰创教育科技有限公司

澳门建筑插画&#xff1a;绘就东方之珠的斑斓画卷 在浩瀚的中华大地上&#xff0c;澳门以其独特的地理位置和丰富的历史文化&#xff0c;如同一颗璀璨的明珠镶嵌在南国海疆。这座城市&#xff0c;不仅是东西方文化交融的典范&#xff0c;更是建筑艺术的宝库。当画笔轻触纸面&a…

STM32MP135裸机编程:唯一ID(UID)、设备标识号、设备版本

0 资料准备 1.STM32MP13xx参考手册1 唯一ID&#xff08;UID&#xff09;、设备标识号、设备版本 1.1 寄存器说明 &#xff08;1&#xff09;唯一ID 唯一ID可以用于生成USB序列号或者为其它应用所使用&#xff08;例如程序加密&#xff09;。 &#xff08;2&#xff09;设备…

conda install问题记录

最近想用代码处理sar数据&#xff0c;解放双手。 看重了isce这个处理平台&#xff0c;在安装包的时候遇到了一些问题。 这一步持续了非常久&#xff0c;然后我就果断ctrlc了 后面再次进行尝试&#xff0c;出现一大串报错&#xff0c;不知道是不是依赖项的问题 后面看到说mam…

前端预览图片的两种方式:转Base64预览或转本地blob的URL预览,并再重新转回去

&#x1f9d1;‍&#x1f4bb; 写在开头 点赞 收藏 学会&#x1f923;&#x1f923;&#x1f923; 预览图片 一般情况下&#xff0c;预览图片功能&#xff0c;是后端返回一个图片地址资源&#xff08;字符串&#xff09;给前端&#xff0c;如&#xff1a;ashuai.work/static…

搜维尔科技:scalefit人体工程学分析表明站立式工作站的高度很重要

搜维尔科技&#xff1a;scalefit人体工程学分析表明站立式工作站的高度很重要 搜维尔科技&#xff1a;scalefit人体工程学分析表明站立式工作站的高度很重要

红酒与未来科技:传统与创新的碰撞

在岁月的长河中&#xff0c;红酒以其深邃的色泽、丰富的口感和不同的文化魅力&#xff0c;成为人类文明中的一颗璀璨明珠。而未来科技&#xff0c;则以其迅猛的发展速度和无限的可能性&#xff0c;领着人类走向一个崭新的时代。当红酒与未来科技相遇&#xff0c;一场传统与创新…

【2024最新】C++扫描线算法介绍+实战例题

扫描线介绍&#xff1a;OI-Wiki 【简单】一维扫描线&#xff08;差分优化&#xff09; 网上一维扫描线很少有人讲&#xff0c;可能认为它太简单了吧&#xff0c;也可能认为这应该算在差分里&#xff08;事实上讲差分的文章里也几乎没有扫描线的影子&#xff09;。但我认为&am…

1.26、基于概率神经网络(PNN)的分类(matlab)

1、基于概率神经网络(PNN)的分类简介 PNN(Probabilistic Neural Network,概率神经网络)是一种基于概率论的神经网络模型,主要用于解决分类问题。PNN最早由马科夫斯基和马西金在1993年提出,是一种非常有效的分类算法。 PNN的原理可以简单概括为以下几个步骤: 数据输入层…

Tomcat的服务部署于优化

一、tomcat是一个开源的web应用服务器&#xff0c;nginx主要处理静态页面&#xff0c;那么静态请求&#xff08;连接数据库&#xff0c;动态页面&#xff09;并不是nginx的强项&#xff0c;动态的请求会交给Tomcat进行处理&#xff0c;tomcat是用java代码写的程序&#xff0c;运…

[leetcode]partition-list 分隔链表

. - 力扣&#xff08;LeetCode&#xff09; class Solution { public:ListNode* partition(ListNode* head, int x) {ListNode *smlDummy new ListNode(0), *bigDummy new ListNode(0);ListNode *sml smlDummy, *big bigDummy;while (head ! nullptr) {if (head->val &l…

【数学建模】——【线性规划】及其在资源优化中的应用

目录 线性规划问题的两类主要应用&#xff1a; 线性规划的数学模型的三要素&#xff1a; 线性规划的一般步骤&#xff1a; 例1&#xff1a; 人数选择 例2 &#xff1a;任务分配问题 例3: 饮食问题 线性规划模型 线性规划的模型一般可表示为 线性规划的模型标准型&…

Oracle各种连接写法介绍

1、左连接 左连接&#xff08;左外连接&#xff09;&#xff1a; 基表全部查出来&#xff0c;外连接表有的匹配&#xff0c;没有则为null&#xff1b; 记录数与基表的记录数相同&#xff0c;前提是where后未加条件过滤&#xff1b; 两种写法&#xff08;left join&#xff09…

DP讨论——建造者模式

学而时习之&#xff0c;温故而知新。 敌人出招&#xff08;使用场景&#xff09; 组合关系中&#xff0c;如果要A对象创建B对象&#xff0c;或者要A对象创建一堆对象&#xff0c;这种是普遍的需求。 你出招 这种适合创建者模式&#xff0c;我感觉也是比较常见的。 构造函数…

《从零开始学习Linux》——开篇

前言 近日笔者新开专栏&#xff0c;《从零开始学习Linux》&#xff0c;Linux水深而且大&#xff0c;学了一圈之后&#xff0c;有懂得有不懂的&#xff0c;一直没有机会整体的全部重新捋一遍&#xff0c;本专栏的目的是&#xff0c;带着大家包括我自己重新学习Linux一遍这些知识…

Taro自定义FromData实现本地路径转换为文件

在用Taro写头像上传功能时&#xff0c;因为需要对获得的图片进行剪切成圆形或方形。使用组件剪切完之后返回的是一个本地图片的相对路径。这个时候我们就需要自己实现将本地路径重新转换为二进制文件。 引入两个js文件 mimeMap.js module.exports {"0.001": &quo…

Java集合类常见面试题

一些常见的Java集合类高频面试题包括&#xff1a; ArrayList和LinkedList的区别是什么&#xff1f;HashMap和HashTable的区别是什么&#xff1f;HashSet和TreeSet的区别是什么&#xff1f;ConcurrentHashMap的实现原理是什么&#xff1f;如何遍历HashMap和HashTable&#xff1…

UDP通讯实现

服务器端&#xff1a; 1.获取套接字 int fd;fdsocket(AF_INET,SOCK_DGRAM,0);if(fd<0){perror("socket");exit(0);} #include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol); -domain: 指定通信域&…

Spring 事务管理配置方法

Spring中声明式的事务配置方法有两种&#xff0c;一种是注解方式&#xff0c;另一种可能用AOP切片方式来实现。 一、注解方式 在Spring配置文件中加入配置 <!-- DataSource配置 --><bean id"dataSource"class"com.mchange.v2.c3p0.ComboPooledDataSo…

IC后端设计中的shrink系数设置方法

我正在「拾陆楼」和朋友们讨论有趣的话题,你⼀起来吧? 拾陆楼知识星球入口 在一些成熟的工艺节点通过shrink的方式(光照过程中缩小特征尺寸比例)得到了半节点,比如40nm从45nm shrink得到,28nm从32nm shrink得到,由于半节点的性能更优异,成本又低,漏电等不利因素也可以…

计算机视觉之ResNet50图像分类

前言 图像分类是计算机视觉应用中最基础的一种&#xff0c;属于有监督学习类别。它的任务是给定一张图像&#xff0c;判断图像所属的类别&#xff0c;比如猫、狗、飞机、汽车等等。本章将介绍使用ResNet50网络对CIFAR-10数据集进行分类。 ResNet网络介绍 ResNet50网络是由微…