[切圖仔救贖]炒冷飯--在線手擼vue2響應式原理

vue2.6超時空要塞

--圖片來源vue2.6正式版本(代號:超時空要塞)發(fā)布時,尤雨溪推送配圖。

前言

其實這個冷飯我并不想炒,畢竟vue3馬上都要出來。我還在這里炒冷飯,那明顯就是搞事情。

起因:

作為切圖仔搬磚汪,長期切圖jq一把梭。重復繁瑣的切圖,讓自己陷入了一個無限的圍城。想出去切圖這個圍城看一看,但是又害怕因為切圖時間久了,自己會的也只有切圖了。

為了后面能夠繼續(xù)搬磚恰飯,幫助自己跳出切圖仔的圍城。也去看了vue相關文檔,當時記憶深刻覺得還行??墒荊胖這個時候發(fā)動小紫本和打折魔咒,不知不覺又沉迷于DOTA小本子上面了。關于vue響應式原理很快忘得一塌糊涂,只記得一個屬性Object.defindProperty,然后就沒有然后了......

為了避免自己后面再次忘記,所以這里炒一個冷飯加深記憶。

炒vue2冷飯

響應式vue

在講解vue響應式的原理之前,讓我們來一段Vue代碼作為示例:

<div id="app">
  <div>主食: {{ food }}</div>
  <div>飲料: {{ drink }}</div>
  <div>菜單: {{ menu }}</div>
</div>
<script>
  let vue = new Vue({
    el: '#app',
    data: {
      food: '煎餅果子',
      drink: '熱豆?jié){'
    },
    computed: {
      menu() {
        return  this.food + this.drink
      }
    }
  })
</script>

fooddrink發(fā)生變化后,Vue會做兩件事:

  • 在頁面上更新fooddrink的值。

  • 再次調(diào)用menu, 重新計算food + drink的值, 并在頁面上面更新。

更新值+計算值做的事情其實很簡單,幾行代碼的事情。問題是當food或者drink變化時,Vue是怎么知道誰變化,然后馬上響應其行為,去執(zhí)行那"簡單的幾行代碼"?

所以,當看到Vue案例時,詞窮的我當時第一反應就是牛皮。

牛批

之所以發(fā)出感嘆,是因為通常的JavaScript代碼是實現(xiàn)不了這樣的功能的。話不多說,讓我們直接上代碼來說明:

    let food = "煎餅果子"
    let drink = "熱豆?jié){"
    let menu = null
    menu = food + drink
    food = '炸雞漢堡'
    drink = '快樂水'
    console.log(menu) 

最終控制臺打印結果:

煎餅果子熱豆?jié){

如果是在Vue當中,fooddrink發(fā)生了變化,那么Vue會跟著做出響應動作,從而在控制臺輸出我們想要的結果:

炸雞漢堡快樂水

菜單響應

這里就出現(xiàn)第一個問題,當food或者drink 發(fā)生變化之后,menu并不會響應其變化。這個時候就需要我們來解決這個問題,滿足menu響應。

借鑒Vue一樣,我們先把menu的計算方法。也寫成一個函數(shù),取名為target。然后每次food或者drink變化的時候調(diào)用target函數(shù)

    let food = "煎餅果子"
    let drink = "熱豆?jié){"
    let menu = null
    let target = () => {
        menu = food + drink
    }
    target() // 初始化菜單menu
    food = '炸雞漢堡'
    drink = '快樂水'
    target()
    console.log(menu) 

控制臺輸出:

炸雞漢堡快樂水

浴室沉思

image

前面一把梭直接調(diào)用的滿足menu響應的問題,但是也間接留下一個新的疑惑點。這里針對一個菜單,就寫了一個target。假設有多個菜單需要響應呢?

例如:

  • 單人早餐 = 煎餅果子 + 熱豆?jié){
  • 豪華套餐: 煎餅果子加兩雞蛋 + 熱豆?jié){ + 油條一根午餐
  • ......

如果這個時候切換成:

  • 單人午餐 = 炸雞漢堡 + 快樂水
  • 豪華套餐: 雙層炸雞漢堡 + 快樂水 + 快樂薯條一包
  • ......

按照前面的邏輯, 估計得寫N個target。這個時候響應式又是一個麻煩事情,可是有句話說的好。梭哈一時爽,一直梭哈一直爽。既然前面直接采用target一把梭完成,所以針對N個target方法,我也可以直接來個for循環(huán)一把梭能完成響應式問題。

for循環(huán)一把梭

  • 定義一個數(shù)組,每定義了一個target函數(shù)。就存儲到數(shù)組當中。
let storge = [] // 用來存儲target
function record (){  // 
  storge.push(target)
}
  • 定義循環(huán)函數(shù),每次data有變更。就調(diào)用這個函數(shù),進行一把for循環(huán).
function replay (){
  storge.forEach(run => run())
}
  • 合并成完整的代碼:
    let food = "煎餅果子"
    let drink = "熱豆?jié){"
    let menu = null
    food = '炸雞漢堡'
    drink = '快樂水'
    let target = () => {
        menu = food + drink
    }
    let storge = []; //用來存儲更多的target
    function record(target) {
        storge.push(target)
    }    
    function replay() {
        storge.forEach(run => run())
    }
    record(target)
    replay()
    food = '炸雞漢堡'
    drink = '快樂水'
    replay()
    console.log(menu)

最后控制臺成功輸出:

炸雞漢堡快樂水
image

Dep依賴類

通過一把梭實現(xiàn)功能,那么接下來就開始思考優(yōu)化部分了。繼續(xù)記錄target這類的代碼,這樣有點怪怪的。為了后面方便管理,我們把代碼進行簡單的優(yōu)化,封裝成一個類:

    class Dep {
        constructor() {
            this.subs = []
        }
        // 收集依賴
        depend(sub) {
            if (sub && !this.subs.includes(sub)) {  // 做一個判斷
                this.subs.push(sub)
            }
        }

        notify() {
            console.log("暗號:下雨啦,收衣服啦!")
            this.subs.forEach(sub => sub()) // 運行我們的target
        }
    }

就這樣target函數(shù)存儲在類的subs中,record也變成了depend,使用notify來代替replay

封裝成類之后,每次當data數(shù)據(jù)更新的時候,就會發(fā)出一個暗號下雨啦,收衣服啦! 然后就開始遍歷運行相應的target依賴了。

新的調(diào)用代碼就更加清晰明了:

    let dep = new Dep()
    let food = "煎餅果子"
    let drink = "熱豆?jié){"
    let menu = null
    let target = () => {
        menu = food + drink
    }
    dep.depend(target)
    target() // 完成menu第一次初始化
    console.log(menu)
    food = '炸雞漢堡'
    drink = '快樂水'
    dep.notify()
    console.log(menu)

控制臺輸出:

煎餅果子熱豆?jié){
暗號:'下雨啦,收衣服啦!'
炸雞漢堡快樂水

觀察者亮相

當前的代碼,是確定一個依賴事件,就定義target,然后調(diào)用依賴類dep.depend將其存儲起來。

let target = () => { menu = food + drink }
dep.depend(target)
target()

這個時候又新來一個target事件又該如何做:

新添加一個target事件?

let target2 = () => { 新的依賴事件 }
dep.depend(target2)
target2()

要是有幾百個依賴,那還不得上天。我估計要是這樣寫代碼,估計你的同事要說你寫代碼像CXK

image

觀察者函數(shù)

借鑒觀察者模式,封裝一個watcher函數(shù). 幫你觀察記錄相關target事件,避免多次聲明變量。

    function watcher(myFun) {
        target = myFun
        dep.depend(target)
        target()
        target = null
    }
    watcher(() => {
        menu = food + drink
    })

正如你所看到的,watcher函數(shù)接受myFunc參數(shù),將其賦給全局的target上,調(diào)用dep.depend()將其添加到數(shù)組里,之后調(diào)用并重置target。

既然又封裝一個新的函數(shù),那么驗證又將是必不可少的了。這里我們修改一下drink來試試:

drink = "快樂水"
console.log(menu)
dep.notify()
console.log(menu)

控制臺輸出結果:

煎餅果子熱豆?jié){
暗號:下雨啦,收衣服啦!
煎餅果子快樂水

Object.defineProperty()

基本用法

鋪墊了這么久,一個關鍵性角色這個時候也登場了。

該方法允許精確添加或修改對象的屬性。通過賦值操作添加的普通屬性是可枚舉的,能夠在屬性枚舉期間呈現(xiàn)出來(for...in 或 Object.keys 方法), 這些屬性的值可以被改變,也可以被刪除。這個方法允許修改默認的額外選項(或配置)。默認情況下,使用 Object.defineProperty() 添加的屬性值是不可修改的。
--《MDN文檔》

不明覺厲? 那就先熱身一下,進入快樂的舉例子環(huán)節(jié):

image
    let data = {
         food: '煎餅果子',
         drink: '熱豆?jié){'
    }
    Object.defineProperty(data, 'food', {
        get() {
            console.log(`觸發(fā)get方法`)
    },
        set(newVal) {
            console.log(`設置food為${newVal}`)
        }
    })
data.food
data.food = 炸雞漢堡 

控制臺輸出:

觸發(fā)get方法
設置food為炸雞漢堡

簡單封裝

但是僅僅憑借object.defineProperty是無法完成當一個數(shù)據(jù)更新了,完成數(shù)據(jù)響應。而且代碼這里也是只是對food做了一個處理, 還有drink沒有處理,所以為了完成data所以屬性都做相應的處理。接下來就是對于Object.defineProperty()進行簡單的封裝處理了:

    Object.keys(data).forEach(key => {
        let value = data[key]
        Object.defineProperty(data, key, {
            get() {
                return value
            },
            set(newVal) {
                value = newVal
            }
        })
    })

遍歷了data每個屬性,然后對每個屬性進行偵聽。這樣data的屬性一旦改變,就會自動發(fā)出通知.

代碼整合

前面零零散散分別講了 Dep、watcherobject.defineProperty, 那么接下來就讓我們把這個幾個部分整合到一起,完整查看整個代碼:

    let data = {
        food: '煎餅果子',
        drink: '熱豆?jié){'
    }
    class Dep {
        constructor() {
            this.subs = []
        }
        // 收集依賴
        depend(sub) {
            if (sub && !this.subs.includes(sub)) { // 做一個判斷
                this.subs.push(sub)
            }
        }

        notify() {
            console.log("暗號:下雨啦,收衣服啦!")
            this.subs.forEach(sub => sub()) // 運行我們的target
        }
    }
    Object.keys(data).forEach(key => {
        let value = data[key]
        let dep = new Dep()

        Object.defineProperty(data, key, {
            get() {
                dep.depend(target)
                return value
            },
            set(newVal) {
                value = newVal
                dep.notify()
            }
        })
    })

    function watcher(myFun) {
        target = myFun
        // dep.depend(target)  這里修改,移動到Object.defineProperty當中去
        target()
        target = null
    }
    watcher(() => {
        data.menu = data.food + data.drink
    })
    console.log(data.menu)
    data.food = "炸雞漢堡"
    data.drink = "快樂水"
    console.log(data.menu)

控制臺輸出:

煎餅果子熱豆?jié){
暗號:下雨啦,收衣服啦!
炸雞漢堡快樂

這里完全實現(xiàn)了文章開頭所提出的需求,每當fooddrink更新時,我們的menu也會跟著響應并更新。

這時候Vue文檔的插圖的意義就很明顯了:

image

免責聲明

以上就是我的炒冷飯內(nèi)容,怕忘記重寫總結一下,有說錯的地方多擔待。(特拿前端勸退師騷聲明一份,窺伺好久了。)

image

意思就是寫得略粗糙,別噴我。。。

我是車大棒,我為我自己插眼。

image
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容