學(xué)習(xí)筆記(十三)模擬 Vue.js 響應(yīng)式原理

模擬Vue.js響應(yīng)式原理

數(shù)據(jù)驅(qū)動

  • 數(shù)據(jù)響應(yīng)式
    • 數(shù)據(jù)模型是普通的JavaScript對象,當(dāng)我們修改數(shù)據(jù)時,視圖會進(jìn)行相應(yīng)的更新,避免了繁瑣的DOM操作,提高開發(fā)效率
  • 雙向綁定
    • 數(shù)據(jù)改變,視圖發(fā)生相應(yīng)變化,視圖變化,數(shù)據(jù)發(fā)生相應(yīng)的變化
    • 可以使用v-model指令在表單元素上創(chuàng)建雙向數(shù)據(jù)綁定(自定義組件也可以自己實現(xiàn)v-model)
  • 數(shù)據(jù)驅(qū)動
    • 數(shù)據(jù)驅(qū)動是Vue最獨特的特性之一,開發(fā)過程只需要關(guān)心數(shù)據(jù),而不需要關(guān)心數(shù)據(jù)如何被渲染到視圖

數(shù)據(jù)響應(yīng)式核心原理

Vue2.x

  • 遍歷對象中的屬性,并通過Object.defineProperty 方法,將對象中的屬性,轉(zhuǎn)換成getter/setter方法
  • Object.defineProperty是ES5中新增的,不支持IE8以下瀏覽器

Vue3.x

  • 基于ES6新增的Proxy來實現(xiàn)
  • Proxy代理的是整個對象,而不是對象的屬性,因此不需要對對象的屬性進(jìn)行遍歷
  • Proxy的性能由瀏覽器優(yōu)化,要優(yōu)于Object.defineProperty
  • 同樣不支持IE8以下瀏覽器

發(fā)布訂閱模式和觀察者模式

發(fā)布訂閱模式

發(fā)布訂閱模式包含發(fā)布者、訂閱者、消息中心,發(fā)布者與訂閱者相互之間不知道彼此存在,通過消息中心進(jìn)行消息轉(zhuǎn)發(fā)

  • 發(fā)布者

  • 訂閱者

  • 消息中心broker

  • 模擬實現(xiàn)代碼

    class EventEmitter {
        constructor() {
            this.subs = Object.create(null)
        }
    
        // 訂閱 - 注冊事件
        $on (topic, handler) {
            this.subs[topic] = this.subs[topic] || []
            this.subs[topic].push(handler)
        }
    
        // 發(fā)布 - 觸發(fā)事件
        $emit (topic, ...args) {
            this.subs[topic]?.forEach(handler => {
                typeof handler === 'function' && handler.apply(null, args)
            });
        }
    }
    

觀察者模式

觀察者模式包含觀察者(訂閱者)和目標(biāo)(發(fā)布者),不存在消息中心,被觀察的目標(biāo)需要知道觀察者的存在

  • 觀察者(訂閱者)Watcher

    • update方法:處理函數(shù)
  • 目標(biāo)(發(fā)布者)

    • notify方法:通知觀察者,調(diào)用所有觀察者的update方法
    • subs數(shù)組:儲存所有的觀察者
    • addSub:添加觀察者
  • 模擬實現(xiàn)代碼

    export class Target {
        constructor() {
            this.subs = []
        }
    
        addSub(sub) {
            sub && typeof sub.update === 'function' && this.subs.push(sub)
        }
    
        notify(...args) {
            this.subs.forEach(sub => sub.update.apply(null, args))
        }
    }
    
    export class Watcher {
        update(...args) {
            console.log(args)
        }
    }
    

發(fā)布訂閱模式 vs 觀察者模式

image-20201109182709237

模擬Vue.js響應(yīng)式原理

Vue類的簡單實現(xiàn)

  • 功能

    • 負(fù)責(zé)接收初始化的參數(shù)(選項)
    • 負(fù)責(zé)把data中的屬性轉(zhuǎn)換成getter/setter,并注入到Vue實例中
    • 負(fù)責(zé)調(diào)用observer監(jiān)聽data中所有屬性的變化
    • 負(fù)責(zé)調(diào)用compiler解析指令/差值表達(dá)式
  • 屬性

    • $options
      • 保存?zhèn)魅氲倪x項
    • $data
      • 保存?zhèn)魅氲膁ata
    • $el
      • 保存掛載的DOM元素
  • 方法

    • _proxyData
      • 把data中的屬性轉(zhuǎn)換成getter/setter,并注入到Vue實例中
  • 實現(xiàn)代碼

    export default class Vue {
        // 1. 通過屬性保存選項的數(shù)據(jù)
        constructor(options) {
            this.$options = options || {}
            this.$data = options.data || {}
            this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
            this._proxyData(this.$data)
        }
        // 2. 把data中的屬性轉(zhuǎn)換成getter/setter注入到vue實例中
        _proxyData(data) {
            // Vue2.x處理方式
            Object.keys(data).forEach(key => {
                Object.defineProperty(this, key, {
                    enumerable: true,
                    configurable: true,
                    get() {
                        return data[key]
                    },
                    set(newValue) {
                        if (data[key] === newValue) {
                            return
                        }
                        data[key] = newValue
                    },
                })
            })
        }
        // 3. 調(diào)用observer對象,監(jiān)聽數(shù)據(jù)變化
        new Observer(this.$data)
        // 4. 調(diào)用compiler對象,解析指令和差值表達(dá)式
        new Compiler(this)
    }
    

Observer的簡單實現(xiàn)

  • 功能

    • 負(fù)責(zé)把data中的數(shù)據(jù)轉(zhuǎn)換成響應(yīng)式數(shù)據(jù)
    • data中的某個屬性也是對象,把該屬性轉(zhuǎn)換成響應(yīng)式數(shù)據(jù)
    • 數(shù)據(jù)變化發(fā)送通知
  • 方法

    • walk(data)
      • 遍歷data中的所有屬性
    • defineReactive(data, key, value)
      • 轉(zhuǎn)換屬性為getter/setter
  • 實現(xiàn)代碼

    export default class Observer {
        constructor(data) {
            this.walk(data)
        }
        walk(data) {
            // 1. 判斷data是否是對象
            // 2. 遍歷data的所有屬性
            if (!data || typeof data !== 'object') {
                return
            }
            Object.keys(data).forEach(key => {
                this.defineReactive(data, key, data[key])
            })
        }
    
        defineReactive(data, key, value) {
            // 為什么需要第三個參數(shù)傳遞value,而不直接使用data[key]?
            // 在get中使用data[key]會循環(huán)觸發(fā)getter,導(dǎo)致棧溢出
        
            // 為什么value屬性在defineProperty執(zhí)行完成后還可以被訪問
            // 外部對內(nèi)部屬性存在引用,形成了閉包
    
            // 如果value是對象,則將對象的屬性也轉(zhuǎn)換成響應(yīng)式數(shù)據(jù)
            const _this = this
    
        // 創(chuàng)建Dep對象收集依賴,發(fā)送通知
            let dep = new Dep()
    
            this.walk(value)
            
            Object.defineProperty(data, key, {
                configurable: true,
                enumerable: true,
                get() {
                    // 收集依賴
                    Dep.target && dep.addSub(Dep.target)
    
                    return value
                },
                set(newValue) {
                    if (value === newValue) {
                        return
                    }
                    value = newValue
                    // 當(dāng)屬性被重新賦值為一個對象,將對象屬性也轉(zhuǎn)換為響應(yīng)式數(shù)據(jù)
                    _this.walk(newValue)
                    dep.notify()
                }
            })
        }
    }
    

Compiler的簡單實現(xiàn)

  • 功能

    • 負(fù)責(zé)編譯模板,解析指令/差值表達(dá)式
    • 負(fù)責(zé)頁面的首次渲染
    • 當(dāng)數(shù)據(jù)變化后,重新渲染視圖
  • 屬性

    • el
      • DOM對象
    • vm
      • vue實例
  • 方法

    compiler的方法主要用來進(jìn)行DOM操作

    • compile(el)
      • 編譯解析入口
    • compileElement(node)
      • 解析元素節(jié)點的指令
    • compileText(node)
      • 解析文本節(jié)點的差值表達(dá)式
    • isDirective(attrName)
      • 判斷屬性是否是指令
    • isElementNode(node)
      • 判斷是否是元素節(jié)點
    • isTextNode(node)
      • 判斷是否文本節(jié)點
  • 實現(xiàn)代碼

    import Watcher from "./watcher.js"
    
    export default class Compiler {
        constructor(vm) {
            this.el = vm.$el
            this.vm = vm
            this.compile(this.el)
        }
        // 編譯模板,處理文本節(jié)點和元素節(jié)點
        compile (el) {
            [...el.childNodes].forEach(node => {
                if (this.isTextNode(node)) {
                    this.compileText(node)
                }
                if (this.isElementNode(node)) {
                    this.compileElement(node)
                }
                // 判斷childNodes并遞歸調(diào)用compile
                if (node.childNodes && node.childNodes.length) {
                    this.compile(node)
                }
            })
        }
    
        // 編譯元素節(jié)點,處理指令
        compileElement (node) {
            // 獲取并遍歷元素的屬性節(jié)點
            [...node.attributes].forEach(attr => {
                const { name, value } = attr
                if (this.isDirective(name)) {
                    this.update(node, name.slice(2), value)
                }
            })
        }
    
        // 處理指令
        update (node, name, value) {
            const fn = this[`${name}Updater`]
            typeof fn === 'function' && fn.call(this, node, value)
        }
    
        // 處理v-text指令
        textUpdater(node, value) {
            node.textContent = this.vm[value]
            // 創(chuàng)建watcher對象,當(dāng)數(shù)據(jù)改變時更新視圖
            new Watcher(this.vm, value, newValue => {
                node.textContent = newValue
            })
        }
    
        // 處理v-model指令
        modelUpdater(node, value) {
            node.value = this.vm[value]
            // 創(chuàng)建watcher對象,當(dāng)數(shù)據(jù)改變時更新視圖
            new Watcher(this.vm, value, newValue => {
                node.value = newValue
            })
            // 注冊表單input事件實現(xiàn)雙向綁定
            node.addEventListener('input', () => {
                this.vm[value] = node.value
            })
        }    
    
        // 處理其他指令
        // ...
    
        // 編譯文本節(jié)點,處理差值表達(dá)式
        compileText (node) {
            // 使用正則表達(dá)式匹配差值表達(dá)式 {{ xxx }},并提取獲取表達(dá)式內(nèi)容
            let reg = /\{\{(.+?)\}\}/
            const text = node.textContent
            node.textContent = text.replace(reg, (word, key) => {
                key = key.trim()
                if (key in this.vm) {
                    // 創(chuàng)建watcher對象,當(dāng)數(shù)據(jù)改變時更新視圖
                    new Watcher(this.vm, key, newValue => {                    
                        node.textContent = text.replace(reg, (w, k) => this.vm[k.trim()])
                    })
                    return this.vm[key]
                }
                return word            
            })        
        }
    
        // 判斷屬性是否為指令
        isDirective (attrName) {
            return attrName.startsWith('v-')
        }
    
        // 判斷是否為元素節(jié)點
        isElementNode (node) {
            return node.nodeType === 1
        }
        // 判斷是否為文本節(jié)點
        isTextNode (node) {
            return node.nodeType === 3
        }
    }
    

Dep的簡單實現(xiàn)

  • 功能

    • 收集依賴,添加觀察者(watcher)
    • notify通知所有觀察者
  • 屬性

    • subs
      • 儲存所有的觀察者
  • 方法

    • addSubs(sub)
      • 添加觀察者
    • notify()
      • 通知觀察者,調(diào)用所有觀察者的update方法
  • 實現(xiàn)代碼

    export default class Dep {
        constructor() {
            this.subs = []
        }
     
        addSub(sub) {
            sub && typeof sub.update === 'function' && this.subs.push(sub)
        }
     
        notify(...args) {
            this.subs.forEach(sub => sub.update(...args))
        }
    }
    

Watcher的簡單實現(xiàn)

  • 功能

    • 數(shù)據(jù)變化觸發(fā)依賴,接收dep通知更新視圖
    • 自身實例化的時候向Dep中添加自己
  • 屬性

    • vm
      • vue實例
    • key
      • 觀察的屬性名稱
    • cb
      • 更新時的回調(diào)處理函數(shù)
    • oldValue
      • 觀察的屬性數(shù)據(jù)更新之前的值
  • 方法

    • update()
      • 更新處理函數(shù)
  • 實現(xiàn)代碼

    import Dep from "./dep.js"
    
    export default class Watcher {
        constructor(vm, key, cb) {
            this.vm = vm
            this.key = key
            this.cb = cb
            // 把watcher對象記錄到Dep類的靜態(tài)屬性target
            Dep.target = this
            // 觸發(fā)get方法,在get中調(diào)用addSub
            this.oldValue = vm[key]
            // 清空Dep.target,避免重復(fù)添加
            Dep.target = null
        }
        // 數(shù)據(jù)變化時更新視圖
        update(...args) {
            const newValue = this.vm[this.key]
            if (newValue !== this.oldValue) {
                this.cb(newValue)
            }
        }
    }
    

總結(jié)

問題

  • 將屬性重新賦值成對象,是否是響應(yīng)式的?
    • 是響應(yīng)式的,重新賦值成對象時會調(diào)用屬性的set方法,此時會將新賦值的內(nèi)容轉(zhuǎn)換為響應(yīng)式數(shù)據(jù)
  • 為vue實例添加新的屬性時,此屬性是否是響應(yīng)式的?
    • 不是響應(yīng)式的
    • 可以通過Vue.set()或vm.$set()方法設(shè)置新的響應(yīng)式屬性

整體流程

?
image-20201201234840455
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,668評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內(nèi)容