Vue.js設計與實現學習總結(第四章6)計算屬性

瑪拉-以琳發表於2022-12-23

目前的副作用函式effect是立即執行的:

effect(() => {
  console.log(obj.foo)
})

在某些場景下並不希望effect立即執行, 因此就可以新增options新增屬性:

effect(() => {
  console.log(obj.foo)
},
// options
{
  lazy: true
})

這裡的lazy就是前面文章介紹的排程, 當options.lazytrue時不立即執行副作用函式:

function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn
    effectStack.push(effectFn)
    fn()
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
  }
  effectFn.options = options
  options.deps = []
  // 只有非懶載入時才執行副作用函式
  if (!options.lazy) effectFn()
  // 將副作用函式作為返回值返回
  return effectFn
}

由於最後一行將副作用函式暴露在了函式外部因此可以手動執行改副作用函式:

const effectFn = effect(() => {
  console.log(obj.foo)
},
// options
{
  lazy: true
})
// 手動執行
effectFn()

僅僅是這樣的意義其實不太大, 但手動執行後如果可以拿到傳入effect函式(fn)的返回值要好的多:

const effectFn = effect(() => obj.foo + obj.bar,
// options
{
  lazy: true
})

// val 是 傳函式的返回值
const val = effectFn()

因此需要對effect函式進行修改:

function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn
    effectStack.push(effectFn)
    // 將fn的結果返回到res中
    const res = fn()
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
    // 將 res 作為 effectFn 的返回值
    return res
  }
  effectFn.options = options
  effectFn.deps = []
  // 只有非懶載入時才執行副作用函式
  if (!options.lazy) effectFn()
  // 將副作用函式作為返回值返回
  return effectFn
}

其實傳遞給effectfn才是真正的副作用函式, 而effectFn是對fn的再包裝, 也正因此effectFn執行後也應該返回fn得出來的值也就新增了const res = fn()return res

說句題外話, 根據前一段時間發的文章<閉包淺談>, 在這裡面新增的res變數是閉包哦, fn也正好是回撥函式, 當然effectFn也是閉包

現在實現了懶執行的副作用函式並且可以拿到副作用函式的執行結果, 可實現計算屬性了:

function computed (getter) {
  // 將 getter 作為副作用函式
  const effectFn = effect(getter, { lazy: true })

  const obj = {
    // 當讀取 value 時才執行 effectFn
    get value () {
      return effectFn()
    }
  }

  return obj
}

現在可以使用computed函式建立一個計算屬性:

const data = { foo: 1, bar: 2 }
const obj = new Proxy(data, { /* ... */ })

const sumRes = computed(() => obj.foo + obj.bar)

console.log(sumRes.value)

到目前完整的程式碼是:

// 儲存副作用函式的桶
const bucket = new WeakMap()

// 用於儲存被註冊的副作用的函式
let activeEffect = undefined
// 副作用函式棧
const effectStack = []

function cleanup (effectFn) {
  for (let itme of effectFn.deps) {
    itme.delete(effectFn)
  }
  effectFn.deps.length = []
}

function effect (fn, options = {}) {
  const effectFn = () => {
    console.log('effect');
    cleanup(effectFn)
    activeEffect = effectFn
    effectStack.push(effectFn)
    // 將fn的結果返回到res中
    const res = fn()
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
    // 將 res 作為 effectFn 的返回值
    return res
  }
  effectFn.options = options
  effectFn.deps = []
  // 只有非懶載入時才執行副作用函式
  if (!options.lazy) effectFn()
  // 將副作用函式作為返回值返回
  return effectFn
}


// const data = {
//   text: 'hello world',
//   ok: true
// }
const data = { foo: 1, bar: 2 }



const obj = new Proxy(data, {
  // 攔截讀取操作
  get (target, key) {
    track(target, key)
    // 返回屬性值
    return target[key]
  },

  // 攔截設定操作
  set (target, key, newVal) {
    // 設定屬性值
    target[key] = newVal
    trigger(target, key)
  }
})

function track (target, key) {
  // 沒有 activeEffect, 直接 return
  if (!activeEffect) return target[key]

  // 根據 target 從'桶'中回去 depsMap, 它也是一個 Map 型別: key ---> effects
  let depsMap = bucket.get(target)
  // 如果 depsMap 不存在, 則新建一個 Map 並與 target 關聯
  if (!depsMap) bucket.set(target, (depsMap = new Map()))
  // 再根據 key 從depsMap 中去的 deps, 它是一個 Set 型別
  // 裡面存貯所有與當前 key 相關的副作用函式: effects
  let deps = depsMap.get(key)
  // 如果 deps 不存在, 同樣新建一個 Set 並與 key 關聯0
  if (!deps) depsMap.set(key, (deps = new Set()))
  // 最後將當前啟用的副作用函式新增到'桶'裡
  deps.add(activeEffect)
}

function trigger (target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)

  const effectsToRun = new Set()
  effects && effects.forEach(effectFn => {
    // 如果 trigger 觸發執行的副作用函式與當前正在執行的函式相同則不觸發執行
    if (effectFn !== activeEffect) {
      effectsToRun.add(effectFn)
    }
  })

  effectsToRun.forEach(effectFn => {
    // 如果一個副作用函式有排程器則呼叫改排程器, 並將副作用函式作為引數傳遞
    if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn)
    } else {
      // 否則直接執行副作用函式
      effectFn()
    }
  })
}

function computed (getter) {
  // 將 getter 作為副作用函式
  const effectFn = effect(getter, { lazy: true })

  const obj = {
    // 當讀取 value 時才執行 effectFn
    get value () {
      return effectFn()
    }
  }

  return obj
}

const sumRes = computed(() => obj.foo + obj.bar)
console.log(sumRes.value) // 3
console.log(sumRes.value) // 3
console.log(sumRes.value) // 3
console.log(sumRes.value) // 3

在函式effectFn中列印了字串'effect'會發現sumRes.value取了四次, 函式effectFn就執行了四次:
image.png
因此在computed需要新增值得快取:

function computed(getter) {
  // value 用來快取上一次計算的值
  let value
  // dirty 標誌, 用來標識是否需要重新計算, 為 true 標識需要計算
  let dirty = true

  const effectFn = effect(getter, { lazy: true })

  const obj = {
    get value () {
      if (dirty) {
        value = effectFn()
        // 將 dirty 設定為 false, 下一次直接訪問 value 中儲存的值
        dirty = false
      }
      return value
    }
  }

  return obj
}

此時確實只會計算一次, 且每次訪問不會重新執行副作用函式, 但是相信聰明如你已經發現問題了, 如果我們改變obj中的值後在訪問sumRes.value會發現訪問的值沒有變化, 這裡就不做演示了. 解決方法就是當其中的某一個值放生改變時將dirty重新設為true就可以了, 這時我們可以新增排程器(請參看: 執行排程):

function computed(getter) {
  // value 用來快取上一次計算的值
  let value
  // dirty 標誌, 用來標識是否需要重新計算, 為 true 標識需要計算
  let dirty = true

  const effectFn = effect(getter, {
    lazy: true,
    // 新增排程器, 將 dirty 重置
    scheduler () {
      dirty = true
    }
  })

  const obj = {
    get value () {
      if (dirty) {
        value = effectFn()
        // 將 dirty 設定為 false, 下一次直接訪問 value 中儲存的值
        dirty = false
      }
      return value
    }
  }

  return obj
}

現在基本完美了, 只是在某些情況下出現一個缺陷:

const sumRes = computed(() => obj.foo + obj.bar)
effect(() => {
  // 在副作用函式中讀取計算屬性
  console.log(sumRes.value) // 3
})

obj.bar++

obj.bar++時期望是觸發計算屬性, 重新渲染, 但實際上並沒有, 原因是因為計算屬性是有自己的effect並且是懶執行的, 只有真正在讀取計算屬性的值才會執行. 對於計算屬性的getter函式, 它裡面訪問的響應資料只會把計算屬性函式內部的effect收集為依賴, 而在上面的例子中把計算屬性用於另一個effect時就發生了effect巢狀, 且外層的effect不會被內層的effect中的響應式資料收集

computed函式中我們也可以看到裡面重新頂一個了一個物件obj並手動賦予它get函式, 並沒有像這樣:

const data = { foo: 1, bar: 2 }



const obj = new Proxy(data, {
  // 攔截讀取操作
  get (target, key) {
    track(target, key)
    // 返回屬性值
    return target[key]
  },

  // 攔截設定操作
  set (target, key, newVal) {
    // 設定屬性值
    target[key] = newVal
    trigger(target, key)
  }
})

使用代理, 兩個其實是完全分開的, 只是使用了同一個副作用函式
解決方法很簡單既然計算屬性沒有繫結跟蹤那就手動呼叫:

function computed(getter) {
  // value 用來快取上一次計算的值
  let value
  // dirty 標誌, 用來標識是否需要重新計算, 為 true 標識需要計算
  let dirty = true

  const effectFn = effect(getter, {
    lazy: true,
    // 新增排程器, 將 dirty 重置
    scheduler () {
      dirty = true
      // 當計算屬性依賴的響應式資料發生變化時, 手動觸發響應
      trigger(obj, 'value')
    }
  })

  const obj = {
    get value () {
      if (dirty) {
        value = effectFn()
        // 將 dirty 設定為 false, 下一次直接訪問 value 中儲存的值
        dirty = false
      }
      // 當讀取 value 時, 手動呼叫 track 函式進行跟蹤
      track(obj, 'value')
      return value
    }
  }

  return obj
}

到此就完成了!

裡面有許多的閉包...., 及其常見的閉包

目前的完整程式碼為:

// 儲存副作用函式的桶
const bucket = new WeakMap()

// 用於儲存被註冊的副作用的函式
let activeEffect = undefined
// 副作用函式棧
const effectStack = []

function cleanup (effectFn) {
  for (let itme of effectFn.deps) {
    itme.delete(effectFn)
  }
  effectFn.deps.length = []
}

function effect (fn, options = {}) {
  const effectFn = () => {
    console.log('effect');
    cleanup(effectFn)
    activeEffect = effectFn
    effectStack.push(effectFn)
    // 將fn的結果返回到res中
    const res = fn()
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
    // 將 res 作為 effectFn 的返回值
    return res
  }
  effectFn.options = options
  effectFn.deps = []
  // 只有非懶載入時才執行副作用函式
  if (!options.lazy) effectFn()
  // 將副作用函式作為返回值返回
  return effectFn
}


// const data = {
//   text: 'hello world',
//   ok: true
// }
const data = { foo: 1, bar: 2 }



const obj = new Proxy(data, {
  // 攔截讀取操作
  get (target, key) {
    track(target, key)
    // 返回屬性值
    return target[key]
  },

  // 攔截設定操作
  set (target, key, newVal) {
    // 設定屬性值
    target[key] = newVal
    trigger(target, key)
  }
})

function track (target, key) {
  // 沒有 activeEffect, 直接 return
  if (!activeEffect) return target[key]

  // 根據 target 從'桶'中回去 depsMap, 它也是一個 Map 型別: key ---> effects
  let depsMap = bucket.get(target)
  // 如果 depsMap 不存在, 則新建一個 Map 並與 target 關聯
  if (!depsMap) bucket.set(target, (depsMap = new Map()))
  // 再根據 key 從depsMap 中去的 deps, 它是一個 Set 型別
  // 裡面存貯所有與當前 key 相關的副作用函式: effects
  let deps = depsMap.get(key)
  // 如果 deps 不存在, 同樣新建一個 Set 並與 key 關聯0
  if (!deps) depsMap.set(key, (deps = new Set()))
  // 最後將當前啟用的副作用函式新增到'桶'裡
  deps.add(activeEffect)
}

function trigger (target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)

  const effectsToRun = new Set()
  effects && effects.forEach(effectFn => {
    // 如果 trigger 觸發執行的副作用函式與當前正在執行的函式相同則不觸發執行
    if (effectFn !== activeEffect) {
      effectsToRun.add(effectFn)
    }
  })

  effectsToRun.forEach(effectFn => {
    // 如果一個副作用函式有排程器則呼叫改排程器, 並將副作用函式作為引數傳遞
    if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn)
    } else {
      // 否則直接執行副作用函式
      effectFn()
    }
  })
}

function computed(getter) {
  // value 用來快取上一次計算的值
  let value
  // dirty 標誌, 用來標識是否需要重新計算, 為 true 標識需要計算
  let dirty = true

  const effectFn = effect(getter, {
    lazy: true,
    // 新增排程器, 將 dirty 重置
    scheduler () {
      dirty = true
      // 當計算屬性依賴的響應式資料發生變化時, 手動觸發響應
      trigger(obj, 'value')
    }
  })

  const obj = {
    get value () {
      if (dirty) {
        value = effectFn()
        // 將 dirty 設定為 false, 下一次直接訪問 value 中儲存的值
        dirty = false
      }
      // 當讀取 value 時, 手動呼叫 track 函式進行跟蹤
      track(obj, 'value')
      return value
    }
  }

  return obj
}

const sumRes = computed(() => obj.foo + obj.bar)
effect(() => {
  // 在副作用函式中讀取計算屬性
  console.log(sumRes.value) // 3
})

obj.bar++

// effect(() => {
//   console.log('effect run');
//   document.body.innerText = obj.ok ? obj.text : 'not'
// })




// setTimeout(() => {
//   obj.ok = false
// }, 2000)

相關文章