Vue 2.x源码学习:数据响应式改造

news2024/11/26 10:39:39

众所周知,Vue是以数据驱动视图展示的,即Vue会监听数据的变化,从而自动重新渲染页面的结构。

Vue主要通过三步走来实现这个功能:

第一步是对数据进行响应式改造,即对数据的读写操作进行劫持;

第二步是对模板依赖的数据进行收集;

第三步是在数据发生变化时,触发组件更新。

数据响应式改造

0. defineReactive

对数据进行响应式改造的核心代码

// core/observer/index.js
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
​
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return}
​
  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]}
​
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) { // 当前`Dep.target`不为空时,通常指向一个`watcher`实例
        dep.depend() // 属性被收集到当前`watcher`实例的依赖数组中
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
        }
      }
    }
      return value
  },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
    }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
    }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
    } else {
        val = newVal
    }
      childOb = !shallow && observe(newVal)
      dep.notify()
  }})
} 

通过Object.defineProperty修改对象属性的属性描述符descriptor,来实现劫持对象属性的读写操作。

前置知识,对象的属性分为data型和accessor型。

data型的属性描述符包含value和writable;accessor型的属性描述符包含getter和setter函数(两者至少存在一个)。

由上述代码可以看出,所有属性被处理成了accessor型属性,即通过getter和setter来完成读写,比如当我们读取person对象上的属性name,实际得到的是name的属性访问符中的getter函数执行后的返回值。

上述的响应式改造中,每个属性会对应一个dep实例:const dep = new Dep(),假如属性值val是对象或数组,会被列入观察对象,他的属性会被递归进行响应式改造let childOb = !shallow && observe(val)

get函数被用于收集依赖

get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
        }
      }
    }
      return value
  }, 

该函数在对象属性被访问时会执行,如果Dep.target不为空,即当下有一个监听器watcher在收集依赖,就进行依赖的收集,dep实例会被收集到该watcher的依赖数组newDeps中,同时dep也会将此watcher记录到自己的subs订阅数组中,记录有谁订阅了自己的变更。

如果childOb不为空(即属性值val为数组或对象,且可扩展),就对val的__ob__属性也进行收集操作。

如果value是数组,对数组中的对象元素也进行依赖收集。

就是一层层的递归收集。

set函数被用于通知变更:

set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
    }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
    }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
    } else {
        val = newVal
    }
      childOb = !shallow && observe(newVal)
      dep.notify()
  } 

如果属性的新值是属性或对象,就更新childOb

完成属性赋值操作后,调用dep.notify(),通知所有订阅了自己的watcher实例执行update操作,即下面代码中的for循环操作。

// core/observer/dep.js
export default class Dep {
  // ...
​
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
  }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
  }}
} 

如果是同步this.sync的watcher会立即被执行,否则会插入到watcher队列queueWatcher(this)排队等待执行:

// core/observer/watcher.js
update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
  } else if (this.sync) {
      this.run()
  } else {
      queueWatcher(this)
  }} 

1. initInjections

// core/instance/inject.js
export function initInjections (vm: Component) {
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    toggleObserving(false)
    Object.keys(result).forEach(key => {
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production') {
        defineReactive(vm, key, result[key], () => {
          warn(
            `Avoid mutating an injected value directly since the changes will be ` +
            `overwritten whenever the provided component re-renders. ` +
            `injection being mutated: "${key}"`,
            vm
        )
      })
    } else {
        defineReactive(vm, key, result[key])
    }
  })
    toggleObserving(true)}
} 

通过执行resolveInject解析inject中的数据,解析结果赋值给result。result包含inject中所有的key,如果上级组件中没有对应inject数据的provide,就赋默认值,简单来说大致就是result[key] = inject[key] || default

再调用defineReactive(vm, key, result[key])将这些key加到vm实例上,即inject中的数据也会进行响应式处理。

假设存在一个inject:["person"],如果person的值是个对象,它是一个被观察对象,当前子组件的watcher会对该对象的__ob__属性依赖收集,在上级组件中更改了原始person的某个属性,就会触发子组件的更新。

2. initProps

// core/instance/state.js
function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // root instance props should be converted
  if (!isRoot) {
    toggleObserving(false)}
  for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
      )
    }
      defineReactive(props, key, value, () => {
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
            `overwritten whenever the parent component re-renders. ` +
            `Instead, use a data or computed property based on the prop's ` +
            `value. Prop being mutated: "${key}"`,
            vm
        )
      }
    })
  } else {
      defineReactive(props, key, value)
  }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
  }}
  toggleObserving(true)
} 

propsOptions,接收的是vm.$options.props,是声明接收的props的配置,;vm.$options.propsData是实际接收到的props数据。

调用defineReactive(props, key, result[key])将propsOptions上的key加到props对象上,即vm._props上,进行响应式处理,如果是在vm上不存在的key,通过proxy(vm, '_props', key)操作,使得可以通过vm直接访问到_props的属性,而不需要通过_props对象来访问。

3. initData

// core/instance/state.js
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
  : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
  )}
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
      )
    }
  }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
    )
  } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
  }}
  // observe data
  observe(data, true /* asRootData */)
} 

data选项会被挂在vm._data上,从上述代码中可以看出,data必须是一个对象,或者返回值为对象的函数。

通过proxy(vm, '_data', key)操作,vm可以直接访问到_data的属性,而不需要通过_data对象来访问。

最后通过observe(data, true /* asRootData */)来对数据做响应式改造,可以看到这个observe方法多传了一个参数值为true,标记当前处理的数据是$options.data对象。

observe方法实际是创建一个新的ob实例,数据的__ob__属性相信在控制台打印过vue中数据的同学都不陌生,都是指向ob实例。

// core/observe/index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return}
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__} else if (
    shouldObserve &&
    !isServerRendering() &&
  (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue) {
    ob = new Observer(value)}
  if (asRootData && ob) {
    ob.vmCount++}
  return ob
} 
// core/observe/index.js
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data
​
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
    } else {
        copyAugment(value, arrayMethods, arrayKeys)
    }
      this.observeArray(value)
  } else {
      this.walk(value)
  }}
​
  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
  }}
​
  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
  }}
} 

从上面Observer的构造函数中可以看出,创建ob实例后,这个实例就挂载数据的__ob__属性上了,因为在iniDats时传递给构造函数的参数是个对象,所以会调用walk方法,继续看walk方法的定义,可以看出,是把这个对象的属性逐个取出,调用defineReactive(obj, keys[i])进行响应式改造。

4. initComputed

// core/instance/state.js
function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()
​
  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
    )
  }
​
    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
    )
  }
​
    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
  } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
    } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
    } else if (vm.$options.methods && key in vm.$options.methods) {
        warn(`The computed property "${key}" is already defined as a method.`, vm)
    }
  }}
} 

获得每个计算属性对应watcher的初始值

从上述代码中可以看出,会遍历computed所有的属性,每个属性对应配置一个watcher实例,watcher实例在创建时,会调用每个computed对应的getter获取一遍初始值,放在watcher实例的value属性上

// core/observer/watcher.js
export default class Watcher {
  // ...
​
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
  }
    vm._watchers.push(this)
    // ...
    this.value = this.lazy
      ? undefined
    : this.get()}
​
  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
  } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
        throw e
    }
  } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
    }
      popTarget()
      this.cleanupDeps()
  }
    return value}
​
  // ...
} 
// core/observer/dep.js
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
} 

可以看到,执行watcher实例的get()方法时,会进行一个pushTarget(this)的操作,此操作修改了Dep.target,使它指向了当前的watcher实例,如果某个computed属性依赖了data中的某个属性,需要读取data中的某个属性值,就会触发该data属性的getter函数,使得该data属性被收集到当前watcher实例的依赖数组中。

完成computed属性的取值后,执行popTarget(),即下面的代码:

// core/observer/dep.js
export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
} 

会使Dep.target指回上一个watcher实例。

最后清理依赖this.cleanupDeps(),将不再关联的依赖dep其订阅数组中对应的watcher移除,将newDeps赋值给deps并清空newDeps,代表该watcher实例一次依赖收集完毕。

计算属性被读取时,其对应watcher依赖的数据会被当前watcher收集为自身的依赖

如果computed某个属性的标识符不在vm实例上,就继续执行defineComputed(vm, key, userDef),会将给vm实例添加一个名为key的属性,该属性的getter函数由下述代码定义:

// core/instance/state.js
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
    }
      if (Dep.target) {
        watcher.depend()
    }
      return watcher.value
  }}
} 

即在这个计算属性被读取时,会拿到它所对应的watcher实例,如果当前Dep.target不为null时,watcher会执行实例方法depend()

export default class Watcher {
  // ...
​
  /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
  }}
​
  // ...
} 

可以看到此watcher实例的依赖deps会被一一取出,执行dep实例的depend方法:

// core/observerdep.js
depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
  }} 

即此watcher实例的依赖都会被收集到当前Dep.target指向的watcher实例的依赖数组中。

5. initWatch

// core/instance/state.js
function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
    }
  } else {
      createWatcher(vm, key, handler)
  }}
}
​
function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler}
  if (typeof handler === 'string') {
    handler = vm[handler]}
  return vm.$watch(expOrFn, handler, options)
}
​
Vue.prototype.$watch = function ( expOrFn: string | Function,
    cb: any,
    options?: Object ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
  }
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      const info = `callback for immediate watcher "${watcher.expression}"`
      pushTarget()
      invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
      popTarget()
  }
    return function unwatchFn () {
      watcher.teardown()
  }} 

initWatch的内容比较简单,就是通过调用createWatcher(vm, key, handler),一一对应生成watcher实例,并且给watcher实例标记options.user = true,代表这个watcher是用户配置的。

每个watch通常是有一个对应的表达式(通常是vm的data数据)和一个对应的回调函数,使用场景通常是当vm中的某些数据发生改变时,用户需要做一些自定义的操作来做处理。

与computed中生成对应watcher实例类似,watcher实例在创建时,每个watch对应的表达式就会被求值一遍,即vm实例上的某些数据属性被读取,这些属性对应的dep会被收集到该watcher实例的依赖数组中,求得的值会放在watcher实例的value属性上,如果某个watch配置了immediate,就立即执行一遍watch对应的回调函数,入参为watchervalue属性值。

可以看到在执行回调函数前,执行了一个pushTarget(),此时Dep.target会指向空,所以在回调函数执行过程中,如果vm的某些数据属性被访问,这些属性不会被收集依赖,因为属性的getter函数中在属性被收集依赖前有个对Dep.target的判空检查。

6. 一些说明

__ob__是给对象加的属性,指向observer实例,ob和对象是一对一,代表这个对象被观察,该对象的属性的读写操作会被做响应式处理,即被劫持。

dep是给属性配置的用于依赖收集的,通常对象的某个属性与dep是一对一,可以被多个watcher收集,即多个watcher实例在监听这个属性的变化;__ob__是对象的特殊属性,它也有自己的dep,可以被watcher收集。

一个watcher实例只会关联一个vm,一个vm实例可以关联多个watcher,watcher实例会放在vm._watchers数组中;渲染watcher还会放在vm._watcher上,渲染watcher从字面上理解就是与组件渲染有关的watcher

最后

最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

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

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

相关文章

字符数组编程题(C语言)

文章目录1、用字符%c的形式给一个字符数组赋初值&#xff0c;然后以字符串%s的形式输出2、用字符getchar&#xff08;&#xff09;的形式给一个字符数组赋初值&#xff0c;然后以字符串puts&#xff09;的形式输出4、从键盘上输入一串字符gets&#xff0c;然后输出%s5、从键盘输…

Mysql导出100万条数据,9种导出方法优缺点和速度、文件大小测试

这里写目录标题1.DBase文件2.文本文件3. CSV文件4.HTML文件5.Excel数据表低版本6.Excel文件2007年以后版本7.SQL脚本文件8.XML文件9.JSON文件总结这一次我主要就是想针对mysql导出的速度和文件大小进行优缺点测试&#xff0c;这次主要就是用上之前生成的天气表这是表里面的数据…

除夕New Year Eve,祝大家团圆幸福!

除夕&#xff0c;为岁末的最后一天夜晚,意为旧岁至此而除&#xff0c;另换新岁。New years Eve is the last night at the end of the year.which means that the old year will be removed and a new year will be replaced.除夕&#xff0c;在国人心中是具有特殊意义的&#…

第一章 TCP/IP 协议

作者简介&#xff1a;一名云计算网络运维人员、每天分享网络与运维的技术与干货。 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a;网络豆的主页​​​​​​ 目录 前言 一.什么是TCP/IP ​编辑 二.什么是协议 1.三要素 2.协议与标准区别 三.广…

中小型企业如何从企业内容管理 (ECM) 系统提供的工具中受益?

在当今快节奏的商业世界中&#xff0c;时间变得非常宝贵。团队越大&#xff0c;就越难有效地管理时间。但即使是中小型企业也可以从企业内容管理 (ECM) 系统提供的工具中受益。 ECM 系统使各种规模的企业能够通过数字化纸质文档、捕获电子文档和电子邮件、自动化文档驱动的流…

(转)Chrome的最小字体12px限制最终解决办法

相信不少做网站的用户会有这样一个问题&#xff0c;Chrome 默认最小字体是12px&#xff08;最新版英文也有此问题&#xff09;&#xff0c;这个是 Chrome 为了更好显示中文设计的&#xff0c;但是这样一来就会出现某些上标、下标字体过大&#xff0c;影响用户体验。之前在开发H…

ue4c++日记2(继承|设置位置|对象移动)

目录 语句速查 静态网格定义 创建静态网格对象 设置对象位置 编辑器可编辑 编辑器可见 仅类默认值处可见 速览定义可查剩下 对象移动 1继承 例子 1.创建actor 2.命名和填写路径 小插曲 3.新建一个蓝图类继承上面创建的actor 4.其可蓝图化是因为创建的c类可蓝图化 5…

【算法基础】1.8离散化

文章目录离散化如何离散确定映射方式区间和当数据范围的跨度很大&#xff0c;但是数据很稀疏时&#xff0c;可以使用离散化。 离散化 如何离散 数据范围很大&#xff0c;但是并不是每个数字都会出现&#xff0c;就可以将原始数据按照顺序映射到一个小的数据范围。 确定映射方…

第五届字节跳动青训营 前端进阶学习笔记(二)JavaScript编码规范

文章目录1.前言2.写好JS的一些基本原则这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天 1.前言 本次课程主要讲解了有关JavaScript编码原则和代码优化的相关问题。 重点内容&#xff1a; HTML、CSS、JS各司其责具备正确性、扩展性、复用性的组件封装函数式编程思想代…

进程、线程及python的多线程编程

目录 一.进程、线程和并行执行 1.什么是进程、线程 注意 2.什么是并行执行 二.python的多线程编程 threading模块 语法 多线程编程的传参 演示 三.总结 一.进程、线程和并行执行 1.什么是进程、线程 现代操作系统比如Mac OS X,UNIX, Linux, Windows等&#xff0c;都是…

CSC|2023年艺术类人才培养特别项目解读及建议

目前国家留学基金委&#xff08;CSC&#xff09;官网已经发布了2023年艺术类人才培养特别项目通知&#xff0c;知识人网小编现将其选派工作流程、选派办法、申请材料及说明&#xff08;访问学者、博士后&#xff09;原文转载并加以解读、提出建议。知识人网解读及建议一、2023年…

新产品Digi XBee RR无线模块迁移指南

&#xff08;咨询申请 - 上海皕科电子有限公司--专注物联网 登记后&#xff0c;可提供相关资料&#xff0c;购买XBee模块&#xff0c;有机会获得USB评估底板及相关中文资料&#xff0c;或者免费申请借用评估套件&#xff09; 目录 一、简介 1、MicroPython 2、蓝牙 3、文件…

SpringBoot+easypol前后端分离实现excel导出(保姆级教程)

本篇博文目录1.后端2.前端(采用axios)3.运行效果1.后端 导入easypoi的依赖 <!--用来处理POL相关的操作:easypol--><dependency><groupId>cn.afterturn</groupId><artifactId>easypoi-base</artifactId><version>4.2.0</version&…

Cadence OrCAD: 层次化设计中的电源和地符号

Cadence OrCAD: 层次化设计中的电源和地符号 层次化设计操作步骤&#xff1a;Cadence OrCAD: 层次化设计 Hierarchical Design 本文介绍一个小问题&#xff1a;分层设计中的电源和地符号的作用范围。 电源符号默认作用范围 OrCAD中电源和地符号默认是全局的&#xff0c;也就…

2023年1月中国数据库排行榜:OceanBase 持续两月登顶,前四甲青云直上开新局

一元复始&#xff0c;万象更新。 国产数据库在经历过耕获菑畲的一年后&#xff0c;产品、生态、人才队伍建设等都取得了重大的进展。2023年1月 墨天轮中国数据库流行度排行 火热出炉&#xff0c;本月排行榜“属性”列新增“多模型”&#xff0c;榜单前十名变动较小&#xff0c;…

基于springboot,vue影院订票系统

开发工具&#xff1a;IDEA服务器&#xff1a;Tomcat9.0&#xff0c; jdk1.8项目构建&#xff1a;maven数据库&#xff1a;mysql5.7系统用户前台和管理后台两部分&#xff0c;项目采用前后端分离前端技术&#xff1a;vue elementUI服务端技术&#xff1a;springbootmybatis项目功…

JAVA开发(Netty框架与NIO)

所谓IO即input和output的缩写&#xff0c;是对数据的流入和流出的一种抽象。其中NIONIO主要有三大核心部分&#xff1a;Channel&#xff08;通道&#xff09;&#xff0c;Buffer&#xff08;缓冲区&#xff09;,Selector&#xff08;选择器&#xff09;。Channel(通道)&#xf…

Day54 跨域CORS资源JSONP回调域名接管劫持

前言&#xff1a; #知识点&#xff1a; 1、子域名接管-检测&探针&利用 2、COSP跨域资源-检测&探针&利用 3、JSONP跨域回调-检测&探针&利用 #前置知识点&#xff1a; -同源策略(SOP)-“同源”包括三个条件&#xff1a;同协议 同域名 同端口 同源策…

C/C++之(五)洛谷刷题基础题 --- 新年好

学习之路&#xff0c;长路漫漫&#xff0c;写学习笔记的过程就是把知识讲给自己听的过程。 唯有热爱&#xff0c;可抵岁月漫长&#xff0c;唯有热爱&#xff0c;不畏世间无常&#xff01; 新的一年祝你大展宏“兔” 新的一年&#xff0c;大家记得不要忘记刷题(⊙o⊙)&#xff…

NTN(四) RRC related

微信同步更新欢迎关注同名modem协议笔记。 这篇主要是与RRC层相关的内容&#xff0c;按照cell selection/re-selection->idle->connected 的顺序&#xff0c;对涉及NTN的内容进行总结。首先看下NTN RF相关的内容&#xff0c;这部分对应38.101-5这本spec。 NTN freq inf…