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

zane1發表於2018-05-12

歡迎關注我的部落格:github.com/wangweiange…

Vue內部實現了一組觀察陣列的變異方法,例如:push(),pop(),shift()等。

Object.definePropert只能把物件屬性改為getter/setter,而對於陣列的方法就無能為力了,其內部巧妙的使用了陣列的屬性來實現了資料的雙向繫結,下面我們來一步一步的實現一個簡單版。

下文大部分程式碼摘自Vue原始碼

首先我們來實現給定一個陣列 呼叫相關方法時觸發自定義的函式

定義一個需要監聽變化的陣列

let obarr = []複製程式碼

來copy一份陣列的原型方法,防止汙染原生陣列方法

const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)複製程式碼

我們先把arrayMethods物件上的push轉換為觀察者物件

Object.defineProperty(arrayMethods,'push',{
    value:function mutator(){
    	console.log('obarr.push會走這裡')
    }
})複製程式碼

此時arrayMethods定義了一個push的新屬性,那麼我們如何把它和 let obarr = [] 繫結起來呢,來看看下面的實現?

obarr.__proto__ = arrayMethods複製程式碼

使用arrayMethods覆蓋obarr的所有方法

到此現在完整程式碼如下:

let obarr = []
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)	
Object.defineProperty(arrayMethods,'push',{
    value:function mutator(){
    	console.log('obarr.push會走這裡')
    }
})
obarr.__proto__ = arrayMethods;複製程式碼

向obarr中push一個值看看,是不是走了console呢,肯定的答覆你:yes 走了。

obarr.push(0)複製程式碼

針對於不支援__proto__的瀏覽器實現如下:

let obarr = []
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)	
Object.defineProperty(arrayMethods,'push',{
    value:function mutator(){
    	console.log('obarr.push會走這裡')
    }
})
Object.defineProperty(obarr,'push',{
	value:arrayMethods.push
})複製程式碼

來真正的為arr賦值程式碼如下:

let obarr = []
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
Object.defineProperty(arrayMethods,'push',{
    value:function mutator(){
    	  //快取原生方法,之後呼叫
    	  const original = arrayProto['push']	
    	  let args = Array.from(arguments)
	  original.apply(this,args)
	  console.log(obarr)
    }
})
obarr.__proto__ = arrayMethods;複製程式碼

現在每次執行obarr.push(0)時,obarr都會新增一項。

上面實現了push方法,其他的方法同理,我們只需要把所有需要實現的方法迴圈遍歷執行即可,升級後程式碼如下:

const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
;[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
].forEach(item=>{
	Object.defineProperty(arrayMethods,item,{
	    value:function mutator(){
	    	//快取原生方法,之後呼叫
	    	const original = arrayProto[item]	
	    	let args = Array.from(arguments)
		    original.apply(this,args)
	    },
	})
})
function protoAugment (target,src) {
  target.__proto__ = src
}
// 呼叫
let obarr = []
protoAugment(obarr, arrayMethods)複製程式碼

來多試幾次吧:

obarr.push(1)
obarr.push(2)
obarr.push(3)
obarr.push(4)複製程式碼

分析:

1、經過以上的程式碼可以看出,只會更改我們給定陣列(obarr)的相關方法,而不會汙染Array的原生方法,因此其他普通陣列不受影響。

2、從新賦值陣列的__proto__屬性為arrayMethods,而arrayMethods我們從新定義了push,pop等相關屬性方法,因此當我們使用陣列的push,pop等方法時會呼叫arrayMethods的相關屬性方法,達到監聽陣列變化的能力。

3、對於不支援__proto__屬性的瀏覽器,直接使用Object.defineProperty從新定義相關屬性。

4、而Vue的實現方法正如上,更改我們需要監聽的Array陣列屬性值(屬性值為函式),在監聽函式裡執行陣列的原生方法,並通知所有註冊的觀察者進行響應式處理。


下面來簡單的實現Vue對陣列的依賴收集和通知更新

實現Vue的資料雙向繫結有3大核心:Observer,Dep,Watcher,來個簡單實現

首先來實現dep,dep主要負責依賴的收集,get時觸發收集,set時通知watcher通訊:

class Dep{
	constructor () {
		// 存放所有的監聽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 賦值
function pushTarget (Watcher) {
  Dep.target = Watcher
}複製程式碼

再來簡單的實現Watcher,Watcher負責資料變更之後呼叫Vue的diff進行檢視的更新:

class Watcher{
	constructor(vm,expOrFn,cb,options){
		//傳進來的物件 例如Vue
		this.vm = vm
		//在Vue中cb是更新檢視的核心,呼叫diff並更新檢視的過程
		this.cb = cb
		//收集Deps,用於移除監聽
		this.newDeps = []
		this.getter = expOrFn
		//設定Dep.target的值,依賴收集時的watcher物件
		this.value =this.get()
	}

	get(){
		//設定Dep.target值,用以依賴收集
	    pushTarget(this)
	    const vm = this.vm
	    let value = this.getter.call(vm, vm)
	    return value
	}

	//新增依賴
  	addDep (dep) {
  		// 這裡簡單處理,在Vue中做了重複篩選,即依賴只收集一次,不重複收集依賴
	    this.newDeps.push(dep)
	    dep.addSub(this)
  	}

  	//更新
  	update () {
	    this.run()
	}

	//更新檢視
	run(){
		//這裡只做簡單的console.log 處理,在Vue中會呼叫diff過程從而更新檢視
		console.log(`這裡會去執行Vue的diff相關方法,進而更新資料`)
	}
}複製程式碼

簡單實現Observer,Observer負責資料的雙向繫結,並把物件屬性改為getter/setter

//獲得arrayMethods物件上所有屬性的陣列
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

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)) {
	    	//處理陣列
	        const augment = value.__proto__ ? protoAugment : copyAugment  
	        //此處的 arrayMethods 就是上面使用Object.defineProperty處理過
	        augment(value, arrayMethods, arrayKeys)
	        // 迴圈遍歷陣列children進行oberve
	        this.observeArray(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]])
    	}
  	}

	observeArray (items) {
    	for (let i = 0, l = items.length; i < l; i++) {
	      observe(items[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()
		        }
		        //此處是對Array資料型別的依賴收集
		        if (Array.isArray(value)) {
		          	dependArray(value)
		        }
	      	}
      		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
  })
}

//重新賦值Array的__proto__屬性
function protoAugment (target,src) {
  target.__proto__ = src
}
//不支援__proto__的直接修改相關屬性方法
function copyAugment (target, src, keys) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}
//收集陣列的依賴
function dependArray (value) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
    	//迴圈遍歷chindren進行依賴收集
        dependArray(e)
    }
  }
}複製程式碼

Observer中寫了一些相關需要的方法。


讓我們來修改下處理陣列的相關方法,當使用Array.push相關方法時可以呼叫Watcher更新檢視

const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
;[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
].forEach(item=>{
	Object.defineProperty(arrayMethods,item,{
	    value:function mutator(){
	    	//快取原生方法,之後呼叫
	    	const original = arrayProto[item]	
	    	let args = Array.from(arguments)
		    original.apply(this,args)
		    const ob = this.__ob__
		    ob.dep.notify()
	    },
	})
})複製程式碼

大功至此告成,把所有程式碼整理完整如下:

/*----------------------------------------處理陣列------------------------------------*/
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
;[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
].forEach(item=>{
	Object.defineProperty(arrayMethods,item,{
	    value:function mutator(){
	    	//快取原生方法,之後呼叫
	    	const original = arrayProto[item]	
	    	let args = Array.from(arguments)
		    original.apply(this,args)
		    const ob = this.__ob__
		    ob.dep.notify()
	    },
	})
})
/*----------------------------------------Dep---------------------------------------*/
class Dep{
	constructor () {
		// 存放所有的監聽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 賦值
function pushTarget (Watcher) {
  Dep.target = Watcher
}

/*----------------------------------------Watcher------------------------------------*/
class Watcher{
	constructor(vm,expOrFn,cb,options){
		//傳進來的物件 例如Vue
		this.vm = vm
		//在Vue中cb是更新檢視的核心,呼叫diff並更新檢視的過程
		this.cb = cb
		//收集Deps,用於移除監聽
		this.newDeps = []
		this.getter = expOrFn
		//設定Dep.target的值,依賴收集時的watcher物件
		this.value =this.get()
	}

	get(){
		//設定Dep.target值,用以依賴收集
	    pushTarget(this)
	    const vm = this.vm
	    let value = this.getter.call(vm, vm)
	    return value
	}

	//新增依賴
  	addDep (dep) {
  		// 這裡簡單處理,在Vue中做了重複篩選,即依賴只收集一次,不重複收集依賴
	    this.newDeps.push(dep)
	    dep.addSub(this)
  	}

  	//更新
  	update () {
	    this.run()
	}

	//更新檢視
	run(){
		//這裡只做簡單的console.log 處理,在Vue中會呼叫diff過程從而更新檢視
		console.log(`這裡會去執行Vue的diff相關方法,進而更新資料`)
	}
}

/*----------------------------------------Observer------------------------------------*/
//獲得arrayMethods物件上所有屬性的陣列
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

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)) {
	    	//處理陣列
	        const augment = value.__proto__ ? protoAugment : copyAugment  
	        //此處的 arrayMethods 就是上面使用Object.defineProperty處理過
	        augment(value, arrayMethods, arrayKeys)
	        // 迴圈遍歷陣列children進行oberve
	        this.observeArray(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]])
    	}
  	}

	observeArray (items) {
    	for (let i = 0, l = items.length; i < l; i++) {
	      observe(items[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()
		        }
		        //此處是對Array資料型別的依賴收集
		        if (Array.isArray(value)) {
		          	dependArray(value)
		        }
	      	}
      		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
  })
}

//重新賦值Array的__proto__屬性
function protoAugment (target,src) {
  target.__proto__ = src
}
//不支援__proto__的直接修改相關屬性方法
function copyAugment (target, src, keys) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}
//收集陣列的依賴
function dependArray (value) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
    	//迴圈遍歷chindren進行依賴收集
        dependArray(e)
    }
  }
}複製程式碼

依賴收集流程圖:

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

測試:

定義一個data物件:

let data={
    name:'zane',
    blog:'https://blog.seosiwei.com/',
    hobby:['basketball','football'],
    list:[
        {name:'zhangsan'},
        {name:'lishi'}
    ]
}複製程式碼

呼叫watcher,並進行資料監聽

let getUpdates = (vm)=>{
	console.log('預設呼叫一次,進行依賴收集')
}
new Watcher(this,getUpdates)
observe(data)複製程式碼

呼叫get收集依賴

//收集name依賴
data.name
//收集hobby依賴
data.hobby複製程式碼

測試資料監聽

//都會列印這裡會去執行Vue的diff相關方法,進而更新資料
data.name = 'zhangshan'
data.hobby.push('volleyball')複製程式碼

是不時出現可愛的 這裡會去執行Vue的diff相關方法,進而更新資料 日誌呢。

沒進行依賴收集的屬性會列印日誌嗎,來嘗試一下吧

//不會列印更新
data.blog = 'http://www.seosiwei.com/'
//不會呼叫每一個children的列印更新
data.list.push({name:'xiaowang'})複製程式碼


以上基本實現了Vue對陣列和物件的雙向繫結處理方式,收集依賴和更新視同原理,當然程式碼並沒有做太多的優化,比如(Watcher重複的收集)。

大部分程式碼摘自與Vue原始碼,部分實現的比較簡單,做了一些更改,程式碼進行的從新組織,整體能很好的說明Vue的核心MVVM實現方式。

下一篇:徹底搞懂Vue中watch的實現方式和原理


相關文章