vue原始碼中computed和watch的解讀

只做你的向日葵發表於2022-01-01

computed

  • 會基於其內部的 響應式依賴 進行快取
  • 只在相關 響應式依賴發生改變 時 它們才會重新求值。
  • 可以在將模板中使用的常量放在計算屬性中。

watch

  • 監聽資料變化,並在監聽回撥函式中返回資料變更前後的兩個值。
  • 用於在資料變化後執行 非同步操作 或者開銷較大的操作。

watchEffect

在 composition API中 watchEffect會在它所依賴的資料發生改變時立即執行,並且執行結果會返回一個函式,我們稱它為stop函式

,可以用於停止監聽資料變化,下面是示例程式碼演示:

const count = ref(0)

// -> log 0
const stop = watchEffect(() => {
	console.log(count.value)
})

setTimeout(()=>{
	// -> log 1
	count.value++
},100)

// -> later
stop()

下面我們來實現以上介紹的幾個composition API

  1. computed -> let x = computed(()=> count.value + 3);
  2. watch -> watch(()=> count.value, (curVal, preVal) => {}, { deep, immediate })
  3. watchEffect -> let stop = watchEffect(()=> count.value + 3)

computed 

核心思路是

// 簡單定義

let computed = (fn) => {
    let value;
    return {
      get value() {
        return value
      }
    }
  }

// 呼叫

 let computedValue = computed(() => count.value + 3)
 
 // 監聽
 watchEffect(() => {
    document.getElementById('computed').innerText = computedValue.value
  });

 

下面我們在此基礎之上實現依賴更新的操作

let computed = (fn) => {
  let value;
  return {
    get value() {
      // 5手動執行一次依賴
      value = fn()
      return value
    }
  }
}
let count = ref(1);
let computedValue = computed(() => count.value + 3)

function add() {
  document.getElementById('add').addEventListener('click',()=>{
    count.value++
  })
}

add()

watchEffect(() => {
  document.getElementById('text').innerText = count.value
  document.getElementById('computed').innerText = computedValue.value
});

依賴快取計算

 呈上頁面 -html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vue3 - computed</title>
  </head>
  <body>
    <div id="app">
      result:
      <span id="text">0</span>
      <br />
      computed:
      <span id="computed">0</span>
    </div>
    <button id="add">add</button>
  </body>
 
</html>

包含了computed的實現的完整js程式碼。

;(function () {
  let active
  /*
       * @params fn -> 要執行的函式
       * @params option -> 可選引數
       * @return effect -> 執行watchEffect
       */
  let effect = (fn, options = {}) => {
    let effect = (...args) => {
      try {
        active = effect
        // 避免了死迴圈
        return fn(...args)
      } finally {
        active = null
      }
    }

    // 更新資料時也需要讓schedular執行
    effect.options = options

    return effect
  }

  let watchEffect = function (cb) {
    let runner = effect(cb)
    runner()
  }
  // 需要有個佇列來儲存各項任務
  let queue = []
  // 通過微任務方式去執行佇列中的任務
  let nextTick = (cb) => Promise.resolve().then(cb)
  // 將任務新增到佇列
  let queueJob = (job) => {
    if (!queue.includes(job)) {
      queue.push(job)
      nextTick(flushJobs)
    }
  }

  // 執行佇列中的任務
  let flushJobs = () => {
    let job
    while ((job = queue.shift()) !== undefined) {
      job()
    }
  }

  // 收集更多依賴
  class Dep {
    // 依賴收集,將響應依賴新增到deps中
    constructor() {
      this.deps = new Set()
    }

    depend() {
      if (active) {
        this.deps.add(active)
      }
    }
    // 通知所有依賴更新
    notify() {
      // 將任務加到佇列中
      this.deps.forEach((dep) => {
        dep.options && dep.options.schedular && dep.options.schedular()
        queueJob(dep)
      })
    }
  }

  let ref = (initValue) => {
    let value = initValue
    let dep = new Dep()

    return Object.defineProperty({}, 'value', {
      get() {
        dep.depend()
        return value
      },
      set(newValue) {
        value = newValue
        dep.notify()
      }
    })
  }

  let computed = (fn) => {
    let value
    let dirty = true

    let runner = effect(fn, {
      // 通過鉤子函式處理dirty引數
      schedular: () => {
        if (!dirty) {
          dirty = true
        }
      }
    })
    return {
      get value() {
        if (dirty) {
          value = runner()
          // 快取標識
          dirty = false
          // 這裡在dirty改變為false之後需要在依賴發生變化時候重置為true,
        }
        return value
      }
    }
  }

  let count = ref(1)
  // 同93 資料發生更新時讓dirty 重置
  let computedValue = computed(() => count.value + 3)

  function add() {
    document.getElementById('add').addEventListener('click', () => {
      count.value++
    })
  }

  add()

  watchEffect(() => {
    document.getElementById('text').innerText = count.value
    document.getElementById('computed').innerText = computedValue.value
  })
})()

watch

// watch(()=> count.value, (curVal, preVal) => {}, { deep, immediate })

;(function () {
      let active
      /*
       * @params fn -> 要執行的函式
       * @params option -> 可選引數
       * @return effect -> 執行watchEffect
       */
      let effect = (fn, options = {}) => {
        let effect = (...args) => {
          try {
            active = effect
            // 避免了死迴圈
            return fn(...args)
          } finally {
            active = null
          }
        }

        // 更新資料時也需要讓schedular執行
        effect.options = options

        return effect
      }

      let watchEffect = function (cb) {
        let runner = effect(cb)
        runner()
      }
      // 需要有個佇列來儲存各項任務
      let queue = []
      // 通過微任務方式去執行佇列中的任務
      let nextTick = (cb) => Promise.resolve().then(cb)
      // 將任務新增到佇列
      let queueJob = (job) => {
        if (!queue.includes(job)) {
          queue.push(job)
          nextTick(flushJobs)
        }
      }

      // 執行佇列中的任務
      let flushJobs = () => {
        let job
        while ((job = queue.shift()) !== undefined) {
          job()
        }
      }

      // 收集更多依賴
      class Dep {
        // 依賴收集,將響應依賴新增到deps中
        constructor() {
          this.deps = new Set()
        }

        depend() {
          if (active) {
            this.deps.add(active)
          }
        }
        // 通知所有依賴更新
        notify() {
          // 將任務加到佇列中
          this.deps.forEach((dep) => {
            dep.options && dep.options.schedular && dep.options.schedular()
            queueJob(dep)
          })
        }
      }

      let ref = (initValue) => {
        let value = initValue
        let dep = new Dep()

        return Object.defineProperty({}, 'value', {
          get() {
            dep.depend()
            return value
          },
          set(newValue) {
            value = newValue
            dep.notify()
          }
        })
      }

      let watch = (source, cb, options = {}) => {
        const { immediate } = options
        const getter = () => {
          return source()
        }
        let oldValue
        const runner = effect(getter, {
          schedular: () => applyCbk()
        })

        const applyCbk = () => {
          let newValue = runner()
          if (newValue !== oldValue) {
            cb(newValue, oldValue)
            oldValue = newValue
          }
        }

        // 有預設值時執行回撥
        if (immediate) {
          applyCbk()
        } else {
          oldValue = runner()
        }
      }

      let count = ref(1)

      function add() {
        document.getElementById('add').addEventListener('click', () => {
          count.value++
        })
      }

      add()

      watch(
        () => count.value,
        (newValue, oldValue) => {
          console.log(newValue, oldValue)
        },
        { immediate: true }
      )
    })()

引數1響應式更新,引數2使用schedular執行回撥,引數3 如果存在時就預設執行回撥2

vue原始碼中computed和watch的解讀

watchEffect

  • stop方法的實現
  • 陣列API響應式執行依賴更新
  • Vue.set的實現,陣列索引加入代理中
// let stop = watchEffect(()=> count.value + 3)

;(function () {
  let active
  /*
       * @params fn -> 要執行的函式
       * @params option -> 可選引數
       * @return effect -> 執行watchEffect
       */
  let effect = (fn, options = {}) => {
    // 包裹一次effect 避免對fn的汙染,保證fn純淨
    let effect = (...args) => {
      try {
        active = effect
        // 避免了死迴圈
        return fn(...args)
      } finally {
        active = null
      }
    }

    // 更新資料時也需要讓schedular執行
    effect.options = options
    // 用於反向查詢
    effect.deps = [];

    return effect
  }

  let cleanUpEffect = (effect) => {
    const { deps } = effect;
    deps.forEach(dep => dep.delete(effect))
  }

  let watchEffect = function (cb) {
    let runner = effect(cb)
    runner()
    // 返回一個stop函式,清楚當前的監聽
    return () => {
      cleanUpEffect(runner)
    }
  }
  // 需要有個佇列來儲存各項任務
  let queue = []
  // 通過微任務方式去執行佇列中的任務
  let nextTick = (cb) => Promise.resolve().then(cb)
  // 將任務新增到佇列
  let queueJob = (job) => {
    if (!queue.includes(job)) {
      queue.push(job)
      nextTick(flushJobs)
    }
  }

  // 執行佇列中的任務
  let flushJobs = () => {
    let job
    while ((job = queue.shift()) !== undefined) {
      job()
    }
  }

  // 收集更多依賴
  class Dep {
    // 依賴收集,將響應依賴新增到deps中
    constructor() {
      this.deps = new Set()
    }

    depend() {
      if (active) {
        this.deps.add(active)
        // 新增依賴時追加當前的deps, 實現雙向互通。雙向索引
        active.deps.push(this.deps)
      }
    }
    // 通知所有依賴更新
    notify() {
      // 將任務加到佇列中
      this.deps.forEach((dep) => {
        dep.options && dep.options.schedular && dep.options.schedular()
        queueJob(dep)
      })
    }
  }

  let ref = (initValue) => {
    let value = initValue
    let dep = new Dep()

    return Object.defineProperty({}, 'value', {
      get() {
        dep.depend()
        return value
      },
      set(newValue) {
        value = newValue
        dep.notify()
      }
    })
  }

  let count = ref(1)

  function add() {
    document.getElementById('add').addEventListener('click', () => {
      count.value++
    })
  }

  add()

  let stop = watchEffect(() => {
    document.getElementById('text').innerText = count.value
  })

  setTimeout(() => {
    stop();
  }, 3000);

})()

免責宣告

本文是通過對vue響應式computed計算屬性,watch, watchEffect原始碼學習的一些筆記分享,會涉及到一些引用,出處不詳,如商業用途謹慎轉載。

相關文章