Vue 響應式原理模擬

一、三個概念


1. 數據驅動

2. 數據響應式核心原理

// 模擬 Vue 中的 data 選項
let data = {
    msg: 'hello'
}
// 模擬 Vue 的實例
let vm = {}
// 數據劫持:當訪問或者設置 vm 中的成員的時候,做一些干預操作
Object.defineProperty(vm, 'msg'{
    // 可枚舉(可遍歷)
    enumerable: true,
    // 可配置(可以使用 delete 刪除,可以通過 defineProperty 重新定義)
    configurable: true,
    // 當獲取值的時候執行
    get () {
        console.log('get: ', data.msg)
        return data.msg
    },
    // 當設置值的時候執行
    set (newValue) {
        console.log('set: ', newValue)
        if (newValue === data.msg) {
            return
        }
        data.msg = newValue
        // 數據更改,更新 DOM 的值
        document.querySelector('#app').textContent = data.msg
    }
})
// 測試
vm.msg = 'Hello World'
console.log(vm.msg)
// 模擬 Vue 中的 data 選項
let data = {
    msg: 'hello',
    count: 0
}
// 模擬 Vue 實例
let vm = new Proxy(data, {
    // 當訪問 vm 的成員會執行
    get (target, key) {
        console.log('get, key: ', key, target[key])
        return target[key]
    },
    // 當設置 vm 的成員會執行
    set (target, key, newValue) {
        console.log('set, key: ', key, newValue)
        if (target[key] === newValue) {
            return
        }
        target[key] = newValue
        document.querySelector('#app').textContent = target[key]
    }
})
// 測試
vm.msg = 'Hello World'
console.log(vm.msg)

3. 發佈訂閱模式和觀察者模式

Ⅰ. 發佈訂閱模式

我們假定,存在一個 "信號中心",某個任務執行完成,就向信號中心 "發佈"(publish)一個信號,其他任務可以向信號中心 "訂閱"(subscribe)這個信號,從而知道什麼時候自己可以開始執行。這就叫做 "發佈 / 訂閱模式"(publish-subscribe pattern)

let vm = new Vue()
vm.$on('dataChange'() ={
    console.log('dataChange')
})
vm.$on('dataChange'() ={
    console.log('dataChange1')
})
vm.$emit('dataChange')
// eventBus.js
// 事件中心
let eventHub = new Vue()
// ComponentA.vue
// 發佈者
addTodo: function () {
    // 發佈消息(事件)
    eventHub.$emit('add-todo'{ text: this.newTodoText })
    this.newTodoText = ''
}
// ComponentB.vue
// 訂閱者
created: function () {
    // 訂閱消息(事件)
    eventHub.$on('add-todo', this.addTodo)
}
class EventEmitter {
    constructor () {
        // { eventType: [ handler1, handler2 ] }
        this.subs = {}
    }
    // 訂閱通知
    $on (eventType, handler) {
        this.subs[eventType] = this.subs[eventType] || []
        this.subs[eventType].push(handler)
    }
    // 發佈通知
    $emit (eventType) {
        if (this.subs[eventType]) {
            this.subs[eventType].forEach(handler ={
                handler()
            })
        }
    }
}
// 測試
var bus = new EventEmitter()
// 註冊事件
bus.$on('click'function () {
    console.log('click')
})
bus.$on('click'function () {
    console.log('click1')
})
// 觸發事件
bus.$emit('click')

Ⅱ. 觀察者模式

// 目標(發佈者)
// Dependency
class Dep {
    constructor () {
        // 存儲所有的觀察者
        this.subs = []
    }
    // 添加觀察者
    addSub (sub) {
        if (sub && sub.update) {
        this.subs.push(sub)
        }
    }
    // 通知所有觀察者
    notify () {
        this.subs.forEach(sub ={
            sub.update()
        })
    }
}
// 觀察者(訂閱者)
class Watcher {
    update () {
        console.log('update')
    }
}
// 測試
let dep = new Dep()
let watcher = new Watcher()
dep.addSub(watcher)
dep.notify()

Ⅲ. 總結

image.png

二、Vue 響應式原理模擬

1. Vue

image.png

class Vue {
    constructor (options) {
        // 1. 保存選項的數據
        this.$options = options || {}
        this.$data = options.data || {}
        const el = options.el
        this.$el = typeof options.el === 'string' ? document.querySelector(el) : el
        // 2. 負責把 data 注入到 Vue 實例
        this._proxyData(this.$data)
        // 3. 負責調用 Observer 實現數據劫持
        new Observer(this.$data)
        // 4. 負責調用 Compiler 解析指令/插值表達式等
        new Compiler(this)
    }
    _proxyData (data) {
        // 遍歷 data 的所有屬性
        Object.keys(data).forEach(key ={
            Object.defineProperty(this, key, {
                get () {
                    return data[key]
                },
                set (newValue) {
                    if (data[key] === newValue) {
                        return
                    }
                    data[key] = newValue
                }
            })
        })
    }
}

2. Observer

image.png

// 負責數據劫持
// 把 $data 中的成員轉換成 getter/setter
class Observer {
    constructor(data) {
        this.walk(data)
    }
    // 1. 判斷數據是否是對象,如果不是對象返回
    // 2. 如果是對象,遍歷對象的所有屬性,設置爲 getter/setter
    walk(data) {
        if (!data || typeof data !== 'object') {
            return
        }
        // 遍歷 data 的所有成員
        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, {
            configurable: true,
            enumerable: true,
            get() {
                return val
            },
            set(newValue) {
                if (newValue === val) {
                    return
                }
                // 如果 newValue 是對象,設置 newValue 的成員爲響應式
                that.walk(newValue)
                val = newValue
            }
        })
    }
}

3. Compiler

image.png

① compile()

// 負責解析指令/插值表達式
class Compiler {
    constructor(vm) {
        this.vm = vm
        this.el = vm.$el
        // 編譯模板
        this.compile(this.el)
    }
    // 編譯模板
    // 處理文本節點和元素節點
    compile(el) {
        const nodes = el.childNodes
        Array.from(nodes).forEach(node ={
            // 判斷是文本節點還是元素節點
            if (this.isTextNode(node)) {
                this.compileText(node)
            } else if (this.isElementNode(node)) {
                this.compileElement(node)
            }
            if (node.childNodes && node.childNodes.length) {
                // 如果當前節點中還有子節點,遞歸編譯
                this.compile(node)
            }
        })
    }
    // 判斷是否是文本節點
    isTextNode(node) {
        return node.nodeType === 3
    }
    // 判斷是否是屬性節點
    isElementNode(node) {
        return node.nodeType === 1
    }
    // 判斷是否是以 v- 開頭的指令
    isDirective(attrName) {
        return attrName.startsWith('v-')
    }
    // 編譯文本節點
    compileText(node) {
    }
    // 編譯屬性節點
    compileElement(node) {
    }
}

② compileText()

// 編譯文本節點
compileText(node) {
    const reg = /\{\{(.+)\}\}/
    // 獲取文本節點的內容
    const value = node.textContent
    if (reg.test(value)) {
        // 插值表達式中的值就是我們要的屬性名稱
        const key = RegExp.$1.trim()
        // 把插值表達式替換成具體的值
        node.textContent = value.replace(reg, this.vm[key])
    }
}

③ compileElement()

// 編譯屬性節點
compileElement(node) {
    // 遍歷元素節點中的所有屬性,找到指令
    Array.from(node.attributes).forEach(attr ={
        // 獲取元素屬性的名稱
        let attrName = attr.name
        // 判斷當前的屬性名稱是否是指令
        if (this.isDirective(attrName)) {
            // attrName 的形式 v-text v-model
            // 截取屬性的名稱,獲取 text model
            attrName = attrName.substr(2)
            // 獲取屬性的名稱,屬性的名稱就是我們數據對象的屬性 v-text="name",獲取的是name
            const key = attr.value
            // 處理不同的指令
            this.update(node, key, attrName)
        }
    })
}
// 負責更新 DOM
// 創建 Watcher
update(node, key, dir) {
    // node 節點,key 數據的屬性名稱,dir 指令的後半部分
    const updaterFn = this[dir + 'Updater']
    updaterFn && updaterFn(node, this.vm[key])
}
// v-text 指令的更新方法
textUpdater(node, value) {
    node.textContent = value
}
// v-model 指令的更新方法
modelUpdater(node, value) {
    node.value = value
}

4. Dep

image.png

class Dep {
    constructor() {
        // 存儲所有的觀察者
        this.subs = []
    }
    // 添加觀察者
    addSub(sub) {
        if (sub && sub.update) {
            this.subs.push(sub)
        }
    }
    // 通知所有觀察者
    notify() {
        this.subs.forEach(sub ={
            sub.update()
        })
    }
}
// 以下代碼在 Observer 類中 defineReactive 方法中添加
// 創建 dep 對象收集依賴
const dep = new Dep()
// getter 中
// get 的過程中收集依賴
Dep.target && dep.addSub(Dep.target)
// setter 中
// 當數據變化之後,發送通知
dep.notify()

5. Watcher

image.png

class Watcher {
    constructor(vm, key, cb) {
        this.vm = vm
        // data 中的屬性名稱
        this.key = key
        // 當數據變化的時候,調用 cb 更新視圖
        this.cb = cb
        // 在 Dep 的靜態屬性上記錄當前 watcher 對象,當訪問數據的時候把 watcher 添加到dep 的 subs 中
        Dep.target = this
        // 觸發一次 getter,讓 dep 爲當前 key 記錄 watcher
        this.oldValue = vm[key]
        // 清空 target
        Dep.target = null
    }
    update() {
        const newValue = this.vm[this.key]
        if (this.oldValue === newValue) {
            return
        }
        this.cb(newValue)
    }
}

// 在 compiler.js(即Compiler類) 中爲每一個指令/插值表達式創建 watcher 對象,監視數據的變化
compileText(node) {
    const reg = /\{\{(.+?)\}\}/
    const value = node.textContent
    if (reg.test(value)) {
        const key = RegExp.$1.trim()
        node.textContent = value.replace(reg, this.vm[key])
        // 編譯差值表達式中創建一個 watcher,觀察數據的變化
        new Watcher(this.vm, key, newValue ={
            node.textContent = newValue
        })
    }
}
// 因爲在 textUpdater等中要使用 this
updaterFn && updaterFn.call(this, node, this.vm[key], key)
// v-text 指令的更新方法
textUpdater(node, value, key) {
    node.textContent = value
    // 每一個指令中創建一個 watcher,觀察數據的變化
    new Watcher(this.vm, key, value ={
        node.textContent = value
    })
}

// 視圖變化更新數據
// v-model 指令的更新方法
modelUpdater(node, value, key) {
    node.value = value
    // 每一個指令中創建一個 watcher,觀察數據的變化
    new Watcher(this.vm, key, value ={
        node.value = value
    })
    // 監聽視圖的變化
    node.addEventListener('input'() ={
        this.vm[key] = node.value
    })
}

三、總結

1. 兩個問題你會了嗎

2. 通過下圖回顧整體流程

image.png

3. Vue

4. Observer

5. Compiler

6. Dep

7. Watcher

關於本文

作者:昆蘭

https://juejin.cn/post/6946120511713705992

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/8QWMOaGIN0yKgbl2yMuj6w