vue3学习源码笔记(小白入门系列)------ 重点!响应式原理 代码逐行分析

news2024/12/27 10:56:53

目录

    • 备注
    • 响应式数据创建
      • 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 = false
          this.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 = 5
          age.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 是在哪个阶段执行的吗?

patch
processComponent
mountComponent
 // packages/runtime-dom/src/renderer.ts
  patch 阶段 组件首次挂载时
// 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 = 5
          age.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 做分析

  1. 先判断 代理对象 做类型分类
function targetTypeMap(rawType) {
  switch (rawType) {
    case 'Object':
    case 'Array':
      return TargetType.COMMON
    case 'Map':
    case 'Set':
    case 'WeakMap':
    case 'WeakSet':
      return TargetType.COLLECTION
    default:
      return TargetType.INVALID
  }
}
  1. 根据 不同分类 选择 不同的 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

  1. 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 就继续处理成 reactive
      return 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 = false
  if (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 执行后动态切换 activeEffect
  parent = undefined
  // ...
  run() {
    // 若当前 ReactiveEffect 对象脱离响应式上下文
    // 那么其对应的副作用函数被执行时不会再收集依赖
    if (!this.active) {
      return this.fn()
    }
    
    // 缓存是否需要收集依赖
    let lastShouldTrack = shouldTrack
    
    try {
      // 保存上一个 activeEffect 到当前的 parent 上
      this.parent = activeEffect
      // activeEffect 指向当前的 effect
      activeEffect = this
      // shouldTrack 置成 true
      shouldTrack = true
      // 左移操作符 << 将第一个操作数向左移动指定位数
      // 左边超出的位数将会被清除,右边将会补零。
      // trackOpBit 是基于 1 左移 effectTrackDepth 位
      trackOpBit = 1 << ++effectTrackDepth
      
      // 如果未超过最大嵌套层数,则执行 initDepMarkers
      if (effectTrackDepth <= maxMarkerBits) {
        initDepMarkers(this)
      } else {
        cleanupEffect(this)
      }
      // 这里执行了 fn
      return this.fn()
    } finally {
      if (effectTrackDepth <= maxMarkerBits) {
        // 用于对曾经跟踪过,但本次副作用函数执行时没有跟踪的依赖采取删除操作。
        // 新跟踪的 和 本轮跟踪过的都会被保留
        finalizeDepMarkers(this)
      }
      
      // << --effectTrackDepth 右移动 effectTrackDepth 位
      trackOpBit = 1 << --effectTrackDepth
      
      // 返回上个 activeEffect
      activeEffect = this.parent
      // 返回上个 shouldTrack
      shouldTrack = 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 } = effect
  if (deps.length) {
    let ptr = 0
    for (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 bits
      dep.w &= ~trackOpBit
      dep.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.value
   return ()=>{
      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 查到对应的 depsMap
  const depsMap = targetMap.get(target)
  // 不存在depsMap 不触发更新
  if (!depsMap) {
    // never been tracked
    return
  }
  
  // 用于 暂存 effect
  let deps: (Dep | undefined)[] = []
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    deps = [...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 | DELETE
    if (key !== void 0) {
      deps.push(depsMap.get(key))
    }

    // also run for iteration key on ADD | DELETE | Map.SET
    switch (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 changes
          deps.push(depsMap.get('length'))
        }
        break
      case TriggerOpTypes.DELETE:
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case 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 stabilization
  const 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 rendering
    const effect = (instance.effect = new ReactiveEffect(
      componentUpdateFn,
      () => queueJob(update),// 这个就是 scheduler
      instance.scope // track it in component's effect scope
    ))

总结:当响应式数据被更新 且 对应的 depsMap 不为空 就会触发 组件更新(如何更新 则下个问题给出答案)

扩展: setup阶段 响应式数据被修改 会触发组件更新吗

setup(){
   const age = ref(20)
   // 修改操作
   age.value = 10
   return ()=>{
      return h('div',[age.value]) 
   }
}
会触发 setter 操作 由于 depsMap 为空 所以不会发生派发更新

vue 是如何根据派发更新来触发组件的更新渲染的?

在这里插入图片描述
派发更新的核心就是 触发 effect.scheduler(常规的组件写法 就是 会给activeEffect 创建 scheduler)

const effect = (instance.effect = new ReactiveEffect(
      componentUpdateFn,
      () => queueJob(update), // effect.schedule
      instance.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 = true
    currentFlushPromise = 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.run
        callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
      }
    }
  } finally {
   // 队列任务执行完,重置队列索引
    flushIndex = 0
     // 清空队列
    queue.length = 0
    // 执行后置队列任务
    flushPostFlushCbs(seen)
    // 重置队列执行状态
    isFlushing = false
    // 重置当前微任务为 Null
    currentFlushPromise = 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 = 40
     num1.value = 50
     num2.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 = false
          this.add = '24'
      },
  })
// mounted 阶段 改变了 status 触发了 组件更新 重新 render 的 时候 会发生新的一轮依赖收集 
// 之前 组件 是有两个 dep 一个 属于 status 一个属于 add 但是,由于新的依赖收集 add 不会被用到 所以 在 effect.run 执行完 后 add 的 dep 会被清除掉 是根据 dep 赋值的 w 和 n 属性 去比较

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

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

相关文章

第26章_瑞萨MCU零基础入门系列教程之独立看门狗定时器-IWDT

本教程基于韦东山百问网出的 DShanMCU-RA6M5开发板 进行编写&#xff0c;需要的同学可以在这里获取&#xff1a; https://item.taobao.com/item.htm?id728461040949 配套资料获取&#xff1a;https://renesas-docs.100ask.net 瑞萨MCU零基础入门系列教程汇总&#xff1a; ht…

《经营者集中反垄断合规指引》发布,合规管理平台助力企业合规建设

近日&#xff0c;《经营者集中反垄断合规指引》的发布为企业合规建设提供了更具体和明确的指导。在这一新形势下&#xff0c;道本科技智慧合规管理平台作为一项强大的工具&#xff0c;成为企业实现合规建设的重要支持。本文将重点介绍智慧合规管理平台&#xff0c;探讨其对企业…

LeetCode算法心得——和可被 K 整除的子数组(前缀和+HashMap)

大家好&#xff0c;我是晴天学长&#xff0c;同余定理的应用&#xff0c;需要的小伙伴可以关注支持一下哦&#xff01;后续会继续更新的。 1) .和可被 K 整除的子数组 题目描述 给定一个整数数组 A&#xff0c;返回其中元素之和可被 K 整除的&#xff08;连续、非空&#xff0…

如何在matlab绘图的标题中添加变量?变量的格式化字符串输出浅析

文章目录 matlab的格式化输出控制符字段宽度、精度和对齐方式的控制matlab的格式化输出总结 matlab的格式化输出控制符 Matlab在画图的时候&#xff0c;采用title函数可以增加标题&#xff0c;该函数的输入是一个字符串&#xff0c;有时候我们想在字符串中添加一些变量&#x…

uni-app:标签中对数据进行判断,看数据前中后是否含有需要的字符startsWith(),endsWith(),includes()

效果图 核心代码 判断数据是否存在并且此数据是否以‘-’开头 <image v-if"line && line.startsWith(-)" :src"img1" alt""></image> 判断数据是否存在并且此数据是不是以‘ing’结尾 <image v-if"line &&…

代码块的介绍

代码块 局部代码块 局部代码块&#xff0c;用大括号进行圈着&#xff0c;然后控制这个变量的内存占用时间&#xff0c;用完这个变量我们就回收&#xff0c;不占用多余空间。但是随着这个电脑的技术的发展&#xff0c;几个变量的定义对于内存那么大的电脑影响已经不大了。 构…

ubuntu 安装截图软件 flameshot

linux安装flameshot 1、去github下载对应deb版本 Releases flameshot-org/flameshot GitHub 2、先更新一次依赖 sudo apt-get install -f 3、安装依赖 # Compile-time apt install g cmake build-essential qt5-default qttools5-dev-tools libqt5svg5-dev qttools5-dev…

[Qt]控件

文章摘于 爱编程的大丙 文章目录 1. 按钮类型控件1.1 按钮基类 QAbstractButton1.1.1 标题和图标1.1.2 按钮的 Check 属性1.1.3 信号1.1.4 槽函数 1.2 QPushButton1.2.1 常用API1.2.2 按钮的使用 1.3 QToolButton1.3.1 常用API1.3.2 按钮的使用 1.4 QRadioButton1.4.1 常用API…

高压电缆护层接地环流及温度在线监测系统

高压电缆的金属护层是电缆的重要组成部分&#xff0c;当缆芯通过电流时&#xff0c;会在金属护层上产生环流&#xff0c;外护套的绝缘状态差、接地不良、金属护层接地方式不正确等等都会引起护套环流异常现象&#xff0c;严重威胁电缆运行安全。 当电缆金属护层环流出现异常时…

FFmpeg入门之Windows/Linux下FFmpeg源码编译

1.源码下载: git clone https://github.com/FFmpeg/FFmpeg.git windows : macos: ubuntu: 2.编译FFmpeg CompilationGuide – FFmpeg windows: 1.下载yasm并安装 : Download - The Yasm Modular Assembler Project 下载后复制到c:/windows 2.下载SDL 3.下载H264/265源码 git…

对缓存穿透、雪崩、击穿的理解,引入分布式锁

缓存实战 1、缓存穿透 先来了解一个小图&#xff0c; 1.1 概念&#xff1a; 缓存穿透指一个一定不存在的数据&#xff0c;由于缓存未命中这条数据&#xff0c;就会去查询数据库&#xff0c;数据库也没有这条数据&#xff0c;所以返回结果是 null。如果每次查询都走数据库&a…

Vue中的动态 Class Style

动态 Class & Style 我们平时可以直接给元素设置静态的 Class 或者是 Style&#xff0c;但是这种方式会带来很多限制&#xff0c;假设我想要内容动态的改变 Class 或者是 Style&#xff0c;通过原生的方式要通过 JavaScript 频繁操作 dom 才能够实现。而在 Vue 中我们无需…

java入坑之注解

一、注解入门 注解&#xff1a;Annotation 从JDK 1.5 引入位于源码中(代码/注释/注解)&#xff0c;使用其他工具进行处理的标签注解用来修饰程序的元素&#xff0c;但不会对被修饰的对象有直接的影响只有通过某种配套的工具才会对注解信息进行访问和处理 主要用途 提供信息给编…

【职场篇】五年游戏开发老兵,我为什么劝你学UE?

我是水曜日鸡&#xff0c;一个在游戏行业摸爬滚打了5年的行业老兵。在Unity和UE4各有两年半的开发经验。曾参与开发了索尼中国之星计划之一的 《硬核机甲》 项目&#xff0c;用的是Unity引擎。目前在上海某大厂参与研发 开放世界项目&#xff0c;用的是UE4引擎。今天我就来终结…

《重构改善代码设计》

文章目录 1.重构的原则2.代码的坏味道3.第一组重构3.1.提炼函数3.2.内联函数3.3.提炼变量3.4.内联变量3.5.修改函数名称3.6.封装变量3.7.变量改名3.8.引入参数对象3.9.函数组合成类3.10.函数组合成变换3.11.拆分阶段 4. 封装4.1. 封装记录4.2. 封装集合4.3. 以对象取代基本类型…

使用cpolar配合Plex搭建私人媒体站

文章目录 1.前言2. Plex网站搭建2.1 Plex下载和安装2.2 Plex网页测试2.3 cpolar的安装和注册 3. 本地网页发布3.1 Cpolar云端设置3.2 Cpolar本地设置 4. 公网访问测试5. 结语 1.前言 用手机或者平板电脑看视频&#xff0c;已经算是生活中稀松平常的场景了&#xff0c;特别是各…

如何开启Win10虚拟机Hyper-V功能

操作步骤: 使用前提&#xff1a; 1、确保系统是 Windows 10 专业版/企业版/教育版&#xff0c;且必须是64位操作系统才支持。 提示&#xff1a;Win10家庭版不支持hyper-v。 2、使用Hyper-V需要cpu支持虚拟化并处于开启状态。 3、硬件要求及如何验证硬件兼容性&#xff1a; 硬件…

Hive 使用达梦DM8 无法识别 “COMMENT” 问题

将达梦数据库驱动 DmJdbcDriver18-8.1.2.192.jar 导入到 hive 的 lib 文件夹下 修改 hive 配置文件&#xff0c;增加 dm 数据库相关信息 <property><name>javax.jdo.option.ConnectionURL</name><value>jdbc:dm://127.0.0.1:5236?SCHEMAhive</val…

vmware官网下载(VMware workstation 下载与安装)

Oracle RAC 安装配置part1 注册VMware 个人帐号 浏览器打开官方网站 下面是主页&#xff0c;单击Customer connter 单击立即注册 填入相关信息注册完成 需要到您邮件里的激活一下帐号 注册完成 下载VMware Workstation 单击转到CUSTOMER CONNECT (您也可以在主页login…

修饰符的笔记

修饰符 完整的教程参考下面的链接 菜鸟教程-修饰符 下面是我总结的比较精简的内容 常见的修饰符有权限修饰符和非权限修饰符 权限修饰符 权限修饰符就是我们所熟知的 private&#xff0c;public&#xff0c;protect&#xff0c;default&#xff08;什么都不写&#xff0c;默…