继上一篇: 继续讲解
Vue的computed实现相对于watch和data来说比较难以理解,要真正的理解computed的工作方式,你需要深入理解Vue的双向数据绑定原理和实现方式。
如果你还不是很理解推荐你先看此文章:
首先来看一波Vue中computed的使用方式:
var vm = new Vue({ data: { a: 1 }, computed: { // 仅读取 aDouble: function () { return this.a * 2 }, // 读取和设置 aPlus: { get: function () { return this.a + 1 }, set: function (v) { this.a = v - 1 } } }})vm.aPlus // => 2vm.aPlus = 3vm.a // => 2vm.aDouble // => 4复制代码
计算属性的主要应用场景是代替模板内的表达式,或者data值的任何复杂逻辑都应该使用computed来计算,它有两大优势:
1、逻辑清晰,方便于管理
2、计算值会被缓存,依赖的data值改变时才会从新计算
此文我们需要核心理解的是:
1、computed是如何初始化,初始化之后干了些什么
2、为何触发data值改变时computed会从新计算
3、computed值为什么说是被缓存的呢,如何做的
如果以上三个问题你都已知,你可以忽略下文了,若未知或一知半解,请抱着学习的态度看看别人的理解。
备注:以下只是我的个人理解,并不保证绝对的正确性,若有问题欢迎指正
以下大部分代码摘自Vue源码。
如果你看到了这里,就当做你已经深入理解了Vue的MVVM原理及其实现方式。相关Vue的MVVM实现直接取自上一篇文章。
Dep代码的实现:
//标识当前的Dep idlet uidep = 0class Dep{ constructor () { this.id = uidep++ // 存放所有的监听watcher this.subs = [] } //添加一个观察者对象 addSub (Watcher) { this.subs.push(Watcher) } //依赖收集 depend () { //Dep.target 作用只有需要的才会收集依赖 if (Dep.target) { Dep.target.addDep(this) } } // 调用依赖收集的Watcher更新 notify () { const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } }}Dep.target = nullconst targetStack = []// 为Dep.target 赋值function pushTarget (Watcher) { if (Dep.target) targetStack.push(Dep.target) Dep.target = Watcher}function popTarget () { Dep.target = targetStack.pop()}复制代码
Watcher代码的实现:
//去重 防止重复收集let uid = 0class 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 } }}复制代码
在Watcher中对于computed来说核心注意点是以下方法:
//如果计算熟悉依赖的data值发生变化时会调用//案例中 当data.name值发生变化时会执行此方法evaluate () { this.value = this.get() this.dirty = false}复制代码
当computed中用到的data值发生变化时,视图更新调用computed值时会从新执行,获得新的计算属性值。
Observer代码实现
class Observer{ constructor (value) { this.value = value // 增加dep属性(处理数组时可以直接调用) this.dep = new Dep() //将Observer实例绑定到data的__ob__属性上面去,后期如果oberve时直接使用,不需要从新Observer, //处理数组是也可直接获取Observer对象 def(value, '__ob__', this) if (Array.isArray(value)) { //这里只测试对象 } else { //处理对象 this.walk(value) } } walk (obj) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { //此处我做了拦截处理,防止死循环,Vue中在oberve函数中进行的处理 if(keys[i]=='__ob__') return; defineReactive(obj, keys[i], obj[keys[i]]) } }}//数据重复Observerfunction observe(value){ if(typeof(value) != 'object' ) return; let ob = new Observer(value) return ob;}// 把对象属性改为getter/setter,并收集依赖function defineReactive (obj,key,val) { const dep = new Dep() //处理children let childOb = observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { console.log(`调用get获取值,值为${val}`) const value = val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() } } return value }, set: function reactiveSetter (newVal) { console.log(`调用了set,值为${newVal}`) const value = val val = newVal //对新值进行observe childOb = observe(newVal) //通知dep调用,循环调用手机的Watcher依赖,进行视图的更新 dep.notify() } })}//辅助方法function def (obj, key, val) { Object.defineProperty(obj, key, { value: val, enumerable: true, writable: true, configurable: true })}复制代码
此文的重点Computed代码实现:
//空函数const noop = ()=>{}// computed初始化的Watcher传入lazy: true就会触发Watcher中的dirty值为trueconst computedWatcherOptions = { lazy: true }//Object.defineProperty 默认value参数const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop}// 初始化computedclass 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 } } }}复制代码
代码已经写完,完整代码如下:
//标识当前的Dep idlet uidep = 0class Dep{ constructor () { this.id = uidep++ // 存放所有的监听watcher this.subs = [] } //添加一个观察者对象 addSub (Watcher) { this.subs.push(Watcher) } //依赖收集 depend () { //Dep.target 作用只有需要的才会收集依赖 if (Dep.target) { Dep.target.addDep(this) } } // 调用依赖收集的Watcher更新 notify () { const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } }}Dep.target = nullconst targetStack = []// 为Dep.target 赋值function pushTarget (Watcher) { if (Dep.target) targetStack.push(Dep.target) Dep.target = Watcher}function popTarget () { Dep.target = targetStack.pop()}/*----------------------------------------Watcher------------------------------------*///去重 防止重复收集let uid = 0class 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 } }}/*----------------------------------------Observer------------------------------------*/class Observer{ constructor (value) { this.value = value // 增加dep属性(处理数组时可以直接调用) this.dep = new Dep() //将Observer实例绑定到data的__ob__属性上面去,后期如果oberve时直接使用,不需要从新Observer, //处理数组是也可直接获取Observer对象 def(value, '__ob__', this) if (Array.isArray(value)) { //这里只测试对象 } else { //处理对象 this.walk(value) } } walk (obj) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { //此处我做了拦截处理,防止死循环,Vue中在oberve函数中进行的处理 if(keys[i]=='__ob__') return; defineReactive(obj, keys[i], obj[keys[i]]) } }}//数据重复Observerfunction observe(value){ if(typeof(value) != 'object' ) return; let ob = new Observer(value) return ob;}// 把对象属性改为getter/setter,并收集依赖function defineReactive (obj,key,val) { const dep = new Dep() //处理children let childOb = observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { console.log(`调用get获取值,值为${val}`) const value = val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() } } return value }, set: function reactiveSetter (newVal) { console.log(`调用了set,值为${newVal}`) const value = val val = newVal //对新值进行observe childOb = observe(newVal) //通知dep调用,循环调用手机的Watcher依赖,进行视图的更新 dep.notify() } })}//辅助方法function def (obj, key, val) { Object.defineProperty(obj, key, { value: val, enumerable: true, writable: true, configurable: true })}/*----------------------------------------初始化watch------------------------------------*///空函数const noop = ()=>{}// computed初始化的Watcher传入lazy: true就会触发Watcher中的dirty值为trueconst computedWatcherOptions = { lazy: true }//Object.defineProperty 默认value参数const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop}// 初始化computedclass 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 } } }}复制代码
computed测试:
//1、首先来创建一个Vue构造函数:function Vue(){}//2、设置data和computed的值:let data={ name:'Hello',}let computed={ getfullname:function(){ console.log('-----走了computed 之 getfullname------') console.log('新的值为:'+data.name + ' - world') return data.name + ' - world' }}//3、实例化Vue并把data挂载到Vue上let vue = new Vue()vue.data = data//4、创建Watcher对象let updateComponent = (vm)=>{ // 收集依赖 data.name }let watcher1 = new Watcher(vue,updateComponent,()=>{})//5、初始化Data并收集依赖observe(data)//6、初始化computedlet watcher2 = new initComputed(vue,computed)复制代码
在浏览器console中测试:
//首先获得一次getfullnamevue.getfullname//第二次调用getfullname看看会有什么变化呢vue.getfullname复制代码
分析:调用vue.getfullname第一次会打印 '-----走了computed 之 getfullname------',即计算属性第一次计算了值,第二次调用时,不会再打印值
即直接获取的缓存值,为什么第二次是获得的缓存值呢,因为第二次执行时watcher.dirty=true,就会直接返回watcher.value值。
//为data.name赋值data.name = 'Hi'复制代码
分析:执行data.name时会触发两个Watcher监听函数(为什么是两个Watcher自己去脑补一下额!),一个是全局的watcher,一个是computed的watcher,第一个Watcher会更新视图,第二个Watcher会触发watcher.dirty=true。
//name值变更之后再次执行会是什么结果呢vue.getfullname//再执行一次vue.getfullname复制代码
分析:运行vue.getfullname时会执行computedGetter函数,因为watcher.dirty=true因此会从新计算值,因此会打印 '-----走了computed 之 getfullname------',值为'HI world', 再次执行只会获得计算属性的缓存值。
所有测试代码如下:
/*----------------------------------------Vue------------------------------------*/function Vue(){}/*----------------------------------------测试代码------------------------------------*/// 调用let data={ name:'Hello',}let computed={ getfullname:function(){ console.log('-----走了computed 之 getfullname------') console.log('新的值为:'+data.name + ' - world') return data.name + ' - world' }}let vue = new Vue()vue.data = datalet updateComponent = (vm)=>{ // 收集依赖 data.name}let watcher1 = new Watcher(vue,updateComponent,()=>{})observe(data)let watvher2 = new initComputed(vue,computed)//测试 浏览器console中相继运行一下代码测试vue.getfullnamevue.getfullnamedata.name='Hi'vue.getfullnamevue.getfullname复制代码
若有疑问欢迎交流。