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

zane1發表於2019-03-04

繼上一篇:深入理解Vue的watch實現原理及其實現方式 繼續講解

Vue的computed實現相對於watch和data來說比較難以理解,要真正的理解computed的工作方式,你需要深入理解Vue的雙向資料繫結原理和實現方式。

如果你還不是很理解推薦你先看此文章:

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


首先來看一波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   // => 2
vm.aPlus = 3
vm.a       // => 2
vm.aDouble // => 4複製程式碼

計算屬性的主要應用場景是代替模板內的表示式,或者data值的任何複雜邏輯都應該使用computed來計算,它有兩大優勢:

1、邏輯清晰,方便於管理

2、計算值會被快取,依賴的data值改變時才會從新計算

此文我們需要核心理解的是:

1、computed是如何初始化,初始化之後幹了些什麼

2、為何觸發data值改變時computed會從新計算

3、computed值為什麼說是被快取的呢,如何做的


如果以上三個問題你都已知,你可以忽略下文了,若未知或一知半解,請抱著學習的態度看看別人的理解。

備註:以下只是我的個人理解,並不保證絕對的正確性,若有問題歡迎指正

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


如果你看到了這裡,就當做你已經深入理解了Vue的MVVM原理及其實現方式。相關Vue的MVVM實現直接取自上一篇文章。

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
	      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]])
    	}
  	}
}
//資料重複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
  })
}複製程式碼


此文的重點Computed程式碼實現:

//空函式
const noop = ()=>{}
// computed初始化的Watcher傳入lazy: true就會觸發Watcher中的dirty值為true
const computedWatcherOptions = { lazy: true }
//Object.defineProperty 預設value引數
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
// 初始化computed
class 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 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
	      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]])
    	}
  	}
}
//資料重複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------------------------------------*/
//空函式
const noop = ()=>{}
// computed初始化的Watcher傳入lazy: true就會觸發Watcher中的dirty值為true
const computedWatcherOptions = { lazy: true }
//Object.defineProperty 預設value引數
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
// 初始化computed
class 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、初始化computed
let watcher2 = new initComputed(vue,computed)複製程式碼


在瀏覽器console中測試:

//首先獲得一次getfullname
vue.getfullname

//第二次呼叫getfullname看看會有什麼變化呢
vue.getfullname複製程式碼

分析:呼叫vue.getfullname第一次會列印 '-----走了computed 之 getfullname------',即計算屬性第一次計算了值,第二次呼叫時,不會再列印值

即直接獲取的快取值,為什麼第二次是獲得的快取值呢,因為第二次執行時watcher.dirty=true,就會直接返回watcher.value值。

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


//為data.name賦值
data.name = 'Hi'複製程式碼

分析:執行data.name時會觸發兩個Watcher監聽函式(為什麼是兩個Watcher自己去腦補一下額!),一個是全域性的watcher,一個是computed的watcher,第一個Watcher會更新檢視,第二個Watcher會觸發watcher.dirty=true。

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

//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 		= data
let updateComponent = (vm)=>{
	// 收集依賴
	data.name
}
let watcher1 = new Watcher(vue,updateComponent,()=>{})
observe(data)
let watvher2 = new initComputed(vue,computed)

//測試 瀏覽器console中相繼執行一下程式碼測試
vue.getfullname
vue.getfullname
data.name='Hi'
vue.getfullname
vue.getfullname複製程式碼


若有疑問歡迎交流。


相關文章