【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的其他字段赋值,也会触发proxy
的set
,这是不对的,我们的初衷是只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--effectFn1target2-- 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 -> effectslet 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 也不会被回收,最终可能导致内存溢出。
最后,把收集、触发副作用函数封装到track
、trigger
中:
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++
语句,会读取也会设置,因此它会触发track
和trigger
,不断地添加副作用函数。
解决方法:可以在 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=1function effect(){val=2
}
这个函数改变了全局变量val,产生了副作用,因此是副作用函数。
而如果val这个数据的变化改变了视图的变化,那么val就称作响应式数据。
想要实现响应式数据,需要依赖两个行为:
getter
:数据读取setter
:数据修改
vue2中这两个行为通过
Object.defineProperty
实现
vue3中这两个行为通过Proxy
实现。
响应式数据的实现原理:
Vue在组件和实例初始化的时候,会将data里的数据进行数据劫持,即Object.defineProperty
对数据进行处理。被劫持后的数据会有两个属性:getter
和setter
。
使用数据时触发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 findDatawatch(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
进行的。总体可以分为两部分:
- 在浏览器端渲染时,利用DOM API完成DOM操作:如渲染DOM用
createElement
,删除DOM使用removeChild
- 渲染器不能和宿主环境(浏览器)强耦合:vue中有浏览器渲染和服务端渲染。若与浏览器强耦合就不好实现服务端渲染了。
vnode
是一个普通的JS对象,代表了渲染的内容。 对象中通过type表示渲染的DOM。如type===div
表示div标签。type===Fragment
表示文档片段。
第八章:挂载与更新
对于渲染器,它所作的最核心的事情是:对节点进行挂载、更新。
第八章分为两部分来讲解这件事:
- DOM节点操作
- 属性节点操作
DOM节点操作
分为3部分:
- 挂载:节点的初次渲染。如用
createElement
新建一个节点。 - 更新:当响应性数据发生变化时,可能会涉及到DOM的更新。本质上属于属性的更新。
- 卸载:旧节点不再需要了,就删除旧节点。如通过
parentEl.removeChild
进行。
属性节点操作
属性可以分为两类:
- 属性,如:class,id,value…
- 事件,如:click、input…
先了解非事件的属性部分。
想了解vue对属性的处理,需要先了解浏览器中的属性分类。
浏览器中,DOM属性被分为两类:
HTML Attributes
:直接定义在HTML上的属性DOM Properties
:拿到DOM对象后定义的属性。
对于HTML Attributes
,它只能在html中操作。而想要在JS中操作DOM,需要通过DOM Properties
来实现。因为JS本身特性的问题,会导致某些DOM Properties
的设置存在特殊性,如class
、type
、value
。为了保证DOM Properties
的成功设置,我们需要知道不同属性的DOM Properties
定义方式。
el.setAttribute('属性名','属性值')
. 属性赋值
:el.属性名=属性值
或el[属性名]=属性值
事件
相关文章:

【Vue.js设计与实现】第二篇:响应系统-阅读笔记(持续更新)
从高层设计的角度去探讨框架需要关注的问题。 系列目录: 标题博客第一篇:框架设计概览【Vue.js设计与实现】第一篇:框架设计概览-阅读笔记第二篇:响应系统【Vue.js设计与实现】第二篇:响应系统-阅读笔记第三篇&#x…...

微信小程序之本地生活案例的实现
学习的最大理由是想摆脱平庸,早一天就多一份人生的精彩;迟一天就多一天平庸的困扰。各位小伙伴,如果您: 想系统/深入学习某技术知识点… 一个人摸索学习很难坚持,想组团高效学习… 想写博客但无从下手,急需…...

智能决策的艺术:探索商业分析的最佳工具和方法
文章目录 一、引言二、商业分析思维概述三、数据分析在商业实践中的应用四、如何培养商业分析思维与实践能力五、结论《商业分析思维与实践:用数据分析解决商业问题》亮点内容简介作者简介目录获取方式 一、引言 随着大数据时代的来临,商业分析思维与实…...

C#(C Sharp)学习笔记_前言及Visual Studio Code配置C#运行环境【一】
前言 这可以说是我第一次正式的踏入C#的学习道路,我真没想过我两年前是怎么跳过C#去学Unity3D游戏开发的(当然了,游戏开发肯定是没有成功的,都是照搬代码)。而现在,我真正地学习一下C#,就和去年…...

政安晨的AI笔记——Bard大模型最新提示词创作绘画分析
AI大模型进入商业应用元年后的第一年,顶级模型大混战终于开始了。 Bard在追赶OpenAI的过程中,还是补上了画图的短板。 (相比于视频的5阶张量处理而言,图画做为4阶张量处理虽然不新鲜,但却是跨不过去的基础条件&#…...
基础算法bfs -剪枝问题
问题描述:一个迷宫有 NXM 格,有一些格子是地板,能走;有一些格子是障碍,不能走。给一个起点S和一个终点D。一只小狗从 S出发,每步走一块地板,在每块地员不能停留,而且走过的地板都不能再走。给定一个 T,问小狗能正好走 T步到达D吗?输入:有很多测试样例。…...

在Meteor Lake上测试基于Stable Diffusion的AI应用
上个月刚刚推出的英特尔新一代Meteor Lake CPU,预示着AI PC的新时代到来。AI PC可以不依赖服务器直接在PC端处理AI推理工作负载,例如生成图像或转录音频。这些芯片的正式名称为Intel Core Ultra处理器,是首款配备专门用于处理人工智能任务的 …...

情人节心动礼物:共度情人节美好时刻的礼物推荐
情人节,这个充满浪漫与爱意的特殊日子,总是让人心跳加速,期待着与爱人共享甜蜜时光。在这一天,送出一份精心挑选的礼物,不仅能够表达你对另一半无尽的爱意,更能让这份爱升华,成为你们爱情故事中…...

远程手机搭建Termux环境,并通过ssh连接Termux
背景 Termux只能通过鼠标点击,无法使用电脑键盘,输入速度很慢,你想通过ssh 连接Termux,获得友好体验搞了个云手机,想像普通手机那样充当服务器想把自己的手机公开到局域网中供同事调试想把自己的模拟器公开到局域网中…...

基于EdgeWorkers的边缘应用如何进行单元测试?
随着各行各业数字化转型的持续深入,越来越多企业开始选择将一些应用程序放在距离最终用户更近的边缘位置来运行,借此降低延迟,提高应用程序响应速度,打造更出色的用户体验。 相比传统集中部署和运行的方式,这种边缘应…...

【linux】校招中的“熟悉linux操作系统”一般是指达到什么程度?
这样,你先在网上找一套完整openssh升级方案(不是yum或apt的,要源码安装的),然后在虚拟机上反复安装测试,直到把他理解了、背下来。 面试的时候让你简单说说linux命令什么的,你就直接把这个方案…...
【CSS系列】常用容易忽略的css
user-select user-select 是一个 CSS 属性,用于控制用户是否可以选择文本。通过设置 user-select 的值,可以决定用户是否可以选择元素中的文本,以及如何选择文本。 auto:默认值。浏览器可以选择文本。none:用户不能选…...

Java 数据结构 二叉树(二)红黑树
目录 数据结构图-树 简介 规则 旋转 重新着色 红黑树构建过程 前言-与正文无关 生活远不止眼前的苦劳与奔波,它还充满了无数值得我们去体验和珍惜的美好事物。在这个快节奏的世界中,我们往往容易陷入工作的漩涡,忘记了停下脚步…...
React18-完成弹窗封装
弹框封装 用法 // 创建 userRef.current?.open(create) // 修改 userRef.current?.open(edit,values){/* 创建用户 */} <CreateUser mRef{userRef} update{} />组件暴露open方法 文档地址:https://react.dev/reference/react/useImperativeHandle useIm…...
蓝桥杯2024/1/31-----底层测试模板
和之前一样建好工程文件夹,里边包含User(放工程文件,mian.c)、Driver(存放底层文件如Led.c,Led.h等) 新建的工程先搭建框架,可以先书写底层函数(此次书写了四个函数并包含…...
蓝桥杯备战(AcWing算法基础课)-高精度-乘-低精度
目录 前言 1 题目描述 2 分析 2.1 关键代码 2.2 关键代码分析 3 代码 前言 详细的代码里面有自己的理解注释 1 题目描述 给定两个非负整数(不含前导 00) A 和 B,请你计算 AB 的值。 输入格式 共两行,第一行包含整数 A&a…...
C++设计模式-里氏替换原则
里氏替换原则定义了继承规范。(封装、继承、多态) 定义1:类型S对象o1,类型T对象o2,o1换成o2时程序意图不变,那么S是T的子类。 定义2:使用子类不破坏父类的意图。 注意:如果子类不…...
compose LazyColumn + items没有自动刷新问题
val dataLists by remember { mutableStateOf(datas) } 数据更改后列表不刷新问题。 val dataLists by remember { mutableStateOf(datas) } LazyColumn(modifier Modifier.padding(top 5.dp)) {items(dataLists) {....}} 可以将mutableStateOf 改为mutableStateListOf解决…...

Java八大常用排序算法
1冒泡排序 对于冒泡排序相信我们都比较熟悉了,其核心思想就是相邻元素两两比较,把较大的元素放到后面,在一轮比较完成之后,最大的元素就位于最后一个位置了,就好像是气泡,慢慢的浮出了水面一样 Jave 实现 …...
编程笔记 html5cssjs 075 Javascript 常量和变量
编程笔记 html5&css&js 075 Javascript 常量和变量 一、JavaScript 变量二、JavaScript 常量三、示例:小结: 在JavaScript中,变量和常量是用来存储数据的占位符。它们的主要区别在于可变性:变量的值可以改变,而…...
web vue 项目 Docker化部署
Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段: 构建阶段(Build Stage):…...
golang循环变量捕获问题
在 Go 语言中,当在循环中启动协程(goroutine)时,如果在协程闭包中直接引用循环变量,可能会遇到一个常见的陷阱 - 循环变量捕获问题。让我详细解释一下: 问题背景 看这个代码片段: fo…...

Cinnamon修改面板小工具图标
Cinnamon开始菜单-CSDN博客 设置模块都是做好的,比GNOME简单得多! 在 applet.js 里增加 const Settings imports.ui.settings;this.settings new Settings.AppletSettings(this, HTYMenusonichy, instance_id); this.settings.bind(menu-icon, menu…...

基于Docker Compose部署Java微服务项目
一. 创建根项目 根项目(父项目)主要用于依赖管理 一些需要注意的点: 打包方式需要为 pom<modules>里需要注册子模块不要引入maven的打包插件,否则打包时会出问题 <?xml version"1.0" encoding"UTF-8…...

html-<abbr> 缩写或首字母缩略词
定义与作用 <abbr> 标签用于表示缩写或首字母缩略词,它可以帮助用户更好地理解缩写的含义,尤其是对于那些不熟悉该缩写的用户。 title 属性的内容提供了缩写的详细说明。当用户将鼠标悬停在缩写上时,会显示一个提示框。 示例&#x…...

MySQL 知识小结(一)
一、my.cnf配置详解 我们知道安装MySQL有两种方式来安装咱们的MySQL数据库,分别是二进制安装编译数据库或者使用三方yum来进行安装,第三方yum的安装相对于二进制压缩包的安装更快捷,但是文件存放起来数据比较冗余,用二进制能够更好管理咱们M…...

Kafka入门-生产者
生产者 生产者发送流程: 延迟时间为0ms时,也就意味着每当有数据就会直接发送 异步发送API 异步发送和同步发送的不同在于:异步发送不需要等待结果,同步发送必须等待结果才能进行下一步发送。 普通异步发送 首先导入所需的k…...

【从零学习JVM|第三篇】类的生命周期(高频面试题)
前言: 在Java编程中,类的生命周期是指类从被加载到内存中开始,到被卸载出内存为止的整个过程。了解类的生命周期对于理解Java程序的运行机制以及性能优化非常重要。本文会深入探寻类的生命周期,让读者对此有深刻印象。 目录 …...
Java求职者面试指南:计算机基础与源码原理深度解析
Java求职者面试指南:计算机基础与源码原理深度解析 第一轮提问:基础概念问题 1. 请解释什么是进程和线程的区别? 面试官:进程是程序的一次执行过程,是系统进行资源分配和调度的基本单位;而线程是进程中的…...
虚拟电厂发展三大趋势:市场化、技术主导、车网互联
市场化:从政策驱动到多元盈利 政策全面赋能 2025年4月,国家发改委、能源局发布《关于加快推进虚拟电厂发展的指导意见》,首次明确虚拟电厂为“独立市场主体”,提出硬性目标:2027年全国调节能力≥2000万千瓦࿰…...