vue3的數據響應原理和實現

話說vue3已經發布,就引發了大量前端人員的關注,木得辦法,學不動也得硬着頭皮學呀,本篇文章就簡單介紹一下「vue3的數據響應原理」,以及簡單實現其reactiveeffectcomputed函數,但願能對你們理解vue3響應式有一點點的幫助。話很少說,看下面栗子的代碼和其運行的結果。html

<div id="root"></div>
<button id="btn">年齡+1</button>
const root = document.querySelector('#root')
const btn = document.querySelector('#btn')
const ob = reactive({
  name: '張三',
  age: 10
})

let cAge = computed(() => ob.age * 2)
effect(() => {
  root.innerHTML = `<h1>${ob.name}---${ob.age}---${cAge.value}</h1>`
})
btn.onclick = function () {
  ob.age += 1
}

上面帶代碼,是每點擊一次按鈕,就會給obj.age + 1 而後執行effect,計算屬性也會相應的 ob.age * 2 執行,以下圖:前端

vue3-project

因此,針對上面的栗子,制定一些小目標,而後一一實現,以下:vue

  • 一、實現reactive函數
  • 二、實現effect函數
  • 三、把reactive 和 effect 串聯起來
  • 四、實現computed函數

實現reactive函數

reactive其實數據響應式函數,其內部經過es6proxy api 來實現,
下面面其實經過簡單幾行代碼,就能夠對一個對象進行代理攔截了。react

const handlers = {
  get (target, key, receiver) {
    return Reflect.get(target, key, receiver)
  },
  set (target, key, value, receiver) {
    return Reflect.set(target, key, value, receiver)
  }
}
function reactive (target) {
  observed = new Proxy(target, handlers)
  return observed
}
let person = {
  name: '張三',
  age: 10
}

let ob = reactive(person)

可是這麼作的話有缺點,一、重複屢次寫ob = reactive(person)就會一直執行new Proxy,這不是咱們想要的。理想狀況應該是,代理過的對象緩存下來,下次訪問直接返回緩存對象就能夠了;二、同理屢次這麼寫ob = reactive(person); ob = reactive(ob) 那也要緩存下來。下面咱們改造一下上面的reactive函數代碼。git

const toProxy = new WeakMap() // 緩存代理過的對象
const toRaw = new WeakMap() // 緩存被代理過的對象
// handlers 跟上面的同樣,爲了篇幅這裏省略
function reactive (target) {
  let observed = toProxy.get(target)
  // 若是是緩存代理過的
  if (observed) {
    return observed
  }
  if (toRaw.has(target)) {
    return target
  }
  observed = new Proxy(target, handlers)
  toProxy.set(target, observed) // 緩存observed
  toRaw.set(observed, target) // 緩存target
  return observed
}

let person = {
  name: '張三',
  age: 10
}

let ob = reactive(person)
ob = reactive(person) // 返回都是緩存的
ob = reactive(ob) // 返回都是緩存的

console.log(ob.age) // 10
ob.age = 20
console.log(ob.age) // 20

這樣子調用reactive()返回都是咱們第一次的代理對象啦(ps:WeakMap是弱引用)。緩存作好了,可是還有新的問題,若是代理target對象層級嵌套比較深的話,上面的proxy是作不到深層代理的。例如es6

let person = {
  name: '張三',
  age: 10,
  hobby: {
    paly: ['basketball', 'football']
  }
}
let ob = reactive(person)
console.log(ob)

vue3-proxy

從上面的打印結果能夠看出hobby 對象沒有咱們上面的handlers 代理,也就是說當咱們對hobby作一些依賴收集的時候是沒有辦法的,因此咱們改寫一下handlers對象。github

// 對象類型判斷
const isObject = val => val !== null && typeof val === 'object'
const toProxy = new WeakMap() // 緩存代理過的對象
const toRaw = new WeakMap() // 緩存被代理過的對象
const handlers = {
  get (target, key, receiver) {
    const res = Reflect.get(target, key, receiver)
    // TODO: effect 收集
    return isObject(res) ? reactive(res) : res
  },
  set (target, key, value, receiver) {
    const result = Reflect.set(target, key, value, receiver)
    // TODO: trigger effect
    return result
  }
}
function reactive (target) {
  let observed = toProxy.get(target)
  // 若是是緩存代理過的
  if (observed) {
    return observed
  }
  if (toRaw.has(target)) {
    return target
  }
  observed = new Proxy(target, handlers)
  toProxy.set(target, observed) // 緩存observed
  toRaw.set(observed, target) // 緩存target
  return observed
}

上面的代碼經過在get裏面添加 return isObject(res) ? reactive(res) : res,意思是當訪問到某一個對象時候,若是判斷類型是「object」,那麼就繼續調用reactive代理。上面也是咱們的reactive函數的完整代碼。api

實現effect函數

到了這裏離咱們的目標又近了一步,這裏來實現effect函數,首先咱們先看看effect的用法。數組

effect(() => {
  root.innerHTML = `<h1>${ob.name}---${ob.age}---${cAge.value}</h1>`
})

第一感受看起來很簡單嘛,就是函數當作參數傳進去,而後調用傳進來函數,完事。下面代碼最簡單實現緩存

function effect(fn) {
  fn()
}

可是到這裏,全部人都看出來缺點了,這只是執行一次呀?怎麼跟響應式聯繫起來呀?還有後面computed怎麼基於這個實現呀?等等。帶着一大堆問題,經過改寫effect和增長effect功能去解決這一系列問題。

function effect (fn, options = {}) {
  const effect = createReactiveEffect(fn, options)
  // 不是理解計算的,不須要調用此時調用effect
  if (!options.lazy) {
    effect()
  }
  return effect
}
function createReactiveEffect(fn, options) {
  const effect = function effect(...args) {
    return run(effect, fn, args) // 裏面執行fn
  }
  // 給effect掛在一些屬性
  effect.lazy = options.lazy
  effect.computed = options.computed
  effect.deps = []
  return effect
}

createReactiveEffect函數中:建立一個新的effect函數,而且給這個effect函數掛在一些屬性,爲後面作computed準備,這個effect函數裏面調用run函數(此時尚未實現), 最後在返回出新的effect

effect函數中:若是判斷options.lazyfalse就調用上面建立一個新的effect函數,裏面會調用run函數。

把reactive 和 effect 串聯起來

其實上面尚未寫好的這個run函數的做用,就是把reactive effect 的邏輯串聯起來,下面去實現它,目標又近了一步。

const activeEffectStack = [] // 聲明一個數組,來存儲當前的effect,訂閱時候須要
function run (effect, fn, args) {
  if (activeEffectStack.indexOf(effect) === -1) {
    try {
      // 把effect push到數組中
      activeEffectStack.push(effect)
      return fn(...args)
    }
    finally {
      // 清除已經收集過得effect,爲下個effect作準備
      activeEffectStack.pop()
    }
  }
}

上面的代碼,把傳進來的effect推送到一個activeEffectStack數組中,而後執行傳進來的fn(...args),這裏的fn就是

fn = () => {
  root.innerHTML = `<h1>${ob.name}---${ob.age}---${cAge.value}</h1>`
}

執行上面的fn訪問到ob.nameob.agecAge.value(這是computed得來的),這樣子就會觸發到proxygetter,就是執行到下面的handlers.get函數

const handlers = {
  get (target, key, receiver) {
    const res = Reflect.get(target, key, receiver)
    // effect 收集
    track(target, key)
    return isObject(res) ? reactive(res) : res
  },
  set (target, key, value, receiver) {
    const result = Reflect.set(target, key, value, receiver)
    const extraInfo = { oldValue: target[key], newValue: value }
    // trigger effect
    trigger(target, key, extraInfo)
    return result
  }
}

聰明的小夥伴看到這裏已經看出來,上面handlers.get函數裏面track的做用是依賴收集,而handlers.set裏面trigger是作派發更新的。
下面補全track函數代碼

// 存儲effect
const targetMap = new WeakMap()
function track (target, key) {
  // 拿到上面push進來的effect
  const effect = activeEffectStack[activeEffectStack.length - 1]
  if (effect) {
    let depsMap = targetMap.get(target)
    if (depsMap === void 0) {
      depsMap = new Map()
      // targetMap若是不存在target 的 Map 就設置一個
      targetMap.set(target, depsMap)
    }
    let dep = depsMap.get(key)
    if (dep === void 0) {
      dep = new Set()
      // 若是depsMap裏面不存在key 的 Set 就設置一個
      depsMap.set(key, dep)
    }
    if (!dep.has(effect)) {
      // 收集當前的effect
      dep.add(effect)
      // effect 收集當前的dep
      effect.deps.push(dep)
    }
  }
}

看到這裏呀,你們別方,上面的代碼意思就是,從run函數裏面的activeEffectStack拿到當前的effect,若是有effect,就從targetMap裏面拿depsMaptargetMap若是不存在targetMap 就設置一個targetMap.set(target, depsMap),再從depsMap 裏面拿 keySet ,若是depsMap裏面不存在 keySet 就設置一個depsMap.set(key, dep),下面就是收集前的effecteffect 收集當前的dep了。收集完畢後,targetMap的數據結構就相似下面的樣子的了。

// track的做用就是完成下面的數據結構
targetMap = {
  target: {
    name: [effect],
    age: [effect]
  }
}
// ps: targetMap 是WeakMap 數據結構,爲了直觀和理解就用對象表示
//     [effect] 是 Set數據結構,爲了直觀和理解就用數組表示

track執行完畢以後,handlers.get就會返回 res,進行一系列收集以後,fn執行完畢,run函數最後就執行finally {activeEffectStack.pop()},由於effect已經收集結束了,清空爲了下一個effect收集作處理。

依賴收集已經完畢了,可是當咱們更新數據的時候,例如ob.age += 1,更改數據會觸發proxygetter,也就是會調用handlers.set函數,裏面就執行了trigger(target, key, extraInfo)trigger函數以下

// effect 的觸發
function trigger(target, key, extraInfo) {
  // 拿到全部target的訂閱
  const depsMap = targetMap.get(target)
  // 沒有被訂閱到
  if (depsMap === void 0) {
    return;
  }
  const effects = new Set() // 普通的effect
  const computedRunners = new Set() // computed 的 effect
  if (key !== void 0) {
    let deps = depsMap.get(key)
    // 拿到deps訂閱的每一個effect,而後放到對應的Set裏面
    deps.forEach(effect => {
      if (effect.computed) {
        computedRunners.add(effect)
      } else {
        effects.add(effect)
      }
    })
  }
  const run = effect => {
    effect()
  }
  // 循環調用effect
  computedRunners.forEach(run)
  effects.forEach(run)
}

上面的代碼的意思是,拿到對應keyeffect,而後執行effect,而後執行run,而後執行fn,而後就是get上面那一套流程了,最後拿到數據是更改後新的數據,而後更改視圖。

下面簡單弄一個幫助理解的流程圖,實在不能理解,你們把倉庫代碼拉下來,debuger執行一遍

targetMap = {
  name: [effect],
  age: [effect]
}
ob.age += 1 -> set() -> trigger() -> age: [effect] -> effect() -> run() -> fn() -> getget() -> 渲染視圖

實現computed函數

仍是先看用法,let cAge = computed(() => ob.age * 2),上面寫effect的時候,有不少次提到爲computed作準備,其實computed就是基於effect來實現的,下面咱們看代碼

function computed(fn) {
  const getter = fn
  // 手動生成一個effect,設置參數
  const runner = effect(getter, { computed: true, lazy: true })
  // 返回一個對象
  return {
    effect: runner,
    get value() {
      value = runner()
      return value
    }
  }
}

值得注意的是,咱們上面 effet函數裏面有個判斷

if (!options.lazy) {
  effect()
}

若是options.lazytrue就不會馬上執行,就至關於let cAge = computed(() => ob.age * 2)不會馬上執行runner函數,當cAge.value才真正的執行。

最後,全部的函數畫成一張流程圖。

若是文章有哪些不對,請各位大佬指出來,我有摸魚時間必定會修正過來的。

vue3-reactive

至此,全部的的小目標咱們都已經完成了,撒花✿✿ヽ(°▽°)ノ✿

ps:

源碼地址(你們能夠clone下來執行一遍)

博客文章地址(這裏有新的閱讀體驗,也有微信,歡迎來撩)

相關文章
相關標籤/搜索