深入理解Vue的watch實現原理及其實現方式

zane1發表於2018-05-14

理解Vue中Watch的實現原理和方式之前,你需要深入的理解MVVM的實現原理,如果你還不是很理解,推薦你閱讀我之前的幾篇文章:

徹底搞懂Vue針對陣列和雙向繫結(MVVM)的處理方式

vue.js原始碼解讀系列 - 雙向繫結具體如何初始化和工作

vue.js原始碼解讀系列 - 剖析observer,dep,watch三者關係 如何具體的實現資料雙向繫結

也可以關注我的部落格檢視關於Vue更多的原始碼解析:github.com/wangweiange…

備註:

1、此文大部分程式碼來自於Vue原始碼

2、此文MVVM部分程式碼來自於【徹底搞懂Vue針對陣列和雙向繫結(MVVM)的處理方式】,若有不懂之處,建議先看上文

3、部分程式碼為了相容測試做了部分更改,但原理跟Vue一致


畫一張watch的簡單工作流程圖:

深入理解Vue的watch實現原理及其實現方式

把上文的 Dep,Oberver,Wather拿過來並做部分更改(增加收集依賴去重處理):

Dep程式碼如下:

//標識當前的Dep id
let uidep = 0
class 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 = null
const 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 = 0
class Watcher{
	constructor(vm,expOrFn,cb,options){
		//傳進來的物件 例如Vue
		this.vm = vm
		if (options) {
	      this.deep = !!options.deep
	      this.user = !!options.user
	    }else{
	    	this.deep = this.user = false
	    }
		//在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.get()
	}

	get(){
		//設定Dep.target值,用以依賴收集
	    pushTarget(this)
	    const vm = this.vm
	    //此處會進行依賴收集 會呼叫data資料的 get
	    let value = this.getter.call(vm, vm)
	    //深度監聽
	    if (this.deep) {
	      traverse(value)
	    }
	    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 () {
	    this.run()
	}

	//更新檢視
	run(){
		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過程從而更新檢視
			console.log(`這裡會去執行Vue的diff相關方法,進而更新資料`)
        }
		
	}
	// 此方法獲得每個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
		 }
	}
}
//深度監聽相關程式碼 為了相容有一小點改動
const 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.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]])
    	}
  	}
}
//資料重複Observer
function 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程式碼的實現

watch程式碼大部摘自於Vue原始碼,我做了部分修改,把Watch改寫成一個cass類,程式碼如下:

class stateWatch{
	constructor (vm, watch) {
		this.vm = vm
		//初始化watch
	    this.initWatch(vm, watch)
	}
	initWatch (vm, watch) {
		//遍歷watch物件
	  	for (const key in watch) {
		    const handler = watch[key]
		    //陣列則遍歷進行createWatcher
		    if (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]' ) {
	  	//處理物件
	    options = handler
	    handler = handler.handler
	  }
	  if (typeof handler === 'string') {
	    handler = vm[handler]
	  }
	  vm.$watch(key, handler, options)
	}
}複製程式碼

初始化watch的類已經寫好,其中createWatcher有呼叫到vm.$watch,下面來實現$watch方法

新建一個Vue建構函式:

function Vue(){
}複製程式碼

為Vue新增原型方法$watch程式碼如下:

Vue.prototype.$watch=function(expOrFn,cb,options){
	const vm = this
    options = options || {}
    //此引數用於給data從新賦值時走watch的監聽函式
    options.user = true
    //watch依賴收集的Watcher
    const watcher = new Watcher(vm, expOrFn, cb, options)
    //immediate=true時 會呼叫一次 watcher.run 方法,因此會呼叫一次watch中相關key的函式
    if (options.immediate) {
      cb.call(vm, watcher.value)
    }
    //返回一個取消監聽的函式
    return function unwatchFn () {
      watcher.teardown()
    }
}複製程式碼


OK 萬事具備,所有的程式碼已經寫完,完整程式碼如下:

/*----------------------------------------Dep---------------------------------------*/
//標識當前的Dep id
let uidep = 0
class 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 = null
const 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 = 0
class Watcher{
	constructor(vm,expOrFn,cb,options){
		//傳進來的物件 例如Vue
		this.vm = vm
		if (options) {
	      this.deep = !!options.deep
	      this.user = !!options.user
	    }else{
	    	this.deep = this.user = false
	    }
		//在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.get()
	}

	get(){
		//設定Dep.target值,用以依賴收集
	    pushTarget(this)
	    const vm = this.vm
	    //此處會進行依賴收集 會呼叫data資料的 get
	    let value = this.getter.call(vm, vm)
	    //深度監聽
	    if (this.deep) {
	      traverse(value)
	    }
	    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 () {
	    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 監聽走此處
        	
			
        }
		
	}
	// 此方法獲得每個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
		 }
	}
}
//深度監聽相關程式碼 為了相容有一小點改動
const 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.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]])
    	}
  	}
}
//資料重複Observer
function 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------------------------------------*/
class stateWatch{
	constructor (vm, watch) {
		this.vm = vm
		//初始化watch
	    this.initWatch(vm, watch)
	}
	initWatch (vm, watch) {
		//遍歷watch物件
	  	for (const key in watch) {
		    const handler = watch[key]
		    //陣列則遍歷進行createWatcher
		    if (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]' ) {
	  	//處理物件
	    options = 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 vm = this
    options = options || {}
    //此引數用於給data從新賦值時走watch的監聽函式
    options.user = true
    //watch依賴收集的Watcher
    const watcher = new Watcher(vm, expOrFn, cb, options)
    //immediate=true時 會呼叫一次 watcher.run 方法,因此會呼叫一次watch中相關key的函式
    if (options.immediate) {
      cb.call(vm, watcher.value)
    }
    //返回一個取消監聽的函式
    return function unwatchFn () {
      watcher.teardown()
    }
}複製程式碼


程式碼測試:

再回頭看看上面那張簡單的Vue watch流程圖,測試程式碼我們嚴格按照流程圖順序進行

為了方便觀看此處複製一份流程圖:

深入理解Vue的watch實現原理及其實現方式

1、新建vue物件,並定義data和watch值:

let vue = new Vue()複製程式碼

定義一個data值並掛載到vue中,並給vue新增一個doSomething的方法:

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`)
}複製程式碼

定義一個watch值

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)
	},
}複製程式碼

2、始化Wathcer

let updateComponent = (vm)=>{
	// 收集依賴
	data.age
	data.blog
	data.name
	data.some
	data.some.f
	data.fn
}
new Watcher(vue,updateComponent)複製程式碼

3、初始化Data資料並收集依賴

observe(data)
//此處會呼叫上面的函式updateComponent,從而呼叫 get 收集依賴複製程式碼

4、初始化watch

其中會新建立watcher物件即(Dep.target=watcher),呼叫watch物件key對應的data資料的set,從而收集依賴

new stateWatch(vue, watch)複製程式碼

5、觸發set更新

所有依賴都已經收集好是時候觸發了

//首先會立即呼叫一次watch中的some的函式

//會觸發vue下的doSomething方法
data.age=25

//會觸發watch中監聽的blog的函式
data.blog='http://www.seosiwei.com/'

//會觸發watch中監聽的name的函式
data.name='xiaozhang'

//會觸發watch中some.f監聽的函式
data.some.f='deep f'

//會觸發watch中fn監聽的兩個函式
data.fn='go fn'複製程式碼


完整測試程式碼如下:

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`)
}
let updateComponent = (vm)=>{
	// 收集依賴
	data.age
	data.blog
	data.name
	data.some
	data.some.f
	data.fn
}
new Watcher(vue,updateComponent)
observe(data)
new stateWatch(vue, watch)複製程式碼


watch實現完畢。

下一篇:深入理解Vue的computed實現原理及其實現方式 


相關文章