Dep.target什么时候存在

Dep.target是由依赖赋值的。依赖又称为Watcher(侦听者)或者订阅者。在Vue中有三种依赖,其中两种是很常见的,就是watch(侦听器)和computed(计算属性)。还有一种隐藏的依赖———渲染Watcher,在模板首次渲染的过程中创建的。
Dep.target是在依赖创建时被赋值,依赖是用构造函数Watcher创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
//...
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
}
this.value = this.lazy ? undefined : this.get();
};
Watcher.prototype.get = function get() {
pushTarget(this);
try {
value = this.getter.call(vm, vm);
} catch (e) {

}
return value
};
Dep.target = null;
var targetStack = [];
function pushTarget(target) {
targetStack.push(target);
Dep.target = target;
}

在构造函数Watcher最后会执行实例方法get,在实例方法get中执行pushTarget(this)中给Dep.target赋值的。
而依赖是在Vue页面或组件初次渲染时创建,所以产生的性能问题应该是首次渲染过慢的问题。

watch(user-watcher)和computed(computed-watcher)区别和运用场景

  • computed:是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;

  • watch:没有缓存性,更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;当我们需要深度监听对象中的属性时,可以打开deep:true选项,这样便会对对象中的每一项进行监听

运用场景:

  • 当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
  • 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用watch选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

更简洁的回答:

  • 当模板中的某个值需要通过一个或多个数据计算得到时,就可以使用计算属性,还有计算属性的函数不接受参数;
  • 监听属性watch主要是监听某个值发生变化后,对新值去进行逻辑处理。

$watch源码分析

关键词:

1
2
3
$watch: function(key, cb, options) {
new Watcher(this, key, cb);
},

用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
watch: {
'test': function(newVal, oldVal) {
console.log('foucus', newVal, oldVal)
},
'name': {
handler: 'changeName', // methods中的方法名
immediate: true
},
'test.age': 'watchTestAge' // methods中的方法名
}
// 或者这样
vm.$watch('test.age', function() {
console.log(arguments);
});
created() {
this.$watch('name', newName => {...})
}

官方描述:

watcher: 一个对象,键是需要观察的表达式,值是对应回调函数。值也可以是方法名,或者包含选项的对象。Vue 实例将会在实例化时调用 $watch(),遍历 watch 对象的每一个 property。

initState => initWatch() => createWatcher => vm.$watch(expOrFn, handler, options)

注意,不应该使用箭头函数来定义 watcher 函数 (例如 searchQuery: newValue => this.updateAutocomplete(newValue))。理由是箭头函数绑定了父级作用域的上下文,所以 this 将不会按照期望指向 Vue 实例,this.updateAutocomplete 将是 undefined。

监听属性初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function initState(vm) {  // 初始化所有状态时
vm._watchers = [] // 当前实例watcher集合
const opts = vm.$options // 合并后的属性
... // 其他状态初始化
if(opts.watch) { // 如果有定义watch属性
initWatch(vm, opts.watch) // 执行初始化方法
}
}
---------------------------------------------------------
function initWatch (vm, watch) { // 初始化方法
for (const key in watch) { // 遍历watch内多个监听属性
const handler = watch[key] // 每一个监听属性的值
if (Array.isArray(handler)) { // 如果该项的值为数组
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]) // 将每一项使用watcher包装
}
} else {
createWatcher(vm, key, handler) // 不是数组直接使用watcher
}
}
}
---------------------------------------------------------
function createWatcher (vm, expOrFn, handler, options) {
if (isPlainObject(handler)) { // 如果是对象,参数移位
options = handler
handler = handler.handler
}
if (typeof handler === 'string') { // 如果是字符串,表示为方法名
handler = vm[handler] // 获取methods内的方法
}
return vm.$watch(expOrFn, handler, options) // 封装
}

监听原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  Vue.prototype.$watch = function (
expOrFn, // 对应上文的test/name/test.age
cb, // 回调函数
options
) {
var vm = this;
// 如果是对象则需要走createWatcher
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options.user = true; // 手动表示user: true
var watcher = new Watcher(vm, expOrFn, cb, options); // 核心就是一句话 new 一个Watcher实例
// {depp: true, immediate}
if (options.immediate) {
cb.call(vm, watcher.value); // 立即执行回调函数
}
// 返回函数,执行取消监听
return function unwatchFn () {
watcher.teardown();
}
};
}

虽然watch内部是使用this.$watch,但是我们也是可以手动调用this.$watch来创建监听属性的,所以第二个参数cb会出现是对象的情况。接下来设置一个标记位options.user为true,表明这是一个user-watcher。再给watch设置了immediate属性后,会将实例化后得到的值传入回调,并立即执行一次回调函数,这也是immediate的实现原理。最后的返回值是一个方法,执行后可以取消对该监听属性的监听。

watch监听属性收集依赖
root => init() 根组件初始化
root => vm._update(vm._render(), hydrating) 根组件渲染 没状态 不用收集依赖
=> initData
=> observer(name) // name转为响应式
=> initWatch(watch) // 初始化watch
=> $watch(name) // 触发name的get 收集user-watcher
=> Sub.$mount() // 子组件挂载
=> new Watcher(vm, getter) // 实例化一个render-watcher
=> vm.render // 触发name的get 收集render-watcher
Watch 监听属性派发更新
=> name = ‘cpp’ // 触发set
=> dep.notify() // dep通知收集到的watcher
=> user-watcher // 派发新值和旧值给回调函数
=> render-watcher // 视图改变

deep原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
Watcher.prototype.get = function get () {
pushTarget(this);
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
// 用户手动的user watcher
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
};
var seenObjects = new _Set();
/**
* Recursively traverse an object to evoke all converted
* getters, so that every nested property inside the object
* is collected as a "deep" dependency.
*/
function traverse (val) {
_traverse(val, seenObjects);
seenObjects.clear();
}
function _traverse (val, seen) {
var i, keys;
var isA = Array.isArray(val);
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
// 只有数字和对象才有—__ob__属性
if (val.__ob__) {
var depId = val.__ob__.dep.id; // 手动依赖收集器的id
if (seen.has(depId)) {
return
}
seen.add(depId);
}
if (isA) {
i = val.length;
while (i--) { _traverse(val[i], seen); } // 递归触发每一项的get进行依赖收集
} else {
keys = Object.keys(val);
i = keys.length;
while (i--) { _traverse(val[keys[i]], seen); }
}
}

watch总结

这里说明了为什么watch和this.$watch的实现是一致的,以及简单解释它的原理就是为需要观察的数据创建并收集user-watcher,当数据改变时通知到user-watcher,并将新值和旧值传递给用户自己定义的回调函数。最后分析了定义watch时会被使用到的三个参数:sync、immediate、deep它们的实现原理。简单说明它们的实现原理就是:

  • sync是不将watcher加入到nextTick队列而同步的更新
  • immediate是立即以得到的值执行一次回调函数
  • deep是递归的对它的子值进行依赖收集

computed

renderWatcher 和 computedWatcher, watcher的核心是update,计算watcher的核心是为了驱动 render watcher的 update,而render watcher的update则是重新调用vm.update(vm.render())更新真正的视图

关键词

1
2
3
4
5
6
7
8
9
10
11
this.initComputed();
function initComputed(vm, computed) {
const watchers = vm._computedWatchers = Object.create(null) // 创建一个纯净对象
for(const key in computed) {
const getter = computed[key] // computed每项对应的回调函数
watchers[key] = new Watcher(vm, getter, noop, {lazy: true}) // 实例化computed-watcher
if (!(key in vm)) {
defineComputed(vm, key, getter)
}
}
}

用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
computed: {
displayName() {
return this.name.charAt(0).toUpperCase() + this.name.slice(1)
},
mockComputed() {
return this.focus + '!!!'
},
aPlus: {
get: function() {
return 'computed aplus'
},
set: function(val) {
console.log('触发 set aPlus')
this.name = val
}
}
}

官方描述

计算属性将被混入到 Vue 实例中。所有 getter 和 setter 的 this 上下文自动地绑定为 Vue 实例。
计算属性的结果会被缓存,计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。

源码分析 初始化

  • initState =>
  • initComputed(vm, opts.computed) =>
  • new Watcher(vm,getter || noop, noop, computedWatcherOptions) =>
  • defineComputed(vm, key, userDef) =>
  • createComputedGetter(key) =>
  • Object.defineProperty(target, key, sharedPropertyDefinition);

其中Watcher中的lazy/dirty: true, 表示的计算属性的Watcher

1
2
3
4
5
6
computedWatcherOptions = {lazy: true}
this.lazy = !!options.lazy; //
this.dirty = this.lazy; // for lazy watchers
this.value = this.lazy
? undefined
: this.get();

开始聚焦源码 vm._computedWatchers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function initState(vm) {  // 初始化所有状态时
vm._watchers = [] // 当前实例watcher集合
const opts = vm.$options // 合并后的属性
... // 其他状态初始化
if(opts.computed) { // 如果有定义计算属性
initComputed(vm, opts.computed) // 进行初始化
}
...
}
function initComputed(vm, computed) {
const watchers = vm._computedWatchers = Object.create(null) // 创建一个纯净对象
for(const key in computed) {
const getter = computed[key] // computed每项对应的回调函数
watchers[key] = new Watcher(vm, getter, noop, {lazy: true}) // 计算属性中的lazy为true 说明计算watcher不会立即执行
if (!(key in vm)) {
defineComputed(vm, key, getter)
}
}
}

计算属性实现原理

这里还是按照惯例,将定义的computed属性的每一项使用Watcher类进行实例化,不过这里是按照computed-watcher的形式,来看下如何实例化的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Watcher{
constructor(vm, expOrFn, cb, options) {
this.vm = vm
this._watchers.push(this)
if(options) {
this.lazy = !!options.lazy // 表示是computed
}
this.dirty = this.lazy // dirty为标记位,表示是否对computed计算
this.getter = expOrFn // computed的回调函数
this.value = this.lazy
? undefined
: this.get();
}
}

没有执行get方法,说明计算属性是惰性求值
这里的App组件在执行extend创建子组件的构造函数时,已经将key挂载到vm的原型中了,不过之前也是执行的defineComputed方法,所以不妨碍我们看它做了什么:

1
2
3
4
5
6
7
8
9
function defineComputed(target, key) {
...
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get: createComputedGetter(key),
set: noop
})
}

此方法的作用就是让computed成为一个响应式数据,并定义它的get属性,也就是说当页面执行渲染访问到computed时,才会触发get然后执行createComputedGetter方法,所以之前的点到为止再这里会续上,看下get方法是怎么定义的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function createComputedGetter (key) { // 高阶函数
return function () { // 返回函数
const watcher = this._computedWatchers && this._computedWatchers[key]
// this还可以这样用,得到key对应的computed-watcher
if (watcher) {
if (watcher.dirty) { // 在实例化watcher时为true,表示需要计算
watcher.evaluate() // 进行计算属性的求值,把computed-watcher放到依赖收集器deps数组里
}
if (Dep.target) { // 当前的watcher,这里是页面渲染触发的这个方法,所以为render-watcher
watcher.depend() // 收集当前watcher
}
return watcher.value // 返回求到的值或之前缓存的值
}
}
}
class Watcher {
...
evaluate () {
this.value = this.get() // 计算属性求值 会将当前的computed-watcher作为依赖收集到自己的dep
this.dirty = false // 表示计算属性已经计算,不需要再计算
}
depend () {
let i = this.deps.length // deps内是计算属性内能访问到的响应式数据的dep的数组集合
while (i--) {
this.deps[i].depend() // 让每个dep收集当前的render-watcher
}
}
}
  • 这里的变量watcher就是之前computed对应的computed-watcher实例,接下来会执行Watcher类专门为计算属性定义的两个方法,在执行evaluate方法进行求值的过程中又会触发computed内可以访问到的响应式数据的get()方法,它们会将当前的computed-watcher作为依赖收集到自己的dep里,计算完毕之后将dirty置为false,表示已经计算过了。

  • 如果Dep.target=true 当前页面渲染用到了这个Watcher,会执行watcher.depend()让计算属性内的响应式数据订阅当前的render-watcher,所以computed内的响应式数据会收集computed-watcher和render-watcher两个watcher

  • 当computed内的状态发生变更触发set后,首先通知 computed watcher需要进行重新计算,然后通知到视图执行渲染render watcher,执行渲染中会访问到computed计算后的值,最终渲染到页面。

计算属性内的值须是响应式数据才能触发重新计算。

computed 总结

为什么计算属性有缓存功能?因为当计算属性经过计算后,内部的标志位dirty会表明已经计算过了,再次访问时会直接读取计算后的值;

为什么计算属性内的响应式数据发生变更后,计算属性会重新计算?
因为内部依赖的响应式数据会收集computed-watcher,变更后通知计算属性要进行计算,也会通知页面重新渲染,渲染时会读取到重新计算后的值。

触发生命周期beforeCreate后,会做一系列事,包含对computed处理,会遍历computed中的所有属性,为每一个属性创建一个Watcher对象,并传入一个函数,该函数会收集依赖,函数的本质就是computed中的getter方法,getter方法会在初始化过程中收集依赖,如果getter中的依赖改变则会运行watcher
跟渲染函数不同的是,computed watcher不会立即执行,因为考虑到有的计算属性可能没有被使用,比如v-if。因此在创建watcher的时候会有一个lazy配置,该配置不会让watcher立即执行

Watcher创建好后,vue会使用代理模式,将计算属性挂载到组件实例中
当读取计算属性时,vue会检查其对应的Watcher是否为脏值dirty,如果是,则运行函数,计算getter函数用到的依赖,并拿到对应的值,保存在Watcher的value中,然后设置dirty为false,然后返回。
如果dirty为false,则直接返回watcher的value

值得注意的是,在收集依赖时,被依赖的数据不仅会收集到计算属性的Watcher,还会收集到组件的Watcher,当计算属性的依赖变化时,会先触发计算属性的Watcher执行,此时,它只需要设置dirty为true就可以了,不会做其它处理。
由于依赖同时会收集到组件的渲染Watcher,因此组件会重新渲染,而重新渲染的同时又读取到了计算属性,由于此时dirty为true,因此会重新运行getter进行计算

参考