vue3学习源码笔记(小白入门系列)------ 重点!响应式原理 代码逐行分析
目录
- 备注
- 响应式数据创建
- ref 和 reactive 核心 作用
- 第一轮的 依赖收集 发生时机
- setup 阶段 去更改了 响应式数据 会发生依赖收集吗
- 派发更新
- 派发更新是什么时候 触发的?
- 扩展: setup阶段 响应式数据被修改 会触发组件更新吗
- vue 是如何根据派发更新来触发组件的更新渲染的?
- 组件副作用函数执行时 有多个响应式数据更新 是如何保证组件只会触发一次更新渲染的?
- 多余的组件依赖 是如何被清理掉的?
备注
本文中 只会涉及到 setup 主流程的 更新 , watch computed 等后面再分析
带着问题,去源码寻找答案。
响应式数据是什么时候创建的?
什么时候进行的依赖收集?
响应式数据更新后 怎么做的派发更新?
本文中 用到的测试用例
一定得 debug 跟着调试看 不然很容易绕晕
it('should support runtime template compilation', async () => {const container = document.createElement('div')container.classList.add('app')const child = defineComponent({template: `<div><p>{{age}}---{{status?add:'hihi'}}</p></div>`,props:{age:{type: Number,default:20}},data(){return {add: '12',status: true}},mounted() {this.status = falsethis.add = '24'},})const App = {components:{child},beforeMount() {console.log('beforeMount');},data() {return {}},setup() {const count = ref(1)const age = ref('20')const obj = reactive({name:'ws',address:'usa'})onMounted(()=>{obj.name = 'kd'count.value = 5age.value = '2'})return ()=>{return h('div',[obj.name,h(child,{age:age.value})])}}}createApp(App).mount(container)await nextTick()expect(container.innerHTML).toBe(`0`)})
响应式数据创建
还记得 之前文章中 初始化 setup 是在哪个阶段执行的吗?
// packages/runtime-dom/src/renderer.tspatch 阶段 组件首次挂载时
// mountComponent 方法1. 先创建 组件 instance 实例
2. 初始化 setup props 等属性
3. 设置并运行带副作用的渲染函数
初始化 setup 时 ,就会创建响应式数据
测试用例中 会先 执行 App 组件中 的setup 函数
setup() {// 会创建一个 ref 响应式数据const count = ref(1)// 会创建一个 ref 响应式数据const age = ref('20')// 会创建一个 reactive 响应式数据const obj = reactive({name:'ws',address:'usa'})onMounted(()=>{obj.name = 'kd'count.value = 5age.value = '2'})return ()=>{return h('div',[obj.name,h(child,{age:age.value})])}}}
ref 和 reactive 核心 作用
先说结论 :
就是 数据 驱动 视图 更新的 桥梁 。依赖收集(getter) 和 派发更新(setter) 都在里面
ref 和 reactive 差别不大(对于基本数据类型 proxy 无法做代理 ,所以vue3 自己利用 class 类中 get set 做的 代理工作 后续 依赖收集 和 派发更新 原理 和 reactive 基本一致 ) 下面 只对 reactive 做分析
- 先判断 代理对象 做类型分类
function targetTypeMap(rawType) {switch (rawType) {case 'Object':case 'Array':return TargetType.COMMONcase 'Map':case 'Set':case 'WeakMap':case 'WeakSet':return TargetType.COLLECTIONdefault:return TargetType.INVALID}
}
- 根据 不同分类 选择 不同的 getter setter 方法
只分析下 最常见的 Object Array 代理
export function reactive(target: object) {// if trying to observe a readonly proxy, return the readonly version.if (isReadonly(target)) {return target}return createReactiveObject(target,false,mutableHandlers,mutableCollectionHandlers,reactiveMap)
}
。。。。function createReactiveObject(target: Target,isReadonly: boolean,baseHandlers: ProxyHandler<any>,collectionHandlers: ProxyHandler<any>,proxyMap: WeakMap<Target, any>
) {// 省略。。。const proxy = new Proxy(target,targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers)proxyMap.set(target, proxy)return proxy
}
会走到 baseHandlers 也就是 mutableHandlers
- mutableHandlers 中 如何 做的 数据代理工作
先看 get 核心 就是 依赖收集 track方法
function createGetter(isReadonly = false, shallow = false) {return function get(target: Target, key: string | symbol, receiver: object) {// 对 ReactiveFlags 的处理部分if (key === ReactiveFlags.IS_REACTIVE) {return !isReadonly} else if (key === ReactiveFlags.IS_READONLY) {return isReadonly} else if (key === ReactiveFlags.IS_SHALLOW) {return shallow} else if (key === ReactiveFlags.RAW &&receiver ===(isReadonly? shallow? shallowReadonlyMap: readonlyMap: shallow? shallowReactiveMap: reactiveMap).get(target)) {return target}const targetIsArray = isArray(target)if (!isReadonly) {// 数组的特殊方法处理if (targetIsArray && hasOwn(arrayInstrumentations, key)) {return Reflect.get(arrayInstrumentations, key, receiver)}// 对象 hasOwnProperty 方法处理if (key === 'hasOwnProperty') {return hasOwnProperty}}// 取值const res = Reflect.get(target, key, receiver)// Symbol Key 不做依赖收集if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {return res}// 进行依赖收集if (!isReadonly) {track(target, TrackOpTypes.GET, key)}// 一个浅层响应式对象里只有根级别的属性是响应式的。属性的值会被原样存储和暴露if (shallow) {return res}if (isRef(res)) {//跳过数组、整数 key 的展开// ref unwrapping - skip unwrap for Array + integer key.return targetIsArray && isIntegerKey(key) ? res : res.value}if (isObject(res)) {// Convert returned value into a proxy as well. we do the isObject check// here to avoid invalid value warning. Also need to lazy access readonly// and reactive here to avoid circular dependency.// 如果res 是 对象 且不是 readonly 就继续处理成 reactivereturn isReadonly ? readonly(res) : reactive(res)}return res}
}
track 依赖收集
export function track(target: object, type: TrackOpTypes, key: unknown) {if (shouldTrack && activeEffect) {let depsMap = targetMap.get(target)if (!depsMap) {targetMap.set(target, (depsMap = new Map()))}let dep = depsMap.get(key)if (!dep) {depsMap.set(key, (dep = createDep()))}const eventInfo = __DEV__? { effect: activeEffect, target, type, key }: undefined// 将 activeEffect 存入到 dep 同时将 dep[] 存入到 activeEffect 中 deps 属性 上 trackEffects(dep, eventInfo)}
}export function trackEffects(dep: Dep,debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {let shouldTrack = falseif (effectTrackDepth <= maxMarkerBits) {// 如果本轮副作用函数执行过程中已经访问并收集过,则不用再收集该依赖if (!newTracked(dep)) {dep.n |= trackOpBit // set newly tracked 标识本轮已经被收集过shouldTrack = !wasTracked(dep)}} else {// Full cleanup mode. 判断现在有没有activeEffect 有activeEffect才发生依赖收集// activeEffect 每个组件初始化的时候会有一个activeEffect // 这一步的作用 是为了避免多余的依赖收集 例如在setup 创建了 响应式数据 同步 访问 或者 修改 这个数据 这时候 都不会发生 依赖收集。只会在 执行render函数的时候 才发生依赖收集 shouldTrack = !dep.has(activeEffect!)}if (shouldTrack) {dep.add(activeEffect!)activeEffect!.deps.push(dep)}
}
reactiveEffect 核心代码
// 用于记录位于响应上下文中的effect嵌套层次数
let effectTrackDepth = 0
// 二进制位,每一位用于标识当前effect嵌套层级的依赖收集的启用状态
export left trackOpBit = 1
// 表示最大标记的位数
const maxMarkerBits = 30// 当前活跃的 effect
let activeEffect;export class ReactiveEffect {// 用于标识副作用函数是否位于响应式上下文中被执行active = true// 副作用函数持有它所在的所有依赖集合的引用,用于从这些依赖集合删除自身deps = []// 指针为,用于嵌套 effect 执行后动态切换 activeEffectparent = undefined// ...run() {// 若当前 ReactiveEffect 对象脱离响应式上下文// 那么其对应的副作用函数被执行时不会再收集依赖if (!this.active) {return this.fn()}// 缓存是否需要收集依赖let lastShouldTrack = shouldTracktry {// 保存上一个 activeEffect 到当前的 parent 上this.parent = activeEffect// activeEffect 指向当前的 effectactiveEffect = this// shouldTrack 置成 trueshouldTrack = true// 左移操作符 << 将第一个操作数向左移动指定位数// 左边超出的位数将会被清除,右边将会补零。// trackOpBit 是基于 1 左移 effectTrackDepth 位trackOpBit = 1 << ++effectTrackDepth// 如果未超过最大嵌套层数,则执行 initDepMarkersif (effectTrackDepth <= maxMarkerBits) {initDepMarkers(this)} else {cleanupEffect(this)}// 这里执行了 fnreturn this.fn()} finally {if (effectTrackDepth <= maxMarkerBits) {// 用于对曾经跟踪过,但本次副作用函数执行时没有跟踪的依赖采取删除操作。// 新跟踪的 和 本轮跟踪过的都会被保留finalizeDepMarkers(this)}// << --effectTrackDepth 右移动 effectTrackDepth 位trackOpBit = 1 << --effectTrackDepth// 返回上个 activeEffectactiveEffect = this.parent// 返回上个 shouldTrackshouldTrack = lastShouldTrack// 情况本次的 parent 指向this.parent = undefined}}
}
说明 :
depsMap 中 effect[] 用于 每次 派发更新时候 去执行 effect 数组中的 reactiveEffect (实际调用 reactiveEffect 实例的 run 方法)
reactiveEffect 中 会在执行 run 方法的时候 给 initDepMarkers 方法来 给 deps 数组中每个对象 添加 w 属性 表示 已经收集处理 在 依赖收集中 track----> trackEffects 会给 depsMap 中 dep(这个 dep 和 effect 实例的 deps 中 每一个对象 相对应) 赋值 n (表示 是 新收集的)
export const finalizeDepMarkers = (effect: ReactiveEffect) => {const { deps } = effectif (deps.length) {let ptr = 0for (let i = 0; i < deps.length; i++) {const dep = deps[i]if (wasTracked(dep) && !newTracked(dep)) {// dep 类型是 set<ReativeEffect>dep.delete(effect)} else {deps[ptr++] = dep}// clear bitsdep.w &= ~trackOpBitdep.n &= ~trackOpBit}deps.length = ptr}
}
当 effect.run() 中 注册的fn 函数执行完后 会调用 finalizeDepMarkers 去 删除掉 这一轮 dep 没有被收集到的 effect 避免 多余的 触发更新逻辑
那测试用例来说明
第一轮的 依赖收集 发生时机
在App 组件 初始化 副作用函数, 会先创建 reactiveEffect 并挂载到 app.instance
app 会主动触发 instance.update() 发生第一次 组件挂载 。
之前章节 说明过 组件首次挂载流程
实际调用的是 reactiveEffect.run ----> 执行componentUpdateFn —>render(生成subtree)----> patch ----> processElement
在render 过程中 使用到的响应式数据 就发生依赖收集 。
这时候 app 组件的 这一轮的依赖收集完成 使用到了 obj 和 age
这时候进行app 组件 processElement 由于存在子组件child 执行 mountChild ----> patch —> child 组件的processComponent child组件 也会和 app 组件 一样 去 初始化 instance 创建 ReactiveEffect 触发update 执行属于 child 的 componentUpdateFn 再 执行 child组件的 render 函数 child组件发生依赖收集
age status add
本轮的依赖收集全部完成。
总结:
组件的首次依赖收集 发生在 render阶段 顺序是 父组件 setup---->父组件 render ---->子组件 setup
----> 子组件render
setup 阶段 去更改了 响应式数据 会发生依赖收集吗
例如:
setup(){const age = ref(20)// 这里发生了访问操作const temp = age.valuereturn ()=>{return h('div',[age.value]) }
}这时候会触发响应式数据的 get 操作
但是由于 没有 activeEffect(这时候 组件还没开始设置副作用函数(SetupRenderEffectFn)所以没有activeEffect) 所以不会发生依赖收集 扩展:
setup(){const age = ref(20)setTimeout(()=>{// 这里发生了访问操作console.log(age.value); })return ()=>{return h('div',[age.value]) }
}
这时候 也会触发响应式数据的 get 操作 ,也是没有activeEffect(组件已经完成 effect.run 方法了,这时候 activeEffect 已经被置为空) 所以也不会发生依赖收集后续:
在setup函数之后的生命周期(如mounted、updated等钩子函数)中访问响应式数据会触发依赖收集 (后面再分析)
派发更新
派发更新是什么时候 触发的?
上面 组件 挂载完成后,我在mouted 生命周期钩子里面 写的修改响应式数据操作,会触发 setter 下面看看 reactive 的 setter 源码
function createSetter(shallow = false) {return function set(target: object,key: string | symbol,value: unknown,receiver: object): boolean {// 。。。 省略部分逻辑const hadKey =isArray(target) && isIntegerKey(key)? Number(key) < target.length: hasOwn(target, key)const result = Reflect.set(target, key, value, receiver)// 如果target是原型链上的东西,不要触发if (target === toRaw(receiver)) {if (!hadKey) {// 新增操作trigger(target, TriggerOpTypes.ADD, key, value)} else if (hasChanged(value, oldValue)) {// 更新操作trigger(target, TriggerOpTypes.SET, key, value, oldValue)}}return result}
}export function trigger(target: object,type: TriggerOpTypes,key?: unknown,newValue?: unknown,oldValue?: unknown,oldTarget?: Map<unknown, unknown> | Set<unknown>
) {// 根据 target 查到对应的 depsMapconst depsMap = targetMap.get(target)// 不存在depsMap 不触发更新if (!depsMap) {// never been trackedreturn}// 用于 暂存 effectlet deps: (Dep | undefined)[] = []if (type === TriggerOpTypes.CLEAR) {// collection being cleared// trigger all effects for targetdeps = [...depsMap.values()]} else if (key === 'length' && isArray(target)) {const newLength = Number(newValue)depsMap.forEach((dep, key) => {if (key === 'length' || key >= newLength) {deps.push(dep)}})} else {// schedule runs for SET | ADD | DELETEif (key !== void 0) {deps.push(depsMap.get(key))}// also run for iteration key on ADD | DELETE | Map.SETswitch (type) {case TriggerOpTypes.ADD:if (!isArray(target)) {deps.push(depsMap.get(ITERATE_KEY))if (isMap(target)) {deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))}} else if (isIntegerKey(key)) {// new index added to array -> length changesdeps.push(depsMap.get('length'))}breakcase TriggerOpTypes.DELETE:if (!isArray(target)) {deps.push(depsMap.get(ITERATE_KEY))if (isMap(target)) {deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))}}breakcase TriggerOpTypes.SET:if (isMap(target)) {deps.push(depsMap.get(ITERATE_KEY))}break}}const eventInfo = __DEV__? { target, type, key, newValue, oldValue, oldTarget }: undefined// 最终处理 在这里if (deps.length === 1) {if (deps[0]) {if (__DEV__) {triggerEffects(deps[0], eventInfo)} else {triggerEffects(deps[0])}}} else {const effects: ReactiveEffect[] = []for (const dep of deps) {if (dep) {effects.push(...dep)}}// 下面操作 是为了 去重 保证相同的effect 只会有一个if (__DEV__) {triggerEffects(createDep(effects), eventInfo)} else {triggerEffects(createDep(effects))}}
}export function triggerEffects(dep: Dep | ReactiveEffect[],debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {// spread into array for stabilizationconst effects = isArray(dep) ? dep : [...dep]for (const effect of effects) {if (effect.computed) {triggerEffect(effect, debuggerEventExtraInfo)}}for (const effect of effects) {if (!effect.computed) {triggerEffect(effect, debuggerEventExtraInfo)}}
}function triggerEffect(effect: ReactiveEffect,debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {if (effect !== activeEffect || effect.allowRecurse) {if (__DEV__ && effect.onTrigger) {effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))}// 最终会执行 scheduler 是在 初始化的时候 创建的if (effect.scheduler) {effect.scheduler()} else {effect.run()}}
}// 在SetupRenderEffectFn 阶段中 create reactive effect for renderingconst effect = (instance.effect = new ReactiveEffect(componentUpdateFn,() => queueJob(update),// 这个就是 schedulerinstance.scope // track it in component's effect scope))
总结:当响应式数据被更新 且 对应的 depsMap 不为空 就会触发 组件更新(如何更新 则下个问题给出答案)
扩展: setup阶段 响应式数据被修改 会触发组件更新吗
setup(){const age = ref(20)// 修改操作age.value = 10return ()=>{return h('div',[age.value]) }
}
会触发 setter 操作 由于 depsMap 为空 所以不会发生派发更新
vue 是如何根据派发更新来触发组件的更新渲染的?
派发更新的核心就是 触发 effect.scheduler(常规的组件写法 就是 会给activeEffect 创建 scheduler)
const effect = (instance.effect = new ReactiveEffect(componentUpdateFn,() => queueJob(update), // effect.scheduleinstance.scope // track it in component's effect scope))
分析下 queueJob
export function queueJob(job: SchedulerJob) {// the dedupe search uses the startIndex argument of Array.includes() 确保不会重复设置 schedule// by default the search index includes the current job that is being run 默认包括正在运行的 schedule// so it cannot recursively trigger itself again. 避免递归触发自身再次运行// if the job is a watch() callback, the search will start with a +1 index to 运行在watch 中 重复运行// allow it recursively trigger itself - it is the user's responsibility to// 确保它不会陷入无限循环// 去重判断if (!queue.length ||!queue.includes(job,isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex)) {//添加到队列尾部if (job.id == null) {queue.push(job)} else {// 按照 job id 自增的顺序添加 (一般父组件的id 要小于子组件 保证 父组件永远先于子组件触发更新)// 这个id 是由 instance.uid 决定 就是在初始化组件实例 确定( 具体代码 runtime-core/src/component) 先初始化的 uid(每次创建组件实例 全局 uid会加1) 会小,queue.splice(findInsertionIndex(job.id), 0, job)}queueFlush()}
}// 通过promise.then 创建 微任务(去执行flushjob)
function queueFlush() {if (!isFlushing && !isFlushPending) {isFlushPending = truecurrentFlushPromise = resolvedPromise.then(flushJobs)}
}function flushJobs(seen?: CountMap) {// 是否正在等待执行isFlushPending = false// 正在执行isFlushing = true// 在更新前,重新排序好更新队列 queue 的顺序// 这确保了:// 1. 组件都是从父组件向子组件进行更新的。(因为父组件都在子组件之前创建的// 所以子组件的渲染的 effect 的优先级比较低)// 2. 如果父组件在更新前卸载了组件,这次更新将会被跳过。queue.sort(comparator)try {// 遍历主任务队列,批量执行更新任务for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {const job = queue[flushIndex]if (job && job.active !== false) {if (__DEV__ && check(job)) {continue}// 这个 job 就是 effect.runcallWithErrorHandling(job, null, ErrorCodes.SCHEDULER)}}} finally {// 队列任务执行完,重置队列索引flushIndex = 0// 清空队列queue.length = 0// 执行后置队列任务flushPostFlushCbs(seen)// 重置队列执行状态isFlushing = false// 重置当前微任务为 NullcurrentFlushPromise = null// 如果主任务队列、后置任务队列还有没被清空,就继续递归执行if (queue.length || pendingPostFlushCbs.length) {flushJobs(seen)}}
}
总结:在组件mounted 触发的派发更新 会被收集到 一个微任务执行任务队列中 ,在主流程宏任务 执行完后 就会 去执行 微任务任务队列 开始 触发 执行 job (effect.run — > updateComponentFn)
组件副作用函数执行时 有多个响应式数据更新 是如何保证组件只会触发一次更新渲染的?
有了 上面源码的分析 我们已经可以得出答案 为啥 在一个组件的 副作用函数执行时 多个响应式数据更新 只会触发一次 组件更新
演示代码:
setup(){const num1 = ref(20)const num2 = ref(10)onMounted(()=>{num1.value = 40num1.value = 50num2.value = 100})return ()=>{return h('div',[num1.value+num2.value]) }
}
在 onMounted 中 更新了三次 触发三次 triggerEffect 会有三次 往微任务 放入 update 操作,由于 传入的 job.id 都是同一个 所以 在更新队列中 只会被创建一个更新任务 组件也只会被更新一次
多余的组件依赖 是如何被清理掉的?
组件 再每次 渲染后 会 去 清理 后续没被收集的 effect (对应的是 每个响应式数据 对应的dep(set) 中的 reactiveEffect )
例子:
const child = defineComponent({template: `<div><p>{{age}}---{{status?add:'hihi'}}</p></div>`,props:{age:{type: Number,default:20}},data(){return {add: '12',status: true}},mounted() {this.status = falsethis.add = '24'},})
// mounted 阶段 改变了 status 触发了 组件更新 重新 render 的 时候 会发生新的一轮依赖收集
// 之前 组件 是有两个 dep 一个 属于 status 一个属于 add 但是,由于新的依赖收集 add 不会被用到 所以 在 effect.run 执行完 后 add 的 dep 会被清除掉 是根据 dep 赋值的 w 和 n 属性 去比较
相关文章:

vue3学习源码笔记(小白入门系列)------ 重点!响应式原理 代码逐行分析
目录 备注响应式数据创建ref 和 reactive 核心 作用第一轮的 依赖收集 发生时机setup 阶段 去更改了 响应式数据 会发生依赖收集吗 派发更新派发更新是什么时候 触发的?扩展: setup阶段 响应式数据被修改 会触发组件更新吗 vue 是如何根据派发更新来触发…...

62、SpringBoot 使用RestTemplate 整合第三方 RESTful 服务
这节的要点: 就是弄两个项目 , 从 端口9090 这个项目,通过 restTemplate, 去访问 端口8080 的项目,并获取8080项目的数据。 ★ RESTful服务包含两方面的含义 1. 自己的应用要暴露一些功能供别人来调用。此时我们是服…...

Linux基本认识
一、Linux基本概念 Linux 内核最初只是由芬兰人林纳斯托瓦兹(Linus Torvalds)在赫尔辛基大学上学时出于个人爱好而编写的。 Linux 是一套免费使用和自由传播的类 Unix 操作系统,是一个基于 POSIX 和 UNIX 的多用户、多任务、支持多线程和多…...
leetcode top 100 (8)无重复字符的最长子串(滑动窗口
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。 示例 1: 输入: s "abcabcbb" 输出: 3 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。 package TOP1_10;import java.util.HashMap; import java.…...

我也惊呆了!原来软件开发根本不需要会编码
一、前言 众所周知,完成一个大型的企业级系统,公司往往需要大量的人力做支持后盾,例如需要需求分析师、数据库管理员、前台美工、后台程序员、测试人员等。在快速发展中的企业,尤其是中小企业,都是一个萝卜多个坑&…...

定制化图标——Element UI 组件图标替换指南
本篇博客将介绍如何在使用 Element UI 组件时对原生图标进行定制化替换,提供了适用于满足个性化需求的方法和技巧。 引言 Element UI 是一款基于 Vue.js 的流行 UI 组件库,在前端开发中得到广泛应用。然而,在使用 Element UI 的组件时&#…...

63、SpringBoot---定制 RestTemplate--消息转化器、拦截器
★ 定制RestTemplate 如要对RestTemplate进行自定义设置,Spring Boot也提供了两种主要方式:▲ 局部式:在调用RestTemplateBuilder构建RestTemplate之前,先调用RestTemplateBuilder的方法对其定制,通过这种方式设置的R…...
面试系列 - Redis持久化机制详解
目录 一、Redis 持久化机制 二、混合使用 RDB 和 AOF 三、 RDB(Redis DataBase)详解 四、AOF(Append-Only File)详解 Redis 是一个内存数据库,为了持久化数据以确保数据不会在服务器重启时丢失,Redis 提供了两种主要的持久化机…...
Ceph入门到精通-存储集群ceph df 用量统计算法说明
3.2.5. Ceph 如何计算数据使用量 used 值反映了使用的实际原始存储量。xxx GB / xxx GB 代表可用的存储(其中较小的数字)和总存储容量。总容量反映了在复制、克隆或快照前存储数据的大小。因此,实际存储的数据量通常会超过名义上的存储量。这…...

堡垒机的相关介绍
描述 堡垒机,即在一个特定的网络环境下,为了保障网络和数据不受来自外部和内部用户的入侵和破坏,而运用各种技术手段监控和记录运维人员对网络内的服务器、网络设备、安全设备、数据库等设备的操作行为,以便集中报警、及时处理及审…...

无涯教程-JavaScript - INDIRECT函数
描述 INDIRECT函数返回由文本字符串指定的引用。 如果您在Excel公式中键入引用B1,则Excel会理解这引用了单元格B1。但是,Excel无法将文本字符串" B1"理解为引用。因此,如果单元格引用采用文本字符串的形式,则需要使用INDIRECT函数将其转换为实际的单元格引用。 立…...

LiveNVR监控流媒体Onvif/RTSP功能-支持海康摄像头海康NVR通过EHOME协议ISUP协议接入分发视频流或是转GB28181
LiveNVR支持海康NVR摄像头通EHOME接入ISUP接入LiveNVR分发视频流或是转GB28181 1、海康 ISUP 接入配置2、海康设备接入2.1、海康EHOME接入配置示例2.2、海康ISUP接入配置示例 3、通道配置3.1、直播流接入类型 海康ISUP3.2、海康 ISUP 设备ID3.3、启用保存3.4、接入成功 4、相关…...

一年一度的中秋节马上又要到了,给你的浏览器也来点氛围感吧
说在前面 一年一度的中秋节马上又要到了,给你的浏览器也来点氛围感吧 🌕🌕🌕 插件设计 效果 首先我们应该要先确定一下我们想要实现的效果是怎样的,如上图,我们希望在页面上鼠标点击的时候会在点击区域随…...

CentOS8安装mysql-community-client错误解决
安装MySQL5.7.37的mysql-community-client-5.7.37-1.el7.x86_64.rpm时,提示如下: 提示的意思是缺少依赖软件包。 使用如下命令安装依赖包: yum install libncurse* 实际安装如下两个软件包。 成功后再次安装mysql-community-client-5.7.37…...

故障排除指南:解决 Kibana Discover 加载中的 6 个常见问题
作者:Steffanie Nestor Discover 是 Elastic 的核心 Kibana UI,用于搜索、过滤和检查(时间序列)数据。 可视化用于数据聚合/摘要。 Discover UI 对于大数据 Elasticsearch 响应具有弹性,但有时会因(未压缩的…...

创建一个简单的外卖订餐系统
在今天的快节奏生活中,外卖订餐系统已经成为了人们日常生活中不可或缺的一部分。这些系统通过在线点餐和配送服务,为用户提供了便捷的用餐体验。在本文中,我们将创建一个简单的外卖订餐系统,使用Python和Flask框架构建后端&#x…...

《追逐胜利:编程之路上的三子棋游戏实践》
文章目录 前言一、三子棋游戏规则二、步骤详解1.游戏菜单的实现2.棋盘的实现2.1 初始化棋盘2.2 打印棋盘 3.游戏逻辑实现3.1 玩家下棋3.2 电脑下棋 4.判断输赢4.1 win函数实现 5.完整代码 总结 前言 大家好!我是艾老虎尤!今天我很高兴来和大家分享我最近…...

身份和访问管理解决方案:混合型IAM
对于依赖于本地 IT 基础结构和传统安全模型的组织,可以更轻松地验证和授权企业网络内的所有内容,包括设备、用户、应用程序和服务器。尝试从公司网络外部获取访问权限的用户使用虚拟专用网络 (VPN) 和网络访问控制 (NA…...
微信小程序动态添加表单模块
先来看看效果: 屏幕录制 2023-09-14 16.33.21 点击添加请假按钮,就会新增一个请假信息表单;点击左上角红色删除按钮,删除当前表单; 源码下载地址:https://download.csdn.net/download/xq30397022/88339822…...

HTML5+CSS3小实例:纯CSS实现彩虹倒映水面的唯美背景
实例:纯CSS实现彩虹倒映水面的唯美背景 技术栈:HTML+CSS 效果: 源码: 【html】 <!DOCTYPE html> <html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"><meta name="viewport" …...

超短脉冲激光自聚焦效应
前言与目录 强激光引起自聚焦效应机理 超短脉冲激光在脆性材料内部加工时引起的自聚焦效应,这是一种非线性光学现象,主要涉及光学克尔效应和材料的非线性光学特性。 自聚焦效应可以产生局部的强光场,对材料产生非线性响应,可能…...

工业安全零事故的智能守护者:一体化AI智能安防平台
前言: 通过AI视觉技术,为船厂提供全面的安全监控解决方案,涵盖交通违规检测、起重机轨道安全、非法入侵检测、盗窃防范、安全规范执行监控等多个方面,能够实现对应负责人反馈机制,并最终实现数据的统计报表。提升船厂…...

剑指offer20_链表中环的入口节点
链表中环的入口节点 给定一个链表,若其中包含环,则输出环的入口节点。 若其中不包含环,则输出null。 数据范围 节点 val 值取值范围 [ 1 , 1000 ] [1,1000] [1,1000]。 节点 val 值各不相同。 链表长度 [ 0 , 500 ] [0,500] [0,500]。 …...

ServerTrust 并非唯一
NSURLAuthenticationMethodServerTrust 只是 authenticationMethod 的冰山一角 要理解 NSURLAuthenticationMethodServerTrust, 首先要明白它只是 authenticationMethod 的选项之一, 并非唯一 1 先厘清概念 点说明authenticationMethodURLAuthenticationChallenge.protectionS…...

Android15默认授权浮窗权限
我们经常有那种需求,客户需要定制的apk集成在ROM中,并且默认授予其【显示在其他应用的上层】权限,也就是我们常说的浮窗权限,那么我们就可以通过以下方法在wms、ams等系统服务的systemReady()方法中调用即可实现预置应用默认授权浮…...

Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
2023赣州旅游投资集团
单选题 1.“不登高山,不知天之高也;不临深溪,不知地之厚也。”这句话说明_____。 A、人的意识具有创造性 B、人的认识是独立于实践之外的 C、实践在认识过程中具有决定作用 D、人的一切知识都是从直接经验中获得的 参考答案: C 本题解…...

【电力电子】基于STM32F103C8T6单片机双极性SPWM逆变(硬件篇)
本项目是基于 STM32F103C8T6 微控制器的 SPWM(正弦脉宽调制)电源模块,能够生成可调频率和幅值的正弦波交流电源输出。该项目适用于逆变器、UPS电源、变频器等应用场景。 供电电源 输入电压采集 上图为本设计的电源电路,图中 D1 为二极管, 其目的是防止正负极电源反接, …...

Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...

Qemu arm操作系统开发环境
使用qemu虚拟arm硬件比较合适。 步骤如下: 安装qemu apt install qemu-system安装aarch64-none-elf-gcc 需要手动下载,下载地址:https://developer.arm.com/-/media/Files/downloads/gnu/13.2.rel1/binrel/arm-gnu-toolchain-13.2.rel1-x…...