vue計算屬性Computed的小祕密

vue中computed小祕密的發現之旅

首先咱們看一段代碼

<body>
    <div id="app">
        {{ count }}
    </div>
</body>
<script>
    new Vue({
        el: '#app',
        data () {
            return {
                num: 66
            }
        },
        computed: {
            count () {
                console.log(1)
                return this.num
            }
        },
        methods: {
            add () {
                setInterval(() => {
                    this.num ++
                }, 1000) 
            }
        },
        created () {
            this.add()
        }
    })
</script>

請問

  • console.log(1)會間隔的打印出來嗎?
  • html中去掉{{ count }},再問console.log(1)會間隔的打印出來嗎?
  • 若是第二問沒有打印出來,那麼在第二問的基礎上怎麼修改才能再次打印出來呢?

我先來揭曉答案

  • 會打印出來
  • 不會打印出來
  • 能夠用過添加watch監聽count,來打印`console.log(1)
watch: {
        count: function (oldValue, newValue) {

        }
    }

請問爲何呢?

如下是個人理解,有誤還請指出,共同進步html

  • 一句話總結就是computed是惰性求值,在new watcher時是計算屬性時,this.value=undefined因此一開始不會觸發get進行依賴收集即僅僅定義computed的話是沒有進行計算屬性count的依賴收集(能夠相似當作data中的數值,僅僅進行了響應式get,set的定義,並無觸發dep.depend,因此當值發生變化的時候,他並不知道要通知誰,也就不會執行相應的回調函數了)

源碼中有這麼一段:vue

depend () {
  if (this.dep && Dep.target) {  //由於惰性求值,因此Dep.target爲false
    this.dep.depend()
  }
}

因此若是僅僅是computed的初始化的話並Dep.target就是undefined,因此實例化的watch並不會加入dep的中express

看看Computed的實現

  • computed初始化
function initComputed (vm: Component, computed: Object) {
    const watchers = vm._computedWatchers = Object.create(null)  //(標記1)新建一個沒有原型鏈的對象,用來存`computed`對象每一個值的watch實例對象
    const isSSR = isServerRendering()  //與服務端渲染有關,暫時忽略
    for (const key in computed) {
        const userDef = computed[key]  //取key的值,該值大部分是function類型
        //下面主要做用就是在非生產環境中沒有getter,保警告
        const getter = typeof userDef === 'function' ? userDef : userDef.get
        if (process.env.NODE_ENV !== 'production' && getter == null) {
          warn(
            `Getter is missing for computed property "${key}".`,
            vm
          )
        }
    }
    if (!isSSR) {
      //computed中不一樣的key,也就是計算屬性生成watch實例,
      //watch做用:簡單看就是當值發生變化時會觸通知到watch,觸發更新,執行回調函數
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }
    if (!(key in vm)) {
      //做用是將{key: userDef}變成響應式,重寫其get和set
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
}
  • defineComputed 先看這個函數作了什麼
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
    const shouldCache = !isServerRendering()
    if (typeof userDef === 'function') {
      sharedPropertyDefinition.get = shouldCache
        ? createComputedGetter(key)
        : userDef
      sharedPropertyDefinition.set = noop
    } else {
      sharedPropertyDefinition.get = userDef.get
        ? shouldCache && userDef.cache !== false
          ? createComputedGetter(key)
          : userDef.get
        : noop
      sharedPropertyDefinition.set = userDef.set
        ? userDef.set
        : noop
    }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

上面函數的做用就是改寫get與set,關鍵就是這個createComputedGetter在作什麼?
早版本createComputedGetter的實現是:數組

function createComputedGetter(){
    return function computedGetter () {
        //這個就是以前用來收集watch實例的一個對象,可看註釋:標記1
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if(watcher) {
            if(watcher.dirty) {
                watcher.evaluate()
            }
            if(Dep.target){ //這裏也能夠看出Dep.target爲false時是不會觸發depend,即添加依賴
                watcher.depend()
            }
            return watcher.value
        }
    }
}

重點看看watch

export default class Watcher {

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    //進行初始化的定義,忽略無關代碼
      if(options) {
          this.lazy = !!options.lazy
      }else {
          this.lazy = false
      }
      this.getter = parsePath(expOrFn) //返回一個取data值得函數
      this.dirty = this.lazy   //true
      this.value = this.lazy ? undefined : this.get()  //undefined,當不會執行get時也就不會觸發get實例方法中的depend的了
    }

  get () {
    // 僞代碼
    Dep.target = this
    //取值也就是訪問觸發屬性的get,get中又觸發dep.depend(),而dep.depend內部觸發的是Dep.target.addDep(this),這裏的this實際上是Dep實例
    let value = this.getter.call(vm, vm) 
    Dep.target = undefined
  }

  addDep (dep: Dep) {
    //僞代碼
    const id = dep.id
    if(!this.depIds.has(id)) {
        this.depIds.add(id)
        this.deps.push(dep)
        dep.addSub(this)  //this是watch實例對象
    }
  }

  update () {
    // 省略...
  }

  getAndInvoke (cb: Function) {
    // 省略...
  }

  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  depend () {
    let i = this.deps.length
    while(i --) {
        this.deps[i].depend()
    }
  }
  ...

}

總結: 1.watcher.dirty默認爲true,執行watcher.evaluate()因此computed第一次默認會渲染,與watch不一樣;2.當默認渲染,觸發了get,Dep.target就不是false,就會執行watcher.depend()app

watcher.depend() 早版的實現,它有什麼問題

  • this.dep這個數組中元素都是Dep的實例對象,watcher所依賴的全部Dep實例化列表;
    舉個例子:當計算屬性中return this.num + this.num1,當讀取計算屬性時會分別觸發num與num1的get,get中又觸發dep.depend(),而dep.depend內部觸發的是Dep.target.addDep(this),這裏的this實際上是Dep實例,這樣就會分別將不一樣編號的num與num1的dep,加入到deps中,最後將計算屬性的依賴加入到num,num1的Dep中,this.deps[i].depend()也會加,但以前已加入改id因此猜想會直接return掉
  • 這樣當num發生改變,觸發set,觸發其notify 方法即遍歷dep.subDeps數組(subDeps中放的是各類依賴),觸發依賴的update方法。但以前的update方法看了一下
update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
}

能夠看出直接走queueWatcher(this)因此就算內容沒有變化,也會走渲染流程,這就形成了浪費函數

新版本,發生了變化

  • 第一個createComputedGetter
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      watcher.depend()
      return watcher.evaluate()
    }
  }
}
  • 第二個watcher.depend()
if (this.dep && Dep.target) {  
    this.dep.depend()
  }
}

上面這裏的dep又是哪裏來的呢?在watch類中加了下面代碼oop

if (this.computed) {
    this.value = undefined
    this.dep = new Dep()   //相似一個Object對象,進行observer設置get,set響應式時會進let dep = new Dep, 來收集改值得依賴
  } else {
    this.value = this.get()
  }

因此從上面的實現能夠看出,對當前計算屬性自身也生成一個dep列表進行收集;徹底能夠把一個computed的初始化看出data中數據的初始化,只不過該值又依賴多個依賴this

  • 第三個evaluate
evaluate () {
  if (this.dirty) {
    this.value = this.get()
    this.dirty = false
  }
  return this.value
}
  • 關鍵的update也作了修改,
update () {
  /* istanbul ignore else */
  if (this.computed) {
    if (this.dep.subs.length === 0) {
      this.dirty = true
    } else {
      this.getAndInvoke(() => {
        this.dep.notify()
      })
    }
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
},
//當計算屬性的值發生變化時,改觸發回調函數或者進行渲染,而不是經過以前值(例如num改變)變化就觸發回調
getAndInvoke (cb: Function) {
    const value = this.get()
    if (
      value !== this.value ||
      isObject(value) ||
      this.deep
    ) {
      const oldValue = this.value
      this.value = value
      this.dirty = false
      if (this.user) {
        try {
          cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        cb.call(this.vm, value, oldValue)
      }
    }
  }

當觸發update時首先經過getAndInvoke函數進行值得比較,看是否發生變化,即只有在變化時纔會執行,執行的是this.dep.notify(),而這邊打的this是當前watch實例對象;由於以前就添加了依賴this.dep.depend()因此接着觸發cb.call(this.vm, value, oldValue)cb是:this.dep.notify()但this指向了vm用來觸發渲染更新lua

總結

  • 計算屬性的觀察者是惰性求值,須要手動經過get
  • 怎麼手動get,因此有了問題的第二問,和第三問
  • 觸發了get,也就是觸發了createComputedGetter函數,就會去取值this.value = this.get(),進行第一次渲染或取值;同時watcher.depend(),將計算屬性的依賴添加至dep中,
  • 值發送變化時,輸出watch.update,首先判斷是否存在依賴,存在則只需watcher.getAndInvoke(cb),
相關文章
相關標籤/搜索