前面的话
前端日问,巩固基础,不打烊!!!
解答
如有错误欢迎指出,感谢!!!
提出问题
提出几个问题:
- computed 是如何初始化的?
- 为何data值的变化computed会重新计算?
- 为什么computed值是缓存的呢?
想了解computed的原理,你需要了解Vue的响应式原理,了解computed其实就是一个惰性watcher
。
下面一一解答这几个问题。
Watcher 的实现
给出watcher的实现,方便下文好看。
//去重 防止重复收集
let uid = 0
class Watcher{
constructor(vm,expOrFn,cb,options){
//传进来的对象 例如Vue
this.vm = vm
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
}else{
this.deep = this.user = this.lazy = false
}
this.dirty = this.lazy
//在Vue中cb是更新视图的核心,调用diff并更新视图的过程
this.cb = cb
this.id = ++uid
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
if (typeof expOrFn === 'function') {
//data依赖收集走此处
this.getter = expOrFn
} else {
//watch依赖走此处
this.getter = this.parsePath(expOrFn)
}
//设置Dep.target的值,依赖收集时的watcher对象
this.value = this.lazy ? undefined : this.get()
}
get(){
//设置Dep.target值,用以依赖收集
pushTarget(this)
const vm = this.vm
//此处会进行依赖收集 会调用data数据的 get
let value = this.getter.call(vm, vm)
popTarget()
return value
}
//添加依赖
addDep (dep) {
//去重
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
//收集watcher 每次data数据 set
//时会遍历收集的watcher依赖进行相应视图更新或执行watch监听函数等操作
dep.addSub(this)
}
}
}
//更新
update () {
if (this.lazy) {
this.dirty = true
}else{
this.run()
}
}
//更新视图
run(){
console.log(`这里会去执行Vue的diff相关方法,进而更新数据`)
const value = this.get()
const oldValue = this.value
this.value = value
if (this.user) {
//watch 监听走此处
this.cb.call(this.vm, value, oldValue)
}else{
//data 监听走此处
//这里只做简单的console.log 处理,在Vue中会调用diff过程从而更新视图
this.cb.call(this.vm, value, oldValue)
}
}
//如果计算熟悉依赖的data值发生变化时会调用
//案例中 当data.name值发生变化时会执行此方法
evaluate () {
this.value = this.get()
this.dirty = false
}
//收集依赖
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
// 此方法获得每个watch中key在data中对应的value值
//使用split('.')是为了得到 像'a.b.c' 这样的监听值
parsePath (path){
const bailRE = /[^w.$]/
if (bailRE.test(path)) return
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
//此处为了兼容我的代码做了一点修改
//此处使用新获得的值覆盖传入的值 因此能够处理 'a.b.c'这样的监听方式
if(i==0){
obj = obj.data[segments[i]]
}else{
obj = obj[segments[i]]
}
}
return obj
}
}
}
computed 的实现
在Vue响应式原理这篇文章已经重点提了data数据的初始化(即initDate),computed的初始化是在其后面定义的:(为什么一定要在data数据的后面初始化,这也是有原因的,下面这接着说)
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)
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)
}
}
接着看一看initComputed
函数的定义:
//空函数
const noop = ()=>{}
// computed初始化的Watcher传入lazy: true就会触发Watcher中的dirty值为true
const computedWatcherOptions = { lazy: true }
//Object.defineProperty 默认value参数
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
// 初始化computed
class initComputed {
constructor(vm, computed){
//新建存储watcher对象,挂载在vm对象执行
const watchers = vm._computedWatchers = Object.create(null)
//遍历computed
for (const key in computed) {
const userDef = computed[key]
//getter值为computed中key的监听函数或对象的get值
let getter = typeof userDef === 'function' ? userDef : userDef.get
//新建computed的 watcher
watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions)
if (!(key in vm)) {
/*定义计算属性*/
this.defineComputed(vm, key, userDef)
}
}
}
//把计算属性的key挂载到vm对象下,并使用Object.defineProperty进行处理
//因此调用vm.somecomputed 就会触发get函数
defineComputed (target, key, userDef) {
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = this.createComputedGetter(key)
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? userDef.cache !== false
? this.createComputedGetter(key)
: userDef.get
: noop
//如果有设置set方法则直接使用,否则赋值空函数
sharedPropertyDefinition.set = userDef.set
? userDef.set
: noop
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
//计算属性的getter 获取计算属性的值时会调用
createComputedGetter (key) {
return function computedGetter () {
//获取到相应的watcher
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
//watcher.dirty 参数决定了计算属性值是否需要重新计算,默认值为true,即第一次时会调用一次
if (watcher.dirty) {
/*每次执行之后watcher.dirty会设置为false,只要依赖的data值改变时才会触发
watcher.dirty为true,从而获取值时从新计算*/
watcher.evaluate()
}
//获取依赖
if (Dep.target) {
watcher.depend()
}
//返回计算属性的值
return watcher.value
}
}
}
}
开头的参数很重要。
初始化大致流程:
- 新建存储watcher对象的数组,并挂载在
vm
的_computedWatchers
属性上。 - 遍历整个computed上的属性key
-
获取key上的监听函数(赋给getter)
-
创建watcher (即每一个computed属性都是一个watcher)
-
调用
defineComputed
函数 :将每一个计算属性key
挂载到vm
上,并使用Object.defineProperty
进行处理。具体看一看defineComputed函数做了什么:
既然要是用
Object.defineProperty
函数将每个计算属性key挂载到vm
上,需要三个值:挂载目标 vm
、挂载对象 key
、设置key的value对象
。前面两个已经具备,只差value对象了
。还记得开头的参数么?这个value对象就是开头设置的sharedPropertyDefinition对象
。sharedPropertyDefinition
对象的get属性刚开始是空函数,在这里我们调用createComputedGetter
函数来设置get属性。具体看一看createComputedGetter 函数做了什么:
它返回了一个函数给
sharedPropertyDefinition.get
. 其内部:根据计算属性key找到对应的watcher。(因为计算属性watcher创建时,会传入computedWatcherOptions 对象
,这个对象里面定义了lazy: true
,导致了计算属性watcher
的lazy为true,dirty值初始时为lazy的值。)找到对应的watcher后,会执行这段代码:
if (watcher.dirty) { /*每次执行之后watcher.dirty会设置为false, 只要依赖的data值改变时才会触发 watcher.dirty为true,从而获取值时从新计算*/ watcher.evaluate() }
watcher.evaluate()就是本文的核心: 初始时,就会执行这个函数
//如果计算熟悉依赖的data值发生变化时会调用 //案例中 当data.name值发生变化时会执行此方法 evaluate () { this.value = this.get() this.dirty = false }
了解响应式原理的应该知道
wacther.get()
函数的用途:就是绑定Dep.target 为当前的watcher。
并且进行data数据的依赖收集。看代码: 调用
watcher.get
会触发data数据的getter,进行依赖收集,收集这个watcher
。前面讲过data里面的数据会比computed
里面的数据先初始化,就是这个原因。get(){ //设置Dep.target值,用以依赖收集 pushTarget(this) const vm = this.vm //此处会进行依赖收集 会调用data数据的 get let value = this.getter.call(vm, vm) popTarget() return value }
-
到此为止,上面的问题应该有答案了吧! 计算属性的值,是依赖于其他data属性的,而计算属性本质是计算watcher
, 它所依赖的data属性的dep会将这个watcher加入到subs数组中,当其变化时,就会通知这个watcher改变,所以说计算属性时缓存的。
总结
-
初始化一个computed,会为每个属性key,创建相应的watcher(将key的监听函数传入,作为watcher的getter)。
-
挂载属性key到vm上,并用
Object.defineProperty
为其添加getter。 -
第一次调用属性key时,触发
watcher.evaluate
--> 触发watcher.get
-->watcher.getter(就是key的监听函数)
--> 触发属性key所依赖的data属性的getter
,进而收集这个watcher。 -
当data属性变化时,触发其setter,通知watcher更新,执行
watcher.run
,进而触发diff,更新视图。
通过一个computed实例来走一下上面的流程
computed:{
sayHello() {
return this.hello + ' ' + this.world;
}
},
sayHello
计算属性依赖两个属性:this.hello
与this.world
-
在初始化时会创建一个sayHello对应的
计算属性watcher
,watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions) -
将sayHello计算属性挂载到vm上,用
Object.defineProperty
定义其get属性。初次获取sayHello属性,触发监听函数 :
-
获取this.hello -->触发hello的getter --> hello的dep.depend收集收集sayHello的watcher.
-
获取this.world时,触发world的getter,dep.depend收集sayHello的watcher.
-
-
所以sayhello的watcher最后,会有两个dep收集它。hello与world其中任意一个更新,都会触发更新sayhello watcher的run函数
-
最后触发diff算法,更新虚拟dom,进而更新界面
computed 与watch的区别
- computed属性不是data中的属性值,是一个新值,初始化时使用
Object.defineProperty
方法挂载到vm上;而watch是监听已经存在于data中的属性 - computed本质是一个
惰性的观察者
,具有缓存性
,之后依赖的data值变化时,才会变化;watch没有缓存性,数据变化就更新 - computed适合与一个数据被多个数据影响;而watch适用于一个数据影响多个数据。
参考