仔细阅读注解内容。会针对源码原理深度讲解 原文转载地址
使用vuex
中store
中的数据,基本上离不开vue
中一个常用的属性computed
。官方一个最简单的例子如下
var vm = new Vue({
el: '#example',
data: {
message: 'Hello'
},
computed: {
// 计算属性的 getter
reversedMessage: function () {
// `this` 指向 vm 实例
return this.message.split('').reverse().join()
}
}
})
不知大家有没有思考过,vue的
computed
是如何更新的,为什么当vm.message
发生变化时,vm.reversedMessage
也会自动发生变化?
我们来看看vue
中data
属性和computed
相关的源代码。
// src/core/instance/state.js
// 初始化组件的state
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
// 当组件存在data属性
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
// 当组件存在 computed属性
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
initState
方法当组件实例化时会自动触发,该方法主要完成了初始化data,methods,props,computed,watch
这些我们常用的属性,我们来看看我们需要关注的initData
和initComputed
(为了节省时间,去除了不太相关的代码)
先看看 initData 这条线
// src/core/instance/state.js
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
// .....省略无关代码
// 将vue的data传入observe方法
observe(data, true /* asRootData */)
}
// src/core/observer/index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value)) {
return
}
let ob: Observer | void
// ...省略无关代码
ob = new Observer(value)
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
在初始化的时候observe
方法本质上是实例化了一个Observer
对象,这个对象的类是这样的
// src/core/observer/index.js
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
this.value = value
// 关键代码 new Dep对象
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
// ...省略无关代码
this.walk(value)
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
// 给data的所有属性调用defineReactive
defineReactive(obj, keys[i], obj[keys[i]])
}
}
}
在对象的构造函数中,最后调用了
walk
方法,该方法即遍历data
中的所有属性,并调用defineReactive
方法,defineReactive
方法是vue
实现MDV(Model-Driven-View)
的基础,本质上就是代理了数据的set,get
方法,当数据修改或获取的时候,能够感知(当然vue
还要考虑数组,Object
中嵌套Object
等各种情况,本文不在分析)。
我们具体看看defineReactive
的源代码
// src/core/observer/index.js
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 重点,在给具体属性调用该方法时,都会为该属性生成唯一的dep对象
const dep = new Dep()
// 获取该属性的描述对象
// 该方法会返回对象中某个属性的具体描述
// api地址https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor
const property = Object.getOwnPropertyDescriptor(obj, key)
// 如果该描述不能被更改,直接返回,因为不能更改,那么就无法代理set和get方法,无法做到响应式
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
let childOb = !shallow && observe(val)
// 重新定义data当中的属性,对get和set进行代理。
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// 收集依赖, reversedMessage为什么会跟着message变化的原因
if (Dep.target) {
dep.depend()
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
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
// 通知依赖进行更新
dep.notify()
}
})
}
我们可以看到,在所代理的属性的get
方法中,当dep.Target
存在的时候会调用dep.depend()
方法,这个方法非常的简单,不过在说这个方法之前,我们要认识一个新的类Dep
Dep
是vue
实现的一个处理依赖关系的对象,
主要起到一个纽带的作用,就是连接reactive data
与watcher
,代码非常的简单
// src/core/observer/dep.js
export default class Dep {
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)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
// 更新 watcher 的值,与 watcher.evaluate() 类似,
// 但 update 是给依赖变化时使用的,包含对 watch 的处理
subs[i].update()
}
}
}
// 当首次计算 computed 属性的值时,Dep 将会在计算期间对依赖进行收集
Dep.target = null
const targetStack = []
export function pushTarget (_target: Watcher) {
// 在一次依赖收集期间,如果有其他依赖收集任务开始(比如:当前 computed 计算属性嵌套其他 computed 计算属性),
// 那么将会把当前 target 暂存到 targetStack,先进行其他 target 的依赖收集,
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
export function popTarget () {
// 当嵌套的依赖收集任务完成后,将 target 恢复为上一层的 Watcher,并继续做依赖收集
Dep.target = targetStack.pop()
}
代码非常的简单,回到调用dep.depend()
方法的时候,当Dep.Target
存在,就会调用,而depend
方法则是将该dep
加入watcher
的newDeps
中,同时,将所访问当前属性的dep
对象中的subs
插入当前Dep.target
的watcher
.看起来有点绕,不过没关系,我们一会跟着例子讲解一下就清楚了。
讲完了代理的
get
,方法,我们讲一下代理的set
方法,set
方法的最后调用了dep.notify(),
当设置data
中具体属性值的时候,就会调用该属性下面的dep.notify()
方法,通过class Dep
了解到,notify
方法即将加入该dep
的watcher
全部更新,也就是说,当你修改data
中某个属性值时,会同时调用dep.notify()
来更新依赖该值的所有watcher
。
介绍完了initData
这条线,我们继续来介绍initComputed
这条线,这条线主要解决了什么时候去设置Dep.target
的问题(如果没有设置该值,就不会调用dep.depend()
, 即无法获取依赖)。
// src/core/instance/state.js
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
// 初始化watchers列表
const watchers = vm._computedWatchers = Object.create(null)
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (!isSSR) {
// 关注点1,给所有属性生成自己的watcher, 可以在this._computedWatchers下看到
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
if (!(key in vm)) {
// 关注点2
defineComputed(vm, key, userDef)
}
}
}
在初始化computed
时,有2个地方需要去关注
对每一个属性都生成了一个属于自己的
Watcher
实例,并将{ lazy: true }
作为options
传入
对每一个属性调用了defineComputed
方法(本质和data
一样,代理了自己的set
和get
方法,我们重点关注代理的get
方法)
我们看看Watcher
的构造函数
// src/core/observer/watcher.js
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: Object
) {
this.vm = vm
vm._watchers.push(this)
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 // 如果初始化lazy=true时(暗示是computed属性),那么dirty也是true,需要等待更新
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.getter = expOrFn // 在computed实例化时,将具体的属性值放入this.getter中
// 省略不相关的代码
this.value = this.lazy
? undefined
: this.get()
}
除了日常的初始化外,还有2行重要的代码
this.dirty = this.lazy
this.getter = expOrFn
在
computed
生成的watcher
,会将watcher
的lazy
设置为true
,以减少计算量。因此,实例化时,this.dirty
也是true
,标明数据需要更新操作。我们先记住现在computed
中初始化对各个属性生成的watcher
的dirty
和lazy
都设置为了true
。同时,将computed
传入的属性值(一般为funtion
),放入watcher
的getter
中保存起来。
我们在来看看第二个关注点defineComputed
所代理属性的get
方法是什么
// src/core/instance/state.js
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
// 如果找到了该属性的watcher
if (watcher) {
// 和上文对应,初始化时,该dirty为true,也就是说,当第一次访问computed中的属性的时候,会调用 watcher.evaluate()方法;
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
当第一次访问
computed
中的值时,会因为初始化watcher.dirty = watcher.lazy
的原因,从而调用evalute()
方法,evalute()
方法很简单,就是调用了watcher
实例中的get
方法以及设置dirty = false
,我们将这两个方法放在一起
// src/core/instance/state.js
evaluate () {
this.value = this.get()
this.dirty = false
}
get () {
// 重点1,将当前watcher放入Dep.target对象
pushTarget(this)
let value
const vm = this.vm
try {
// 重点2,当调用用户传入的方法时,会触发什么?
value = this.getter.call(vm, vm)
} catch (e) {
} finally {
popTarget()
// 去除不相关代码
}
return value
}
在get
方法中中,第一行就调用了pushTarget
方法,其作用就是将Dep.target
设置为所传入的watcher,
即所访问的computed
中属性的watcher
,
然后调用了value = this.getter.call(vm, vm)
方法,想一想,调用这个方法会发生什么?
this.getter
在Watcher
构建函数中提到,本质就是用户传入的方法,也就是说,this.getter.call(vm, vm)
就会调用用户自己声明的方法,那么如果方法里面用到了 this.data
中的值或者其他被用defineReactive
包装过的对象,那么,访问this.data.
或者其他被defineReactive
包装过的属性,是不是就会访问被代理的该属性的get
方法。我们在回头看看
get
方法是什么样子的。
注意:我讲了其他被用
defineReactive
,这个和后面的vuex
有关系,我们后面在提
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// 这个时候,有值了
if (Dep.target) {
// computed的watcher依赖了this.data的dep
dep.depend()
if (childOb) {
childOb.dep.depend()
}
if (Array.isArray(value)) {
dependArray(value)
}
}
return value
}
代码注释已经写明了,就不在解释了,这个时候我们走完了一个依赖收集流程,知道了
computed
是如何知道依赖了谁。最后根据this.data
所代理的set
方法中调用的notify
,就可以改变this.data
的值,去更新所有依赖this.data
值的computed
属性value
了。
那么,我们根据下面的代码,来简易拆解获取依赖并更新的过程
var vm = new Vue({
el: '#example',
data: {
message: 'Hello'
},
computed: {
// 计算属性的 getter
reversedMessage: function () {
// `this` 指向 vm 实例
return this.message.split('').reverse().join()
}
}
})
vm.reversedMessage // => olleH
vm.message = 'World' //
vm.reversedMessage // => dlroW
初始化
data
和computed
,分别代理其set
以及get
方法, 对data
中的所有属性生成唯一的dep
实例。
对computed
中的reversedMessage
生成唯一watcher
,并保存在vm._computedWatchers
中
访问reversedMessage
,设置Dep.target
指向reversedMessage
的watcher
,调用该属性具体方法reversedMessage
。
方法中访问
this.message
,即会调用this.message
代理的get
方法,将this.message
的dep
加入reversedMessage
的watcher
,同时该dep
中的subs
添加这个watcher
设置vm.message = 'World'
,调用message
代理的set
方法触发dep
的notify
方法’
因为是computed
属性,只是将watcher
中的dirty
设置为true
最后一步vm.reversedMessage
,访问其get
方法时,得知reversedMessage
的watcher.dirty
为true
,调用watcher.evaluate()
方法获取新的值。
这样,也可以解释了为什么有些时候当
computed
没有被访问(或者没有被模板依赖),当修改了this.data
值后,通过vue-tools
发现其computed
中的值没有变化的原因,因为没有触发到其get
方法。
现在在回头看 vuex的使用会有一种豁然开朗的感觉 回看vuex 的设计思路