【Vue.js设计与实现】第二篇:响应系统-阅读笔记(持续更新)

从高层设计的角度去探讨框架需要关注的问题。

系列目录:

标题博客
第一篇:框架设计概览【Vue.js设计与实现】第一篇:框架设计概览-阅读笔记
第二篇:响应系统【Vue.js设计与实现】第二篇:响应系统-阅读笔记
第三篇:渲染器【Vue.js设计与实现】第三篇:渲染器-阅读笔记
第四篇:组件化【Vue.js设计与实现】第四篇:组件化-阅读笔记
第五篇:编译器【Vue.js设计与实现】第五篇:编译器-阅读笔记
第六篇:服务端渲染【Vue.js设计与实现】第六篇:服务端渲染-阅读笔记

第二篇 响应系统

  • 第 4 章 响应系统的作用与实现
  • 第 5 章 非原始值的响应式方案
  • 第 6 章 原始值的响应式方案

文章目录

    • 第四章:响应系统的作用与实现
      • 4.1 响应式数据与副作用函数
      • 4.2 响应式数据的基本实现
      • 4.3 设计一个完善的响应系统
      • 4.4 分支切换与 cleanup
      • 4.5 嵌套的 effect 与 effect 栈
      • 4.6 避免无限递归循环
      • 4.7 调度执行
      • 其他
      • 响应式数据
      • 调度系统
      • 计算属性
      • 懒惰执行
      • watch的实现原理
      • 过期的副作用
    • 第七章:渲染器的设计
    • 第八章:挂载与更新
      • DOM节点操作
      • 属性节点操作
      • 事件

第四章:响应系统的作用与实现

4.1 响应式数据与副作用函数

副作用函数,即会产生副作用的函数。

如:effect 函数的执行会直接或间接影响其他函数的执行,如修改了全局变量,它就是一个副作用函数。

function effect() {
	document.body.innerHTML = obj.text;
}

响应式数据:若obj.text变化了,我们希望副作用函数effect 重新执行,如果能实现这个目标,就是响应式。

const obj = { text: "hello world" };

function effect() {
	document.body.innerHTML = obj.text;
}

4.2 响应式数据的基本实现

如何让obj实现响应式?我们发现:

  • 当副作用函数 effect 执行时,会触发字段 obj.text 的读取操作
  • 当修改 obj.text 的值时,会触发字段 obj.text 的设置操作

也就是说,我们可以通过拦截一个对象的读取和设置操作来实现响应式:

  • 设置一个桶,来存放与响应式数据相关的副作用函数
  • 读取数据时,把与这个响应式数据相关的副作用函数放进一个桶里
  • 设置数据时(即修改数据时),桶里所有的函数都是与这个响应式数据相关的函数,因此 执行桶中所有函数

在这里插入图片描述
在这里插入图片描述

拦截一个对象属性的读取和设置操作,在vue2用Object.defineProperty实现,在vue3用代理对象Proxy实现。

我们根据上述思路,简单地用proxy实现响应式数据:

桶与响应式数据:

// 存储副作用函数的桶
const bucket = new Set();

// 原始数据
const data = { text: "hello world" };
// 对原始数据的代理
const obj = new Proxy(data, {
	// 拦截读取操作
	get(target, key) {
		// 将副作用函数 effect 添加到存储副作用函数的桶中
		bucket.add(effect);
		// 返回属性值
		return target[key];
	},
	// 拦截设置操作
	set(target, key, newVal) {
		// 设置属性值
		target[key] = newVal;
		// 把副作用函数从桶里取出并执行
		bucket.forEach((fn) => fn());
		// 操作成功
		return true;
	},
});

具体调用:

// 副作用函数
function effect() {
	document.body.innerHTML = obj.text;
}

// 执行副作用函数,触发读取
effect();

 // 1 秒后修改响应式数据
setTimeout(() => {
	obj.text = "hello vue3";
}, 1000);

在浏览器中运行,可以看到字符串的闪动。

4.3 设计一个完善的响应系统

由上一节可知,一个响应系统的工作流程如下:

  • 读取操作发生时,将副作用函数收集到“桶”中;
  • 设置操作发生时,从“桶”中取出副作用函数并执行

但是,上一节的操作有些缺陷:我们硬编码了副作用函数的名字(effect),导致一旦副作用函数的名字不叫 effect,那么这段代码就不能正确地工作了。我们希望,哪怕副作用函数是一个匿名函数,也能够被正确地收集到“桶”中。因此,我们需要提供用来注册副作用函数的机制

// 用一个全局变量存储被注册的副作用函数
let activeEffect;

// effect 函数用于注册副作用函数
function effect(fn) {
	activeEffect = fn;
	fn();
}

这样一来,就可以使用一个匿名的副作用函数作为 effect 函数的参数。 响应系统就不依赖副作用函数的名字了

const obj = new Proxy(data, {
	get(target, key) {
        // 新增
		if(activeEffect){
            bucket.add(activeEffect)
        }

		return target[key];
	},

	set(target, key, newVal) {
		target[key] = newVal;
		bucket.forEach((fn) => fn());
		return true;
	},
});

然而,上述代码还有缺陷:假设我们给obj的其他字段赋值,也会触发proxyset,这是不对的,我们的初衷是只obj.text有响应式。这个问题的根本原因:没有在副作用函数与被操作的目标字段之间建立明确的联系。

解决方法:重新设计“桶”的数据结构。

观察下面代码:

function effect() {
	document.body.innerHTML = obj.text;
}

此副作用函数中有3个角色:

  • 被读取的代理对象obj
  • 被读取的字段text
  • 副作用函数effect

如果用 target 来表示一个代理对象所代理的原始对象,用 key 来表示被操作的字段名,用 effectFn 来表示被注册的副作用函数,则他们的数据结构为树形数据结构,关系如下:

target
	-- key
		--effectFn

举个例子:

两个副作用函数同时读取同一个对象的属性值

target
	-- key
		--effectFn1
		--effectFn2

一个副作用函数中读取了一个对象的两个属性

target
	-- key1
		--effectFn
	-- key2
		--effectFn

不同的副作用函数中读取了两个不同对象的不同属性

target1
	-- key1
		--effectFn1
		
target2
	-- key2
		--effectFn2

构建数据结构与代码:

  • 桶:WeakMap 由 target --> Map 构成
  • 桶里的target: Map 由 key --> Set 构成
const bucket = new WeakMap();

const obj = new Proxy(data, {
	get(target, key) {
		if (!activeEffect) return target[key];

		// key -> effects
		let depsMap = bucket.get(target);
		if (!depsMap) {
			bucket.set(target, (depsMap = new Map()));
		}

		// deps 存储所有与[target,key]相关的副作用函数
		let deps = depsMap.get(key);
		if (!deps) {
			depsMap.set(key, (deps = new Set()));
		}

		deps.add(activeEffect);

		return target[key];
	},
	set(target, key, newVal) {
		target[key] = newVal;
		const depsMap = bucket.get(target);
		if (!depsMap) return;

		const effects = depsMap.get(key);

		effects && effects.forEach((effect) => effect());
	},
});

WeakMap 的键是原始对象 target,WeakMap 的值是一个Map 实例,而 Map 的键是原始对象 target 的 key,Map 的值是一个由副作用函数组成的 Set。

我们把下图中 Set 数据结构所存储的副作用函数集合称为 key 的依赖集合

在这里插入图片描述
为什么桶要使用WeakMap?

const map = new Map();
const weakMap = new WeakMap()

(function () {
	const foo = { foo: 1 };
	const bar = { bar: 2 };

	map.set(foo, 1);
	weakMap.set(bar, 2);
})()

上述代码定义了一个立即执行的函数表达式(IIFE),当它执行完时,map的key 强引用 foo,则它不会被垃圾回收器回收;而WeakMap的 key 是弱引用,垃圾回收器就会把对象 bar 从内存中移除。

简单地说,WeakMap 对 key 是弱引用,不影响垃圾回收器的工作。 常用于存储那些只有当 key 所引用的对象存在时(没有被回收)才有价值的信息,如上述场景:若 target 对象没有引用了,说明用户不需要它了,此时垃圾回收器会完成回收任务。

如果使用 Map 来代替 WeakMap, 那么即使用户的代码对 target 没有任何引用,这个 target 也不会被回收,最终可能导致内存溢出

最后,把收集、触发副作用函数封装到tracktrigger中:

const bucket = new WeakMap();

const obj = new Proxy(data, {
	get(target, key) {
		track(target, key);
		return target[key];
	},
	set(target, key, newVal) {
		target[key] = newVal;
        trigger(target, key)
	},
});

function track(target, key) {
	if (!activeEffect) return;

	let depsMap = bucket.get(target);
	if (!depsMap) {
		bucket.set(target, (depsMap = new Map()));
	}

	let deps = depsMap.get(key);
	if (!deps) {
		depsMap.set(key, (deps = new Set()));
	}

	deps.add(activeEffect);
}

function trigger(target, key) {
	const depsMap = bucket.get(target);
	if (!depsMap) return;

	const effects = depsMap.get(key);

	effects && effects.forEach((effect) => effect());
}

4.4 分支切换与 cleanup

分支切换:当字段 obj.ok 的值发生变化时,代码执行的分支会跟着变化,这就是分支切换

document.body.innerHTML = obj.ok ? obj.text : "not";

分支切换可能会产生遗留的副作用函数。obj.ok的初始值为true时,会读取字段obj.text,则副作用函数与响应式数据之间的联系如下:

data
	--ok
		--effectFn
	--text
		--effectFn
	

在这里插入图片描述

当obj.ok的值为false时,obj.text不会被读取,此时副作
用函数 effectFn 应该被字段 obj.text 所对应的依赖集合收集:

在这里插入图片描述

然而,在obj.ok改为false时,我们并没有删除key为text的副作用函数,这就产生了遗留的副作用函数

遗留的副作用函数会导致不必要的更新。即,会调用不必要的副作用函数。举个例子:

effect(function effectFn(){
    document.body.innerHTML = obj.ok ? obj.text : "not";
})

若此时obj.ok为false,但修改了obj.text的值,副作用函数仍然会执行,尽管我们已经不再读取obj.text了。

解决这个问题的方法:每次副作用函数执行时,我们可以 先把它从所有与之关联的依赖集合中删除。当副作用函数执行完毕后,会重新建立联系,但在新的联系中不会包含遗留的副作用函数。如图:

在这里插入图片描述

要将一个副作用函数从所有与之关联的依赖集合中移除,就需要知道哪些依赖集合中包含它,即,需要一个数组来存储包含当前副作用函数的依赖集合

effectFn 添加一个属性deps ,用来存储会调用此副作用函数的依赖集合。

function effect(fn) {
	const effectFn = () => {
		activeEffect = effect;
		fn();
	};

	// 存储所有与该副作用函数相关联的依赖集合
	effectFn.deps = [];
	effectFn();
}

track中添加:

function track(target, key) {
	if (!activeEffect) return;

	let depsMap = bucket.get(target);
	if (!depsMap) {
		bucket.set(target, (depsMap = new Map()));
	}

	let deps = depsMap.get(key);
	if (!deps) {
		depsMap.set(key, (deps = new Set()));
	}

	deps.add(activeEffect);

	// 新增
	activeEffect.deps.push(deps);
}

在副作用函数执行时,将包含此副作用函数的依赖集合中的此函数移除:

function effect(fn) {
	const effectFn = () => {
		// 调用 cleanup 函数完成清除工作
		cleanup(effectFn);
		activeEffect = effect;
		fn();
	};

	// 存储所有与该副作用函数相关联的依赖集合
	effectFn.deps = [];
	effectFn();
}

function cleanup(effectFn) {
	for (let i = 0; i < effectFn.deps.length; i++) {
		const deps = effectFn.deps[i];
		deps.delete(effectFn);
	}
	// 重置数组
	effectFn.deps.length = 0;
}

到这里,我们已经清除了遗留的副作用函数。然而,运行此代码,会发现:出现了死循环。问题出现在trigger 函数中:

function trigger(target, key) {
	const depsMap = bucket.get(target);
	if (!depsMap) return;

	const effects = depsMap.get(key);

    // 问题出现在这里
	effects && effects.forEach((effect) => effect());
}

原因:

在调用 forEach 遍历 Set 集合时,如果一个值已经被访问过了,但该值被删除并重新添加到集合,如果此时 forEach 遍历没有结束,那么该值会重新被访问

因此,想要解决这个问题,我们可以创建一个新的set集合并遍历它:

function trigger(target, key) {
	const depsMap = bucket.get(target);
	if (!depsMap) return;

	const effects = depsMap.get(key);

	// 新增
	const effectsToRun = new Set(effects);
	effectsToRun.forEach((effect) => effect());
}

4.5 嵌套的 effect 与 effect 栈

上述代码在effect嵌套时会出错,原因是:

同一时刻 activeEffect 所存储的副作用函数只能有一个。当副作用函数发生嵌套时,内层副作用函数的执行会覆盖 activeEffect 的值,并且永远不会恢复到原来的值。

解决方法:需要一个副作用函数栈 effectStack,在副作用函数执行时,将当前副作用函数压入栈中,执行完毕后将其从栈中弹出,并始终让 activeEffect 指向栈顶的副作用函数。

let activeEffect;
const effectStack = [];

function effect(fn) {
	const effectFn = () => {
		cleanup(effectFn);
        
		activeEffect = effectFn;
		effectStack.push(effectFn);

		fn();

		effectStack.pop();
		activeEffect = effectStack[effectStack.length - 1];
	};

	effectFn.deps = [];
	effectFn();
}

4.6 避免无限递归循环

如下代码会无限递归循环:

const data = { foo: 1 };
const obj = new Proxy(data, {
	//
});

effect(() => obj.foo++);

原因是:obj.foo++语句,会读取也会设置,因此它会触发tracktrigger,不断地添加副作用函数。

解决方法:可以在 trigger 动作发生时增加守卫条件:如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行。

function trigger(target, key) {
	const depsMap = bucket.get(target);
	if (!depsMap) return;

	const effects = depsMap.get(key);
    // 新增
	const effectsToRun = new Set();

	effects &&
		effects.forEach((effect) => {
			if (effect !== activeEffect) {
				effectsToRun.add(effect);
			}
		});

	effectsToRun.forEach((effect) => effect());
}

4.7 调度执行


其他

响应式数据

先了解两个概念:副作用函数和响应式数据。

副作用函数:会产生副作用的函数。

如:

let val=1

function effect(){
	val=2
}

这个函数改变了全局变量val,产生了副作用,因此是副作用函数。

而如果val这个数据的变化改变了视图的变化,那么val就称作响应式数据

想要实现响应式数据,需要依赖两个行为:

  • getter:数据读取
  • setter:数据修改

vue2中这两个行为通过Object.defineProperty实现
vue3中这两个行为通过Proxy实现。

响应式数据的实现原理

Vue在组件和实例初始化的时候,会将data里的数据进行数据劫持,即Object.defineProperty对数据进行处理。被劫持后的数据会有两个属性:gettersetter

使用数据时触发getter,修改数据时触发setter,同时也出发了底层的watcher监听,通知DOM修改刷新。 这就实现了响应式数据。

Vue中的数据变页面一定变吗?

不一定。数据变页面也变是因为数据有getter和setter两个属性。如果没有则不会变。

vue中的响应式是什么?
怎么理解响应式原理?_vue响应式属性_J.P_P的博客-CSDN博客
Vue 核心之数据劫持 - Jaye8584 - 博客园 (cnblogs.com)

调度系统

调度系统,指的是响应性的可调度性

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

想要实现一个调度系统,需要依赖:异步Promise和队列jobQueue。需要:基于Set构建出一个基本的队列数组jobQueue,利用Promise的异步特性,来控制执行的顺序。

计算属性

计算属性的本质:一个属性值,当依赖响应式数据发生变化时,重新计算。

计算属性的实现:依赖于调度系统。

懒惰执行

watch监听器:观测一个响应式数据,当数据发生变化时,通知并执行对应的回调函数。

这意味着,watch很多时候不需要立即执行。因此,需要懒惰执行进行控制。

懒惰执行的实现:比调度系统简单。本质上是一个boolean型的值,被添加到effet函数中,用来控制副作用的执行。

if(!lazy){
	//执行副作用函数
}

watch的实现原理

基于调度系统和懒惰执行。

过期的副作用

我们可以在watch完成异步操作。但是,大量的异步操作可能导致竞态问题

竞态问题:在描述一个系统或进程的输出,依赖于不受控制的事件出现顺序或者出现时机。

如:

let findData

watch(obj,async()=>{
	const res=await fetch('/path/to/request')
	findData=res
})

这段代码是异步操作。若obj改变了两次,就会发送两次网络请求。我们无法知道findData最后保存的值是哪一次网络请求的。这种问题就是竞态问题

而如果想要解决这问题,那么就需要使用到 watch 回调函数的第三个参数 onInvalidate,它本身也是一个回调函数。并且该回调函数(onInvalidate)会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求
而onInvalidate 的实现原理也非常简单,只需要 在副作用函数(effct)重新执行前,先触发 onInvalidate 即可。

好抽象,没太懂。

第七章:渲染器的设计

渲染器和渲染函数不是一个东西。

  • 渲染器是createRenderer的返回值,是一个对象
  • 渲染函数是渲染器对象中的render方法

在vue 3.2.37 源码中,createRenderer函数具体是通过baseCreateRenderer进行的。总体可以分为两部分:

  1. 在浏览器端渲染时,利用DOM API完成DOM操作:如渲染DOM用createElement,删除DOM使用removeChild
  2. 渲染器不能和宿主环境(浏览器)强耦合:vue中有浏览器渲染和服务端渲染。若与浏览器强耦合就不好实现服务端渲染了。

vnode

一个普通的JS对象,代表了渲染的内容。 对象中通过type表示渲染的DOM。如type===div表示div标签。type===Fragment表示文档片段。

第八章:挂载与更新

对于渲染器,它所作的最核心的事情是:对节点进行挂载、更新。

第八章分为两部分来讲解这件事:

  1. DOM节点操作
  2. 属性节点操作

DOM节点操作

分为3部分:

  • 挂载:节点的初次渲染。如用createElement新建一个节点。
  • 更新:当响应性数据发生变化时,可能会涉及到DOM的更新。本质上属于属性的更新。
  • 卸载:旧节点不再需要了,就删除旧节点。如通过parentEl.removeChild进行。

属性节点操作

属性可以分为两类:

  1. 属性,如:class,id,value…
  2. 事件,如:click、input…

先了解非事件的属性部分。
想了解vue对属性的处理,需要先了解浏览器中的属性分类。
浏览器中,DOM属性被分为两类:

  1. HTML Attributes:直接定义在HTML上的属性
  2. DOM Properties:拿到DOM对象后定义的属性。

对于HTML Attributes,它只能在html中操作。而想要在JS中操作DOM,需要通过DOM Properties来实现。因为JS本身特性的问题,会导致某些DOM Properties的设置存在特殊性,如classtypevalue。为了保证DOM Properties的成功设置,我们需要知道不同属性的DOM Properties定义方式。

  1. el.setAttribute('属性名','属性值')
  2. . 属性赋值el.属性名=属性值el[属性名]=属性值

在这里插入图片描述
在这里插入图片描述

事件

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

洗地机哪个品牌质量好?盘点当下最值得买的4款洗地机型号推荐

随着生活节奏的加快&#xff0c;人们对于家庭清洁的需求也越来越迫切。而洗地机作为家庭清洁利器备受青睐&#xff0c;但洗地机也分为很多款式&#xff0c;每一个款式都具备不同的清洁效果&#xff0c;可以节省不少时间。接下来&#xff0c;就由笔者为大家详细介绍一下洗地机哪…

如何去除图片水印?三个简单实用方法

随着数字时代的来临&#xff0c;我们每天都会接触到大量的图片&#xff0c;然而&#xff0c;许多图片却因为水印而影响了美观。为了解决这个问题&#xff0c;我们需要图片去水印的方法。今天&#xff0c;我们就来为大家介绍几个简单实用的方法&#xff0c;可以轻松去除水印&…

备战蓝桥杯---搜索(优化1)

显然&#xff0c;我们可以用BFS解决&#xff0c;具体实现与八数码类似&#xff1a; 下面是代码&#xff1a; #include<bits/stdc.h> using namespace std; #define N 3000000 string a,b; int hh,dis[N],cnt; struct node{string u,v; }bian[7]; map<string,int>…

Flutter 和 Android原生(Activity、Fragment)相互跳转、传参

前言 本文主要讲解 Flutter 和 Android原生之间&#xff0c;页面相互跳转、传参&#xff0c; 但其中用到了两端相互通信的知识&#xff0c;非常建议先看完这篇 讲解通信的文章&#xff1a; Flutter 与 Android原生 相互通信&#xff1a;BasicMessageChannel、MethodChannel、…

MongoDB复制集实战及原理分析

文章目录 MongoDB复制集复制集架构三节点复制集模式PSS模式&#xff08;官方推荐模式&#xff09;PSA模式 典型三节点复制集环境搭建复制集注意事项环境准备配置复制集复制集状态查询使用mtools创建复制集安全认证复制集连接方式 复制集成员角色属性一&#xff1a;Priority 0属…

match-case与if/elif/else(python)

if/elif/else语句应对一般场景&#xff0c;match-case主打复杂条件分支语句。 (笔记模板由python脚本于2024年01月28日 18:27:37创建&#xff0c;本篇笔记适合有一定编程基础&#xff0c;对python基础已比较扎实的coder翻阅) 【学习的细节是欢悦的历程】 Python 官网&#xff1…

uniapp使用u-popup组件弹窗出现页面还可滑动

*1、问题所在&#xff1a; 弹窗遮罩层出现了页面依旧可以上下滑动 2、要求: 为了用户更好交互体验&#xff0c;弹窗出现后应禁止页面往下滑动 3、实现思路&#xff1a; 在弹窗盒子外层添加个阻止触摸冒泡事件&#xff0c;使用touchmove.stop.prevent 4、代码如下&#xff…

eosio.token 智能合约介绍

一、目的 eosio.token系统合约定义了允许用户为基于EOSIO的区块链创建、发行和管理代币的结构和操作&#xff0c;它演示了一种实现允许创建和管理代币的智能合约的方法。本文详细介绍了eosio.token系统合约并在本地测试链上实际发行了代币进行演示&#xff0c;适用于EOS智能合…

OJ刷题:《剑指offer》之单身狗1、2 !(巧用位操作符,超详细讲解!)

目录 1.单身狗1 1.1 题目描述 1.2排序寻找 1.3巧用位操作符 2.单身狗2 1.1 题目描述 1.2排序寻找 1.3巧用位操作符 不是每个人都能做自己想做的事&#xff0c;成为自己想成为的人。 克心守己&#xff0c;律己则安&#xff01; 创作不易&#xff0c;宝子们&#xff01;如…

homework day3

第三章 类与构造函数 一&#xff0e;选择题 1、下列不能作为类的成员的是&#xff08;B&#xff09; A. 自身类对象的指针 B. 自身类对象 C. 自身类对象的引用 D. 另一个类的对象 2、假定AA为一个类&#xff0c;a()为该类公有的函数成员&#xff0c;x为该类的一个对象&am…

如何在一台MacBook上构建大模型知识库?

▼最近直播超级多&#xff0c;预约保你有收获 今晚直播&#xff1a;《构建大模型知识库案例实战》 —1— 如何在一台 MacBook 上构建企业知识库&#xff1f; 最核心最重要的是我们手上的文档资料出于安全要求&#xff0c;不能随便上传到云服务&#xff0c;也就无法实际验证知识…

单链表的经典题目练习

哈喽&#xff0c;小伙伴们&#xff0c;上一次我们学习了单链表的知识&#xff0c;这次我们就要运用学到的知识来做一些相关的题目。我们都知道&#xff0c;要学好数据结构与算法&#xff0c;一定要多刷相关的题目才能有所提高。所以我们一起来学习一些单链表的经典题目算法题。…

操作系统透视:从历史沿革到现代应用,剖析Linux与网站服务架构

目录 操作系统 windows macos Linux 服务器搭建网站 关于解释器的流程 curl -I命令 名词解释 dos bash/terminal&#xff0c;(终端) nginx/apache&#xff08;Linux平台下的&#xff09; iis&#xff08;Windows平台下的&#xff09; GUI(图形化管理接口&#xff…

Multisim14.0仿真(四十九)共阴极/阳极7段数码管驱动设计

一、74LS47/48简介: 74LS47/48芯片是一种常用的七段数码管译码器驱动器,常用在各种数字电路和单片机系统的显示系统中. 二、74LS47/48引脚说明及定义: 7段显示译码器74LS47/48是输出低/高电平有效的译码器,74LS47/48除了有实现7段显示译码器基本功能的输入(DCBA)和输出(Ya…

小程序<swiper/>组件详解及使用指南

目录 引言微信小程序的重要性Swiper组件的角色与功能简介Swiper组件基础Swiper组件的定义与使用场景如何在微信小程序中引入Swiper组件Swiper组件的基本结构与属性Swiper组件的高级应用自定义Swiper指示点样式实现Swiper的动态效果(如自动播放、循环播放)说明引言 微信小程序…

时序预测 | MATLAB实现基于CNN-BiLSTM-AdaBoost卷积双向长短期记忆网络结合AdaBoost时间序列预测

时序预测 | MATLAB实现基于CNN-BiLSTM-AdaBoost卷积双向长短期记忆网络结合AdaBoost时间序列预测 目录 时序预测 | MATLAB实现基于CNN-BiLSTM-AdaBoost卷积双向长短期记忆网络结合AdaBoost时间序列预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 1.Matlab实现…

【C++】类和对象之运算符重载(三)

前言&#xff1a;在前面我们知道在类和对象中有六个默认成员函数&#xff0c;并学习了其中三个构造函数、析构函数、拷贝构造函数&#xff0c;今天我们将进一步的学习.赋值运算符重载。 &#x1f496; 博主CSDN主页:卫卫卫的个人主页 &#x1f49e; &#x1f449; 专栏分类:高质…

[SWPUCTF 2021 新生赛]ez_unserialize

根据下面的user_agent和Disallow可以判断这个是在robots.txt 我们看的出来这是一个反序列化需要我们adminadmin passwdctf construct 构造方法&#xff0c;当一个对象被创建时调用此方法&#xff0c;不过unserialize()时却不会被调用 destruct 析构方法&#xff0c;PHP将在对象…

【学网攻】 第(20)节 -- 网络端口地址转换NAPT配置

系列文章目录 目录 系列文章目录 文章目录 前言 一、NAPT是什么&#xff1f; 二、实验 1.引入 实验目的 技术原理 实验步骤 实验设备 实验拓扑图 实验配置 实验验证 文章目录 【学网攻】 第(1)节 -- 认识网络【学网攻】 第(2)节 -- 交换机认识及使用【学网攻】 第…

JavaGUI之SWT框架【阶段练习】

文章目录 效果展示选项卡界面创建划分右侧区域填充右侧上方Composite填充右侧下方Composite填充左侧Composite完整代码 SWT基础部分的内容以全部写完&#xff0c;现在让我们将以前学到的知识综合到一起&#xff0c;写一个小demo&#xff08;无交互功能&#xff09; 效果展示 选…