VUE3之响应系统

VUE3之响应系统

前言
最近在学习VUE3的新特性,记录一下学习成果。

副作用函数

什么是副作用函数?
会产生副作用的函数,或者说会直接或间接影响其他函数的执行结果。
举个简单的例子:一个函数改变了全局变量,这个函数就是副作用函数。

响应式数据

响应式数据的实现思路

拦截对象的读取和设置操作,在读取的时候把相关联的副作用函数存储起来,设置的时候递归执行相关联的副作用函数。
vue3用proxy和reflect实现的,用weakMap、map、set收集依赖集合(与响应式数据相关联的副作用函数)

把get拦截函数里把副作用函数收集起来的逻辑单独封装到一个track函数中,把set拦截函数里把触发副作用函数执行的逻辑单独封装到trigger函数中。

调度执行

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

// 定义一个任务队列const jobQueue = new Set()// 使用 Promise.resolve() 创建一个 promise 实例,我们用它将一个任务添加到微任务队列
const p = Promise.resolve()// 一个标志代表是否正在刷新队列let isFlushing = falsefunction flushJob() {// 如果队列正在刷新,则什么都不做
if (isFlushing) return// 设置为 true,代表正在刷新isFlushing = true// 在微任务队列中刷新 job?ueue 队列p.then(() => {jobQueue.forEach(job => job())}).finally(() => ?// 结束后重置 isFlushingisFlushing = false})
}effect(() => {console.log(obj.foo)}, {scheduler(fn) {// 每次调度时,将副作用函数添加到 jobQueue 队列中jobQueue.add(fn)// 调用 flushJob 刷新队列flushJob()}})obj.foo++
obj.foo++

可能你已经注意到了,这个功能有点类似于在 Vue.js 中连续多次修改响应式数据但只会触发一次更新,实际上 Vue.js 内部实现了一个更加完善的调度器,思路与上文介绍的相同。

computed

computed函数接收一个getter函数作为参数,把getter函数作为副作用函数,当响应式数据发生改变时,就会执行该副作用函数,将计算结果作为值范围。

缓存实现思路

新增两个变量:value和dirty。value是缓存上一次计算的值,dirty是一个标识,代表是否需要重新计算。
当响应式变量发生改变时,dirty = true,访问计算属性值的时候就会调用该副作用函数(getter参数),重新计算并返回值。

watch

watch其本质就是观测一个响应式数据,当数据发生变化时通知并执行相应的回调函数。
watch 的实现本质上就是利用了 effect 以及options.scheduler 选项。

effect(() => console.log(obj.foo)}, {scheduler() {//当obj.foo的值发生变化时,执行scheduler调度函数}
})

如果获取新旧值?
充分利用effect函数的lazy

function watch(source, cb) {let getterif (typeof source === 'function') {getter = source} else {getter = () => traverse(source)}//定义新值与旧值let oldValue, newValue//使用effect注册副作用函数 时,开启lazy选项,并把返回值存储到effectFn中,以便后续手动调用const effectFn = effect (() => getter(),{lazy: true,scheduler() {//在scheduler中重新执行副作用函数,得到的是新值newValue = effectFn()//将旧值和新值作为回调函数的参数cb(newValue, oldValue)//更新旧值,不然下次会得到错误的旧值oldValue = newValue}})//手动调用副作用函数,拿到的值就是旧值oldValue = effectFn()
}

回调执行的时机:flush
pre:默认值,创建时立即执行(组件更新前)
post:异步延迟执行,把副作用函数放到一个微任务(promise实现)队列中,等到dom更新结束后执行(组件更新后)
sync:同步执行

过期的副作用:

let finalData
watch(obj, async () => {//发送并等待网络请求const res = await fetch('/request')//将请求结果赋值给 finalDatafinalData = res
)

在上面的代码片段中,如果在第一次发送请求A的结果返回之前,改变了obj的字段,就会触发第二次请求B的发送,这个时候请求A的结果就是过期的副作用。为了避免竟态问题导致的错误结果,需要一个让副作用过期的手段,具体解决思路如下:

watch(obj, async (newValuw, oldValue, onInvalidate) => {//定义一个标志,代表当前副作用函数是否过期,默认为false,代表没有过期let expired = false//调用onInvalidate() 函数注册一个过期回调onInvalidate(() => {//过期时,将expired设置为trueexpired = true})//发送网络请求const res = await fetch ('/request')//只有当该副作用函数的执行没有过期时,才会执行后续操作if (!expired) {finalData = res}
})

非基本数据类型的响应式reactive

在vue3中,响应式数据是基于Proxy和Reflect实现的,它允许我们拦截并重新定义一个对象的基本操作。

Proxy

const obj = {foo: 'Secret',get secret() {return this.foo;}};const p = new Proxy(obj, {get(target, key, recevier){return key + '11'},})console.log(p.foo) //输出foo11

proxy可以拦截读取或者设置的方法。
拦截方法:
obj.*:用的是get方法拦截
key in obj:用的是has方法拦截,内部方法是hasProperty
for…in: 用的是ownKeys拦截
delete:用的是deleteProperty拦截

Reflect

	const obj = {foo: 'Secret',get secret() {return this.foo;}};// 使用 Reflect.get() 获取 secret 属性的值,并传递 obj 作为 receiverconst secretValue = Reflect.get(obj, 'secret', obj);console.log(secretValue); // 输出: 'Secret'// 如果没有传递 receiver,或者 receiver 不是 obj,getter 函数中的 this 可能不会指向正确的对象const incorrectSecretValue = Reflect.get(obj, 'secret', {});console.log(incorrectSecretValue); // 输出: undefined,因为 _private 属性在 {} 上不存在

Reflect.get(target, key, receiver) 中的receiver可以简单理解为函数调用中的this,this由原始对象obj变成第三个参数receiver。

reactive代码片段

function reactive (obj) {return new Proxy (obj, {get (target, key, reveiver) {//代理对象通过raw属性访问原始数据if (key === 'raw') {return target}track(target, key)return Reflect.get (target, key, reveiver)},set (target, key, receiver) {const oldVal = target[key]const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'const res = Reflect.set(target, key, newVal, receiver)//target === receiver.raw 说明receiver就是target的代理对象if (target === receiver.raw) {if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {trigger(target, key, type)}}return res}})
}

只有当receiver是target的代理对象时才触发更新,这样就能屏蔽由原型引起的更新,从而避免不必要的更新操作。

浅响应和深响应

深响应:在get拦截函数里面加一个判断

const res = Reflect.get(target, key, receiver)
if (typeof res === 'object') && res !== null) {//调用reactive将结果包装成响应式数据并返回return reactive(res)
}

并非所有情况都需要深响应的,这就催生了shallowReactive,即浅响应。

//封装createReactive 函数,接受一个参数isShallow,代表是否浅响应,默认为false,即深响应
function createReactive(obj, isShallow = false) {return new Proxy(obj, {get(target, key, receiver) {//省略部分逻辑//新增一个判断:如果是浅响应,则直接返回原始值if (isShallow) {return res}}})
}

只读和浅只读

只读:拦截set和deleteProperty函数,如果是只读,则打印警告信息并返回。拦截get函数,并在get函数内递归调用readonly将数据包装成只读的代理对象,并将其返回。如果一个数据是只读,那么就没有必要为只读数据建立响应联系了。也不需要调用track函数追踪响应,不需要调用trigger函数执行想关联的副作用函数了。

function createReactive (obj, isShallow = false, isReadonly = false) {return new Proxy(obj, {get (target, key, receiver) {//省略其他逻辑的代码if (!isReadonly) {track(traget, key)}const res = Reflect.get (target, key, receiver)if (isShallow) {return res}if (typeof res === 'object' && res !== null) {//如果为只读,则调用readonly对值进行包装return isReadonly ? readonly(res) : reactive(res)}},set(target, key, receiver) {//省略其他逻辑代码if (isReadonly) {console.warn(`属性${key}是只读的`)return true}},deleteProperty(target, key) {//省略其他逻辑代码if (isReadonly) {console.warn(`属性${key}是只读的`)return true}}})
}
//深只读
fuunction readonly (obj) {return createReactive(obj, false, true)
}

浅只读:shallowReadonly,只需要修改createReadonly的第二个参数即可

function shallowReadonly(obj) {return createReactive(obj, true, true)
}

基本数据类型的响应式ref

javaScript中的proxy无法提供对基本数据类型的代理,对于基本数据类型的代理的解决办法:
用一个对象去包裹基本数据类型。

let str = '123'  //无法代理str//可以使用proxy代理wrapper,间接实现对基本数据类型的拦截
const wrapper = {str: '123'
}
const name = reactive(wrapper)

但是这样用户每创建一个响应式的基本数据类型,就要创建一个包裹对象。为此,我们可以封装ref函数

function ref(val) {const wrapper = {value: val}Object.defineProperty(wrapper, '__v_isRef', {value: true})return reactive(wrapper)
}

使用Object.defineProperty为包裹的wrapper定义一个不可以枚举且不可以写的属性__v_isRef,它的值为true,代表这是一个ref对象,而非普通对象。

响应丢失问题

用reactive定义的响应式数据,解构赋值后会变成普通数据,就会产生响应丢失的问题。
可以封装toRefs函数来解决此问题

//obj是响应式数据, key 是键值
function toRef(obj, key) {const wrapper = {get value(){return obj[key]},set value(val) {obj[key] = val}}Object.defineProperty(wrapper, '__v_isRef', {value: true})return wrapper
}function toRefs(obj) {const ret = {}//使用for...in 循环遍历对象for (const key in obj) {//调用toRef函数完成数据的转换ret[key] = toRef(obj, key)}return ret
}

ref数据模板自动解包

ref数据需要通过value属性访问值,在vue的模板中,会自动解包,可以直接调用。
实现思路:在get拦截函数中加一个判断,用__v_isRef属性判断,是ref数据就返回value.value

function proxyRefs(target) {return new Proxy(target, {get(target, key ,receiver) {const vlaue = Reflect.get(target, key, receiver)return value.__v_isRef ? value.value : value},set(target, key, receiver) {//通过target读取真实值const value = target[key]//如果是ref,则设置其对应的value属性if (value.__v_isRef) {value.value = newValuereturn true}return Reflect.set(target, key, newValue, receiver)}})
}

在编译模板的时候,组件中的setup函数返回的数据会传递给proxyRefs函数进行处理。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/26963.shtml

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

相关文章

电子制造业数字化整体解决方案

电子制造行业有特殊的着重点: 高精度要求:电子制造需要极高的精度和质量控制,因为电子组件和电路板的尺寸通常非常小,且对错误和缺陷非常敏感。 快速技术迭代:电子行业的技术迅速发展,产品生命周期短&…

passwall 自动切换节点

#! /bin/bashconf/etc/config/passwall # 获取所有节点 function getNodes(){f$1;retcat $f | grep "config nodes" | awk {print $3} | sed -r "s///g";echo $ret | tr " " " "; } # 移动旧的自动切换节点 function removeAutoSwitch…

第二节 单机版本redis部署

1. 部署环境 操作系统:centos7.XCPU: 2H内存:4GIP: 192.168.100.102部署版本: redis-7.0.15.tar.gz基础环境: gcc下载 2. 上传Redis安装包 [rootlocalhost opt]# ll 总用量 2932 drwxrwxr-x. 8 root root 4096 1…

刷题笔记2:用位运算找“只出现一次的一个数”

1. & 和 | 的基本操作 137. 只出现一次的数字 II - 力扣(LeetCode) 先对位运算的操作进行复习: 1、>> 右移操作符 移位规则:⾸先右移运算分两种: 1. 逻辑右移:左边⽤0填充,右边丢…

Python数据分析与建模库-02科学计算库Numpy01-05合集

1、该视频主要讲述了南派(NumPy)的核心操作和数据结构,以及如何使用NumPy库读取和处理数据。 2、该视频主要讲述了在编程中,如何对数组或矩阵中的元素进行判断和操作,以及在单排中如何进行类型转换。 3、该视频主要讲…

基于机器学习和奇异值分解SVD的电池剩余使用寿命预测(Python)

采用k-最近邻KNN和随机森林算法建立预测模型。 import pandas as pd from sklearn.model_selection import train_test_split from sklearn.svm import SVC # Support Vector Classifier from sklearn.preprocessing import StandardScaler from sklearn.metrics import accu…

【QT】QSettings读取中文乱码

解决QSettings读取中文乱码 QString sConfigFile sDataDir QDir::separator() "config" QDir::separator() "Info.ini";QSettings configFile(sConfigFile, QSettings::IniFormat);configFile.setIniCodec("utf-8");QSettings存储格式 Nat…

LLaMA Factory多卡微调的实战教程(持续更新)

大家好,我是herosunly。985院校硕士毕业,现担任算法研究员一职,热衷于机器学习算法研究与应用。曾获得阿里云天池比赛第一名,CCF比赛第二名,科大讯飞比赛第三名。拥有多项发明专利。对机器学习和深度学习拥有自己独到的见解。曾经辅导过若干个非计算机专业的学生进入到算法…

Apache HttpClient总览

一、重大版本 Apache HttpClient 4.x 系列 • HttpClient 4.0(发布于2008年左右):这是一个重要的里程碑,标志着HttpClient从Jakarta Commons项目转移到Apache HttpComponents项目。4.0版进行了大量的重构,引入了新…

【OpenVINO™】使用 OpenVINO™ C++ 异步推理接口部署YOLOv8 ——在Intel IGPU 上实现80+FPS视频推理

​ OpenVINO Runtime支持同步或异步模式下的推理。Async API的主要优点是,当设备忙于推理时,应用程序可以并行执行其他任务(例如,填充输入或调度其他请求),而不是等待当前推理首先完成。 当我们使用异步API…

ubuntu20.04设置共享文件夹

ubuntu20.04设置共享文件夹 一,简介二,操作步骤1,设置Windows下的共享目录2,挂载共享文件夹3,测试是否挂载成功 一,简介 在公司电脑上,使用samba设置共享文件夹,IT安全部门权限不通…

PythonWeb项目-Django+vue宾馆管理系统功能介绍

本项目源码:基于Python的Django-vue宾馆管理系统源码-参考文档资源-CSDN文库 项目关键技术 开发工具:Pycharm 编程语言: Python 数据库: MySQL5.7 框架:Django、vue 前端:Vue、ElementUI 关键技术:Django、vue、MYSQL…

【网络安全】【深度学习】【入侵检测】SDN模拟网络入侵攻击并检测,实时检测,深度学习【二】

文章目录 1. 习惯终端2. 启动攻击3. 接受攻击4. 宿主机查看h2机器 1. 习惯终端 上次把ubuntu 22自带的终端玩没了,治好用xterm: 以通过 AltF2 然后输入 xterm 尝试打开xterm 。 然后输入这个切换默认的终端: sudo update-alternatives --co…

全新取图系统搭建,广泛应用,轻松解决找图难问题!

前言 在数字化高速发展的时代,图片已成为人们日常交流不可或缺的一部分。每个社交平台我们都需要头像、背景等去打造属于我们自己的一张名片。为了满足大众日益增长的需求,并创造更多的收益机会,搭建一款先进的取图系统真的很必要。 一、这款…

Android FirebaseApp.initializeApp(this)无法初始化

记录一个奇葩问题 昨天把Android studio升级,迁移项目之后,爆出FirebaseApp.initializeApp(this)无法初始化的错误 在清楚缓存重启,重构项目,升级com.google.firebase:firebase-bom版本,修改初始化位置都失败后,把相关依赖从bom改为单独依赖 implementation("com.googl…

RabbitMQ概述

RabbitMQ RabbitMQ概述 RabbitMQ是一个开源的消息代理(message broker)系统,最初由Rabbit Technologies Ltd开发,并在开源社区的支持下不断发展和完善。它提供了强大的消息传递机制,被广泛应用于构建分布式系统和应用…

1058 选择题(测试点1)

solution 把题目设置为结构体,记录题目的总分,做错该题的人数,题目编号(从1开始),正确答案。对于输入的学生答案提取每道题的回答,与答案对比是否相等,若相等则该同学的分数加上这一…

私域流量转化不济的原因

你是不是也曾感到私域流量的转化一直不如意?让我来告诉你,这六大问题是为什么,以及如何轻松解决它们,提升你的私域流量转化率! 1. 问题:目标不明确 你是否常常感到茫然,不知道私域流量应该有何目…

算法笔记 图论和优先级队列的笔记

图论 DFS stack O(h) 不具有最短性 BFS queue O(2^h) 最短路 迪杰斯特拉算法 Dijkstra算法 初始化: 将起始节点 A 的距离设为 0。将其他所有节点的距离设为无穷大。创建一个优先队列,并将起始节点 A 加入优先队列。 处理队列…

易保全网络赋强公证系统,“公证赋强+科技赋能”双重增信

网络赋强公证系统是一种创新的法律服务模式,旨在通过线上方式赋予债权文书强制执行效力。具体来说,该系统结合了互联网技术与公证业务,允许公证机构根据当事人的申请,利用互联网公证技术手段对互联网上的债权文书进行公证&#xf…