当前位置: 首页 > 编程日记 > 正文

你不知道的Vue响应式原理

文章首发于github Blog。

本文根据Vue源码v2.x进行分析。这里只梳理最源码中最主要的部分,略过非核心的一些部分。响应式更新主要涉及到WatcherDepObserver这几个主要类。

本文主要弄清楚以下几个容易搞混的问题:

  • WatcherDepObserver这几个类之间的关系?
  • Dep中的 subs 存储的是什么?
  • Watcher中的 deps 存储的是什么?
  • Dep.target 是什么,该值是何处赋值的?

本文直接从新建Vue实例入手,一步一步揭开Vue的响应式原理,假设有以下简单的Vue代码:

var vue = new Vue({el: "#app",data: {counter: 1},watch: {counter: function(val, oldVal) {console.log('counter changed...')}}
})
复制代码

1. Vue实例初始化

从Vue的生命周期可知,首先进行init初始化操作,这部分代码在instance/init.js中。

src/core/instance/init.js

initLifecycle(vm) // vm生命周期相关变量初始化操作
initEvents(vm) // vm事件相关初始化
initRender(vm) // 模板解析相关初始化
callHook(vm, 'beforeCreate') // 调用beforeCreate钩子函数
initInjections(vm) // resolve injections before data/props 
initState(vm) // vm状态初始化(重点在这里)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created') // 调用created钩子函数
复制代码

上述源码中的initState(vm)是要研究的重点,里面实现了propsmethodsdatacomputedwatch的初始化操作。这里根据上述例子,重点看datawatch,源码位置在instance/state.js

src/core/instance/state.js

export function initState (vm: Component) {vm._watchers = []const opts = vm.$optionsif (opts.props) initProps(vm, opts.props)if (opts.methods) initMethods(vm, opts.methods)if (opts.data) {initData(vm) // 对vm的data进行初始化,主要是通过Observer设置对应getter/setter方法} else {observe(vm._data = {}, true /* asRootData */)}if (opts.computed) initComputed(vm, opts.computed)// 对添加的watch进行初始化if (opts.watch && opts.watch !== nativeWatch) {initWatch(vm, opts.watch)}
}
复制代码

2. initData

Vue实例为它的每一个data都实现了getter/setter方法,这是实现响应式的基础。关于getter/setter可查看MDN web docs。 简单来说,就是在取值this.counter的时候,可以自定义一些操作,再返回counter的值;在修改值this.counter = 10的时候,也可以在设置值的时候自定义一些操作。initData(vm)的实现在源码中的instance/state.js

src/core/instance/state.js

while (i--) {...// 这里将data,props,methods上的数据全部代理到vue实例上// 使得vm.counter可以直接访问
}
// 这里略过上面的代码,直接看最核心的observe方法
// observe data
observe(data, true /* asRootData */)复制代码

这里observe()方法将data变成可观察的,为什么说是可观察的?主要是实现了getter/setter方法,让Watcher可以观察到该数据的变化。下面看看observe的实现。

src/core/observer/index.js

export function observe (value: any, asRootData: ?boolean): Observer | void {if (!isObject(value) || value instanceof VNode) {return}let ob: Observer | voidif (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {ob = value.__ob__} else if (observerState.shouldConvert &&!isServerRendering() &&(Array.isArray(value) || isPlainObject(value)) &&Object.isExtensible(value) &&!value._isVue) {ob = new Observer(value) // 重点在这里,响应式的核心所在}if (asRootData && ob) {ob.vmCount++}return ob
}
复制代码

这里只关注new Observer(value),这是该方法的核心所在,通过Observer类将vue的data变成响应式。 根据我们的例子,此时入参value的值是{ counter: 1 }。 下面就具体看看Observer类。

3. Observer

首先看看该类的构造方法,new Observer(value)首先执行的是该构造方法。作者的注释说了,Observer Class将每个目标对象的键值(即data中的数据)转换成getter/setter形式,用于进行依赖收集和通过依赖通知更新。

src/core/observer/index.js

/*** Observer class that are attached to each observed* object. Once attached, the observer converts target* object's property keys into getter/setters that* collect dependencies and dispatches updates.*/
export class Observer {value: any;dep: Dep;vmCount: number; // number of vms that has this object as root $dataconstructor (value: any) {this.value = valuethis.dep = new Dep()this.vmCount = 0def(value, '__ob__', this)if (Array.isArray(value)) {const augment = hasProto? protoAugment: copyAugmentaugment(value, arrayMethods, arrayKeys)this.observeArray(value)} else {this.walk(value) // 遍历data对象中{counter : 1, ..} 中的每个键值(如counter),设置其setter/getter方法。}}...
}
复制代码

这里最核心的就是this.walk(value)方法,this.observeArray(value)是对数组数据的处理,实现对应的变异方法,这里先不考虑。

继续看walk()方法,注释中已说明walk()做的是遍历data对象中的每一设置的数据,将其转为setter/getter

  /*** Walk through each property 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], obj[keys[i]])}}
复制代码

那么最终将对应数据转为getter/setter的方法就是defineReactive()方法。从方法命名上也容易知道该方法是定义为可响应的,结合最开始的例子,这里调用就是defineReactive(...)如图所示:

源码如下:

export function defineReactive (obj: Object,key: string,val: any,customSetter?: ?Function,shallow?: boolean
) {// dep 为当前数据的依赖实例// dep 维护着一个subs列表,保存依赖与当前数据(此时是当前数据是counter)的观察者(或者叫订阅者)。观察者即是Watcher实例。const dep = new Dep() ---------------(1)const property = Object.getOwnPropertyDescriptor(obj, key)if (property && property.configurable === false) {return}// cater for pre-defined getter/settersconst getter = property && property.getconst setter = property && property.setlet childOb = !shallow && observe(val)// 定义getter与setterObject.defineProperty(obj, key, {enumerable: true,configurable: true,get: function reactiveGetter () {const value = getter ? getter.call(obj) : val// 这里在获取值之前先进行依赖收集,如果Dep.target有值的话。if (Dep.target) {    -----------------(2)dep.depend()if (childOb) {childOb.dep.depend()if (Array.isArray(value)) {dependArray(value)}}}// 依赖收集完后返回值return value},...
}
复制代码

先看getter方法,该方法最重要的有两处。

  1. 为每个data声明一个dep实例对象,随后dep就被对应的data给闭包引用了。举例来说就是每次对counter取值或修改时,它的dep实例都可以访问到,不会消失。
  2. 根据Dep.target来判断是否收集依赖,还是普通取值。这里Dep.target的赋值后面再将,这里先知道有这么一回事。

然后再看下setter方法,源码如下:

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()}// 这里对数据的值进行修改if (setter) {setter.call(obj, newVal)} else {val = newVal}childOb = !shallow && observe(newVal)// 最重要的是这一步,即通过dep实例通知观察者我的数据更新了dep.notify()
}
复制代码

到这里基本上Vue实例data的初始化就基本结束,通过下图回顾下initData的过程:

随后要进行的是watch的初始化:

src/core/instance/state.js

export function initState (vm: Component) {...if (opts.data) {initData(vm) // 对vm的data进行初始化,主要是通过Observer设置对应getter/setter方法} // initData(vm) 完成后进行 initWatch(..)...// 对添加的watch进行初始化if (opts.watch && opts.watch !== nativeWatch) {initWatch(vm, opts.watch)}
}
复制代码

4. initWatch

这里initWatch(vm, opts.watch)对应到我们的例子中如下所示:

initWatch源码如下:

src/core/instance/state.js

function initWatch (vm: Component, watch: Object) {for (const key in watch) {// handler 是观察对象的回调函数// 如例子中counter的回调函数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)}}
}
复制代码

createWatcher(vm, key, handler)是根据入参构建Watcher实例信息,源码如下:

function createWatcher (vm: Component,keyOrFn: string | Function,handler: any,options?: Object
) {// 判断是否是对象,是的话提取对象里面的handler方法if (isPlainObject(handler)) {options = handlerhandler = handler.handler}// 判断handler是否是字符串,是的话说明是vm实例上的一个方法// 通过vm[handler]获取该方法// 如 handler='sayHello', 那么handler = vm.sayHelloif (typeof handler === 'string') {handler = vm[handler]}// 最后调用vm原型链上的$watch(...)方法创建Watcher实例return vm.$watch(keyOrFn, handler, options)
}
复制代码

$watch是定义在Vue原型链上的方法,源码如下:

core/instance/state.js

  Vue.prototype.$watch = function (expOrFn: string | Function,cb: any,options?: Object): Function {const vm: Component = thisif (isPlainObject(cb)) {return createWatcher(vm, expOrFn, cb, options)}options = options || {}options.user = true// 创建Watcher实例对象const watcher = new Watcher(vm, expOrFn, cb, options)if (options.immediate) {cb.call(vm, watcher.value)}// 该方法返回一个函数的引用,直接调用该函数就会调用watcher对象的teardown()方法,从它注册的列表中(subs)删除自己。return function unwatchFn () {watcher.teardown()}}
复制代码

经过一系列的封装,这里终于看到了创建Watcher实例对象了。下面将详细讲解Watcher类。

5. Watcher

根据我们的例子,new Watcher(...)如下图所示:

首先执行Watcher类的构造方法,源码如下所示,省略了部分代码:

core/observer/watcher.js

  constructor (vm: Component,expOrFn: string | Function,cb: Function,options?: ?Object,isRenderWatcher?: boolean) {...this.cb = cb // 保存传入的回调函数this.id = ++uid // uid for batchingthis.active = truethis.dirty = this.lazy // for lazy watchersthis.deps = [] // 保存观察数据当前的dep实例对象this.newDeps = []  // 保存观察数据最新的dep实例对象this.depIds = new Set()this.newDepIds = new Set()// parse expression for getter// 获取观察对象的get方法// 对于计算属性, expOrFn为函数if (typeof expOrFn === 'function') {this.getter = expOrFn} else {// 通过parsePath方法获取观察对象expOrFn的get方法this.getter = parsePath(expOrFn)...}// 最后通过调用watcher实例的get()方法,// 该方法是watcher实例关联观察对象的关键之处this.value = this.lazy? undefined: this.get()}
复制代码

parsePath(expOrFn)的具体实现方法如下:

core/util/lang.js

/*** Parse simple path.*/
const bailRE = /[^\w.$]/ // 匹配不符合包含下划线的任意单词数字组合的字符串
export function parsePath (path: string): any {// 非法字符串直接返回if (bailRE.test(path)) {return}// 举例子如 'counter'.split('.') --> ['counter']const segments = path.split('.')// 这里返回一个函数给this.getter// 那么this.getter.call(vm, vm),这里vm就是返回函数的入参obj// 实际上就是调用vm实例的数据,如 vm.counter,这样就触发了counter的getter方法。return function (obj) {for (let i = 0; i < segments.length; i++) {if (!obj) returnobj = obj[segments[i]]}return obj}
}
复制代码

这里很巧妙的返回了一个方法给this.getter, 即:

this.getter = function(obj) {for (let i = 0; i < segments.length; i++) {if (!obj) returnobj = obj[segments[i]]}return obj
}
复制代码

this.getter将在this.get()方法内调用,用来获取观察对象的值,并触发它的依赖收集,这里即是获取counter的值。

Watcher构造方法的最后一步,调用了this.get()方法,该方法源码如下:

  /*** Evaluate the getter, and re-collect dependencies.*/get () {// 该方法实际上是设置Dep.target = this// 把Dep.target设置为该Watcher实例// Dep.target是个全局变量,一旦设置了在观察数据中的getter方法就可使用了pushTarget(this)let valueconst vm = this.vmtry {// 调用观察数据的getter方法// 进行依赖收集和取得观察数据的值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 watchingif (this.deep) {traverse(value)}// 此时观察数据的依赖已经收集完// 重置Dep.target=nullpopTarget()// 清除旧的depsthis.cleanupDeps()}return value}
复制代码

关键步骤已经在上面代码中注释了,下面给出一个Observer,Watcher类之间的关联关系,图中还是以我们的例子进行描述:

  • 红色箭头:Watcher类实例化,调用watcher实例的get()方法,并设置Dep.target为当前watcher实例,触发观察对象的getter方法。
  • 蓝色箭头:counter对象的getter方法被触发,调用dep.depend()进行依赖收集并返回counter的值。依赖收集的结果:1.counter闭包的dep实例的subs添加观察它的watcher实例w12. w1的deps中添加观察对象counter的闭包dep
  • 橙色箭头:当counter的值变化后,触发subs中观察它的w1执行update()方法,最后实际上是调用w1的回调函数cb。

Watcher类中的其他相关方法都比较直观这里就直接略过了,详细请看Watcher类的源码。

6. Dep

上图中关联Observer和Watcher类的是Dep,那么Dep是什么呢?

Dep可以比喻为出版社,Watcher好比读者,Observer好比东野圭吾相关书籍。比如读者w1对东野圭吾的白夜行(我们例子中的counter)感兴趣,读者w1一旦买了东野圭吾的书,那么就会自动在这本书的出版社(Dep实例)里面注册填w1信息,一旦该出版社有了东野圭吾这本书最新消息(比如优惠折扣)就会通知w1。

现在看下Dep的源码:

core/observer/dep.js

export default class Dep {static target: ?Watcher;id: number;subs: Array<Watcher>;constructor () {this.id = uid++// 保存观察者watcher实例的数组this.subs = []}// 添加观察者addSub (sub: Watcher) {this.subs.push(sub)}// 移除观察者removeSub (sub: Watcher) {remove(this.subs, sub)}// 进行依赖收集depend () {if (Dep.target) {Dep.target.addDep(this)}}// 通知观察者数据有变化notify () {// stabilize the subscriber list firstconst subs = this.subs.slice()for (let i = 0, l = subs.length; i < l; i++) {subs[i].update()}}
}
复制代码

Dep类比较简单,对应方法也非常直观,这里最主要的就是维护了保存有观察者实例watcher的一个数组subs

7. 总结

到这里,主要的三个类都研究完了,现在基本可以回答文章开头的几个问题了。

Q1:WatcherDepObserver这几个类之间的关系?

A1:Watcher是观察者观察经过Observer封装过的数据,DepWatcher和观察数据间的纽带,主要起到依赖收集和通知更新的作用。

Q2:Dep中的subs存储的是什么?

A2: subs存储的是观察者Watcher实例。

Q3:Watcher中的deps存储的是什么?

A3:deps存储的是观察数据闭包中的dep实例。

Q4:Dep.target是什么, 该值是何处赋值的?

A4:Dep.target是全局变量,保存当前的watcher实例,在new Watcher()的时候进行赋值,赋值为当前Watcher实例。

8. 扩展

这里看一个计算属性的例子:

var vue = new Vue({el: "#app",data: {counter: 1},computed: {result: function() {return 'The result is :' + this.counter + 1;}}
})
复制代码

这里的result的值是依赖与counter的值,通过result更能体现出Vue的响应式计算。计算属性是通过initComputed(vm, opts.computed)初始化的,跟随源码追踪下去会发现,这里也有Watcher实例的创建:

core/instance/state.js

  watchers[key] = new Watcher(vm,  // 当前vue实例getter || noop,  // result对应的方法 function(){ return 'The result is :' + this.counter + 1;}noop, // noop是定义的一个空方法,这里没有回调函数用noop代替computedWatcherOptions // { lazy: true })
复制代码

示意图如下所示:

这里计算属性result因为依赖于this.counter,因此设置一个watcher用来观察result的值。随后通过definedComputed(vm, key, userDef)来定义计算属性。在计算获取result的时候,又会触发this.countergetter方法,这样使得result的值依赖于this.counter的值。

最后会为计算属性result定义它的setter/getter属性:Object.defineProperty(target, key, sharedPropertyDefinition)。更详细信息请查看源码。

9. 参考

  1. vue 官方文档
  2. vue 源码
  3. vue 源码解析

相关文章:

优秀开发者必备技能包:Python调试器

作者 | Roky0429 来源 | Python空间&#xff08;ID:Devtogether&#xff09; 人工智能的现状及今后发展趋势如何&#xff1f; https://edu.csdn.net/topic/ai30?utm_sourcecsdn_bw 写在之前 不管是之前搞 acm 用 c/c 写算法还是后来用 Python 写代码&#xff0c;我发现在程…

【Qt】dumpbin详解

dumpbin简介 DUMPBIN是在Windows平台下用于显示COFF格式文件信息的一个命令行工具。你可以使用DUMPBIN去显示COFF格式的文件信息&#xff0c;比如像vc编译器生成的目标文件&#xff08;obj&#xff09;&#xff0c;可执行文件&#xff08;exe&#xff09;和动态链接库&#xf…

感悟Windows7

Win7已经逐渐在中国电脑用户中普遍起来&#xff0c;绝大多数人对其新增的功能很升级的功能都很是好评&#xff0c;接下来就将简略总结一下我对此系统使用的一些感想。 l 界面更加体现以人为本的思想。更加赏心悦目并且选择更加多元化。比如桌面小工具就有了更多的选择并且还可…

【Qt】Qt5在ubuntu16.04无法输入中文解决方式

参考博客 https://blog.csdn.net/linux_2016/article/details/52356576 博客中没有修改库的执行权限 解决方法 安装&#xff1a;fcitx-frontend-qt5 sudo apt-get install fcitx-frontend-qt5 拷贝动态库到Qt安装目录下的两个目录中 cp /usr/lib/x86_64-linux-gnu/qt5/p…

拿下中科大的计算机课程全靠它了!

整理 | 琥珀 出品 | AI科技大本营&#xff08;ID: rgznai100&#xff09; 人工智能的现状及今后发展趋势如何&#xff1f; https://edu.csdn.net/topic/ai30?utm_sourcecsdn_bw 中国科学技术大学&#xff0c;简称“中科大”&#xff0c;是国内有名的 985、211 重点高校。近年…

【阿里Q3财报:阿里云去年营收破百亿,亚洲市场遥遥领先】

2月1号晚间&#xff0c;阿里巴巴公布2018财年第三季度财报&#xff0c;阿里云保持持续强劲的增长&#xff0c;季度营收同比增长104%到35.99亿元。2017年累计营收突破百亿&#xff0c;达112亿&#xff0c;这是国内首次出现百亿规模的云计算服务商&#xff0c;在亚洲市场遥遥领先…

“神仙”打架,“凡人”遭殃

神仙是啥&#xff1f;古时神话时代举手翻江倒海的人物啊&#xff0c;那神仙打架是啥后果&#xff0c;相信大家心里都有个底吧。那现代社会的“神仙”是啥&#xff1f;那就是有钱有权的大人物或者商家&#xff0c;那真是让咱仰望的对象啊~ 话说最近这360跟QQ的巅峰对决&am…

120种小狗图像傻傻分不清?用fastai训练一个分类器

作者&#xff1a;一杯奶茶的功夫 链接&#xff1a;https://www.jianshu.com/p/ab35ed21df87 程序员转行学什么语言&#xff1f; https://edu.csdn.net/topic/ai30?utm_sourcecsdn_bw 这篇文章会讲解如何制作能够分类120种小狗的图像分类器。 这篇文章中所讲述的内容都是基于…

View_01_LayoutInflater的原理、使用方法

View_01_LayoutInflater的原理、使用方法 本篇博客是郭神博客Android视图状态及重绘流程分析&#xff0c;带你一步步深入了解View&#xff08;一&#xff09;的读书笔记的笔记。 LayoutInflater简单介绍 setContentView()内部是使用LayoutInflater来完毕载入布局的。setContent…

【ubuntu】在ubuntu下无法输出拼音输入法中的中括号“【” 和 “】”的解决方法

问题 在新装的ubuntu16.04中&#xff0c;打不出中括号【】&#xff0c;而变成 “”和“「” 解决方法 修改文件/usr/share/fcitx/data/punc.mb.zh_CN sudo vi /usr/share/fcitx/data/punc.mb.zh_CN将18、19行改为如下内容 18 [ 【 19 ] 】

配置数据源和分页

1.tomcat的版本&#xff0c;最好不要是安装版的&#xff0c;要解压缩的&#xff0c;不然无法连接数据源。2.conf》context.xml<Resource name"hotel" auth"Container" type"javax.sql.DataSource" driverClassName"oracle.jdbc.…

售价1万7的华为Mate X很贵吗?

整理 | 琥珀 出品 | AI科技大本营&#xff08;ID: rgznai100&#xff09; 60s测试&#xff1a;你是否适合转型人工智能&#xff1f; https://edu.csdn.net/topic/ai30?utm_sourcecxrs_bw 继三星之后&#xff0c;网友们期待已久的华为终于忍不住宣布了一系列重磅消息&#xff…

【C】数组数组初始化总结

C数组初始化总结 发现一个新方法&#xff0c;可以分段初始化数组 eg&#xff1a;int arrayC[MAX_LEN] {[1 … 5]9, [6 … 9] 8}; 代码如下 #include <stdio.h> #define MAX_LEN 10int main (int argc, char *argv[]) { //不初始化&#xff0c;参数值随机分配 //[0][0…

Golang的反射reflect深入理解和示例

[TOC] Golang的反射reflect深入理解和示例 【记录于2018年2月】 编程语言中反射的概念 在计算机科学领域&#xff0c;反射是指一类应用&#xff0c;它们能够自描述和自控制。也就是说&#xff0c;这类应用通过采用某种机制来实现对自己行为的描述&#xff08;self-representati…

如何读取多个文件,文件后缀名不一致,不过类似source.1 source.2 source.3等

#include <stdio.h> #include <stdlib.h> //为了使用exit() char *itoa(int num,char *str,int radix); int main() { int ch; FILE* fp; // char fname[50]"scan1.source.2100"; //用于存放文件名 char fname[20]"source."; …

AtCoder Petrozavodsk Contest 001

第一场apc&#xff0c;5H的持久战&#xff0c;我当然水几个题就睡了 A - Two Integers Time limit : 2sec / Memory limit : 256MB Score : 100 points Problem Statement You are given positive integers X and Y. If there exists a positive integer not greater than 1018…

【Qt】使用QCamera获取摄像头,并使用图像视图框架QGraphics*来显示

代码下载 https://download.csdn.net/download/u010168781/10373174 #####头文件 #ifndef CAMERATEST_H#define CAMERATEST_H#include <QMainWindow> #include <QGraphicsView> #include <QKeyEvent> #include <QTimer>namespace Ui { class Camera…

CVPR 2019收录论文ID公开,你上榜了吗?

整理 | 琥珀 出品 | AI科技大本营&#xff08;ID: rgznai100&#xff09; 计算机视觉和模式识别大会 CVPR&#xff08;Conference on Computer Vision and Pattern Recognition&#xff09;作为人工智能领域计算机视觉方向的重要学术会议&#xff0c;每年都会吸引全球最顶尖的…

什么是 prelink

2019独角兽企业重金招聘Python工程师标准>>> Most programs require libraries to function. Libraries can be integrated into a program once, by a linker, when it is compiled (static linking) or they can be integrated when the program is run by a load…

PythonR爬取分析赶集网北京二手房数据(附详细代码)

本文转载自数据森麟&#xff08;ID:shujusenlin&#xff09; 作者介绍&#xff1a;徐涛&#xff0c;19年应届毕业生&#xff0c;专注于珊瑚礁研究&#xff0c;喜欢用R各种清洗数据。 知乎&#xff1a;parkson 如何挑战百万年薪的人工智能&#xff01; https://edu.csdn.net/t…

【Qt】QCloseEvent的使用小结

问题描述 在程序中使用QCloseEvent时,有时没有反应,没有关闭程序。 原因 经测试只有在界面起来以后,使用event->accept()才能关闭程序 测试如下 在构造函数中调用close() 在构造函数中调用close()时,会触发QCloseEvent事件,但是程序界面没有关闭。 使用按钮触发…

Java反射 - 私有字段和方法

尽管普遍认为通过Java Reflection可以访问其他类的私有字段和方法。 这并不困难。 这在单元测试中可以非常方便。 本文将告诉你如何。 访问私有字段 要访问私有字段&#xff0c;您需要调用Class.getDeclaredField&#xff08;String name&#xff09;或Class.getDeclaredFields…

.Net 程序员面试 C# 语言篇 (回答Scott Hanselman的问题)

过去几年都在忙着找项目&#xff0c;赶项目&#xff0c;没有时间好好整理深究自己在工作中学到的东西。现在好了&#xff0c;趁着找工作的这段空余时间&#xff0c;正好可以总结和再继续夯实自己的.Net, C#基本功。在05年的时候&#xff0c;Scott Hanselman(微软的一个Principa…

一个小小的AI训练营竟然卧虎藏龙

年前&#xff0c;我来到了一个近墨者黑的地方&#xff0c;黑的不能再黑。。。这个神秘的组织叫做 21 天入门机器学习训练营。讲真的&#xff0c;当初报名这个训练营&#xff0c;我是冲着机器学习来的&#xff0c;主要是好奇想转型&#xff0c;而且听说这个课程对小白很友好&…

【Qt】QCamera查询和设置摄像头的分辨率

查询和设置摄像头分辨率的API QCamera::supportedViewfinderResolutions() QCamera::setViewfinderSettings() 设置摄像头帧率、比例、分辨率、格式的类&#xff1a;QCameraViewfinderSettings 使用注意事项 查询和设置摄像头分辨率时&#xff0c;需要在摄像头启动后调用&a…

附录G Netty与NettyUtils

版权声明&#xff1a;本文为博主原创文章&#xff0c;未经博主允许不得转载。 https://blog.csdn.net/beliefer/article/details/77450134 注&#xff1a;本文是为了配合《Spark内核设计的艺术 架构设计与实现》一书的内容而编写&#xff0c;目的是为了节省成本、方便读者查阅。…

grails日志系统的研究

对于grails的日志输出&#xff0c;我真的是给弄吐血了。开始以为很简单&#xff0c;后来发现grails封装log4j做的有点太多了&#xff0c;很多东西的封装理解了觉得还挺合理&#xff0c;但是不理解的话真是无比迷茫。对于是否有必要做这么多强制性约束&#xff0c;我保留意见...…

给老婆写个Python教程

作者 | 水风 来源 | 水风知乎问答 如何挑战百万年薪的人工智能&#xff01; https://edu.csdn.net/topic/ai30?utm_sourcecsdn_bw 什么是code code就是一种语言&#xff0c;一种计算机能读懂的语言。计算机是一个傻逼&#xff0c;他理解不了默认两可的任何东西。比如&#xf…

SpringBoot的修改操作

今天学习SpringBoot 的 CRUD 操作&#xff0c;练习 修改操作 时&#xff0c;发生了如下的异常&#xff1a; [nio-8080-exec-7] .m.m.a.ExceptionHandlerExceptionResolver : Resolved exception caused by Handler execution: org.springframework.dao.InvalidDataAccessApiUsa…

【Qt】QImage、QPixmap、QBitmap和QPicture

简述 Qt 提供了四个用于处理图像数据的类: QImage、 QPixmap、 QBitmap和QPicture。QImage是为 I/O 设计和优化的, 用于直接像素访问和操作, 而QPixmap是为在屏幕上显示图像而设计和优化的。QBitmap继承自QPixmap&#xff0c;用在位深为1&#xff08;黑白图片&#xff09;上。…