Vue版本: [email protected],简单记录自己的学习过程
目录
实现双向绑定的方式区别
Vue实现数据劫持的方式
了解defineProperty
Observer/Dep/Watcher三者实现
1. Observer
2. Dep
3.Watcher
脏值检测(angularjs)
这种模式下,使用之只有触发指定的事件进入脏值检测,才会进行双向数据绑定更新,一般是通过预先定义的事件行为触发,这也就是在angularjs中,使用普通的dom操作更改input的内容后,没有办法直接更新model的主要原因.
数据劫持(vue.js)
这种模式下,通过defineProperty()为每个属性在setter/getter添加拦截监听,不论以何种方式(vue预定义事件或者原生操作)变动或获取该属性,均可被监听到.必须注意的是,只有设置了setter/getter拦截的属性才可被监听,这也就是当向component的data根节点插入新的属性时,不会被监听的原因.因为新设置的属性没有设置相应的拦截.
首先了解Object.defineProperty()方法:defineProperty;
也就是说,object的每一个属性均可设置setter&getter,当数据更新和数据调用时,相应方法会被调用.实现数据劫持的核心思想,就是在可能会变化的Object的每个属性的setter&getter中设置订阅和更新,从而触发相应的watcher事件,实现双向绑定.
先看一下源码中如何定义: observer/index.js
1.1. Observer中存在三个属性:
- value:需要观察的数据
- dep:数据订阅和通知watcher的事件处理方
- vmCount:一个计数器
1.2. constructor
constructor (value: any) {
this.value = value //设置属性value
this.dep = new Dep() //设置属性dep
this.vmCount = 0 //设置属性vmCount
def(value, '__ob__', this) //为观察数据添加观察者引用
if (Array.isArray(value)) {//观察数据为Array时的处理方式(observeArray),实现数组每项的观察
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {//观察数据为Object时的处理方式(walk),实现每个属性的观察
this.walk(value)
}
}
1.3. 观察数组时的处理方式:observeArray
对每项使用 observe方法为其创建observer.这里的处理方式经过递归,可以与处理object合并
export function observe (value: any, asRootData: ?boolean): Observer | void { ... if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ // 如果value已经有对应的observer } else if ( observerState.shouldConvert && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value) //用value创建新的observer } ... return ob }
1.4. 观察对象时的处理方式:walk
针对每个属性使用defineReactive,为其设置了setter/getter
export function defineReactive (obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean){ const dep = new Dep() ... let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { ... get: function reactiveGetter () { ... if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend()//针对本属性的value值调用item.dep.depend() if (Array.isArray(value)) { dependArray(value) //本属性value值是array时,就对array每一项分别调用item.dep.depend() } } } return value }, set: function reactiveSetter (newVal) { ... dep.notify()//通知watcher更新 } }) }
1.5. 总结
可以看出,针对对象的每一个属性,其getter/setter处理中附加的处理包含两部分:
以上是observer的主要实现内容,总的来说,针对一个value,建立对应的observer步骤是:
Dep是订阅者Watcher对应的数据依赖,Dep源码:observer/dep.js
2.1. Dep 内部属性:
subs: Array
2.2 constructor:
constructor () { this.id = uid++ //初始化id和subs,但是并未操作target this.subs = [] }
2.3. 在observer中被调用的两个方法: depend/notify:
depend () { if (Dep.target) { Dep.target.addDep(this) //说明在Dep.target存在时,dep.depend()相当于target.addDep(this); //也就是说,在observer对应getter触发时,会将observer的dep通过addDep(),添加到Dep.target中,具体实现可以查看Watcher源码 } } notify () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() //将当前dep事例中的subs各项全部更新 //也就是说,当observer对应setter触发时,会向前期对应的dep中全部watcher发送更新消息,具体事项可以查看Watcher源码 } }
2.4 Dep.target:
(这部分我也还比较困惑,先简单记录一下自己的理解)
// the current target watcher being evaluated. // this is globally unique because there could be only one // watcher being evaluated at any time. Dep.target = null const targetStack = [] export function pushTarget (_target: Watcher) { if (Dep.target) targetStack.push(Dep.target) Dep.target = _target } export function popTarget () { Dep.target = targetStack.pop() }
上面这部分代码主要实现两个方法:pushTarget/popTarget;
当push被调用的时候,会将要push的watcher实例赋值给Dep.target并且加入队列中;当pop被调用时,会将最近的一个Watcher出栈并赋值到Dep.target.
这两个方法均在Watcher.js中被调用,也可以看出,watcher在定义或执行过程会控制Dep中的target更新,具体还需要查看watcher代码来证实.
2.5. 总结
订阅者,通过Dep,在observer执行getter时添加订阅关系.源码:observer/watcher.js
这部分的理解参考了:https://juejin.im/post/5b5fc363f265da0fad0d1611
官方注释:watcher解析表达式,收集依赖项,并在表达式值更改时激发回调。被用于$watch()api和vue directives。
先简单了解一下watcher.js内容,再逐步了解其实现:
3.1. 属性:
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array;//watcher需要订阅的一系列observer.dep
newDeps: Array;//获取value值过程中临时变量,用于去重
depIds: SimpleSet;//watcher需要订阅的一系列observer.dep
newDepIds: SimpleSet; //获取value值过程中临时变量,用于去重
getter: Function; //获取value监听值方法
value: any; // watcher监听值
3.2. constructor
constructor ( vm: Component,//相当于this.$watch中的this expOrFn: string | Function,//要监听的表达式 cb: Function,//监听回调 options?: ?Object,//监听配置(immediate等等) isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } 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 = new Set() this.newDepIds = new Set() this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : '' // parse expression for 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() }
在constructor中,有一系列的初始化过程,比较重要的代码是两部分:
1. this.getter赋值,可以看出,调用this.getter可以获取到要监听的值,举个例子:
export default {
data () {return {msg: 'msg'}},
computed: {upperMsg () {return this.msg.toUpperCase()}}
}
上面这样一个computed,根据constructor代码,其创建的watcher中getter=upperMsg.
2. this.value赋值,只有当非lazy的时候,才会执行this.get获取监听值value(也就是upperMsg的执行结果:'MSG')
总结来说,当非lazy时, constructor会调用this.get获取value值.下面会解释get实现了什么.
3.3. addDep()
在了解get函数前,需要先了解几个会被用到的函数,先看一下源码:
/**
* Add a dependency to this directive.
*/
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)
}
}
}
可以看出,方法中只在操作this.newDepIds,this.newDeps,this.depIds三个属性,执行逻辑可以解释为:
这里简单了解了这个函数的实现内容,置于为什么需要去重以及操作的newDeps等属性具体作用,稍后了解.
3.4. cleanupDeps()
这个函数会把 newDepIds 的值赋给 depIds,然后把 newDepIds 清空
/**
* Clean up for dependency collection.
*/
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
这个方法执行逻辑可以被分割成两部分:
3.5. get()
了解上面两个方法后,再看一下get实现代码
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this) // YT: 先将target设为自己
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) // YT: 就是简单递归遍历收集一下所有的__ob__.dep.id,这就是"touch"的含义,但是实际并没有做更改的操作
}
popTarget() // YT: 将target设为原值
this.cleanupDeps()
}
return value
}
上述代码在不考虑进入catch以及deep的情况,可以简化为几个主要步骤:
pushTarget(this) value = this.getter.call(vm, vm) popTarget() this.cleanupDeps() return value
通过之前对Dep的学习,可以知道,Dep存在一个static的属性target,代表着当前正在计算的watcher实例.通过在watcher中调用函数pushTarget/popTarget更改.那么这里执行的几步操作,翻译一下就是:
1. 将target设为自身,
2. 计算要监听的值,(this.getter)
3. 再将target还原,
4. 清空deps,(this.cleanupDeps)
5. 返回监听值
由于getter===upperMsg,2.中实际是在调用upperMsg,也就是 {return this.msg.toUpperCase()};
这意味着,此处会调用到observer为msg设置的get,这里在贴出get部分代码:
get: function reactiveGetter () { ... if (Dep.target) { dep.depend() ... } ... },
在执行这部分代码时,由于1.设置了Dep.target=this,所以相当于直接执行dep.depend(),这里再贴出depend代码:
depend () { if (Dep.target) { Dep.target.addDep(this) } }
同理,执行这部分代码,等价于执行watcher.addDep(dep);
那么总结以上代码逻辑可以了解到,在执行2.时,不但获取到了this(当前watcher)要监听的value;而且还调用this.addDep方法,将value中对应的observer.dep添加到了this.newDeps中,并且为该dep添加订阅者this.
到此时,我们已经了解的有以下内容:
3.6. update()
上面的讨论过程解释了当获取一个observer值的时候,watcher是怎样收集依赖的,而要了解在改变observer值的时候,是怎样通知到watcher,就需要了解update函数.
回顾observer的设置set的代码:
set: function reactiveSetter (newVal) {
...
dep.notify()
}
主要调用的notify函数如下:
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
所以在触发observer的setter时候,相当于触发了订阅其的watcher.update().
下面看一下update实现:
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
理解这段代码: 在不考虑lazy情况时,sync时,执行run(),async时,执行queueWatcher();前者会调用cb.call()通知回调函数value变化.后者会将watcher条能加到一个队列中,当nextTick时,在执行相关操作.