Before you can understand the implementation principle and method of Watch in Vue, you need to have a deep understanding of the implementation principle of MVVM. If you do not have a good understanding, I recommend you to read my previous articles:

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

Vue.js source code interpretation series – How does bidirectional binding initialize and work

Vue. Js source code interpretation series – analysis observer, DEP,watch the relationship between the three specific data two-way binding

You can also follow my blog for more source code analysis of Vue: github.com/wangweiange…

Remark:

1. Most of the code in this article comes from Vue source code

2. Part of the MVVM code in this article comes from [thoroughly understand Vue’s handling of arrays and bidirectional binding (MVVM)]. If you don’t understand anything, please read above first

3. Some of the code has been changed for compatibility testing, but the principle is the same as Vue


Draw a simple flow chart of watch:



Take the Dep,Oberver,Wather from above and make some changes (add collection dependency to reprocess) :

The Dep code is as follows:

// Identifies the current Dep IDlet uidep = 0
class Dep{
	constructor() {this.id = uidep++ // store all listeners watcher this.subs = []} // add an observer object addSub (watcher) {this.subs.push(watcher)} // Rely on collectiondepend() {// dep.target will collect dependencies only if neededif(dep.target) {dep.target.adddep (this)}} // Calls the dependent collection of Watcher updatesnotify () {
	    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

The Watcher code looks like this:

// Deduplication prevents duplicate collectionsletUid = 0 class Watcher{constructor(VM,expOrFn,cb,options){// Import object e.g. Vue this.vm = vmif(options) { this.deep = !! options.deep this.user = !! options.user }else{
	    	this.deep = this.user = false} // In Vue cb is the core of the updated view, This. Cb = cb this.id = ++uid this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set()if (typeof expOrFn === 'function'}}}}}}}}}}}}}}}}elseGetter = this.parsepath (expOrFn)} // Set the dep.target value, which depends on the watcher object at the time of collection this.value =this.get()}getPushTarget (this) const vm = this.vm (){pushTarget(this) const vm = this.vm (){pushTarget(this) const vm = thisletValue = this.getter.call(vm, vm) // deep listenerif (this.deep) {
	      traverse(value)
	    }
	    popTarget()
	    returnValue} // Add dependency addDep (dep) {// remove 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)}}} // updateupdate() {this.run()} // Update the viewrun(){
		const value = this.get()
		const oldValue = this.value
        this.value = value
		ifCall (this.vm, value, oldValue)} (this.user) {this.cb.call(this.vm, value, oldValue)}else{//data listen to go here // here to do simple console.log processing, in the Vue call the diff procedure to update the view console.log(' here to execute the Vue diff related methods, }} // This method gets the value of each watch key in data // use split()'. ') 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// The new value is used here to override the passed value so it can handle'a.b.c'This way of listeningif(i==0){
		        	obj = obj.data[segments[i]]
		        }else{
		        	obj = obj[segments[i]]
		        }
		    }
		    returnConst seenObjects = new Set()function traverse (val) {
  seenObjects.clear()
  _traverse(val, seenObjects)
}

function _traverse (val, seen) {
  let i, keys
  const isA = Array.isArray(val)
  if(! isA && Object.prototype.toString.call(val)! ='[object Object]') return;
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  if (isA) {
    i = val.length
    while (i--){
    	if(i == '__ob__') return;
    	_traverse(val[i], seen)
    } 
  } else {
    keys = Object.keys(val)
    i = keys.length
    while (i--){
    	if(keys[i] == '__ob__') return;
    	_traverse(val[keys[i]], seen)
    } 
  }
}Copy the code

The Observer code is as follows:

Class Observer{constructor (value) {this.value = value // Add a dep attribute. // Bind the Observer to the __ob__ attribute of data. If oberve is used directly, the Observer object def(value,'__ob__', this)
	    if(array.isarray (value)) {elseThis.walk (value)}} walk(obj) {const keys = object.keys (obj)for (leti = 0; i < keys.length; I++) {// here I do interception to prevent an endless loop, which is what oberve does in Vueif(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 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}') const value = val val = newVal // Observe childOb = observe(newVal) // Notify deP, loop through the Watcher dependency of the phone, Update the view 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 comes to the implementation of watch code

Most of the watch code was extracted from Vue source code, and I made some modifications to rewrite watch into a CASS class, with the following code:

Class stateWatch{constructor (vm, watch) {this.vm = vm this.initWatch(vm, watch)} initWatch(vm, watch) Watch) {// Iterate over the watch objectfor (const key inWatch) {const handler = watch[key] // Array is iterated to createWatcherif (Array.isArray(handler)) {
		      	for (let i = 0; i < handler.length; i++) {
		        	this.createWatcher(vm, key, handler[i])
		      	}
		    } else {
		      	this.createWatcher(vm, key, handler)
		    }
	  	}
	}
	createWatcher (vm, key, handler) {
	  let options
	  if (Object.prototype.toString.call(handler) == '[object Object]'Handler = handler handler = handler. Handler}if (typeof handler === 'string') {
	    handler = vm[handler]
	  }
	  vm.$watch(key, handler, options)
	}
}Copy the code

$watch (); $watch (); $watch ()

Create a new Vue constructor:

function Vue(){
}Copy the code

Add the prototype method $watch code for Vue as follows:

Vue.prototype.$watch=function(expOrFn, cb, options) {const = this vm options = options | | {} / / this parameter is used to give the data from the new assignment go watch the monitor function options. The user =true//watch dependencies on the collected Watcher const Watcher = new Watcher(VM, expOrFn, CB, options) //immediate=trueThe watcher.run method is called once, so the function related to key in watch is called onceif(options.immediate) {cb.call(vm, watcher.value)} // Returns a function to cancel listeningreturn function unwatchFn () {
      watcher.teardown()
    }
}Copy the code


OK everything is ready, all the code has been written, the complete code is as follows:

/ * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- Dep -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * / / / the current Dep idlet uidep = 0
class Dep{
	constructor() {this.id = uidep++ // store all listeners watcher this.subs = []} // add an observer object addSub (watcher) {this.subs.push(watcher)} // Rely on collectiondepend() {// dep.target will collect dependencies only if neededif(dep.target) {dep.target.adddep (this)}} // Calls the dependent collection of Watcher updatesnotify () {
	    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){// Import object e.g. Vue this.vm = vmif(options) { this.deep = !! options.deep this.user = !! options.user }else{
	    	this.deep = this.user = false} // In Vue cb is the core of the updated view, This. Cb = cb this.id = ++uid this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set()if (typeof expOrFn === 'function'}}}}}}}}}}}}}}}}elseGetter = this.parsepath (expOrFn)} // Set the dep.target value, which depends on the watcher object at the time of collection this.value =this.get()}getPushTarget (this) const vm = this.vm (){pushTarget(this) const vm = this.vm (){pushTarget(this) const vm = thisletValue = this.getter.call(vm, vm) // deep listenerif (this.deep) {
	      traverse(value)
	    }
	    popTarget()
	    returnValue} // Add dependency addDep (dep) {// remove 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)}}} // updateupdate() {this.run()} // Update the viewrunGet () const oldValue = this.value this.value this.value = valueifCall (this.vm, value, oldValue)} (this.user) {this.cb.call(this.vm, value, oldValue)}else{//data listens here}} // This method gets the value of each watch key in data // use split('. ') 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// The new value is used here to override the passed value so it can handle'a.b.c'This way of listeningif(i==0){
		        	obj = obj.data[segments[i]]
		        }else{
		        	obj = obj[segments[i]]
		        }
		    }
		    returnConst seenObjects = new Set()function traverse (val) {
  seenObjects.clear()
  _traverse(val, seenObjects)
}

function _traverse (val, seen) {
  let i, keys
  const isA = Array.isArray(val)
  if(! isA && Object.prototype.toString.call(val)! ='[object Object]') return;
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  if (isA) {
    i = val.length
    while (i--){
    	if(i == '__ob__') return;
    	_traverse(val[i], seen)
    } 
  } else {
    keys = Object.keys(val)
    i = keys.length
    while (i--){
    	if(keys[i] == '__ob__') return; _traverse(val[keys[i]], seen) } } } /*----------------------------------------Observer------------------------------------*/ class Observer{ Constructor (value) {this.dep = new dep () // Bind an Observer instance to the __ob__ attribute of data. Def (value, observer.def (value, observer.def));'__ob__', this)
	    if(array.isarray (value)) {elseThis.walk (value)}} walk(obj) {const keys = object.keys (obj)for (leti = 0; i < keys.length; I++) {// here I do interception to prevent an endless loop, which is what oberve does in Vueif(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 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}') const value = val val = newVal // Observe childOb = observe(newVal) // Notify deP, loop through the Watcher dependency of the phone, Update the view dep.notify()}})} // Helper methodfunction def (obj, key, val) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: true,
    writable: true,
    configurable: true})} / * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the initialization watch -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * / class stateWatch { InitWatch (vm, watch)} initWatch(vm, watch) {// Iterate over the watch objectfor (const key inWatch) {const handler = watch[key] // Array is iterated to createWatcherif (Array.isArray(handler)) {
		      	for (let i = 0; i < handler.length; i++) {
		        	this.createWatcher(vm, key, handler[i])
		      	}
		    } else {
		      	this.createWatcher(vm, key, handler)
		    }
	  	}
	}
	createWatcher (vm, key, handler) {
	  let options
	  if (Object.prototype.toString.call(handler) == '[object Object]'Handler = handler handler = handler. Handler}if (typeof handler === 'string') {
	    handler = vm[handler]
	  }
	  vm.$watch(key, handler, options)
	}
}

/*----------------------------------------Vue------------------------------------*/
function Vue(){
}

Vue.prototype.$watch=function(expOrFn, cb, options) {const = this vm options = options | | {} / / this parameter is used to give the data from the new assignment go watch the monitor function options. The user =true//watch dependencies on the collected Watcher const Watcher = new Watcher(VM, expOrFn, CB, options) //immediate=trueThe watcher.run method is called once, so the function related to key in watch is called onceif(options.immediate) {cb.call(vm, watcher.value)} // Returns a function to cancel listeningreturn function unwatchFn () {
      watcher.teardown()
    }
}Copy the code


Code test:

Looking back at the simple Vue Watch flow chart above, we tested the code in strict accordance with the flow chart sequence

Here’s a copy of the flow chart for easy viewing:



1. Create vUE object and define data and watch values:

let vue = new Vue()Copy the code

Define a data value, mount it to vue, and add a doSomething method to vue:

let data={
    name:'zane',
    blog:'https://blog.seosiwei.com/',
    age:20,
    fn:' ',
    some:{
    	f:'xiaowang'
    }
}
vue.data = data
vue.doSomething=()=>{
	console.log(`i will do something`)
}Copy the code

Define a watch value

let watch={
	name: function (val, oldVal) {
		console.log('----------name--------')
      	console.log('new: %s, old: %s', val, oldVal)
    },
    blog:function (val, oldVal) {
    	console.log('----------blog---------')
      	console.log('new: %s, old: %s', val, oldVal)
    },
    age:'doSomething',
    fn:[
      function handle1 (val, oldVal) { console.log('111111')},function handle2 (val, oldVal) { console.log('222222') }
    ],
    some:{
      	handler: function (val, oldVal) {
      		console.log('----------some---------')
      		console.log('new: %s, old: %s', val, oldVal)
      	},
      	immediate: true
    },
    'some.f': function (val, oldVal) { 
		console.log(`----some.f-----`)
		console.log('new: %s, old: %s', val, oldVal)
	},
}Copy the code

2. Initialize Wathcer

letUpdateComponent = (vm)=>{// Collect dependencies data.age data.blog data.name data.some data.some.f data.fn} new Watcher(vue,updateComponent)Copy the code

Initialize Data and collect dependencies

Observe (data) // Call updateComponent to collect dependenciesCopy the code

4. Initialize Watch

Where, a new watcher object (dep. target=watcher) will be created and the set of data corresponding to watch object key will be called to collect dependencies

new stateWatch(vue, watch)Copy the code

5. Trigger set update

All dependencies have been collected and it’s time to trigger

// Some in watch will be called immediately // vue will be triggereddoSomething data.age=25 // triggers the function data.blog= for the blog listening in watch'http://www.seosiwei.com/'// Data.name = triggers the function data.name='xiaozhang'// Data.some. f= will trigger data.some.f='deep f'// Data.fn = triggers two functions that fn listens on in watch'go fn'Copy the code


The complete test code is as follows:

let data={
    name:'zane',
    blog:'https://blog.seosiwei.com/',
    age:20,
    fn:' ',
    some:{
    	f:'xiaowang'}}let watch={
	name: function (val, oldVal) {
		console.log('----------name--------')
      	console.log('new: %s, old: %s', val, oldVal)
    },
    blog:function (val, oldVal) {
    	console.log('----------blog---------')
      	console.log('new: %s, old: %s', val, oldVal)
    },
    age:'doSomething',
    fn:[
      function handle1 (val, oldVal) { console.log('111111')},function handle2 (val, oldVal) { console.log('222222') }
    ],
    some:{
      	handler: function (val, oldVal) {
      		console.log('----------some---------')
      		console.log('new: %s, old: %s', val, oldVal)
      	},
      	immediate: true
    },
    'some.f': function (val, oldVal) { 
		console.log(`----some.f-----`)
		console.log('new: %s, old: %s', val, oldVal)
	},
}

let vue = new Vue()
vue.data = data
vue.doSomething=()=>{
	console.log(`i will do something`)
}
letUpdateComponent = (vm)=>{// Collect dependencies data.age data.blog data.name data.some data.some.f data.fn} new Watcher(vue,updateComponent) observe(data) new stateWatch(vue, watch)Copy the code


Watch is implemented.

Next: An in-depth understanding of Vue’s computed implementation and its implementation methods