1. 程式人生 > 實用技巧 >vue3的資料響應原理和實現

vue3的資料響應原理和實現

話說vue3已經發布,就引起了大量前端人員的關注,木得辦法,學不動也得硬著頭皮學呀,本篇文章就簡單介紹一下「vue3的資料響應原理」,以及簡單實現其reactive、effect、computed函式,希望能對大家理解vue3響應式有一點點的幫助。話不多說,看下面栗子的程式碼和其執行的結果。

<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執行。

所以,針對上面的栗子,制定一些小目標,然後一一實現,如下:

  • 1、實現reactive函式
  • 2、實現effect函式
  • 3、把reactive 和 effect 串聯起來
  • 4、實現computed函式

實現reactive函式

reactive其實資料響應式函式,其內部通過es6的proxy api來實現,
下面面其實通過簡單幾行程式碼,就可以對一個物件進行代理攔截了。

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)

但是這麼做的話有缺點,1、重複多次寫ob = reactive(person)就會一直執行new Proxy,這不是我們想要的。理想情況應該是,代理過的物件快取下來,下次訪問直接返回快取物件就可以了;2、同理多次這麼寫ob = reactive(person); ob = reactive(ob)那也要快取下來。下面我們改造一下上面的reactive函式程式碼。

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是做不到深層代理的。例如

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

從上面的列印結果可以看出hobby物件沒有我們上面的handlers代理,也就是說當我們對hobby做一些依賴收集的時候是沒有辦法的,所以我們改寫一下handlers物件。

// 物件型別判斷
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函式的完整程式碼。

實現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.lazy是false就呼叫上面建立一個新的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.name、ob.age、cAge.value(這是computed得來的),這樣子就會觸發到proxy的getter,就是執行到下面的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裡面拿depsMap,targetMap如果不存在target的Map就設定一個targetMap.set(target, depsMap),再從depsMap裡面拿key的Set,如果depsMap裡面不存在key的Set就設定一個depsMap.set(key, dep),下面就是收集前的effect和effect收集當前的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,更改資料會觸發proxy的getter,也就是會呼叫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)
}

上面的程式碼的意思是,拿到對應key的effect,然後執行effect,然後執行run,然後執行fn,然後就是get上面那一套流程了,最後拿到資料是更改後新的資料,然後更改檢視。

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

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

PPT模板下載大全https://www.wode007.com

實現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.lazy為true就不會立刻執行,就相當於let cAge = computed(() => ob.age * 2)不會立刻執行runner函式,當cAge.value才真正的執行。

最後,所有的函式畫成一張流程圖。

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

至此,所有的的小目標我們都已經完成了