Vue 響應式原理模擬以及最小版本的 Vue的模擬

在模擬最小的vue之前,先複習一下,發佈訂閱模式和觀察者模式

對兩種模式有了瞭解之後,對Vue2.0和Vue3.0的數據響應式核心原理

1.Vue2.0和Vue3.0的數據響應式核心原理

(1).  Vue2.0是採用Object.defineProperty的方式,對數據進行get,set方法設置的, 具體可以詳見Object.defineProperty的介紹

瀏覽器兼容 IE8 以上(不兼容 IE8)
<script>
    // 模擬 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)
  </script>

如果,vm裏的屬性是對象如何處理,可以,對其遍歷,在進行Object.defineProperty

<script>
    // 模擬 Vue 中的 data 選項
    let data = {
      msg: 'hello',
      count: 10,
      person: {name: 'zhangsan'}
    }

    // 模擬 Vue 的實例
    let vm = {}

    proxyData(data)

    function proxyData(data) {
      // 遍歷 data 對象的所有屬性
      Object.keys(data).forEach(key => {
        // 把 data 中的屬性,轉換成 vm 的 setter/setter
        Object.defineProperty(vm, key, {
          enumerable: true,
          configurable: true,
          get () {
            console.log('get: ', key, data[key])
            return data[key]
          },
          set (newValue) {
            console.log('set: ', key, newValue)
            if (newValue === data[key]) {
              return
            }
            data[key] = newValue
            // 數據更改,更新 DOM 的值
            document.querySelector('#app').textContent = data[key]
          }
        })
      })
    }

    // 測試
    vm.msg = 'Hello World'
    console.log(vm.msg)
  </script>

(2). Vue3.x是採用proxy代理的方式實現, 直接監聽對象,而非屬性。ES 6中新增,IE 不支持,性能由瀏覽器優化,具體可以詳見MDN - Proxy

<script>
    // 模擬 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)
  </script>

2.Vue 響應式原理模擬

看圖,整體分析

 Vue
  • 把 data 中的成員注入到 Vue 實例,並且把 data 中的成員轉成 getter/setter
Observer
  • 能夠對數據對象的所有屬性進行監聽,如有變動可拿到最新值並通知 Dep
Compiler
  • 解析每個元素中的指令/插值表達式,並替換成相應的數據
Dep
  • 添加觀察者(watcher),當數據變化通知所有觀察者
Watcher
  • 數據變化更新視圖 

 (1) Vue

功能
  • 負責接收初始化的參數(選項)
  • 負責把 data 中的屬性注入到 Vue 實例,轉換成 getter/setter
  • 負責調用 observer 監聽 data 中所有屬性的變化
  • 負責調用 compiler 解析指令/插值表達式
class Vue {
    constructor (options) {
        //1.通過屬性保存選項的數據
        this.$options = options || {}
        this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
        this.$data = options.data || {}
        //2.把data中的成員轉換成getter和setter方法,注入到vue實例中
        this._proxyData(this.$data)
        //3.調用observer對象,監聽數據變化
        new Observer(this.$data)
        //4.調用compiler對象, 解析指令和差值表達式
        new Compiler(this)
    }

    _proxyData (data) {
        //遍歷data中的所有屬性
        Object.keys(data).forEach( key => {
            //把data的屬性注入到vue實例中
            Object.defineProperty(this, key, {
                enumerable: true,
                configurable: true,
                get () {
                    return data[key]
                },
                set (newValue) {
                    if (newValue === data[key]) {
                        return 
                    }
                    data[key] = newValue
                }

            })
        })

    }

}

(2)Observer

功能
  • 負責把 data 選項中的屬性轉換成響應式數據
  • data 中的某個屬性也是對象,把該屬性轉換成響應式數據
  • 數據變化發送通知 
class Observer {
    constructor (data) {
        this.walk(data)
    }
    //1.
    walk (data) {
        //1.判斷data是不是對象
        if (!data || typeof data !== 'object') {
            return
        }
        //遍歷data對象裏的所有屬性
        Object.keys(data).forEach( key => {
            this.definedReactive(data, key, data[key])
        })
    }

    definedReactive (obj, key, value) {
        let that = this
        //負責收集依賴(觀察者), 併發送通知
        let dep = new Dep()

        this.walk(value)//如果data裏的屬性是對象,對象裏面的屬性也得是響應式的,所以得判斷一下
        
        Object.defineProperty (obj, key, {
            enumerable: true,
            configurable: true,
            get () {
                //收集依賴
                Dep.target && dep.addSubs(Dep.target)
                return value
                // return obj[key]//這麼寫會引起堆棧溢出
            },
            set (newValue) {
                if (newValue === value) {
                    return 
                }
                
                value = newValue
                that.walk(newValue)//如果賦值爲對象,對象裏面的屬性得是響應式數據

                //數據變換 ,發送通知給watcher的update ,在渲染視圖裏的數據
                dep.notify()
            }    
                
        }) 
    }

}

(3).Compiler

功能
  • 負責編譯模板,解析指令/插值表達式
  • 負責頁面的首次渲染
  • 當數據變化後重新渲染視圖 
class Compiler {

    constructor (vm) {//傳個vue實例
        this.el = vm.$el
        this.vm = vm
        this.compile(this.el)
    }

    //編譯模板, 處理文本節點和元素節點
    compile (el) {

        let childNodes = el.childNodes //獲取子節點  僞數組
        console.dir(el.childNodes)
        Array.from(childNodes).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)
            }
        })
    }

    //編譯元素節點, 處理指令
    compileElement (node) {
        //console.log(node.attributes)

        Array.from(node.attributes).forEach( attr => {

            //判斷是不是指令
            let attrName = attr.name //<div v-text="msg"></div> 裏的v-text
            if (this.isDirective(attrName)) {
                //v-text --> text
                attrName = attrName.substr(2)
                let key = attr.value   //<div v-text="msg"></div> 裏的msg
                this.update(node , key, attrName) 
            }
        })
    }

    update (node, key, attrName) {
        let updateFn = this[attrName + 'Updater']
        updateFn && updateFn.call(this, node, this.vm[key], key)//call方法改變this指向
    }
    //處理v-text 命令
    textUpdater (node, value, key) {
        node.textContent = value
        new Watcher(this.vm, key, (newValue) => {
            node.textContent = newValue
        })
    }
    //v-model
    modelUpdater (node, value, key) {
        node.value = value
        new Watcher(this.vm, key, (newValue) => {
            node.value = newValue
        })

        //雙向綁定,視圖改變,數據也會更新
        node.addEventListener('input', () => {
            this.vm[key] = node.value
        })
    }

    //編譯文本節點,處理差值表達式
    compileText (node) {
        //console.dir(node)
        // {{  msg   }}
        let reg = /\{\{(.+?)\}\}/
        let value = node.textContent //裏面的內容, 也可以是nodeValue
        if (reg.test(value)) {
            let key = RegExp.$1.trim()  //匹配到的第一個
            node.textContent = value.replace(reg, this.vm[key])

            //創建watcher對象, 當數據改變更新視圖
            new Watcher(this.vm, key, (newValue) => {
                node.textContent = newValue
            })
        }
    }

    //判斷元素屬性是否是指令
    isDirective (attrName) {
        return attrName.startsWith('v-')
    }

    //判斷節點是否是文本節點
    isTextNode (node) {
        return node.nodeType === 3
    }

    //判斷節點是否是元素節點
    isElementNode (node) {
        return node.nodeType === 1
    }
}

(4).Dep(Dependency)

 

 功能

  • 收集依賴,添加觀察者(watcher)
  • 通知所有觀察者 
class Dep {

    constructor () {
        //收集觀察者
        this.subs = []
    }

    //添加觀察者
    addSubs (watcher) {
        if (watcher && watcher.update) {
            this.subs.push(watcher)
        }
    }
    //數據變換,就調watcher的update方法
    notify () {
        this.subs.forEach(watcher => {
            watcher.update()
        });
    }
}

(5).Watcher

 

 功能

  • 當數據變化觸發依賴, dep 通知所有的 Watcher 實例更新視圖
  • 自身實例化的時候往 dep 對象中添加自己 
class Watcher {
    constructor (vm, key, callback) {
        this.vm = vm
        //data中的屬性名
        this.key = key
        this.callback = callback
        //將watcher對象記錄在Dep的靜態屬性target
        Dep.target = this
        //觸發get方法,觸發get裏的addsubs方法,添加watcher
        this.oldValue = vm[key]
        Dep.target = null
    }

    //當數據變化的時候,更新視圖
    update () {
        let newValue = this.vm[this.key]
        if (this.oldValue === newValue) {
            return
        }
        this.callback(newValue)
    }
}

總結:

 

 Vue

  • 記錄傳入的選項,設置 $data/$el
  • data 的成員注入到 Vue 實例
  • 負責調用 Observer 實現數據響應式處理(數據劫持)
  • 負責調用 Compiler 編譯指令/插值表達式等
Observer
  • 數據劫持
  • 負責把 data 中的成員轉換成 getter/setter
  • 負責把多層屬性轉換成 getter/setter
  • 如果給屬性賦值爲新對象,把新對象的成員設置爲 getter/setter
  • 添加 Dep Watcher 的依賴關係
  • 數據變化發送通知
Compiler
  • 負責編譯模板,解析指令/插值表達式
  • 負責頁面的首次渲染過程
  • 當數據變化後重新渲染
Dep
  • 收集依賴,添加訂閱者(watcher)
  • 通知所有訂閱者
Watcher
  • 自身實例化的時候往dep對象中添加自己
  • 當數據變化dep通知所有的 Watcher 實例更新視圖

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章