Vue學習系列之1、響應式

前言

這是一個Vue源碼學習系列。打算開個手寫Vue的坑,但願能在寫代碼的同時能把其中的細節講清楚,最終目的是實現一個簡版的vue。不知道本身能寫到哪一步,總之盡力而爲。若是能完成的話,應該是自我超越了和無限的自信了。放個倉庫的 傳送門javascript

1、觀察者模式

場景:最近由於快到暑假了,產品韓梅梅提早一個月在大象拉了一個需求羣,把研發李雷、小明拉進了羣。她跟兩位研發說,「咱們過幾天要開發一個新需求,須要兩位研發的支持,是關於暑假歡樂谷門票活動的需求,等產品邏輯梳理完,我們就進入開發。」。一個禮拜以後,韓梅梅在羣裏發出了需求文檔,兩位研發開始加班加點幹活,需求完美上線。html

頗費特~ 好的,觀察者模式講完了。vue

納尼?等等...等等...,您這是講了個啥。java

咳咳...很差意思,從新來。react

咱們看一下上面這個場景,它分了幾步git

  1. 產品韓梅梅通知兩位研發過幾天會有個需求讓他們進行開發
  2. 幾天後,韓梅梅通知研發要開始開發需求了
  3. 研發開始進行開發

總結下來,咱們發現有兩個角色,一個發佈者(產品韓梅梅),一個觀察者(李雷等研發),當發佈者的狀態更新後,會進行通知觀察者,觀察者開始執行對應的動做。github

ok,讓咱們試着寫一下面試

class Dep {
    constructor(state) {
        this.watchers = []
        this.state = state
    }
    // 添加觀察者(研發)
    add(watcher) {
  	!this.watchers.includes(watcher) && this.watchers.push(watcher)
    }
    // 移除觀察者(研發)
    remove(watcher) {
        let index = this.watcher.indexOf(watcher)
        if (~index) this.watcher.splice(index, 1)
    }
    // 狀態更新, 通知所有觀察者
    notify() {
        for (let watcher of this.watchers) watcher.update(this)
    }
}

class Watcher {
    constructor(value) {
        this.value = value
    }
    // 更新
    update() {
        console.log('開始開發!')
    }
}


const HanMeiMei = new Dep()
const XiaoMing = new Watcher()
const LiLei = new Watcher()

// 拉羣!
HanMeiMei.add(XiaoMing)
HanMeiMei.add(LiLei)

await new Promise(resolve => setTimeout(resolve, 7 * 24 * 60 * 60 * 1000, '一週過去了')))

// 過了一週開始通知研發開發

HanMeiMei.notify()
複製代碼

問:那若是換成Vue中的視圖數據之間的關係呢?哪一個是個發佈者,哪一個是觀察者。segmentfault

答:顯而易見,數據是發佈者,視圖是觀察者。當數據改變時,會通知視圖,視圖從新進行渲染。數組

這裏有幾個問題

  1. What,數據都收集什麼樣的觀察者
  2. How,數據怎麼收集的觀察者
  3. When,數據何時收集觀察者

ok,帶着這些問題我們繼續往下看

2、Vue中的觀察者

一、What

首先,明確一點,Vue實例中的響應數據,幾乎所有都來源於data,就是那個Option API中的data。不論是props,computed這些都是基於data的。

其次,Vue中的Watcher分爲了三種,

  1. render Watcher,能夠簡單的理解爲template;
  2. computed Watcher,在Vue文檔中,說到過緩存這個概念,說白了其實就是計算屬性的getter中用到的數據(data) 沒有發生過變化,那麼這個getter就沒必要從新計算,這個我認爲是Vue響應式中最繞的,下面的源碼重點講一下
  3. watch Watcher,沒錯就是那個Option API中的watch,你想一想你數據改了,watch是否是得再執行一遍,那不跟視圖是同樣的麼

因此,what的答案就有了 數據收集了這三種觀察者

二、How

說個面試的段子,面試官:vue怎麼收集依賴的?

這個其實老生常談,getter/setter的存儲器嘛

誒,那你知道Array是怎麼收集的嗎?

知道知道,不就是hack的一些原生方法嘛

哦,那爲何要hack呢,咋hack的呢,hack了哪些呢,不一樣的方法之間又都是怎麼處理的數據呢?

......

好了,回去等通知吧(一面掛)

這裏信息量太大!關於爲何要hack方法,尤大是給出了回答的,主要緣由是由於性能和使用方便之間的取捨,這篇文章有寫道:segmentfault.com/a/119000001…

可是你要問我爲啥數組附個值還能跟性能扯上關係,咱也不懂,咱也不敢問。

三、When

在vue實例化的時候,在beforeCreate和create之間,會有一個初始化數據的過程,這裏會將data、computed所有初始化好,經過getter,哪裏用到就在哪裏收集觀察者。

3、思路

一、先從getter/setter開始

先從轉換數據開始,咱們來簡單實現一個,很簡單就是迭代加遞歸,兩個函數搞定。

// 咱們先來實現第一個函數observe
function observe(data) {
    if (typeof data !== 'object') return
    for (let key of Object.keys(data)) {
        defineReactive(data, key)
    }
    return data
}

// 而後是defineReactive
function defineReactive(data, key) {
    let val = data[key]
    const dep = new Dep()
    observe(val)
    Object.defineProperty(data, key, {
        configurable: false,
        enumerable: true,
        get() {
            dep.depend()
            return val
        },
        set(newVal) {
            if (val === newVal) return
            val = newVal
            observe(val)
            dep.notify()
        }
    })
}
複製代碼

這裏的邏輯很簡單就是經過迭代+遞歸,將全部值都改成存取器。

這裏注意defineReactive方法,我並無直接把data[key] 的這個value經過參數傳進去,而是在函數內部取值,之因此爲何作,這裏先留一個懸念

二、Dep

這裏出現了一個class Dep,這裏其實Dep就是來收集Watcher的。

好的,咱們繼續來實現Dep

class Dep {
  constructor() {
    this.watchers = new Set()
  }
  depend() {
    if (Dep.Target) this.watchers.add(Dep.Target)
  }
  notify() {
    let watchers = this.watchers
    for (let watcher of watchers) {
      watcher.update()
    }
  }
}

複製代碼

這裏Dep的實現也很簡單,就是收集watchers,使用Set確保watcher的惟一。

可是!這裏又雙叒出現了一個新的東西,Dep.Target。這東西是個啥,其實看代碼也能差很少發現,Dep.Target確定是個Watcher實例。

誒~,這麼多Watcher實例它究竟是哪一個呢?

好問題!咱們先想一想一個場景,咱們有個數據好比是data,咱們還有個渲染函數,而後呢~這個渲染函數用到了這個data。

用到data了確定就會觸發data的getter,從而收集依賴,那咱們要收集的依賴確定就是這個渲染函數了。

相應的Dep.Target的值也就是這個渲染函數


噠嘎! 你覺得這樣就結束了嗎,No,No,No,嘛噠噠!

要是渲染函數裏面還有個渲染函數咋整。

納尼!還有這種操做嗎!

有的,並且不少,當咱們組件裏面嵌入了組件的時候就會出現。

我去,那不是很常見嗎!那可怎麼辦。

別慌,咱們只要實現一個棧,有新的函數要執行,咱們就push進來,當函數執行結束,給他pop出去就行了。

ok,那咱們開始實現一下。

Dep.Target = null
const watcherStack = []

// 入棧
function pushTarget(watcher) {
  Dep.Target = watcher
  watcherStack.push(watcher)
}

// 出棧
function popTarget() {
  watcherStack.pop()
  Dep.Target = watcherStack[watcherStack.length - 1]
}
複製代碼

完美解決~ 那麼最後剩下的的就是watcher的實現了。

三、Watcher

RenderWatcher

上面說過,watcher一共有三種,咱們先實現最簡單、最基礎的renderWatcher。

class Watcher {
  constructor(getter) {
    this.getter = getter
    this.value = undefined

    this.value = this.get()
  }
  get() {
    pushTarget(this)
    this.getter()
    popTarget()
  }
  update() {
    this.value = this.get()
  }
}
複製代碼

這裏的邏輯很簡單,參數getter就是要執行的函數。對於RenderWatcher來講getter就是渲染函數。 好的!萬事具有,咱們來試着跑個例子。

<body>
  <div id="app"></div>
  <script src="./reactive/reactive.js"></script>
  <script> const data = observe({ age: 12, name: 'Sunyanzhe' }) // 渲染函數 function renderFunction() { document.querySelector('#app').innerHTML = `我叫${data.name}, 我${data.age}歲` } // renderWatcher const renderWatcher = new Watcher(renderFunction) setTimeout(() => { data.age = 25 }, 2000) </script>
</body>
複製代碼

renderWatcher.gif

來,咱們捋一下流程:

  1. 第一步固然是數據的處理,先變成getter/setter
  2. new Watcher的時候,這時候Dep.TargetrenderWatcher
  3. 開始執行renderFunction,讀取到nameage的屬性時,觸發getter收集Dep.Target,也就是renderWatcher。此時nameage中的Dep實例都存了renderWatcher
  4. 過了2秒,data.age賦值,觸發setter,觸發ageDep中保存的watcherupdate方法。此時更新視圖。
  5. 這時,renderFunction執行,讀取到age時,值爲25

別問爲啥兩秒,一我的就從12變成25了,經歷痛苦會讓人瞬間成長😂

ComputedWatcher

總聽文章裏說,computed有什麼懶加載,緩存。那是個什麼玩意啊

好說,由於computed實際上是一個getter,是函數就要執行嘛。懶加載的意思就是它何時被用到了,它何時執行這個函數。

那緩存又是什麼呢?

也很簡單,就是computed中用到的值若是沒發生改變的話,它的getter函數不進行計算,而是直接用上一次得出的結果。


ok,先到這裏,咱們先捋一捋思路

首先,剛剛說到,computed能夠被緩存,當它用到的值沒有發生改變時,getter不須要執行。

也就是說computed自己也是要有Dep,用來收集數據。

其次,他是lazy的,因此即便數據發生了改變也不用當即執行函數,獲取結果。而是能夠等到,何時再次用到這個computed的值再去計算。好比在render函數中用到

最後,computed中的數據改變後不能只通知computed的值須要從新更新,還須要通知用到computed的地方也要進行一次更新

總結下來就是,若是一個render函數中有用到computed,那麼computed中的數據更新,不只要通知computed的值要改變,還要告訴render函數進行從新執行。而當render函數從新執行的時候,就會再次獲取computed。這時computed纔會執行他的getter函數

好了,思路捋清了,咱們實現一下。爲此咱們要修改一下以前的Dep和Watcher,而且咱們還要實現一個computed方法。

class Dep {
  constructor() {
    this.watchers = new Set()
  }
  // 這裏發生了變化
  addSub(watcher) {
    this.watchers.add(watcher)
  }
   // 這裏發生了變化
  depend() {
    if (Dep.Target) {
      Dep.Target.addDep(this)
    }
  }
  notify() {
    let watchers = this.watchers
    for (let watcher of watchers) {
      watcher.update()
    }
  }
}

class Watcher {
  constructor( getter, options ) {
    this.getter = getter
    this.deps = new Set()
    this.value = undefined
    this.lazy = undefined
    this.dirty = undefined

    if (options) {
      this.lazy = !!options.lazy
    }
    this.dirty = this.lazy
    this.value = this.lazy
      ? undefined
      : this.get()
  }
  get() {
    pushTarget(this)
    let value = this.getter()
    popTarget()
    return value
  }
  addDep(dep) {
    dep.addSub(this)
    this.deps.add(dep)
  }
  depend() {
    let deps = this.deps
    for (let dep of deps) {
      dep.depend()
    }
    
  }
  evalute() {
    this.value = this.get()
    this.dirty = false
  }
  update() {
    if (this.lazy) {
      this.dirty = true
    } else {
      Promise.resolve().then(() => {
        this.run()
      })
    }
  }
  run() {
    this.value = this.get()
  }
}

function computed(computedGetter) {
  const options = { lazy: true }
  const computedWathcer = new Watcher(computedGetter, options)
  const result = {}
  Object.defineProperty(result, 'value', {
    get() {
      if (computedWathcer.dirty) {
        computedWathcer.evalute()
      }
      if (Dep.Target) {
        computedWathcer.depend()
      }
      return computedWathcer.value
    }
  })
  return result
}
複製代碼

看到這裏確定很暈,不要緊,我們再舉一個🌰,結合🌰來看懂這塊邏輯。你們目前只須要關注一點,就是update中咱們用了微任務。

ok,先看例子

<body>
  <div id="app"></div>
  <script src="./reactive/reactive.js"></script>
  <script> const data = observe({ age: 12, name: 'Sunyanzhe' }) function renderFunction() { document.querySelector('#app').innerHTML = `我叫${data.name}, 我${data.age}歲,明年${nextYear.value}` } const nextYear = computed(() => data.age + 1) const renderWatcher = new Watcher(renderFunction) setTimeout(() => { data.age = 25 }, 2000) </script>
</body>

複製代碼

computedWatcher.gif

咱們仍是捋一下執行順序

  1. 首先依舊是observe Data
  2. 而後咱們定義了nextYear這個computed。這時候注意computedWatcher已經生成了,可是因爲它是lazy的咱們並無執行get,且這個watcher的dirtytrue。這個很重要!
  3. 開始renderFunction,執行get,收集依賴...這些都沒問題,可是你們還記不記得以前說的。此時的棧中有RenderWatcher。
  4. 讀取到nextYear,觸發nextYear的getter。因爲dirty是ture,因此開始計算!此時執行了get方法,中推入了ComputedWatcher。重點來了,RenderWatcher還沒執行完!因此目前棧中有兩個watcher。computedWatcher的get執行完,出棧,將獲得的值賦給value屬性。修改dirty屬性爲false
  5. 另外一個關鍵,此時的Dep.Target依舊指向的是RenderWatcher,而後computedWatcher執行了depend方法。這個方法的意思,就是要讓收集到computedWatcher的dep繼續收集那個用到computedWatcher的RenderWatche。(有點繞,多看看代碼仔細體會)
  6. 值改變,computed的dirty再次變爲true,等待renderWatcher的更新,再次出發computedWatcher的計算。

這裏爲何使用了微任務,是由於執行順序的問題,咱們的computed的計算必需要在renderWatcher的更新以後,這樣才能收集到對應的依賴。在Vue源碼中,有一個執行更新的隊列,它會將全部的watcher進行排序,避免報錯。

WatchWatcher

其實,watch也很簡單,就是加了個callback。

watch比較迷惑的地方其實它的getter是什麼,在renderWatcher中,getter是render函數;在computedWatcher中,getter是getter函數;那麼watch是什麼呢。

其實很簡單就是個travers函數,想一想咱們是怎麼寫watch的

watch: {
  prop1(val) {
    console.log(val)
  }
}

// 轉換爲
$watch(() => {vm._data.prop1}, console.log)
複製代碼

這裏面第一個函數是getter,用來收集依賴,第二個就是callback了

那deep呢? deep其實就是深度遍歷

廢話少說,直接開始實現!

其實很簡單,咱們只須要加個callback,找個地方調用一下就行了。

因此咱們就改一下constructor和run這兩個

class Watcher {
  constructor( getter, options, cb ) {
    //...
    this.cb = cb
    this.user = undefined

    if (options) {
      this.user = !!options.user
    }
    // ...
  }
  run() {
    let newValue = this.get()
    if (this.user && newValue !== this.value) {
      // 調用回調
      this.cb(newValue, this.value)
      this.value = newValue
    }
  }
}

function watch(watcheGetter, callback) {
  const options = { user: true }
  new Watcher(watcheGetter, options, callback)
}
複製代碼

就是如此的簡單,比computed簡單多了~

最後看一下效果

<body>
  <div id="app"></div>
  <script src="./reactive/reactive.js"></script>
  <script> const data = observe({ age: 12, name: 'Sunyanzhe' }) function renderFunction() { document.querySelector('#app').innerHTML = `我叫${data.name}, 我${data.age}歲,明年${nextYear.value}` } const nextYear = computed(() => data.age + 1) const renderWatcher = new Watcher(renderFunction) watch( () => data.name, (val, oldVal) => { console.log('new---', val) console.log('old---', oldVal) }) setTimeout(() => { data.name = 'yanzhe' }, 1000) setTimeout(() => { data.age = 25 }, 2000) </script>
</body>

複製代碼

watch.gif

4、源碼以及拓展閱讀

在上文中談到的爲何DefineReactive不傳value的緣由,在這個issue中:github.com/vuejs/vue/p…,主要緣由是,數據自己就能夠是getter/setter

Vue中的源碼思路與本文一致,主要多了邊界問題的處理,以及數組的hack,有關數組的處理須要你們去看源碼去理解

  1. array —— github.com/vuejs/vue/b…
  2. 觀察者—— github.com/vuejs/vue/b…
相關文章
相關標籤/搜索