1. 程式人生 > 實用技巧 >手寫Vue2.0響應式原理

手寫Vue2.0響應式原理

今天來實現一個簡易版的Vue2.0響應式

class Vue {
    constructor(options) {
        this.$options = options
        this.$data = options.data

        // 重寫陣列方法
        let arrayPrototype = Array.prototype
        const methods = ['pop', 'push', 'shift', 'unshift']
        this.proto = Object.create(arrayPrototype)
        methods.forEach(method => {
            this.proto[method] = function() {
                arrayPrototype[method].call(this, ...arguments)
            }
        })

        // 響應化
        this.observe(this.$data)

        // 測試程式碼
        // new Watcher(this, 'test')
        // this.test

        // 建立編譯器
        // new Compile(options.el, this)

        if (options.created) {
            options.created.call(this)
        }
    }
    

    // 遞迴遍歷,使傳遞進來的物件響應化
    observe(value) {
        if (!value || typeof value !== 'object') {
            return
        }

        if (Array.isArray(value)) {
            Object.setPrototypeOf(value, this.proto)
        }

        Object.keys(value).forEach(key => {
            // 對key做響應式處理
            this.defineReactive(value, key, value[key])
            this.proxyData(key)
        })
    }

    // 在Vue根上定義屬性代理data中的資料,這樣就能通過 this 呼叫資料
    proxyData(key) {
        Object.defineProperty(this, key, {
            get() {
                return this.$data[key]
            },
            set(newVal) {
                this.$data[key] = newVal
            }
        })
    }

    defineReactive(obj, key, val) { 
        // 遞迴響應,處理巢狀物件
        this.observe(val)

        // 建立Dep例項: Dep和key一對一對應
        const dep = new Dep()

        // 給obj定義屬性
        Object.defineProperty(obj, key, {
            get() {
                // 將Dep.target指向的Watcher例項加入到Dep中, 這部分是收集依賴
                Dep.target && dep.addDep(Dep.target)
                console.log('get')
                return val
            },

            set(newVal) {
                if (newVal !== val) {
                    val = newVal
                    console.log('set')
                    // console.log(`${key}屬性更新了`)
                    dep.notify() // 通知檢視更新
                }
            }
        })
    }
}

// Dep: 管理若干watcher例項,它和key一對一關係
class Dep {
    constructor() {
        this.deps = []
    }

    addDep(watcher) {
        this.deps.push(watcher)
    }

    notify() {
        this.deps.forEach(watcher => watcher.update())
    }
}

// 實現update函式可以更新
class Watcher {
    constructor(vm, key, cb) {
        this.vm = vm
        this.key = key
        this.cb = cb

        // 將當前例項指向Dep.target
        Dep.target = this
        this.vm[this.key]
        Dep.target = null
    }

    update() {
        console.log(`${this.key}屬性更新了`)
        this.cb.call(this.vm, this.vm[this.key])
    }
}

這樣就實現了一個簡易版的Vue,Vue2.0有兩個比較明顯的問題:

  1. 需要注意的是Object.defineProperty的缺點是不能代理陣列,所以我們需要對陣列的方法進行重寫,詳細部分請重新看上面的程式碼,這部分是面試要考的重點。
  2. Vue2.0還有一個比較明顯的缺點就是,物件上不存在的屬性,不能被代理,因為從上面的程式碼,我們能看出observe遍歷的是物件上已經有的屬性,所以沒有的屬性就不會被代理,我們就必須通過呼叫$set()方法去將新加的屬性響應式,這個缺點會在Vue3.0中彌補,有興趣的同學可以看看Vue3.0的原始碼。
  3. 我們能看到在defineReactive一進來就呼叫了observe方法,一是需要代理根物件,二是能代理巢狀物件,而且在new Vue的過程中一次遞迴代理了所有的物件,這樣就會出現一個明顯的問題,就是一旦data中的資料層級特別深的時候,就會出現頁面渲染比較慢的現象。而且我有一點不明白的地方是為什麼在遞迴代理的時候,為什麼不優化一下演算法,可以利用棧的思想實現深度優先的非遞迴實現,然後迴圈這個棧去代理物件,當然了,尤大大這麼寫,肯定是人家的原因,但是這是我原始碼的時候,比較困惑的地方,如果有知道的同學,希望能在下方評論給我解答一下。