【Ts重構Vue】05-實現computed和watch功能

如何建立computed和watch?

在項目中computed和watch很是實用,它們是如何實現的呢?編程

咱們的編碼目標是下面的demo可以成功渲染,最終渲染結果<h1>未讀消息:2</h1>設計模式

let v = new Vue({
  el: '#app',
  data () {
    return  {
      news: [1]
    }
  },
  computed: {
    newsCount() {
      return this.news.length
    },
  },
  render (h) {
    return h('h1',  '未讀消息:' + this.newsCount)
  }
})

setTimeout(() => {
    v.news.push(2)
}, 1000)
複製代碼

Vue響應式原理

根據上圖能夠知道,Vue將數據構造爲響應式的,若是須要監聽數據則要新建Watch的實例,創建Dep和Watch之間聯繫。緩存

實現watch

watch的常見用法以下:bash

watch: {
    news () {
        console.log('watch news!')
    }
}
複製代碼

watch功能依託Watch類實現,在Vue初始化時,爲全部watch屬性建立Watch實例。閉包

function initWatch(vm: Vue) {
  const watch = vm.$options.watch

  for (let key in watch) {
    new Watch(vm._proxyThis, key, watch[key], { user: true })
  }
}

複製代碼

new Watch實例化過程當中,會將key轉爲函數,執行該函數能夠獲取被監聽的屬性值,另外會將watch[key]函數保存在this.cb變量中。app

this.getter = isFunction(key) ? key : parsePath(key) || noop
this.cb = cb

function parsePath(key: string): any {
  return function(vm: any) {
    return vm[key]
  }
}
複製代碼

接着直接執行上一步的函數this.getter,收集全部依賴。異步

private get(): any {
    let vm = this.vm
    pushTarget(this)
    let value = this.getter.call(vm, vm)
    popTarget()

    return value
}
複製代碼

當被監聽屬性發生變化時,會通知Watch實例進行更新,從而執行this.cb.call(vm, value, this.value)函數。函數

實現computed

computed的調用形式主要有如下兩種:oop

computed: {
    newsCount() {
      return this.news.length
    },
    newsStr: {
        get () {
            return this.news.join(',')
        },
        set (val) {
            this.news = val.split(',')
        }
    }
}
複製代碼

computed屬性能夠定義getset函數,所以比較特殊:1.它的值依賴於其餘數據屬性;2.修改它也會驅動視圖進行更新。post

computed功能一樣依賴Watch類實現,在Vue初始化時,爲全部的computed屬性建立watch實例:new Watch(vm._proxyThis, getter, noop, {lazy: true})

function initComputed(vm: Vue) {
  let proxyComputed: any
  const computed = vm.$options.computed

  if (!isPlainObject(computed)) return

  for (let key in computed) {
    let userDef = computed[key]
    let getter = isFunction(userDef) ? userDef : userDef.get

    vm._computedWatched[key] = new Watch(vm._proxyThis, getter, noop, {
      lazy: true
    })
  }

  vm.$options.computed = proxyComputed = observeComputed(
    computed,
    vm._computedWatched,
    vm._proxyThis
  )
  for (let key in computed) {
    proxyForVm(vm._proxyThis, proxyComputed, key)
  }
}
複製代碼

接着將computed屬性自己設置爲響應式,同時調用createComputedGetter對屬性進行封裝。

當修改computed屬性時,computed觸發閉包變量dep.notify通知渲染更新。

當修改news屬性時,會觸發Vue進行渲染更新,在從新獲取computed屬性值的時候,會執行createComputedGetter封裝後的函數,其本質是執行上一步的getter函數,並將計算結果返回。

function observeComputed(obj: VueComputed, _computedWatched: any, proxyThis: any): Object {
  if (!isPlainObject(obj) || isProxy(obj)) return obj

  let proxyObj = createProxy(obj)

  for (let key in obj) {
    defineComputed(proxyObj, key, obj[key], _computedWatched[key], proxyThis)
  }

  return proxyObj
}

function defineComputed(
  obj: any,
  key: string,
  userDef: VueComputedMethod,
  watcher: any,
  proxyThis: any
): void {
  if (!isProxy(obj)) return

  let dep: Dep = new Dep()

  const handler: any = {}
  if (isFunction(userDef)) {
    handler.get = createComputedGetter(watcher)
    handler.set = noop
  } else if (isObject(userDef)) {
    handler.get = createComputedGetter(watcher)
    handler.set = userDef.set || noop
  }

  defineProxyObject(obj, key, {
    get(target, key) {
      Dep.Target && dep.depend()
      return handler.get.call(proxyThis)
    },
    set(target, key, newVal) {
      handler.set.call(proxyThis, newVal)
      dep.notify()
      return true
    }
  })
}

function createComputedGetter(watcher: Watch): Function {
  return function computedGetter() {
    if (watcher) {
      // 計算值
      watcher.evaluate()
      // 將computed-dep添加watch對象
      Dep.Target && watcher.depend()

      return watcher.value
    }
  }
}
複製代碼

分析computed確定要提到其緩存特性,這又是如何實現的?

咱們知道獲取computed的屬性值時,會執行createComputedGetter封裝後的函數,經過給Watch類添加dirty屬性控制是否從新計算computed的屬性值。Watch類的其餘函數中確定須要配合修改,如evaluateupdate方法。

function createComputedGetter(watcher: Watch): Function {
  return function computedGetter() {
    if (watcher) {
      // 計算值
      if (watcher.dirty) {
        watcher.evaluate()
      }
      // 將computed-dep添加watch對象
      Dep.Target && watcher.depend()

      return watcher.value
    }
  }
}
複製代碼

Vue的數據處理流程

Vue在實例化過程當中,會對傳入的數據進行初始化處理。

首先確定是爲prop、data建立閉包變量dep,接着纔是初始化computed和watch的屬性,在後者中建立Watch實例監聽屬性的變化。

initProps(this)
initMethods(this)
initData(this)
initComputed(this)
initWatch(this)
複製代碼

總結

Vue的響應式渲染依賴Dep和Watch,computed和watch功能也依賴它們,另外,Vue還封裝了方法$watch對屬性進行監聽。爲了支持上述功能,Watch和Dep添加了一些配置項,在理解源碼時,能夠進行必定忽略。

Dep和Watch設計的至關巧妙,咱們本身編程能不能想到這樣的方式?推薦學習下設計模式,或許能有所幫助。

系列文章

【Ts重構Vue】00-Ts重構Vue前言

【Ts重構Vue】01-如何建立虛擬節點

【Ts重構Vue】02-數據如何驅動視圖變化

【Ts重構Vue】03-如何給真實DOM設置樣式

【Ts重構Vue】04-異步渲染

【Ts重構Vue】05-實現computed和watch功能

相關文章
相關標籤/搜索