手写Vue3响应式数据原理

news2025/1/16 4:00:30

Vue3响应式数据

  • 前言
  • 一、proxy是什么?
    • 1.1 proxy基本使用
  • 二、实现最基本的reactive函数
  • 三、实现基本响应式系统
  • 四、完善基本响应式系统
    • 4.1 执行每一个副作用函数
    • 4.2 实现依赖收集
      • 4.2.1 基本实现
    • 4.3 改进桶结构
  • 五、相关面试题
    • 1.Object.defineProperty 和 Proxy 的区别?
    • 2.vue2.0 和 vue3.0 有什么区别? 双向绑定更新?
    • 3.Vue 是如何实现数据双向绑定的?
    • 4.介绍下 Set、Map、WeakSet 和 WeakMap的区别?


前言

我们想要对一个对象数据进行处理,从而实现更改dom。但如何更改对一个对象数据进行更改呢?

vue2 的双向数据绑定是利⽤ES5 的⼀个 API ,Object.defineProperty()对数据进⾏劫持 结合 发布订阅模式的⽅式来实现的。

vue3 中使⽤了 ES6 的 ProxyAPI 对数据代理,通过 reactive() 函数给每⼀个对象都包⼀层 Proxy,通过 Proxy 监听属性的变化,从⽽ 实现对数据的监控。

这⾥是相⽐于vue2版本,使⽤proxy的优势如下:

    1. defineProperty只能监听某个属性,不能对全对象监听可以省去for…in…、闭包等内容来提升效率(直接绑定整个对象即可)
    1. 监听数组,不⽤再去单独的对数组做特异性操作,通过Proxy可以直接拦截所有对象类型数据的操作,完美⽀持对数组的监听。

我们想要知道如何实现Vue3响应式数据,就要知道proxy这个概念。


一、proxy是什么?

Proxy(代理)是一种计算机网络技术,其作用是充当客户端和服务器之间的中间人,转发网络请求和响应。当客户端发送请求时,代理服务器会接收并转发请求到目标服务器,然后将服务器返回的响应转发给客户端。

相当于明星和经纪人,想要找明星办事,需要找他的经纪人,明星的事都交给经纪人做。明星就是源对象,经纪人就相当于proxy。

proxy用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

1.1 proxy基本使用

// 定义一个源对象
let obj = {
	name: 'qx',
	age: 24
}
// 实现一个Proxy,传入要代理的对象和get和set方法
const proxy =  new Proxy(obj, {
	// get中返回代理对象的,target代表源对象(也是上面的obj),key代表obj中每个属性
	get(target, key) {
		return target[key];
	},
	// set中返回代理对象的,target代表源对象(也是上面的obj),key代表obj中每个属性,value是修改的新值
	set(target, key, value) {
		target[key] = value
		return true
	}
})
console.log(proxy)

obj.name = 'xqx'
// 现在打印的是修改后的proxy,看看会变成什么样?  已经修改好了
console.log(proxy)

在这里插入图片描述

二、实现最基本的reactive函数

reactive 用于创建一个响应式对象,该对象可以包含多个属性和嵌套属性。当使用 reactive 创建响应式对象时,返回的对象是一个代理对象,该对象具有与原始对象相同的属性,并且任何对代理对象属性的更改都将触发组件的重新渲染。

既然我们已经知道reactive是个函数,并且返回的是一个代理对象,先把最基本的框架搭出来

function reactive(data) {
	return new Proxy(data, {
		get(target, key) {
			return target[key];
		},
		set(target, key, value) {
			target[key] = value
			return true
		}
	})
}

看似已经完成了,但是当传入非对象时,却报错

提示这个对象是对象类型的,例如数组之类的,并只是{}这个。

const arr = true;
console.log(reactive(arr))

在这里插入图片描述
提示proxy要传入一个对象,所以需要先判断是不是对象

function reactive(data) {
	//判断是不是对象,null也是object要排除
	if(typeof data === Object && data !== null) return 
	
	return new Proxy(data, {
		get(target, key) {
			return target[key];
		},
		set(target, key, value) {
			target[key] = value
			return true
		}
	})
}

三、实现基本响应式系统

我们知道处理数据就是为了让视图更新,但一个系统离不开副作用函数。

副作用函数,顾名思义,会产生副作用的函数被称为副作用函数。通俗来说,就是这个函数可以影响其他的变量。

来看最基本的副作用函数

<div id="app"></div>
<script>
	let obj = {
		name: 'qx'
	}
	function effect(){
		app.innerText = obj.name
	}
	
	effect()
</script>

现在我们需要通过前面reactive函数来完善一个基本的响应式系统

<body>
	<div id="app"></div>
	<script>
		let obj = {
			name: 'qx',
			age: 24
		}
		function reactive(data) {
			if(typeof data === Object && data !== null) return 
			return new Proxy(data, {
				get(target, key) {
					return target[key];
				},
				set(target, key, value) {
					target[key] = value
					return true
				}
			})
		}
		const state = reactive({name:'xqx'});
		
		function effect(){
			app.innerText = state.name
		}
		
		effect()
	</script>
</body>

在这里插入图片描述
到现在一个最基本的响应式系统出现

四、完善基本响应式系统

如果多个副作用函数同时引用一个变量,我们需要当变量改变时,每一个副作用函数都要执行。

4.1 执行每一个副作用函数

可以把多个副作用函数放在一个列表里,在每次对对象操作时,执行proxy中的set方法时,对每一个副作用函数进行遍历。

<body>
	<div id="app"></div>
	<script>
		let obj = {name: 'qx'}
		let effectBucket = [];
		
		function reactive(data) {
			if(typeof data === Object && data !== null) return 
			return new Proxy(data, {
				get(target, key) {
					return target[key];
				},
				set(target, key, value) {
					target[key] = value
					effectBucket.forEach(fn=>fn())
					return true
				}
			})
		}
		
		const state = reactive({name:'xqx'});
		
		function effect(){
			app.innerText = state.name
			console.log('副作用函数1被执行')
		}
		effectBucket.push(effect)
		
		function effect1(){
			app.innerText = state.name
			console.log('副作用函数2被执行')
		}
		effectBucket.push(effect1)
		
		state.name = 'zs'
	</script>
</body>

在这里插入图片描述

但是我们要是传两个同样的副作用函数怎么办。

function effect(){
	app.innerText = state.name
	console.log('副作用函数1被执行')
}
effectBucket.push(effect)
effectBucket.push(effect)

在这里插入图片描述
发现列表里有两个重复的effect函数,如果列表很长,foreach也会浪费时间,那么大大浪费性能。es6有个Set数据结构可以帮助我们解决这个问题。

let effectBucket = new Set();

const state = reactive({name:'xqx'});

function effect(){
	app.innerText = state.name
	console.log('副作用函数1被执行')
}
effectBucket.add(effect)  //添加两次
effectBucket.add(effect)

function effect1(){
	app.innerText = state.name
	console.log('副作用函数2被执行')
}
effectBucket.add(effect1)

console.log(effectBucket)

我们把effect添加两次,看看结果是什么样的
在这里插入图片描述

4.2 实现依赖收集

前面我们只是对一个对象中的属性进行处理,如果多个属性都要更改呢?我们以上的操作会让每一个副作用函数都执行。

假设我们有这样一个结构

let obj = {name: 'qx',age:24}

我想改name属性时,只更新有name的副作用函数,不必把列表里所有副作用函数都更新。这就需要依赖收集。

4.2.1 基本实现

对每一个副作用函数进行一个保存,当调用副作用函数时,会执行proxy中的get方法,在get方法把当前副作用函数添加列表,就实现了当前依赖属性和副作用函数关联在一起。

具体实现步骤如下:

let obj = {name: 'qx',age:24}
let effectBucket = new Set();

let activeEffect = null;   //1.保存当前的副作用函数状态

function reactive(data) {
	if(typeof data === Object && data !== null) return 
	return new Proxy(data, {
		get(target, key) {
			if(activeEffect != null){              //4. 将当前保存的副作用函数添加到副作用函数列表中
				effectBucket.add(activeEffect)  
			}
			return target[key];
		},
		set(target, key, value) {
			target[key] = value
			effectBucket.forEach(fn=>fn())
			return true
		}
	})
}
const state = reactive(obj);
function effectName(){
	console.log('副作用函数1被执行',state.name)
}
activeEffect = effectName()  // 2.将当前副作用函数赋值给activeEffect 
effectName()                 // 3.调用副作用函数,相当于访问proxy的get方法
activeEffect = null;         // 5.将副作用函数状态置空,给下一个副作用函数用

function effectAge(){
 	console.log('副作用函数2被执行',state.age)
}
activeEffect = effectAge()
effectAge()
activeEffect = null;

state.name = 'zs'

在这里插入图片描述
再简化一下,对上面重复代码进行一个封装。调用的时候直接调封装后的方法

function registEffect(fn) {
	if (typeof fn !== 'function') return;
	activeEffect = fn();
	fn();
	activeEffect = null;
}

4.3 改进桶结构

Set结构像数组,只是能做到去重,并不能实现不同属性对应不同集合。需要我们改进成一个属性对应多个集合。

另一个数据结构Map出现在面前,它是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。

创建一个这样的结构。

let a = {
	name: Set(fn,fn),
	age:Set(fn,fn)
}

在这里插入图片描述

let effectBucket = new Map();  //{name:Set(fn,fn),age:Set(fn,fn)}
let activeEffect = null;

function reactive(data) {
	if (typeof data === Object && data !== null) return
	return new Proxy(data, {
		get(target, key) {
			if (activeEffect !== null) {
				let deptSet;
				if(!effectBucket.get(key)){                       //没有得到key,说明没有添加过
					deptSet = new Set();            //重新创建一个集合
					effectBucket.set(key,deptSet);  //每次添加一个属性{name:Set(fn,fn)}结构
				}
				deptSet.add(activeEffect)
			}
			return target[key];
		},
		set(target, key, value) {
			target[key] = value
			//从副作用桶中依次取出每一个副作用函数执行
			let deptSet = effectBucket.get(key);
			if(deptSet){                   
				deptSet.forEach(fn => fn())
			}
			return true
		}
	})
}

继续封装收集依赖
get

function track(target, key) {
	if (!activeEffect) return
	let deptSet;
	if (!effectBucket.get(key)) { //没有得到key,说明没有添加过
		deptSet = new Set(); //重新创建一个集合
		effectBucket.set(key, deptSet);
	}
	deptSet.add(activeEffect)
}

set

function trigger(target, key) {
	let deptSet = effectBucket.get(key);
	if (deptSet) {
		deptSet.forEach((fn) => fn())
	}
}

在这里插入图片描述

function track(target, key) {
	if (!activeEffect) return
	let deptMap =effectBucket.get(key);
	if (!deptMap) { //没有得到key,说明没有添加过
		deptMap = new Map(); //重新创建一个集合
		effectBucket.set(target, deptMap);
	}
	let depSet = deptMap.get(key)
	if(!depSet){
		depSet = new Set()
		deptMap.set(key,depSet)
	}
	deptSet.add(activeEffect)
}

function trigger(target, key) {
	let depMap = effectBucket.get(target)
	if(!depMap) return
	let deptSet = effectBucket.get(key);
	if (deptSet) {
		deptSet.forEach((fn) => fn())
	}
}

五、相关面试题

1.Object.defineProperty 和 Proxy 的区别?

  1. Proxy 可以直接监听对象而非属性;
  2. Proxy 可以直接监听数组的变化;
  3. Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等 是 Object.defineProperty 不具备的;
  4. Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty 只能遍历对象属性直接修改
  5. Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准 的性能红利
  6. Object.defineProperty 兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题, 而且无法用 polyfill 磨平,因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重 写

2.vue2.0 和 vue3.0 有什么区别? 双向绑定更新?

vue2 的双向数据绑定是利⽤ES5 的⼀个 API ,Object.defineProperty()对数据进⾏劫持 结合 发布订阅模式的⽅式来实现的。

vue3 中使⽤了 ES6 的 ProxyAPI 对数据代理,通过 reactive() 函数给每⼀个对象都包⼀层 Proxy,通过 Proxy 监听属性的变化,从⽽ 实现对数据的监控。

这⾥是相⽐于vue2版本,使⽤proxy的优势如下:

  1. defineProperty只能监听某个属性,不能对全对象监听 可以省去for in、闭包等内容来提升效率(直接绑定整个对象即可)

  2. 监听数组,不⽤再去单独的对数组做特异性操作,通过Proxy可以直接拦截所有对象类型数据的操作,完美⽀持对数组的监听。

获取props

vue2在script代码块可以直接获取props,vue3通过setup指令传递

API不同

Vue2使⽤的是选项类型API(Options API),Vue3使⽤的是合成型API(Composition API)

建立数据data

vue2是把数据放入data中,vue3就需要使用一个新的setup()方法,此方法在组件初始化构造得时候触发。

生命周期不同

vue2vue3
beforeCreatesetup() 开始创建组件之前,创建的是data和method
createdsetup()
beforeMountonBeforeMount 组件挂载到节点上之前执行的函数
mountedonMounted 组件挂载完成后执行的函数
beforeUpdateonBeforeUpdate 组件更新之前执行的函数
updatedonUpdated 组件更新完成之后执行的函数
beforeDestroyonBeforeUnmount 组件挂载到节点上之前执行的函数
destroyedonUnmounted 组件卸载之前执行的函数
activatedonActivated 组件卸载完成后执行的函数
deactivatedonDeactivated

关于v-if和v-for的优先级:

vue2 在一个元素上同时使用 v-if 和 v-for v-for会优先执行

vue3 v-if 总会优先于 v-for生效

vue2和vue3的diff算法

vue2

vue2 diff算法就是进行虚拟节点对比,并返回一个patch对象,用来存储两个节点 不同的地方,最后用patch记录的消息去局部更新Dom。

vue2 diff算法会比较每一个vnode,而对于一些不参与更新的元素,进行比较是有 点消耗性能的。

vue3

vue3 diff算法在初始化的时候会给每个虚拟节点添加一个patchFlags,patchFlags 就是优化的标识。

只会比较patchFlags发生变化的vnode,进行更新视图,对于没有变化的元素做静 态标记,在渲染的时候直接复用。

3.Vue 是如何实现数据双向绑定的?

Vue 数据双向绑定主要是指:数据变化更新视图

  • 输入框内容变化时,Data 中的数据同步变化。即 View => Data 的变化。
  • Data 中的数据变化时,文本节点的内容同步变化。即 Data => View 的变化

Vue 主要通过以下 4 个步骤来实现数据双向绑定的:

  • 第一步:需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。

  • 第二步:compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。

  • 第三步:Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情是:

    • 1、在自身实例化时往属性订阅器(dep)里面添加自己
    • 2、自身必须有一个 update()方法
    • 3、待属性变动 dep.notice()通知时,能调用自身的 update()方法,并触发 Compile中绑定的回调,则功成身退。
  • 第四步:MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据 model 变更的双向绑定效果。

4.介绍下 Set、Map、WeakSet 和 WeakMap的区别?

Set
Set是一种叫做集合的数据结构,是由一堆无序的、相关联的,且不重复的内存结构组成的组合。集合是以[值,值]的形式存储元素

  1. 成员不能重复;
  2. 只有键值,没有键名,有点类似数组;
  3. 可以遍历,方法有 add、delete、has、clear

WeakSet

  1. WeackSet只能成员只能是引用类型,而不能是其他类型的值;
  2. 成员都是弱引用,随时可以消失(不计入垃圾回收机制)。可以用来保存 DOM 节点,不容易造成内存泄露;
  3. 不能遍历,没有size属性,方法有 add、delete、has ;

Map
Map是一种叫做字典的数据结构,每个元素有一个称作key 的域,不同元素的key 各不相同。字典是以[键,值]的形式存储。

  1. 本质上是键值对的集合,类似集合。Map的键可以时任何类型数据,就连函数都可以。;
  2. 可以遍历,方法很多,可以跟各种数据格式转换;
  3. Map的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键;

WeakMap

  1. 只接收对象为键名(null 除外),不接受其他类型的值作为键名;
  2. 键名指向的对象,不计入垃圾回收机制;
  3. 不能遍历,没有clear清空方法,方法同 get、set、has、delete ;

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

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

相关文章

AMBA_AXI Protocol_Transaction Identifiers

AXI Protocol_Transaction Identifiers Transaction Identifiers&#xff08;事务标识符&#xff09; 1. AXI事务标识符简介 2. ID信号. 读数据顺序. 写数据顺序. 使用事务标识符的互联1. AXI事务标识符简介 AXI协议包含AXI事务标识符&#xff08;AXI ID&#xff09;&#x…

2023-8-22 双链表

题目链接&#xff1a;双链表 #include <iostream>using namespace std;const int N 100010;int e[N], l[N], r[N], idx;void init() {r[0] 1;l[1] 0;idx 2; }void add(int k, int x) {e[idx] x;r[idx] r[k];l[idx] k;l[r[k]] idx;r[k] idx;idx ; }void remove…

前端需要理解的HTML知识

HTML&#xff08;超文本标记语言&#xff0c;HyperText Markup Language&#xff09;不是编程语言&#xff0c;而是定义了网页内容的含义和结构的标记语言。。“超文本”&#xff08;hypertext&#xff09;是指连接单个网站内或多个网站间的网页的链接。HTML 使用“标记”&…

BootstrapBlazor组件使用:数据注解

文章目录 前言BB数据注解数据注解源码数据注解简介注解简单实例[BB 编辑弹窗](https://www.blazor.zone/edit-dialog)[ValidateForm 表单组件](https://www.blazor.zone/validate-form)使用简介 前言 BootstrapBlazor(一下简称BB)是个特别好用的组件&#xff0c;基本上满足了大…

bh003- Blazor hybrid / Maui 使用蓝牙BLE快速教程

1. 建立工程 bh003_ble 源码 2. 添加 nuget 包 <PackageReference Include"BlazorHybrid.Maui.Permissions" Version"0.0.2" /> <PackageReference Include"BootstrapBlazor" Version"7.*" /> <PackageReference In…

机器学习笔记之优化算法(十九)经典牛顿法的收敛性分析

机器学习笔记之优化算法——经典牛顿法的收敛性分析 引言回顾&#xff1a;算法的收敛性分析 Wolfe \text{Wolfe} Wolfe准则的收敛性分析梯度下降法在凸函数的收敛性分析梯度下降法在强凸函数的收敛性分析 经典牛顿法的收敛性分析收敛性定理介绍证明过程关于隐含条件的说明 引言…

Spring之域对象共享数据

文章目录 前言一、requset域1.使用ServletAPI向request域对象共享数据2.使用ModelAndView向request域对象共享数据3.使用Model向request域对象共享数据4.使用map向request域对象共享数据5.使用ModelMap向request域对象共享数据6.Model、ModelMap、Map的关系 二、session域向ses…

语谱图(一) Spectrogram 的定义与机理

1. 语谱图 spectrogram 在音频、语音信号处理领域&#xff0c;我们需要将信号转换成对应的语谱图(spectrogram)&#xff0c;将语谱图上的数据作为信号的特征。 语谱图的横坐标是时间&#xff0c;纵坐标是频率&#xff0c;坐标点值为语音数据能量。由于是采用二维平面表达三维…

UE4 材质学习笔记

CheapContrast与CheapContrast_RGB都是提升对比度的&#xff0c;一个是一维输入&#xff0c;一个是三维输入&#xff0c;让亮的地方更亮&#xff0c;暗的地方更暗&#xff0c;不像power虽然也是提升对比度&#xff0c;但是使用过后的结果都是变暗或者最多不变&#xff08;值为1…

国标视频云服务平台EasyGBS国标平台内网访问正常但公网无法访问的问题解决方案

国标视频云服务平台EasyGBS可支持通过国标GB28181协议&#xff0c;接入多路视频源设备&#xff0c;实现视频流的接入、转码、处理与分发等功能&#xff0c;对外输出的视频流格式包括RTSP、RTMP、FLV、HLS、WebRTC等。平台视频能力丰富灵活&#xff0c;包括监控直播、视频分发、…

Maven 配置文件修改及导入第三方jar包

设置java和maven的环境变量 修改maven配置文件 &#xff08;D:\app\apache-maven-3.5.0\conf\settings.xml&#xff0c;1中环境变量对应的maven包下的conf&#xff09; 修改131行左右的mirror&#xff0c;设置阿里云的仓库地址 <mirror> <id>alimaven</id&g…

如何选择合适的量化交易服务器

数量技术宅团队在CSDN学院推出了量化投资系列课程 欢迎有兴趣系统学习量化投资的同学&#xff0c;点击下方链接报名&#xff1a; 量化投资速成营&#xff08;入门课程&#xff09; Python股票量化投资 Python期货量化投资 Python数字货币量化投资 C语言CTP期货交易系统开…

sql入门-多表查询

案例涉及表 ----------------------------------建表语句之前翻看之前博客文章 多表查询 -- 学生表 create table studen ( id int primary key auto_increment comment id, name varchar(50) comment 姓名, no varchar(10) comment 学号 ) comment 学生表; insert…

ES基础操作

1.创建索引 在 Postman 中&#xff0c;向 ES 服务器发 PUT 请求 &#xff1a; http://127.0.0.1:9200/shopping 后台日志 重复发送 PUT 请求添加索引 &#xff1a; http://127.0.0.1:9200/shopping &#xff0c;会返回错误信息 : 2.获取单个索引相关信息 在 Postman 中&#…

【SpringSecurity】三、访问授权

文章目录 1、配置用户权限2、针对URL授权3、针对方法的授权 1、配置用户权限 继续上一章&#xff0c;给在内存中创建两个用户配置权限。配置权限有两种方式&#xff1a; 配置roles配置authorities //哪个写在后面哪个起作用 //角色变成权限后会加一个ROLE_前缀&#xff0c;比…

Flask狼书笔记 | 03_模板

文章目录 3 模板3.1 模板基本使用3.2 模板结构组织3.3 模板进阶 3 模板 模板&#xff08;template&#xff09;&#xff1a;包含固定内容和动态部分的可重用文件。Jinja2模板引擎可用于任何纯文本文件。 3.1 模板基本使用 HTML实体&#xff1a;https://dev.w3.org/html5/htm…

启动Vue项目踩坑记录

前言 在启动自己的Vue项目时&#xff0c;遇到一些报错&#xff0c;当时很懵&#xff0c;解决了以后豁然开朗&#xff0c;特写此博客记录一下。 一、<template>里多加了个div标签 [vite] Internal server error: At least one <template> or <script> is req…

EureKa快速入门

EureKa快速入门 远程调用的问题 多个服务有多个端口&#xff0c;这样的话服务有多个&#xff0c;硬编码不太适合 eureKa的作用 将service的所有服务的端口全部记录下来 想要的话 直接从注册中心查询对于所有服务 每隔一段时间需要想eureKa发送请求 保证服务还存活 动手实践 …

odoo安装启动遇到的问题

问题&#xff1a;在第一次加载odoo配置文件的时候&#xff0c;启动失败 方法&#xff1a; 1、先检查odoo.conf的内容&#xff0c;尤其是路径 [options] ; This is the password that allows database operations: ; admin_passwd admin db_host 127.0.0.1 db_port 5432 d…

kotlin协程flow任务意外结束未emit数据retryWhen onEmpty(5)

kotlin协程flow任务意外结束未emit数据retryWhen onEmpty&#xff08;5&#xff09; import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeoutOrNullfun main(args: Array<String&…