深入理解 Vue 响应式原理

2023-05-27 0 450

先期

前段时间一两年在写作 Vue 源标识符,从它的核心理念基本原理侧发力,已经开始了源标识符的自学,而其核心理念基本原理是其统计数据的积极响应式,讲到 Vue 的积极响应式基本原理,他们能从它的相容性讲起,Vue不全力支持 IE8 下列版的应用程序,即使 Vue 是如前所述Object.defineProperty来同时实现统计数据积极响应的,而 Object.defineProperty 是 ES5 中两个难以 shim 的优点,这也是为何 Vue 不支持 IE8 和更旧版应用程序的其原因;Vue 透过 Object.defineProperty 的getter/setter对搜集的倚赖项展开窃听,在优点被出访和修正时通告变动,从而预览快照统计数据;

受现代 JavaScript 的管制(和弃置Object.observe),Vue 无法检验到第一类优点的加进或删掉。虽然 Vue 会在调用示例时对优点继续执行 getter/setter转化成操作过程,因而优点必须在data 第一类上存有就能让 Vue 切换它,这种就能让它是积极响应的。

他们这儿是依照Vue2.3源标识符展开预测,Vue统计数据积极响应式变动主要就牵涉 Observer, Watcher , Dep这四个主要就的类;因而要弄清Vue积极响应式变动须要晓得那个四个类间是怎样运转联络的;和它的基本原理,负责管理的方法论操作方式。因而他们从两个单纯的Vue示例的标识符来预测Vue的积极响应式基本原理

var vue = new Vue({el: “#app”,data: {name: Junga }, created () {this.helloWorld() },methods: {helloWorld: function() {console.log(my name is + this.name) } } …})

Vue 调用示例

依照 Vue 的 开发周期他们晓得,Vue 具体来说会展开 init 调用操作方式;源标识符在src/core/instance/init.js

/*调用开发周期*/initLifecycle(vm)/*调用事件*/initEvents(vm)Object.defineProperty /*调用render*/initRender(vm)/*调用beforeCreate钩子函数并且触发beforeCreate钩子事件*/callHook(vm, beforeCreate)initInjections(vm) // resolve injections before data/props/*调用props、methods、data、computed与watch*/initState(vm)initProvide(vm) // resolve provide after data/props/*调用created钩子函数并且触发created钩子事件*/callHook(vm, created)

以上代码能看到initState(vm)是用来调用 props、methods、data、computed 和 watch,src/core/instance/state.js

/*调用props、methods、data、computed与watch*/exportfunctioninitState (vm: Component) { vm._watchers = []const opts = vm.$options/*调用props*/if (opts.props) initProps(vm, opts.props)/*调用方法*/if (opts.methods) initMethods(vm, opts.methods)/*调用data*/if (opts.data) { initData(vm) } else {/*该组件没有data的时候绑定两个空第一类*/ observe(vm._data = {}, true/* asRootData */) }/*调用computed*/if (opts.computed) initComputed(vm, opts.computed)/*调用watchers*/if (opts.watch) initWatch(vm, opts.watch)}…/*调用data*/functioninitData (vm: Component) {/*得到data统计数据*/let data = vm.$options.data data = vm._data = typeof data === function ? getData(data, vm) : data || {}defi …//遍历data中的统计数据while (i–) {/*保证data中的key不与props中的key重复,props优先,如果有冲突会产生warning*/if (props && hasOwn(props, keys[i])) { process.env.NODE_ENV !== production && warn(`The data property “${keys[i]}” is already declared as a prop. ` +`Use prop default value instead.`, vm ) } elseif (!isReserved(keys[i])) {/*判断是否是保留字段*//*这儿是他们前面讲过的代理,将data上面的优点代理到了vm示例上*/ proxy(vm, `_data`, keys[i]) } }// observe data/*这儿透过observe示例化Observe第一类,已经开始对统计数据展开绑定,asRootData用来根统计数据,用来计算示例化根统计数据的个数,下面会展开递归observe展开对深层第一类的绑定。则asRootData为非true*/ observe(data, true/* asRootData */)}

1、initData

现在他们重点预测下initData,这儿主要就做了两件事,一是将 _data 上面的统计数据代理到 vm 上,二是透过继续执行 observe(data, true /* asRootData */)将所有data变成可观察的,即对 data 定义的每个优点展开 getter/setter 操作方式,这儿是 Vue 同时实现积极响应式的基础;observe 的同时实现如下src/core/observer/index.js

/*尝试创建两个Observer示例(__ob__),如果成功创建Observer示例则返回新的Observer示例,如果已有Observer示例则返回现有的Observer示例。*/exportfunctionobserve (value: any, asRootData: ?boolean): Observer | void{if (!isObject(value)) {return }let ob: Observer | void/*这儿用__ob__那个优点来判断是否已经有Observer示例,如果没有Observer示例则会新建两个Observer示例并赋值给__ob__那个优点,如果已有Observer示例则直接返回该Observer示例,这儿能看Observer示例化的标识符def(value, __ob__, this)*/if (hasOwn(value, __ob__) && value.__ob__ instanceof Observer) { ob = value.__ob__ } elseif (/*这儿的判断是为了确保value是单纯的第一类,而不是函数或者是Regexp等情况。而且该第一类在shouldConvert的时候才会展开Observer。这是两个标识位,避免重复对value展开Observer */ observerState.shouldConvert && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) &&Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value) }if (asRootData && ob) {/*如果是根统计数据则计数,后面Observer中的observe的asRootData非true*/ ob.vmCount++ }return ob}

这儿 new Observer(value)是同时实现积极响应式的核心理念方法之一了,透过它将 data 转变能成观察的,而这儿正是他们开头说的,用了Object.defineProperty 同时实现了data的 getter/setter 操作方式,透过 Watcher 来观察统计数据的变动,从而预览到快照中。

2、Observer

Observer 类是将每个目标第一类(即 data)的键值切换成 getter/setter 形式,用于展开倚赖搜集和调度预览。src/core/observer/index.js

exportclassObserver{ 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 = 0/* 将Observer示例绑定到data的__ob__优点上面去,之前说过observe的时候会先检验是否已经有__ob__第一类存放Observer示例了,def方法定义能参考/src/core/util/lang.js*/ def(value, __ob__, this)if (Array.isArray(value)) {/*如果是数组,将修正后能截获积极响应的数组方法替换掉该数组的原型中的原生方法,达到窃听数组统计数据变动积极响应的效果。这儿如果当前应用程序全力支持__proto__优点,则直接覆盖当前数组第一类原型上的原生数组方法,如果不全力支持该优点,则直接覆盖数组第一类的原型。*/const augment = hasProto ? protoAugment /*直接覆盖原型的方法来修正目标第一类*/ : copyAugment /*定义(覆盖)目标第一类或数组的某两个方法*/ augment(value, arrayMethods, arrayKeys)/*如果是数组则须要遍历数组的每两个成员展开observe*/this.observeArray(value) } else {/*如果是第一类则直接walk展开绑定*/this.walk(value) }, walk (obj: Object) {const keys = Object.keys(obj)/*walk方法会遍历第一类的每两个优点展开defineReactive绑定*/for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i], obj[keys[i]]) } } }

具体来说将 Observer 示例绑定到 data 的ob 优点上面去,防止重复绑定;

若 data 为数组,先同时实现对应的变异方法(这儿变异方法是指Vue重写了数组的7种原生方法,这儿不做赘述,后续再说明),再将数组的每个成员展开 observe,使之成积极响应式统计数据;

否则继续执行 walk() 方法,遍历data所有的统计数据,展开 getter/setter 绑定,这儿的核心理念方法是 defineReative(obj, keys[i], obj[keys[i]])

exportfunctiondefineReactive (

obj: Object,

key: string,

val: any,

customSetter?: Function

) {

/在闭包中定义两个dep第一类/

const dep = new Dep()

const property = Object.getOwnPropertyDescriptor(obj, key)

if (property && property.configurable === false) {

return

}

/如果之前该第一类已经预设了getter和setter函数则将其取出来,新定义的getter/setter中会将其继续执行,保证不会覆盖之前已经定义的getter/setter。/

// cater for pre-defined getter/setters

const getter = property && property.get

const setter = property && property.set

/第一类的子第一类递归展开observe并返回子节点的Observer第一类/

let childOb = observe(val)

Object.defineProperty(obj, key, {

enumerable: true,

configurable: true,

get: functionreactiveGetter () {

/如果原本第一类拥有getter方法则继续执行/

const value = getter ? getter.call(obj) : val

if (Dep.target) {

/展开倚赖搜集/

dep.depend()

if (childOb) {

/子第一类展开倚赖搜集,其实是将同两个watcher观察者示例放进了两个depend中,两个是正在本身闭包中的depend,另两个是子元素的depend/

childOb.dep.depend()

}

if (Array.isArray(value)) {

/是数组则须要对每两个成员都展开倚赖搜集,如果数组的成员还是数组,则递归。/

dependArray(value)

}

}

return value

},

set: functionreactiveSetter (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方法则继续执行setter/

setter.call(obj, newVal)

} else {

val = newVal

}

/新的值须要重新展开observe,保证统计数据积极响应式/

childOb = observe(newVal)

/dep第一类通告所有的观察者/

dep.notify()

}

})

}

其中 getter 方法:

先为每个 data 声明两个 Dep示例第一类,被用于 getter 时继续执行 dep.depend() 展开搜集相关的倚赖;依照 Dep.target 来判断是否搜集倚赖,还是普通取值。Dep.target 是在什么时候,怎样搜集的后面再说明,先单纯了解它的作用,

因而问题来了,他们为啥要搜集相关倚赖呢?

new Vue({template: `<div> <span>text1:</span> {{text1}} <span>text2:</span> {{text2}} <div>`,data: {text1: text1,text2: text2,text3: text3 }});

他们能从以上标识符看出,data 中 text3 并没有被模板实际用到,为了提高标识符继续执行效率,他们没有必要对其展开积极响应式处理,因而,倚赖搜集单纯点认知是搜集只在实际页面中用到的data统计数据,然后打上标记,这儿是标记为 Dep.target。

在 setter 方法中:

dep 第一类通告所有观察者去预览统计数据,从而达到积极响应式效果。

在 Observer 类中,他们能看到在 getter 时,dep 会搜集相关倚赖,即搜集倚赖的 watcher,然后在 setter 操作时候透过 dep 去通告 watcher,此时 watcher 就继续执行变动,他们用一张图描述这三者间的关系。

从图他们能单纯认知:Dep 能看做是书店,Watcher 是书店订阅者,而 Observer 是书店的书,订阅者在书店订阅书籍,就能加进订阅者信息,一旦有新书就会透过书店给订阅者发送消息。

3、Watcher

Watcher 是两个观察者第一类。倚赖搜集以后 Watcher 第一类会被保存有 Dep 的 subs 中,统计数据变动的时候 Dep 会通告 Watcher 示例,然后由 Watcher 示例回调 cb 展开快照的预览。src/core/observer/watcher.js

exportdefaultclassWatcher{

constructor (

vm: Component,

expOrFn: string | Function,

cb: Function,

options?: Object

) {

this.vm = vm

/*_watchers存放订阅者示例*/

vm._watchers.push(this)

// options

if (options) {

this.deep = !!options.deep

this.user = !!options.user

this.lazy = !!options.lazy

this.sync = !!options.sync

} else {

this.deep = this.user = this.lazy = this.sync = false

}

this.cb = cb

this.id = ++uid // uid for batching

this.active = true

this.dirty = this.lazy // for lazy watchers

this.deps = []

this.newDeps = []

this.depIds = newSet()

this.newDepIds = newSet()

this.expression = process.env.NODE_ENV !== production

? expOrFn.toString()

:

// parse expression for getter

/*把表达式expOrFn解析成getter*/

if (typeof expOrFn === function) {

this.getter = expOrFn

} else {

this.getter = parsePath(expOrFn)

if (!this.getter) {

this.getter = function () {}

process.env.NODE_ENV !== production && warn(

`Failed watching path: “${expOrFn}” ` +

Watcher only accepts simple dot-delimited paths. +

For full control, use a function instead.,

vm

)

}

}

this.value = this.lazy

? undefined

: this.get()

}

/**

* Evaluate the getter, and re-collect dependencies.

*/

/*获得getter的值并且重新展开倚赖搜集*/

get () {

/*将自身watcher观察者示例设置给Dep.target,用以倚赖搜集。*/

pushTarget(this)

let value

const vm = this.vm

/*继续执行了getter操作方式,看似继续执行了渲染操作方式,其实是继续执行了倚赖搜集。

在将Dep.target设置为自生观察者示例以后,继续执行getter操作方式。

譬如说现在的的data中可能有a、b、c四个统计数据,getter渲染须要倚赖a跟c,

因而在继续执行getter的时候就会触发a跟c两个统计数据的getter函数,

在getter函数中即可判断Dep.target是否存有然后完成倚赖搜集,

将该观察者对象放入闭包中的Dep的subs中去。*/

if (this.user) {

try {

value = this.getter.call(vm, vm)

} catch (e) {

handleError(e, vm, `getter for watcher “${this.expression}”`)

}

} else {

value = this.getter.call(vm, vm)

}

// “touch” every property so they are all tracked as

// dependencies for deep watching

/*如果存有deep,则触发每个深层第一类的倚赖,追踪其变动*/

if (this.deep) {

/*递归每两个第一类或者数组,触发它的getter,使得第一类或数组的每两个成员都被倚赖搜集,形成两个“深(deep)”倚赖关系*/

traverse(value)

}

/*将观察者示例从target栈中取出并设置给Dep.target*/

popTarget()

this.cleanupDeps()

return value

}

/**

* Add a dependency to this directive.

*/

/*加进两个倚赖关系到Deps集合中*/

addDep (dep: Dep) {

const id = dep.id

if (!this.newDepIds.has(id)) {

this.newDepIds.add(id)

this.newDeps.push(dep)

if (!this.depIds.has(id)) {

dep.addSub(this)

}

}

}

/**

* Clean up for dependency collection.

*/

/*清理倚赖搜集*/

cleanupDeps () {

/*移除所有观察者第一类*/

}

/**

* Subscriber interface.

* Will be called when a dependency changes.

*/

/*

调度者接口,当倚赖发生改变的时候展开回调。

*/

update () {

/* istanbul ignore else */

if (this.lazy) {

this.dirty = true

} elseif (this.sync) {

/*同步则继续执行run直接渲染快照*/

this.run()

} else {

/*异步推送到观察者队列中,下两个tick时调用。*/

queueWatcher(this)

}

}

/**

* Scheduler job interface.

* Will be called by the scheduler.

*/

/*

调度者工作接口,将被调度者回调。

*/

run () {

if (this.active) {

te预览快照 */

const value = this.get()

if (

value !== this.value ||

// Deep watchers and watchers on Object/Arrays should fire even

// when the value is the same, because the value may

// have mutated.

/*

即便值相同,拥有Deep优点的观察者和在第一类/数组上的观察者应该被触发预览,即使它的值可能发生改变。

*/

isObject(value) ||

this.deep

) {

// set new value

const oldValue = this.value

/*设置新的值*/

this.value = value

/*触发回调*/

if (this.user) {

try {

this.cb.call(this.vm, value, oldValue)

} catch (e) {

handleError(e, this.vm, `callback for watcher “${this.expression}”`)

}

} else {

this.cb.call(this.vm, value, oldValue)

}

}

}

}

/**

* Evaluate the value of the watcher.

* This only gets called for lazy watchers.

*/

evaluate () {

this.value = this.get()

this.dirty = false

}

/**

* Depend on all deps collected by this watcher.

*/

/*搜集该watcher的所有deps倚赖*/

depend () {

let i = this.deps.length

while (i–) {

this.deps[i].depend()

}

}

/**

* Remove self from all dependencies subscriber list.

*/

/*将自身从所有倚赖搜集订阅列表删掉*/

teardown () {

}

}

4、Dep

被 Observer 的 data 在触发getter时,Dep就会搜集倚赖的Watcher,其实Dep就像刚才说的是两个书店,能接受多个订阅者的订阅,当有新书时即在data变动时,就会透过DepWatcher发通告展开预览。src/core/observer/dep.js

exportdefaultclassDep{static target: ?Watcher; id: number; subs: Array<Watcher>;constructor () {this.id = uid++this.subs = [] }/*加进两个观察者第一类*/ addSub (sub: Watcher) {this.subs.push(sub) }/*移除两个观察者第一类*/ removeSub (sub: Watcher) { remove(this.subs, sub) }/*倚赖搜集,当存有Dep.target的时候加进观察者第一类*/ 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() } }}

总结

其实在 Vue中调用渲染时,快照上绑定的统计数据就会示例化两个Watcher,倚赖搜集是是透过优点的 getter 函数完成的,文章一已经开始讲到的 ObserverWatcherDep 都与倚赖搜集相关。其中 ObserverDep是一对一的关系,DepWatcher 是多对多的关系,Dep 则是 ObserverWatcher 间的纽带。倚赖搜集完成后,当优点变动会继续执行被 Observer 第一类的 dep.notify()方法,那个方法会遍历订阅者(Watcher)列表向其发送消息,Watcher 会继续执行 run 方法去预览快照,他们再来看一张图总结一下:

深入理解 Vue 响应式原理
Vue 中模板编译操作过程中的指令或者统计数据绑定都会示例化两个 Watcher 示例,示例化操作过程中会触发 get() 将自身指向 Dep.target;data 在 Observer 时继续执行 getter 会触发 dep.depend() 展开倚赖搜集;倚赖搜集的结果:1、data 在 Observer时闭包的 dep 示例的 subs 加进观察它的Watcher 示例;2. Watcher 的deps中加进观察第一类 Observer 时的闭包 dep;当 data 中被 Observer的某个第一类值变动后,触发 subs 中观察它的 watcher 继续执行update() 方法,最后实际上是调用 watcher 的回调函数 cb,从而预览快照。

参考

Vue 源标识符Vue 文档Vue 源标识符自学

举报/反馈

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务