3 / 26 看完這篇你一定懂computed的原理

前面的話

前端日問,鞏固基礎,不打烊!!!

解答

如有錯誤歡迎指出,感謝!!!

提出問題

提出幾個問題:

  • computed 是如何初始化的?
  • 爲何data值的變化computed會重新計算?
  • 爲什麼computed值是緩存的呢?

想了解computed的原理,你需要了解Vue的響應式原理,瞭解computed其實就是一個惰性watcher

下面一一解答這幾個問題。

Watcher 的實現

給出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
		 }
	}
}
computed 的實現

在Vue響應式原理這篇文章已經重點提了data數據的初始化(即initDate),computed的初始化是在其後面定義的:(爲什麼一定要在data數據的後面初始化,這也是有原因的,下面這接着說)

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  // computed初始化
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

接着看一看initComputed函數的定義:

//空函數
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
	    }
	  }
	}
}

開頭的參數很重要。

初始化大致流程:

  • 新建存儲watcher對象的數組,並掛載在vm_computedWatchers屬性上。
  • 遍歷整個computed上的屬性key
    • 獲取key上的監聽函數(賦給getter)

    • 創建watcher (即每一個computed屬性都是一個watcher)

    • 調用defineComputed函數 :將每一個計算屬性key掛載到vm上,並使用Object.defineProperty進行處理。

      具體看一看defineComputed函數做了什麼:

      既然要是用Object.defineProperty函數將每個計算屬性key掛載到vm上,需要三個值:掛載目標 vm掛載對象 key設置key的value對象。前面兩個已經具備,只差value對象了。還記得開頭的參數麼?這個value對象就是開頭設置的sharedPropertyDefinition對象

      sharedPropertyDefinition對象的get屬性剛開始是空函數,在這裏我們調用createComputedGetter函數來設置get屬性。

      具體看一看createComputedGetter 函數做了什麼:

      它返回了一個函數給sharedPropertyDefinition.get. 其內部:根據計算屬性key找到對應的watcher。(因爲計算屬性watcher創建時,會傳入computedWatcherOptions 對象,這個對象裏面定義了lazy: true,導致了計算屬性watcher的lazy爲true,dirty值初始時爲lazy的值。)

      找到對應的watcher後,會執行這段代碼:

      	if (watcher.dirty) {
      	      		/*每次執行之後watcher.dirty會設置爲false,
      	      		只要依賴的data值改變時纔會觸發
      	      		watcher.dirty爲true,從而獲取值時從新計算*/
      	        	watcher.evaluate()
      	    }
      

      watcher.evaluate()就是本文的核心: 初始時,就會執行這個函數

       //如果計算熟悉依賴的data值發生變化時會調用
          //案例中 當data.name值發生變化時會執行此方法
      	evaluate () {
      	    this.value = this.get()
      	    this.dirty = false
      	}
      

      瞭解響應式原理的應該知道wacther.get()函數的用途:就是綁定Dep.target 爲當前的watcher。並且進行data數據的依賴收集。

      看代碼: 調用watcher.get會觸發data數據的getter,進行依賴收集,收集這個watcher。前面講過data裏面的數據會比computed裏面的數據先初始化,就是這個原因。

      get(){
      		//設置Dep.target值,用以依賴收集
      	    pushTarget(this)
      	    const vm = this.vm
      	    //此處會進行依賴收集 會調用data數據的 get
      	    let value = this.getter.call(vm, vm)
      	    popTarget()
      	    return value
      	}
      

到此爲止,上面的問題應該有答案了吧! 計算屬性的值,是依賴於其他data屬性的,而計算屬性本質是計算watcher, 它所依賴的data屬性的dep會將這個watcher加入到subs數組中,當其變化時,就會通知這個watcher改變,所以說計算屬性時緩存的。

總結
  • 初始化一個computed,會爲每個屬性key,創建相應的watcher(將key的監聽函數傳入,作爲watcher的getter)。

  • 掛載屬性key到vm上,並用Object.defineProperty爲其添加getter。

  • 第一次調用屬性key時,觸發watcher.evaluate --> 觸發watcher.get --> watcher.getter(就是key的監聽函數) --> 觸發屬性key所依賴的data屬性的getter,進而收集這個watcher。

  • 當data屬性變化時,觸發其setter,通知watcher更新,執行watcher.run,進而觸發diff,更新視圖。

    在這裏插入圖片描述

通過一個computed實例來走一下上面的流程

computed:{
  sayHello() {
    return this.hello + ' ' + this.world;
  }
},

sayHello計算屬性依賴兩個屬性:this.hellothis.world

  • 在初始化時會創建一個sayHello對應的計算屬性watcher,watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions)

  • 將sayHello計算屬性掛載到vm上,用Object.defineProperty定義其get屬性。

    初次獲取sayHello屬性,觸發監聽函數 :

    • 獲取this.hello -->觸發hello的getter --> hello的dep.depend收集收集sayHello的watcher.

    • 獲取this.world時,觸發world的getter,dep.depend收集sayHello的watcher.

  • 所以sayhello的watcher最後,會有兩個dep收集它。hello與world其中任意一個更新,都會觸發更新sayhello watcher的run函數

  • 最後觸發diff算法,更新虛擬dom,進而更新界面

computed 與watch的區別

  • computed屬性不是data中的屬性值,是一個新值,初始化時使用Object.defineProperty方法掛載到vm上;而watch是監聽已經存在於data中的屬性
  • computed本質是一個惰性的觀察者,具有緩存性,之後依賴的data值變化時,纔會變化;watch沒有緩存性,數據變化就更新
  • computed適合與一個數據被多個數據影響;而watch適用於一個數據影響多個數據。

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章