學(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最獨(dú)特的特性之一,開發(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ù)。

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