Continue with the previous article: In-depth understanding of Vue’s Watch implementation principle and implementation method

Vue’s computed implementation is more difficult to understand than Watch and Data. To truly understand computed, you need to have a deep understanding of Vue’s bidirectional data binding and implementation.

If you still don’t understand it, I recommend you read this article first:

Thoroughly understand how Vue handles arrays and bidirectional binding (MVVM)


Let’s first look at how computed is used in a wave of Vue:

Var vm = new Vue({data: {a: 1}, computed: {// Only aDouble is read:function () {
      returnThis. a * 2}, // read and set aPlus: {get:function () {
        return this.a + 1
      },
      set: function (v) {
        this.a = v - 1
      }
    }
  }
})
vm.aPlus   // => 2
vm.aPlus = 3
vm.a       // => 2
vm.aDouble // => 4Copy the code

The main application scenarios for computed properties are to replace expressions in templates, or any complex logic for data values should be computed, which has two major advantages:

1. Clear logic and easy to manage

2. The computed value is cached and only recomputed when the dependent data value changes

What we need to understand at the core of this article is:

1. How is computed initialized and what is done after initialization

2. Why is computed computed when the Data value is changed

3. Why is a computed value cached? How


If you know the above three questions, you can ignore the rest, if you do not know or have a little knowledge, please hold the attitude of learning to see others understand.

Note: the following is my personal understanding, does not guarantee the absolute correctness, if there is any problem welcome to correct

Most of the following code is taken from the Vue source code.


If you see this, assume that you have a deep understanding of Vue’s MVVM principles and how they are implemented. The MVVM implementation of the related Vue is taken directly from the previous article.

Dep code implementation:

// Identifies the current Dep IDlet uidep = 0
class Dep{
	constructor() {this.id = uidep++ // store all the listener watcher this.subs = []} // add an observer object addSub (watcher) {this.subs.push(watcher)} // Dependency collectiondepend() {// the dep. target function collects dependencies only if it is neededif(dep.target) {dep.target.adddep (this)}} // Call Watcher updates that depend on the collectionnotify () {
	    const subs = this.subs.slice()
	    for (leti = 0, l = subs.length; i < l; I ++) {subs[I].update()}}} dep. target = null const targetStack = [] // Assign a value to dep.targetfunction pushTarget (Watcher) {
	if (Dep.target) targetStack.push(Dep.target)
  	Dep.target = Watcher
}
function popTarget () {
  Dep.target = targetStack.pop()
}Copy the code


Implementation of Watcher code:

// Deduplicate to prevent duplicate collectionletUid = 0 class Watcher{constructor(vm,expOrFn,cb,options){// export objects such as Vue this.vm = vmif(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 // In Vue, cb is the core of the update view, Call diff and update the view procedure this.cb = cb this.id = ++uid this.deps = [] this.newdeps = [] this.depids = new Set() this.newdepids = new Set()if (typeof expOrFn === 'function'}}}}}}}}}}else}}}}}}}}}}}}}}}}}}}}}}}}}}} undefined : this.get() }get(){// set the value dep.target to rely on collection pushTarget(this) const VM = this.vmlet value = this.getter.call(vm, vm)
	    popTarget()
	    returnValue} addDep (dep) {// Const id = dep.idif(! this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep)if(! This.depids.has (id)) {// collect watcher data every timesetDep.addsub (this)}}} // Update the watcher dependenciesupdate () {
  		if (this.lazy) {
      		this.dirty = true
    	}else}} // Update the viewrun(){console.log(' this will execute Vue's diff method and update the data ') const value = this.get() const oldValue = this.value this.value = valueifCall (this.vm, value, oldValue)} (this.vm, value, oldValue)}else{//data listens here // here only do simple console.log processing, Call (this.vm, value, oldValue)}} // This method is called if the value of the data that the calculation is familiar with has changed // This method is executed when the data.name value has changed in the caseevaluate () {
	    this.value = this.get()
	    this.dirty = false} // Collect dependenciesdepend () {
	    let i = this.deps.length
	    while(I --) {this.deps[I].depend(depend)}} (I --) {this.deps[I].depend(depend)}}'. ') is to get the image'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// Here is a little change to be compatible with my code // Here is a new value to override the incoming value so that it can be processed'a.b.c'This way of listeningif(i==0){
		        	obj = obj.data[segments[i]]
		        }else{
		        	obj = obj[segments[i]]
		        }
		    }
		    return obj
		 }
	}
}Copy the code

The core considerations in Watcher for computed are the following methods:

// This method is called if the value of the data for which the calculation is familiar has changed // This method is executed when the data.name value has changed in the caseevaluate () {
    this.value = this.get()
    this.dirty = false
}Copy the code

When the data value used in computed changes, the view updates the computed value to obtain the new computed attribute value.


Observer code implementation

This.dep = new dep () class Observer{constructor (value) {this.dep = new dep () // Bind the Observer instance to the __ob__ property of the data. If it is used directly in the oberve, there is no need to obtain the new Observer object def(value,).'__ob__', this)
	    if(array.isArray (value)) {// Only test objects here}else}} walk(obj) {const keys = object.keys (obj) {const keys = object.keys (obj)for (leti = 0; i < keys.length; I++) {// here I do the interception processing to prevent infinite loop, Vue in oberve function processingif(keys[i]=='__ob__') return; DefineReactive (obj, keys[I], obj[keys[I]])}}function observe(value){
	if(typeof(value) ! ='object' ) return;
	let ob = new Observer(value)
  	returnob; } // Change the object properties to getters/setters and collect dependenciesfunctionDefineReactive (obj,key,val) {const dep = new dep ()let childOb = observe(val)
  	Object.defineProperty(obj, key, {
    	enumerable: true,
    	configurable: true,
    	get: function reactiveGetter() {console.log(' call get to get the value, which is${val}`)
      		const value = val
      		if (Dep.target) {
	        	dep.depend()
		        if (childOb) {
		          	childOb.dep.depend()
		        }
	      	}
      		return value
	    },
	    set: functionReactiveSetter (newVal) {console.log(' calledsetAnd has a value of${newVal}Observe childOb = observe(newVal) const value = val val = newVal // Observe childOb = observe(newVal) Dep.notify ()}})} // Helper methodfunction def (obj, key, val) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: true,
    writable: true,
    configurable: true})}Copy the code


The focus of this article is Computed code implementation:

// Null function const noop = ()=>{} // Watcher for computed initialization passes lazy:trueWill trigger Watcher with the dirty value oftrue
const computedWatcherOptions = { lazy: true} //Object.defineProperty default value const sharedPropertyDefinition = {Enumerable:true,
  configurable: true,
  get: noop,
  set: Noop} // Initialize computed class initComputed {constructor(VM, computed){// create a new storage watcher object, Mount the VM Object and perform const Watchers = vm._computedWatchers = object.create (NULL) // Perform computedfor (const key inComputed) {const userDef = computed[key] // The getter value is the get value of the listening function or object of the key in computedlet getter = typeof userDef === 'function'? Watchers [key] = new watcher (VM, getter, noop, computedWatcherOptions)if(! (keyin{/* define the attribute */ this.definecomputed (vm, key, userDef)}}} // mount the key to the vm object, So calling vm.somecomputed triggers 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? Enclosing createComputedGetter (key) : userDef. Get: it / / if you have setsetMethods are used directly, otherwise empty function assignment sharedPropertyDefinition. Set = userDef. Set? userDef.set : noop } Object.defineProperty(target, key, SharedPropertyDefinition)} createComputedGetter (key) is called when the value of the evaluated property is obtained.return function computedGetterConst watcher = this._computedWatchers && this._computedwatchers [key]if(watcher) {// the watcher.dirty parameter determines whether the value of the computed property needs to be recalculated. The default value istrueThat is, it will be called once the first timeif(watcher.dirty) {/* watcher.dirty is set to after each executionfalseWatcher.dirty is only triggered if the dependent data value changestrue*/ watcher.evaluate()} // Obtain dependenciesif(dep.target) {watcher.depend()} // Return the value of the evaluated attributereturn watcher.value
	    }
	  }
	}
}Copy the code


The complete code is as follows:

// Identifies the current Dep IDlet uidep = 0
class Dep{
	constructor() {this.id = uidep++ // store all the listener watcher this.subs = []} // add an observer object addSub (watcher) {this.subs.push(watcher)} // Dependency collectiondepend() {// the dep. target function collects dependencies only if it is neededif(dep.target) {dep.target.adddep (this)}} // Call Watcher updates that depend on the collectionnotify () {
	    const subs = this.subs.slice()
	    for (leti = 0, l = subs.length; i < l; I ++) {subs[I].update()}}} dep. target = null const targetStack = [] // Assign a value to dep.targetfunction pushTarget (Watcher) {
	if (Dep.target) targetStack.push(Dep.target)
  	Dep.target = Watcher
}
function popTarget() { Dep.target = targetStack.pop() } / * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- Watcher -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * / / / to heavy To prevent repeated collectionletUid = 0 class Watcher{constructor(vm,expOrFn,cb,options){// export objects such as Vue this.vm = vmif(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 // In Vue, cb is the core of the update view, Call diff and update the view procedure this.cb = cb this.id = ++uid this.deps = [] this.newdeps = [] this.depids = new Set() this.newdepids = new Set()if (typeof expOrFn === 'function'}}}}}}}}}}else}}}}}}}}}}}}}}}}}}}}}}}}}}} undefined : this.get() }get(){// set the value dep.target to rely on collection pushTarget(this) const VM = this.vmlet value = this.getter.call(vm, vm)
	    popTarget()
	    returnValue} addDep (dep) {// Const id = dep.idif(! this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep)if(! This.depids.has (id)) {// collect watcher data every timesetDep.addsub (this)}}} // Update the watcher dependenciesupdate () {
  		if (this.lazy) {
      		this.dirty = true
    	}else}} // Update the viewrun(){console.log(' this will execute Vue's diff method and update the data ') const value = this.get() const oldValue = this.value this.value = valueifCall (this.vm, value, oldValue)} (this.vm, value, oldValue)}else{//data listens here // here only do simple console.log processing, Call (this.vm, value, oldValue)}} // This method is called if the value of the data that the calculation is familiar with has changed // This method is executed when the data.name value has changed in the caseevaluate () {
	    this.value = this.get()
	    this.dirty = false} // Collect dependenciesdepend () {
	    let i = this.deps.length
	    while(I --) {this.deps[I].depend(depend)}} (I --) {this.deps[I].depend(depend)}}'. ') is to get the image'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// Here is a little change to be compatible with my code // Here is a new value to override the incoming value so that it can be processed'a.b.c'This way of listeningif(i==0){
		        	obj = obj.data[segments[i]]
		        }else{
		        	obj = obj[segments[i]]
		        }
		    }
		    returnobj } } } /*----------------------------------------Observer------------------------------------*/ class Observer{ This.dep = new dep () // bind the Observer instance to the __ob__ property of the data. This. Dep = new dep (). Def (value, observer_value, observer_value, observer_value, observer_value);'__ob__', this)
	    if(array.isArray (value)) {// Only test objects here}else}} walk(obj) {const keys = object.keys (obj) {const keys = object.keys (obj)for (leti = 0; i < keys.length; I++) {// here I do the interception processing to prevent infinite loop, Vue in oberve function processingif(keys[i]=='__ob__') return; DefineReactive (obj, keys[I], obj[keys[I]])}}function observe(value){
	if(typeof(value) ! ='object' ) return;
	let ob = new Observer(value)
  	returnob; } // Change the object properties to getters/setters and collect dependenciesfunctionDefineReactive (obj,key,val) {const dep = new dep ()let childOb = observe(val)
  	Object.defineProperty(obj, key, {
    	enumerable: true,
    	configurable: true,
    	get: function reactiveGetter() {console.log(' call get to get the value, which is${val}`)
      		const value = val
      		if (Dep.target) {
	        	dep.depend()
		        if (childOb) {
		          	childOb.dep.depend()
		        }
	      	}
      		return value
	    },
	    set: functionReactiveSetter (newVal) {console.log(' calledsetAnd has a value of${newVal}Observe childOb = observe(newVal) const value = val val = newVal // Observe childOb = observe(newVal) Dep.notify ()}})} // Helper methodfunction def (obj, key, val) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: true,
    writable: true,
    configurable: true})} / * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the initialization watch -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * / / / empty function const it = () = > {} // Watcher for computed initialization passes lazy:trueWill trigger Watcher with the dirty value oftrue
const computedWatcherOptions = { lazy: true} //Object.defineProperty default value const sharedPropertyDefinition = {Enumerable:true,
  configurable: true,
  get: noop,
  set: Noop} // Initialize computed class initComputed {constructor(VM, computed){// create a new storage watcher object, Mount the VM Object and perform const Watchers = vm._computedWatchers = object.create (NULL) // Perform computedfor (const key inComputed) {const userDef = computed[key] // The getter value is the get value of the listening function or object of the key in computedlet getter = typeof userDef === 'function'? Watchers [key] = new watcher (VM, getter, noop, computedWatcherOptions)if(! (keyin{/* define the attribute */ this.definecomputed (vm, key, userDef)}}} // mount the key to the vm object, So calling vm.somecomputed triggers 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? Enclosing createComputedGetter (key) : userDef. Get: it / / if you have setsetMethods are used directly, otherwise empty function assignment sharedPropertyDefinition. Set = userDef. Set? userDef.set : noop } Object.defineProperty(target, key, SharedPropertyDefinition)} createComputedGetter (key) is called when the value of the evaluated property is obtained.return function computedGetterConst watcher = this._computedWatchers && this._computedwatchers [key]if(watcher) {// the watcher.dirty parameter determines whether the value of the computed property needs to be recalculated. The default value istrueThat is, it will be called once the first timeif(watcher.dirty) {/* watcher.dirty is set to after each executionfalseWatcher.dirty is only triggered if the dependent data value changestrue*/ watcher.evaluate()} // Obtain dependenciesif(dep.target) {watcher.depend()} // Return the value of the evaluated attributereturn watcher.value
	    }
	  }
	}
}Copy the code


The computed test:

//1, create a Vue constructor:function Vue(){} //2, set data and computed:let data={
    name:'Hello',}let computed={
	getfullname:function(){
		console.log('----- uses getFullname for computed ------')
		console.log('New value is:'+data.name + ' - world')
		return data.name + ' - world'}} //3, instantiate Vue and mount data to VueletVue = new vue () vue.data = data //letUpdateComponent = (vm)=>{// collect dependent data.name}letWatcher1 = new Watcher(vue,updateComponent,()=>{}) //5. Initialize Data and collect dependent observe(Data) //6. Initialize computedlet watcher2 = new initComputed(vue,computed)Copy the code


Test in the browser console:

Vue. Getfullname = vue. Getfullname = vue. Getfullname = vue. Getfullname = vueCopy the code

Analysis: The first call to wue. getFullName prints ‘—– computed getFullName ——‘. This means that the computed attribute is computed for the first time. The second call does not print the value

If watcher.dirty=true, the value watcher.value is returned. If watcher.dirty=true, the value is returned.



// Assign data.name = to data.name'Hi'Copy the code

Data.name triggers two Watcher listening functions. For a global watcher and a computed watcher, the first watcher will update the view, and the second watcher will trigger watcher.dirty= True.



Vue. Getfullname = vue. Getfullname = vue. Getfullname = vueCopy the code

Analysis: When vue. Getfullname is run, computedGetter is executed, because watcher.dirty=true, the value will be computed, so ‘—– has computed getFullname ——‘ will be printed. The value is ‘HI World ‘, and reexecution will only get the cached value of the evaluated property.

All test codes are as follows:

/*----------------------------------------Vue------------------------------------*/
function Vue() {} / * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- test -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * / / / callslet data={
    name:'Hello',}let computed={
	getfullname:function(){
		console.log('----- uses getFullname for computed ------')
		console.log('New value is:'+data.name + ' - world')
		return data.name + ' - world'}}let vue 		= new Vue()
vue.data 		= data
letUpdateComponent = (vm)=>{// collect dependent data.name}let watcher1 = new Watcher(vue,updateComponent,()=>{})
observe(data)
letWatvher2 = new initComputed(vUE,computed) // Run the code test vue.getFullName vue.getFullName Data.name = on the test browser console'Hi'
vue.getfullname
vue.getfullnameCopy the code


If in doubt, please communicate.