大前端進階-Vuejs響應式原理剖析

像React,Vue這類的框架,響應式是其最核心的特性之一。經過響應式能夠實現當改變數據的時候,視圖會自動變化,反之,視圖變化,數據也隨之更新。避免了繁瑣的dom操做,讓開發者在開發的時候只須要關注數據自己,而不須要關注數據如何渲染到視圖。html

實現原理

2.x

在vue2.0中經過Object.defineProperty方法實現數據攔截,也就是爲每一個屬性添加get和set方法,當獲取屬性值和修改屬性值的時候會觸發get和set方法。vue

let vue = {}
let data = {
    msg: 'foo'
}

Object.defineProperty(vue, 'msg', {
    enumerable: true,
    configurable: true,
    get() {
        console.log('正在獲取msg屬性對應的值')
        return data.msg
    },
    set(newValue) {
        if(newValue === data.msg) {
            return 
        }
        console.log('正在爲msg屬性賦值')
        data.msg = newValue
    }
})

console.log(vue.msg)
vue.msg = 'bar'

Object.defineProperty添加的數據攔截在針對數組的時候會出現問題,也就是當屬性值爲一個數組的時候,若是進行push,shift等操做的時候,雖然修改了數組,但不會觸發set攔截。node

爲了解決這個問題,vue在內部重寫了原生的數組操做方法,以支持響應式。正則表達式

3.x

在vue3.0版本中使用ES6新增的Proxy對象替換了Object.defineProperty,不只簡化了添加攔截的語法,同時也能夠支持數組。express

let data = {
    msg: 'foo'
}

let vue = new Proxy(data, {
    get(target, key) {
        console.log('正在獲取msg屬性對應的值')
        return target[key]
    },
    set(target, key, newValue) {
        if(newValue === target[key]) {
            return 
        }

        console.log('正在爲msg屬性賦值')
        target[key] = newValue
    }
})

console.log(vue.msg)
vue.msg = 'bar'

依賴的開發模式

在vue實現響應式的代碼中,使用了觀察者模式。數組

觀察者模式

觀察者模式中,包含兩個部分:框架

  • 觀察者watcher

觀察者包含一個update方法,此方法表示當事件發生變化的時候須要作的事情dom

class Watcher {
    update() {
        console.log('執行操做')
    }
}
  • 目標dep

目標包含一個屬性和兩個方法:函數

  1. subs屬性:用於存儲全部註冊的觀察者。
  2. addSub方法: 用於添加觀察者。
  3. notify方法: 當事件變化的時候,用於輪詢subs中全部的觀察者,並執行其update方法。
class Dep {
    constructor() {
        this.subs = []
    }

    addSub(watcher) {
        if (watcher.update) {
            this.subs.push(watcher)
        }
    }

    notify() {
        this.subs.forEach(watcher => {
            watcher.update()
        })
    }
}
  • 使用方式
// 建立觀察者和目標對象
const w = new Watcher()
const d = new Dep()
// 添加觀察者
d.addSub(w)
// 觸發變化
d.notify()

發佈訂閱模式

與觀察者模式很類似的是發佈訂閱模式,該模式包含三個方面:this

  • 訂閱者

訂閱者相似觀察者模式中的觀察者,當事件發生變化的時候,訂閱者會執行相應的操做。

  • 發佈者

發佈者相似觀察者模式中的目標,其用於發佈變化。

  • 事件中心

在事件中心中存儲着事件對應的全部訂閱者,當發佈者發佈事件變化後,事件中心會通知全部的訂閱者執行相應操做。

與觀察者模式相比,發佈訂閱模式多了一個事件中心,其做用是隔離訂閱者和發佈者之間的依賴。

image.png

vue中的on和emit就是實現的發佈訂閱模式,由於其和響應式原理關係不大,因此此處再也不詳細說明。

自實現簡版vue

簡化版的vue核心包含5大類,以下圖:

image.png

經過實現這5大類,就能夠一窺Vue內部如何實現響應式。

vue

vue是框架的入口,負責存儲用戶變量、添加數據攔截,啓動模版編譯。

Vue類:

  • 屬性

$options 存儲初始化Vue實例時傳遞的參數
$data 存儲響應式數據
$methods 存儲傳入的全部函數
$el 編譯的模版節點

  • 方法

_proxyData 私有方法,負責將data中全部屬性添加到Vue實例上。

_proxyMethods 私有方法,遍歷傳入的函數,將非聲明周期函數添加到Vue實例上。

directive 靜態方法,用於向Vue注入指令。

  • 實現
// 全部聲明週期方法名稱
const hooks = ['beforeCreate', 'created', 'beforeMount', 'mounted',
    'beforeUpdate', 'updated', 'activated', 'deactivated', 'beforeDestroy', 'destroyed']

class Vue {
    constructor(options) {
        this.$options = Object.assign(Vue.options || {}, options || {})
        this.$data = options.data || {}
        this.$methods = options.methods || {}
        if (options && options.el) {
            this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
        }
        this._proxyData(this.$data)
        this._proxyMethods(this.$methods)
        // 實現數據攔截
        // 啓動模版編譯
    }

    _proxyMethods(methods) {
        let obj = {}
        Object.keys(methods).forEach(key => {
            if (hooks.indexOf(key) === -1 && typeof methods[key] === 'function') {
                obj[key] = methods[key].bind(this)
            }
        })

        this._proxyData(obj)
    }

    _proxyData(data) {
        Object.keys(data).forEach(key => {
            Object.defineProperty(this, key, {
                enumerable: true,
                configurable: true,
                get() {
                    return data[key]
                },
                set(newValue) {
                    // 數據未發生任何變化,不須要處理
                    if (newValue === data[key]) {
                        return
                    }
                    data[key] = newValue
                }
            })
        })
    }

    // 用於註冊指令的方法
    static directive(name, handle) {
        if (!Vue.options) {
            Vue.options = {
                directives: {}
            }
        }

        Vue.options.directives[name] = {
            bind: handle,
            update: handle
        }
    }
}

observer

observer類負責爲data對象添加數據攔截。

  • 方法

walk 輪詢對象屬性,調用defineReactive方法爲每一個屬性添加setter和getter。
defineReactive 添加setter和getter。

  • 實現
class Observer {
    constructor(data) {
        this.walk(data)
    }

    // 輪詢對象
    walk(data) {
        // 只有data爲object對象時,才輪詢其屬性
        if (data && typeof data === 'object') {
            Object.keys(data).forEach(key => {
                this.defineReactive(data, key, data[key])
            })
        }
    }

    // 添加攔截
    defineReactive(data, key, val) {
        const that = this
        // 若是val是一個對象,爲對象的每個屬性添加攔截
        this.walk(val)
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get() {
                return val
            },
            set(newValue) {
                if (val === newValue) {
                    return
                }
                // 若是賦值爲一個對象,爲對象的每個屬性添加攔截
                that.walk(newValue)
                val = newValue
            }
        })
    }
}

在Vue的constructor構造函數中添加Observer:

constructor(options) {
        this.$options = Object.assign(Vue.options || {}, options || {})
        this.$data = options.data || {}
        this.$methods = options.methods || {}
        if (options && options.el) {
            this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
        }
        this._proxyData(this.$data)
        this._proxyMethods(this.$methods)
        // 實現數據攔截
        new Observer(this.$data)
        // 啓動模版編譯
        new Compiler(this)
}

directive

因爲在compiler編譯模版的時候,須要用到指令解析,因此此處模擬一個指令初始化方法,用於向vue實例添加內置指令。

在此處模擬實現了四個指令:

// v-text
Vue.directive('text', function (el, binding) {
    const { value } = binding
    el.textContent = value
})

// v-model
Vue.directive('model', function (el, binding) {
    const { value, expression } = binding
    el.value = value
    // 實現雙向綁定
    el.addEventListener('input', () => {
        el.vm[expression] = el.value
    })
})

// v-html
Vue.directive('html', function (el, binding) {
    const { value } = binding
    el.innerHTML = value
})

// v-on
Vue.directive('on', function (el, binding) {
    const { value, argument } = binding

    el.addEventListener(argument, value)
})

compiler

compiler負責html模版編譯,解析模版中的插值表達式和指令等。

  • 屬性

el 保存編譯的目標元素
vm 保存編譯時用到的vue上下文信息。

  • 方法

compile 負責具體的html編譯。

  • 實現
class Compiler {
    constructor(vm) {
        this.vm = vm
        this.el = vm.$el

        // 構造函數中執行編譯
        this.compile(this.el)
    }

    compile(el) {
        if (!el) {
            return
        }
        const children = el.childNodes
        Array.from(children).forEach(node => {
            if (this.isElementNode(node)) {
                this.compileElement(node)
            } else if (this.isTextNode(node)) {
                this.compileText(node)
            }

            // 遞歸處理node下面的子節點
            if (node.childNodes && node.childNodes.length) {
                this.compile(node)
            }
        })
    }

    compileElement(node) {
        const directives = this.vm.$options.directives
        Array.from(node.attributes).forEach(attr => {
            // 判斷是不是指令
            let attrName = attr.name
            if (this.isDirective(attrName)) {
                // v-text --> text
                // 獲取指令的相關數據
                let attrNames = attrName.substr(2).split(':')
                let name = attrNames[0]
                let arg = attrNames[1]
                let key = attr.value

                // 獲取註冊的指令並執行
                if (directives[name]) {
                    node.vm = this.vm
                    // 執行指令綁定
                    directives[name].bind(node, {
                        name: name,
                        value: this.vm[key],
                        argument: arg,
                        expression: key
                    })
                }
            }
        })
    }

    compileText(node) {
        // 利用正則表達式匹配插值表達式
        let reg = /\{\{(.+?)\}\}/
        const value = node.textContent
        if (reg.test(value)) {
            let key = RegExp.$1.trim()
            node.textContent = value.replace(reg, this.vm[key])
        }
    }

    // 判斷元素屬性是不是指令,簡化vue原來邏輯,如今默認只有v-開頭的屬性是指令
    isDirective(attrName) {
        return attrName.startsWith('v-')
    }
    // 判斷節點是不是文本節點
    isTextNode(node) {
        return node.nodeType === 3
    }
    // 判斷節點是不是元素節點
    isElementNode(node) {
        return node.nodeType === 1
    }
}

修改vue的構造函數,啓動模版編譯。

constructor(options) {
        this.$options = Object.assign(Vue.options || {}, options || {})
        this.$data = options.data || {}
        this.$methods = options.methods || {}
        if (options && options.el) {
            this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
        }
        this._proxyData(this.$data)
        this._proxyMethods(this.$methods)
        // 實現數據攔截
        new Observer(this.$data)
        // 啓動模版編譯
        new Compiler(this)
}

dep

dep負責收集某個屬性的全部觀察者,當屬性值發生變化的時候,會依次執行觀察者的update方法。

  • 屬性

subs 記錄全部的觀察者

  • 方法

addSub 添加觀察者
notify 觸發執行全部觀察者的update方法

  • 實現
class Dep {
    constructor() {
        // 存儲全部的觀察者
        this.subs = []
    }
    // 添加觀察者
    addSub(sub) {
        if (sub && sub.update) {
            this.subs.push(sub)
        }
    }
    // 發送通知
    notify() {
        this.subs.forEach(sub => {
            sub.update()
        })
    }
}

如今的問題是什麼時候添加觀察者,什麼時候觸發更新?

image.png

從上圖能夠看出,應該在Observer中觸發攔截的時候對Dep進行操做,也就是get的時候添加觀察者,set時觸發更新。

修改observer的defineReactive方法:

defineReactive(data, key, val) {
        const that = this
        // 建立dep對象
        const dep = new Dep()
        // 若是val是一個對象,爲對象的每個屬性添加攔截
        this.walk(val)
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get() {
                // 添加依賴
                // 在watcher中,獲取屬性值的時候,會把相應的觀察者添加到Dep.target屬性上
                Dep.target && dep.addSub(Dep.target)
                return val
            },
            set(newValue) {
                if (val === newValue) {
                    return
                }
                // 若是賦值爲一個對象,爲對象的每個屬性添加攔截
                that.walk(newValue)
                val = newValue
                // 觸發更新
                dep.notify()
            }
        })
}

watcher

watcher是觀察者對象,在vue對象的屬性發生變化的時候執行相應的更新操做。

  • 方法

update 執行具體的更新操做

  • 實現
class Watcher {
    // vm: vue實例
    // key: 監控的屬性鍵值
    // cb: 回調函數,執行具體更新
    constructor(vm, key, cb) {
        this.vm = vm
        this.key = key
        this.cb = cb

        // 指定在這個執行環境下的watcher實例
        Dep.target = this
        // 獲取舊的數據,觸發get方法中Dep.addSub
        this.oldValue = vm[key]
        // 刪除target,等待下一次賦值
        Dep.target = null
    }

    update() {
        let newValue = this.vm[this.key]
        if (this.oldValue === newValue) {
            return
        }
        this.cb(newValue)
        this.oldValue = newValue
    }
}

因爲須要數據雙向綁定,在compiler編譯模版的時候,建立Watcher實例,並指定具體如何更新頁面。

compileElement(node) {
        const directives = this.vm.$options.directives
        Array.from(node.attributes).forEach(attr => {
            // 判斷是不是指令
            let attrName = attr.name
            if (this.isDirective(attrName)) {
                // v-text --> text
                // 獲取指令的相關數據
                let attrNames = attrName.substr(2).split(':')
                let name = attrNames[0]
                let arg = attrNames[1]
                let key = attr.value

                // 獲取註冊的指令並執行
                if (directives[name]) {
                    node.vm = this.vm
                    // 執行指令綁定
                    directives[name].bind(node, {
                        name: name,
                        value: this.vm[key],
                        argument: arg,
                        expression: key
                    })

                    new Watcher(this.vm, key, () => {
                        directives[name].update(node, {
                            name: name,
                            value: this.vm[key],
                            argument: arg,
                            expression: key
                        })
                    })
                }
            }
        })
    }
相關文章
相關標籤/搜索