Vue3响应系统的作用与实现

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

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.mzph.cn/pingmian/44831.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

死灰复燃的 LockBit, “现身说法”计算机安全的重要性

LockBit 死灰复燃 2024年2月&#xff0c;一场全球性的名为 “Cronos 行动” 的执法行动夺取了对 LockBit 勒索组织基础设施的控制权并扰乱了其运营。 但是&#xff0c;就像希腊神话中的九头蛇一样&#xff0c;砍掉一个 LockBit 的头并不重要——它可以迅速再生成多个新的头。…

南京邮电大学运筹学课程实验报告3 整数规划问题求解 指导

一、题目描述 实验三 整数规划问题求解    实验属性&#xff1a; 设计型    实验目的 1&#xff0e;理解图的整数规划问题概念&#xff1b; 2&#xff0e;掌握运筹学软件的使用方法&#xff1b; 3. 掌握整数规划问题求解原理和方法。 实…

傅里叶变换DFT\FFT

文章目录 直流分量&#xff08;频率为0&#xff09;傅立叶原理离散频率栅栏效应原理方案&#xff1a;提高采样间隔&#xff08;频率分辨力&#xff09; 直流分量&#xff08;频率为0&#xff09; 指信号中的直流成分&#xff0c;信号的直流分量就是信号的平均值&#xff0c;它…

MES系统助力塑料制品行业数字化转型

注塑MES系统助力工厂生产力提升具体体现在&#xff1a;覆盖生产全流程&#xff1b;数据自动收集、科学规划排产&#xff1b;优化配送模型、平衡物流运转&#xff1b;严格把控品质、异常自动分析&#xff1b;实时监控设备&#xff0c;保证正常运转&#xff1b;产品快速追溯&…

c#获取本机的MAC地址(附源码)

在前一次的项目中&#xff0c;突然用到了这个获取本机的MAC地址&#xff0c;然后就研究了一下&#xff0c;记录下来&#xff0c;防止以后再用到&#xff0c; 使用winfrom做的&#xff0c;界面一个button&#xff0c;一个textBox,点了button以后给textBox赋值显示mac地址 附上源…

C#基于事件的异步模式实现实例

1、异步操作类 using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Runtime.Remoting.Messaging; using System.Text; using System.Threading; using System.Threading.Tasks; using static System.Windows.For…

Linux下vim工具应用

1.简介 Vim&#xff08;Vi IMproved&#xff09;是一种高度可配置的文本编辑器&#xff0c;用于有效地创建和更改任何类型的文本。它是从vi发展而来&#xff0c;vi是Unix和类Unix系统上最初的文本编辑器之一。Vim被设计为程序员和文本编辑的爱好者使用&#xff0c;它以其强大的…

广度优先(BFS)

先看一道简单的题&#xff0c;迷宫问题&#xff1a; 洛谷P1746 离开中山路&#xff1a;P1746 离开中山路 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) #include<iostream> #include<cstring> #include<queue> #include <utility> #define N 1002 …

综合监管云平台 DownFile 任意文件读取漏洞复现

0x01 产品简介 综合监管云平台是一种集成了多种先进技术的信息化平台&#xff0c;旨在通过数据采集、分析、预警和应急处理等功能&#xff0c;实现对各类监管对象的全面、高效、精准管理&#xff0c;综合监管云平台利用“互联网物联网”模式&#xff0c;结合云计算、大数据、边…

春招冲刺百题计划|队列

Java基础复习 Java数组的声明与初始化Java ArrayListJava HashMapJava String 类Java LinkedListJava Deque继承LinkedListJava SetJava 队列 第一题&#xff1a;387. 字符串中的第一个唯一字符 class Solution {public int firstUniqChar(String s) {//最简单的方法就是暴力…

p14数组(2)

数组作为函数的参数 冒泡排序 两两比较 void bubble_sort(int arr[],int sz) {int i0;for(i0;i<sz-1;i){//每一趟冒泡排序int j0;for(j0;j<sz-1-i;j){if(arr[j]>arr[j1]){int tmparr[j];arr[j]arr[j1];arr[j1]tmp;}}} } int main(){int arr[]{9,8,7,6,5,4,3,2,1,0}…

关于woocommerce product data tabs, 特别是additional information

woocommerce product data tabs&#xff0c; 也就是默认的这三个&#xff1a; description additional information reviews 包括如何删除&#xff0c;重命名&#xff0c;改显示顺序等等&#xff0c;参考官方文档&#xff1a; https://woocommerce.com/document/editing-p…

《从零到惊艳:快速唱好一首歌的魔法指南》

对于零基础学唱歌的朋友来说&#xff0c;想要快速学会唱好一首歌并非遥不可及。 第一步&#xff0c;精心选歌。如同选择一把适合自己的宝剑&#xff0c;歌曲的选择至关重要。要挑选一首旋律优美、节奏适中、音域符合自身条件且歌词易于理解和记忆的歌曲。比如一些节奏较为缓慢、…

C# CvDnn部署CoupledTPS实现旋转图像矫正

C# CvDnn部署CoupledTPS实现旋转图像矫正 目录 说明 效果 模型信息 项目 代码 下载 说明 TPAMI2024 - Semi-Supervised Coupled Thin-Plate Spline Model for Rotation Correction and Beyond github地址&#xff1a;https://github.com/nie-lang/CoupledTPS 代码实现…

240710_昇思学习打卡-Day22-LSTM+CRF序列标注

240710_昇思学习打卡-Day22-LSTMCRF序列标注 在正式开始LSTMCRF序列标注之前&#xff0c;我们先来了解一下条件随机场&#xff0c;以下仅做简单介绍。 CRF全称Conditional Random Field&#xff0c;按照名字来理解&#xff0c;条件随机&#xff0c;随机输入&#xff0c;条件输…

ReoGrid代替EXCEL显示数据,可视化修改ReoGrid.Mvvm:ReoGrid绑定模型

ReoGrid 是 C&#xff03; 编写的.NET 电子表格控件&#xff08;类似 Excel&#xff09;。支持单元格合并&#xff0c;边框样式&#xff0c;图案背景颜色&#xff0c;数据格式&#xff0c;冻结&#xff0c;公式&#xff0c;宏和脚本执行&#xff0c;表格事件等。支持 Winform\W…

游戏视频是后期配音好还是边录边配 游戏视频怎么剪辑制作才能火 视频剪辑免费软件

游戏视频后期配音是先配还是先剪&#xff1f;游戏视频后期配音没有统一的准则&#xff0c;可以先配&#xff0c;也可以后配&#xff0c;主要是根据内容而定。游戏视频剪辑在游戏玩家中十分流行&#xff0c;那么&#xff0c;游戏视频怎么剪辑制作&#xff1f;下面让我们以具体的…

ai写作软件哪个好用?这些写作工具值得收藏

在创意写作的世界里&#xff0c;每个字词都是作者情感与灵魂的载体。 但灵感的闪现与文字的成型之间&#xff0c;往往存在着一段漫长且充满挑战的旅程。幸运的是&#xff0c;人工智能技术的崛起带来了AI写作软件&#xff0c;它们不仅能够点燃创作的火花&#xff0c;还能辅助我…

探索 ASTRA.AI:打造低代码多模态 AI 应用的开源平台

声网&#xff08;Agora&#xff09;研发的 ASTRA 平台&#xff0c;作为一款面向大语言模型应用开发的开源解决方案&#xff0c;无疑为 AI 领域注入了新的活力。它巧妙地结合了 BaaS&#xff08;后端即服务&#xff09;概念与大型语言模型的运营&#xff0c;使得创建高性能的生成…

手写redis分布式锁

一个靠谱的分布式锁应该有哪些特点&#xff1f; 1.独占性&#xff1a;任何时候有且仅有一个线程持有锁 2.放死锁&#xff1a;有超时控制机制或撤销操作&#xff0c;得有个释放锁的兜底方案 3.不乱抢&#xff1a;不能张冠李戴&#xff0c;不能unlock别人加的锁 4.可重入性&a…