溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

VUE響應式原理實例代碼分析

發(fā)布時間:2022-08-27 11:13:44 來源:億速云 閱讀:132 作者:iii 欄目:編程語言

這篇“VUE響應式原理實例代碼分析”文章的知識點大部分人都不太理解,所以小編給大家總結(jié)了以下內(nèi)容,內(nèi)容詳細,步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“VUE響應式原理實例代碼分析”文章吧。

Vue2.X響應式原理

1.defineProperty 的應用

Vue2.X 響應式中使用到了 defineProperty  進行數(shù)據(jù)劫持,所以我們對它必須有一定的了解,那么我們先來了解它的使用方法把, 這里我們來使用 defineProperty來模擬 Vue 中的 data

<body>
    <div id="app"></div>
    <script>
        // 模擬 Vue的data
        let data = {
            msg: '',
        }
        // 模擬 Vue 實例
        let vm = {}
        // 對 vm 的 msg 進行數(shù)據(jù)劫持
        Object.defineProperty(vm, 'msg', {
            // 獲取數(shù)據(jù)
            get() {
                return data.msg
            },
            // 設(shè)置 msg
            set(newValue) {
                // 如果傳入的值相等就不用修改
                if (newValue === data.msg) return
                // 修改數(shù)據(jù)
                data.msg = newValue
                document.querySelector('#app').textContent = data.msg
            },
        })
        // 這樣子就調(diào)用了 defineProperty vm.msg 的 set
        vm.msg = '1234'
    </script>
</body>

2.defineProperty修改多個參數(shù)為響應式

修改多個參數(shù)

看了上面的方法只能修改一個屬性,實際上我們 data 中數(shù)據(jù)不可能只有一個,我們何不定義一個方法把data中的數(shù)據(jù)進行遍歷都修改成響應式呢

<body>
    <div id="app"></div>
	<script>
        // 模擬 Vue的data
        let data = {
            msg: '哈哈',
            age: '18',
        }
        // 模擬 Vue 實例
        let vm = {}
        // 把多個屬性轉(zhuǎn)化 響應式
        function proxyData() {
            // 把data 中每一項都[msg,age] 拿出來操作
            Object.keys(data).forEach((key) => {
                // 對 vm 的 屬性 進行數(shù)據(jù)劫持
                Object.defineProperty(vm, key, {
                    // 可枚舉
                    enumerable: true,
                    // 可配置
                    configurable: true,
                    // 獲取數(shù)據(jù)
                    get() {
                        return data[key]
                    },
                    // 設(shè)置 屬性值
                    set(newValue) {
                        // 如果傳入的值相等就不用修改
                        if (newValue === data[key]) return
                        // 修改數(shù)據(jù)
                        data[key] = newValue
                        document.querySelector('#app').textContent = data[key]
                    },
                })
            })
        }
        // 調(diào)用方法
        proxyData(data)

	</script>
</body>

3.Proxy

Vue3 中使用 Proxy 來設(shè)置響應式的屬性

先來了解下 Proxy 的兩個參數(shù)

new Proxy(target,handler)

  • target :要使用 Proxy 包裝的目標對象(可以是任何類型的對象,包括原生數(shù)組,函數(shù),甚至另一個代理)

  • handler:一個通常以函數(shù)作為屬性的對象,各屬性中的函數(shù)分別定義了在執(zhí)行各種操作時代理 p 的行為

其實 和 Vue2.X實現(xiàn)的邏輯差不多,不過實現(xiàn)的方法不一樣

那么就放上代碼了

<body>
    <div id="app"></div>
    <script>
            // 模擬 Vue data
            let data = {
                msg: '',
                age: '',
            }
            // 模擬 Vue 的一個實例
            // Proxy 第一個
            let vm = new Proxy(data, {
                // get() 獲取值
                // target 表示需要代理的對象這里指的就是 data
                // key 就是對象的 鍵
                get(target, key) {
                    return target[key]
                },
                // 設(shè)置值
                // newValue 是設(shè)置的值
                set(target, key, newValue) {
                    // 也先判斷下是否和之前的值一樣 節(jié)省性能
                    if (target[key] === newValue) return
                    // 進行設(shè)置值
                    target[key] = newValue
                    document.querySelector('#app').textContent = target[key]
                },
            })
    </script>
</body>

觸發(fā)setget 的方法

// 觸發(fā)了set方法
vm.msg = 'haha'
// 觸發(fā)了get方法
console.log(vm.msg)

4.發(fā)布訂閱模式

在Vue 響應式中應用到了 發(fā)布訂閱模式 我們先來了解下

首先來說簡單介紹下 一共有三個角色

發(fā)布者、 訂閱者、  信號中心  舉個現(xiàn)實中例子 作者(發(fā)布者)寫一篇文章 發(fā)到了掘金(信號中心) ,掘金可以處理文章然后推送到了首頁,然后各自大佬(訂閱者)就可以訂閱文章

在Vue 中的例子 就是EventBus $on $emit

那么我們就簡單模仿一下 Vue 的事件總線吧

之前代碼縮進4個單位有點寬,這里改成2個

<body>
  <div id="app"></div>
  <script>
    class Vue {
      constructor() {
        // 用來存儲事件
        // 存儲的 例子 this.subs = { 'myclick': [fn1, fn2, fn3] ,'inputchange': [fn1, fn2] }
        this.subs = {}
      }
      // 實現(xiàn) $on 方法 type是任務隊列的類型 ,fn是方法
      $on(type, fn) {
        // 判斷在 subs是否有當前類型的 方法隊列存在
        if (!this.subs[type]) {
          // 沒有就新增一個 默認為空數(shù)組
          this.subs[type] = []
        }
        // 把方法加到該類型中
        this.subs[type].push(fn)
      }
      // 實現(xiàn) $emit 方法
      $emit(type) {
        // 首先得判斷該方法是否存在
        if (this.subs[type]) {
          // 獲取到參數(shù)
          const args = Array.prototype.slice.call(arguments, 1)
          // 循環(huán)隊列調(diào)用 fn
          this.subs[type].forEach((fn) => fn(...args))
        }
      }
    }

    // 使用
    const eventHub = new Vue()
    // 使用 $on 添加一個 sum 類型的 方法到 subs['sum']中
    eventHub.$on('sum', function () {
      let count = [...arguments].reduce((x, y) => x + y)
      console.log(count)
    })
    // 觸發(fā) sum 方法
    eventHub.$emit('sum', 1, 2, 4, 5, 6, 7, 8, 9, 10)
  </script>
</body>

5.觀察者模式

與 發(fā)布訂閱 的差異

與發(fā)布訂閱者不同 觀察者中 發(fā)布者和訂閱者(觀察者)是相互依賴的 必須要求觀察者訂閱內(nèi)容改變事件 ,而發(fā)布訂閱者是由調(diào)度中心進行調(diào)度,那么看看觀察者模式 是如何相互依賴,下面就舉個簡單例子

<body>
  <div id="app"></div>
  <script>
    // 目標
    class Subject {
      constructor() {
        this.observerLists = []
      }
      // 添加觀察者
      addObs(obs) {
        // 判斷觀察者是否有 和 存在更新訂閱的方法
        if (obs && obs.update) {
          // 添加到觀察者列表中
          this.observerLists.push(obs)
        }
      }
      // 通知觀察者
      notify() {
        this.observerLists.forEach((obs) => {
          // 每個觀察者收到通知后 會更新事件
          obs.update()
        })
      }
      // 清空觀察者
      empty() {
        this.subs = []
      }
    }

    class Observer {
      // 定義觀察者內(nèi)容更新事件
      update() {
        // 在更新事件要處理的邏輯
        console.log('目標更新了')
      }
    }

    // 使用
    // 創(chuàng)建目標
    let sub = new Subject()
    // 創(chuàng)建觀察者
    let obs1 = new Observer()
    let obs2 = new Observer()
    // 把觀察者添加到列表中
    sub.addObs(obs1)
    sub.addObs(obs2)
    // 目標開啟了通知 每個觀察者者都會自己觸發(fā) update 更新事件
    sub.notify()
  </script>
</body>

6.模擬Vue的響應式原理

這里來實現(xiàn)一個小型簡單的 Vue 主要實現(xiàn)以下的功能

  • 接收初始化的參數(shù),這里只舉幾個簡單的例子 el data options

  • 通過私有方法 _proxyDatadata 注冊到 Vue 中 轉(zhuǎn)成getter setter

  • 使用 observerdata 中的屬性轉(zhuǎn)為 響應式 添加到 自身身上

  • 使用 observer 方法監(jiān)聽 data 的所有屬性變化來 通過觀察者模式 更新視圖

  • 使用 compiler 編譯元素節(jié)點上面指令 和 文本節(jié)點差值表達式

1.vue.js

在這里獲取到 el data

通過 _proxyDatadata的屬性 注冊到Vue 并轉(zhuǎn)成 getter setter

/* vue.js */

class Vue {
  constructor(options) {
    // 獲取到傳入的對象 沒有默認為空對象
    this.$options = options || {}
    // 獲取 el
    this.$el =
      typeof options.el === 'string'
        ? document.querySelector(options.el)
        : options.el
    // 獲取 data
    this.$data = options.data || {}
    // 調(diào)用 _proxyData 處理 data中的屬性
    this._proxyData(this.$data)
  }
  // 把data 中的屬性注冊到 Vue
  _proxyData(data) {
    Object.keys(data).forEach((key) => {
      // 進行數(shù)據(jù)劫持
      // 把每個data的屬性 到添加到 Vue 轉(zhuǎn)化為 getter setter方法
      Object.defineProperty(this, key, {
        // 設(shè)置可以枚舉
        enumerable: true,
        // 設(shè)置可以配置
        configurable: true,
        // 獲取數(shù)據(jù)
        get() {
          return data[key]
        },
        // 設(shè)置數(shù)據(jù)
        set(newValue) {
          // 判斷新值和舊值是否相等
          if (newValue === data[key]) return
          // 設(shè)置新值
          data[key] = newValue
        },
      })
    })
  }
}

2.observer.js

在這里把 data 中的 屬性變?yōu)轫憫郊釉谧陨淼纳砩?,還有一個主要功能就是 觀察者模式在 第 4.dep.js 會有詳細的使用

/* observer.js */

class Observer {
  constructor(data) {
    // 用來遍歷 data
    this.walk(data)
  }
  // 遍歷 data 轉(zhuǎn)為響應式
  walk(data) {
    // 判斷 data是否為空 和 對象
    if (!data || typeof data !== 'object') return
    // 遍歷 data
    Object.keys(data).forEach((key) => {
      // 轉(zhuǎn)為響應式
      this.defineReactive(data, key, data[key])
    })
  }
  // 轉(zhuǎn)為響應式
  // 要注意的 和vue.js 寫的不同的是
  // vue.js中是將 屬性給了 Vue 轉(zhuǎn)為 getter setter
  // 這里是 將data中的屬性轉(zhuǎn)為getter setter
  defineReactive(obj, key, value) {
    // 如果是對象類型的 也調(diào)用walk 變成響應式,不是對象類型的直接在walk會被return
    this.walk(value)
    // 保存一下 this
    const self = this
    Object.defineProperty(obj, key, {
      // 設(shè)置可枚舉
      enumerable: true,
      // 設(shè)置可配置
      configurable: true,
      // 獲取值
      get() {
        return value
      },
      // 設(shè)置值
      set(newValue) {
        // 判斷舊值和新值是否相等
        if (newValue === value) return
        // 設(shè)置新值
        value = newValue
        // 賦值的話如果是newValue是對象,對象里面的屬性也應該設(shè)置為響應式的
        self.walk(newValue)
      },
    })
  }
}

在html中引入的話注意順序

<script src="./js/observer.js"></script>
<script src="./js/vue.js"></script>

然后在vue.js 中使用 Observer

/* vue.js */

class Vue {
  constructor(options) {
    ...
    // 使用 Obsever 把data中的數(shù)據(jù)轉(zhuǎn)為響應式
    new Observer(this.$data)
  }
  // 把data 中的屬性注冊到 Vue
  _proxyData(data) {
   ...
  }
}

看到這里為什么做了兩個重復性的操作呢?重復性兩次把 data的屬性轉(zhuǎn)為響應式

obsever.js 中是把 data 的所有屬性 加到 data 自身 變?yōu)轫憫?轉(zhuǎn)成 getter setter方式

vue.js 中 也把 data的 的所有屬性 加到 Vue 上,是為了以后方面操作可以用 Vue 的實例直接訪問到 或者在 Vue 中使用 this 訪問

使用例子:

<body>
    <div id="app"></div>
    <script src="./js/observer.js"></script>
    <script src="./js/vue.js"></script>
    <script>
      let vm = new Vue({
        el: '#app',
        data: {
          msg: '123',
          age: 21,
        },
      })
    </script>
  </body>

VUE響應式原理實例代碼分析

這樣在Vue$data 中都存在了 所有的data 屬性了 并且是響應式的

3.compiler.js

comilper.js在這個文件里實現(xiàn)對文本節(jié)點 和 元素節(jié)點指令編譯 主要是為了舉例子 當然這個寫的很簡單 指令主要實現(xiàn) v-text v-model

/* compiler.js */

class Compiler {
  // vm 指 Vue 實例
  constructor(vm) {
    // 拿到 vm
    this.vm = vm
    // 拿到 el
    this.el = vm.$el
    // 編譯模板
    this.compile(this.el)
  }
  // 編譯模板
  compile(el) {
    // 獲取子節(jié)點 如果使用 forEach遍歷就把偽數(shù)組轉(zhuǎn)為真的數(shù)組
    let childNodes = [...el.childNodes]
    childNodes.forEach((node) => {
      // 根據(jù)不同的節(jié)點類型進行編譯
      // 文本類型的節(jié)點
      if (this.isTextNode(node)) {
        // 編譯文本節(jié)點
        this.compileText(node)
      } else if (this.isElementNode(node)) {
        //元素節(jié)點
        this.compileElement(node)
      }
      // 判斷是否還存在子節(jié)點考慮遞歸
      if (node.childNodes && node.childNodes.length) {
        // 繼續(xù)遞歸編譯模板
        this.compile(node)
      }
    })
  }
  // 編譯文本節(jié)點(簡單的實現(xiàn))
  compileText(node) {
    // 核心思想利用把正則表達式把{{}}去掉找到里面的變量
    // 再去Vue找這個變量賦值給node.textContent
    let reg = /\{\{(.+?)\}\}/
    // 獲取節(jié)點的文本內(nèi)容
    let val = node.textContent
    // 判斷是否有 {{}}
    if (reg.test(val)) {
      // 獲取分組一  也就是 {{}} 里面的內(nèi)容 去除前后空格
      let key = RegExp.$1.trim()
      // 進行替換再賦值給node
      node.textContent = val.replace(reg, this.vm[key])
    }
  }
  // 編譯元素節(jié)點這里只處理指令
  compileElement(node) {
    // 獲取到元素節(jié)點上面的所有屬性進行遍歷
    ![...node.attributes].forEach((attr) => {
      // 獲取屬性名
      let attrName = attr.name
      // 判斷是否是 v- 開頭的指令
      if (this.isDirective(attrName)) {
        // 除去 v- 方便操作
        attrName = attrName.substr(2)
        // 獲取 指令的值就是  v-text = "msg"  中msg
        // msg 作為 key 去Vue 找這個變量
        let key = attr.value
        // 指令操作 執(zhí)行指令方法
        // vue指令很多為了避免大量個 if判斷這里就寫個 uapdate 方法
        this.update(node, key, attrName)
      }
    })
  }
  // 添加指令方法 并且執(zhí)行
  update(node, key, attrName) {
    // 比如添加 textUpdater 就是用來處理 v-text 方法
    // 我們應該就內(nèi)置一個 textUpdater 方法進行調(diào)用
    // 加個后綴加什么無所謂但是要定義相應的方法
    let updateFn = this[attrName + 'Updater']
    // 如果存在這個內(nèi)置方法 就可以調(diào)用了
    updateFn && updateFn(node, key, this.vm[key])
  }
  // 提前寫好 相應的指定方法比如這個 v-text
  // 使用的時候 和 Vue 的一樣
  textUpdater(node, key, value) {
    node.textContent = value
  }
    
  // v-model
  modelUpdater(node, key, value) {
    node.value = value
  }
    
  // 判斷元素的屬性是否是 vue 指令
  isDirective(attr) {
    return attr.startsWith('v-')
  }
  // 判斷是否是元素節(jié)點
  isElementNode(node) {
    return node.nodeType === 1
  }
  // 判斷是否是 文本 節(jié)點
  isTextNode(node) {
    return node.nodeType === 3
  }
}

4.dep.js

寫一個Dep類 它相當于 觀察者中的發(fā)布者  每個響應式屬性都會創(chuàng)建這么一個 Dep 對象 ,負責收集該依賴屬性的Watcher對象 (是在使用響應式數(shù)據(jù)的時候做的操作)

當我們對響應式屬性在 setter 中進行更新的時候,會調(diào)用 Depnotify 方法發(fā)送更新通知

然后去調(diào)用 Watcher 中的 update 實現(xiàn)視圖的更新操作(是當數(shù)據(jù)發(fā)生變化的時候去通知觀察者調(diào)用觀察者的update更新視圖)

總的來說 在Dep(這里指發(fā)布者) 中負責收集依賴 添加觀察者(這里指Wathcer),然后在 setter 數(shù)據(jù)更新的時候通知觀察者

說的這么多重復的話,大家應該知道是在哪個階段 收集依賴 哪個階段 通知觀察者了吧,下面就來實現(xiàn)一下吧

先寫Dep

/* dep.js */

class Dep {
  constructor() {
    // 存儲觀察者
    this.subs = []
  }
  // 添加觀察者
  addSub(sub) {
    // 判斷觀察者是否存在 和 是否擁有update方法
    if (sub && sub.update) {
      this.subs.push(sub)
    }
  }
  // 通知方法
  notify() {
    // 觸發(fā)每個觀察者的更新方法
    this.subs.forEach((sub) => {
      sub.update()
    })
  }
}

obsever.js 中使用Dep

get 中添加 Dep.target (觀察者)

set 中 觸發(fā) notify (通知)

/* observer.js */

class Observer {
  ...
  }
  // 遍歷 data 轉(zhuǎn)為響應式
  walk(data) {
   ...
  }
  // 這里是 將data中的屬性轉(zhuǎn)為getter setter
  defineReactive(obj, key, value) {
	...
    // 創(chuàng)建 Dep 對象
    let dep = new Dep()
    Object.defineProperty(obj, key, {
	  ...
      // 獲取值
      get() {
        // 在這里添加觀察者對象 Dep.target 表示觀察者
        Dep.target && dep.addSub(Dep.target)
        return value
      },
      // 設(shè)置值
      set(newValue) {
        if (newValue === value) return
        value = newValue
        self.walk(newValue)
        // 觸發(fā)通知 更新視圖
        dep.notify()
      },
    })
  }
}

5.watcher.js

**watcher **的作用 數(shù)據(jù)更新后 收到通知之后 調(diào)用 update 進行更新

/* watcher.js */

class Watcher {
  constructor(vm, key, cb) {
    // vm 是 Vue 實例
    this.vm = vm
    // key 是 data 中的屬性
    this.key = key
    // cb 回調(diào)函數(shù) 更新視圖的具體方法
    this.cb = cb
    // 把觀察者的存放在 Dep.target
    Dep.target = this
    // 舊數(shù)據(jù) 更新視圖的時候要進行比較
    // 還有一點就是 vm[key] 這個時候就觸發(fā)了 get 方法
    // 之前在 get 把 觀察者 通過dep.addSub(Dep.target) 添加到了 dep.subs中
    this.oldValue = vm[key]
    // Dep.target 就不用存在了 因為上面的操作已經(jīng)存好了
    Dep.target = null
  }
  // 觀察者中的必備方法 用來更新視圖
  update() {
    // 獲取新值
    let newValue = this.vm[this.key]
    // 比較舊值和新值
    if (newValue === this.oldValue) return
    // 調(diào)用具體的更新方法
    this.cb(newValue)
  }
}

那么去哪里創(chuàng)建 Watcher 呢?還記得在 compiler.js中 對文本節(jié)點的編譯操作嗎

在編譯完文本節(jié)點后 在這里添加一個 Watcher

還有 v-text v-model 指令 當編譯的是元素節(jié)點 就添加一個 Watcher

/* compiler.js */

class Compiler {
  // vm 指 Vue 實例
  constructor(vm) {
    // 拿到 vm
    this.vm = vm
    // 拿到 el
    this.el = vm.$el
    // 編譯模板
    this.compile(this.el)
  }
  // 編譯模板
  compile(el) {
    let childNodes = [...el.childNodes]
    childNodes.forEach((node) => {
      if (this.isTextNode(node)) {
        // 編譯文本節(jié)點
        this.compileText(node)
      } 
       ...
  }
  // 編譯文本節(jié)點(簡單的實現(xiàn))
  compileText(node) {
    let reg = /\{\{(.+)\}\}/
    let val = node.textContent
    if (reg.test(val)) {
      let key = RegExp.$1.trim()
      node.textContent = val.replace(reg, this.vm[key])
      // 創(chuàng)建觀察者
      new Watcher(this.vm, key, newValue => {
        node.textContent = newValue
      })
    }
  }
  ...
  // v-text 
  textUpdater(node, key, value) {
    node.textContent = value
     // 創(chuàng)建觀察者2
    new Watcher(this.vm, key, (newValue) => {
      node.textContent = newValue
    })
  }
  // v-model
  modelUpdater(node, key, value) {
    node.value = value
    // 創(chuàng)建觀察者
    new Watcher(this.vm, key, (newValue) => {
      node.value = newValue
    })
    // 這里實現(xiàn)雙向綁定 監(jiān)聽input 事件修改 data中的屬性
    node.addEventListener('input', () => {
      this.vm[key] = node.value
    })
  }
}

當 我們改變 響應式屬性的時候 觸發(fā)了 set() 方法 ,然后里面 發(fā)布者 dep.notify 方法啟動了,拿到了 所有的 觀察者 watcher 實例去執(zhí)行 update 方法調(diào)用了回調(diào)函數(shù) cb(newValue) 方法并把 新值傳遞到了 cb() 當中 cb方法是的具體更新視圖的方法 去更新視圖

比如上面的例子里的第三個參數(shù) cb方法

new Watcher(this.vm, key, newValue => {
    node.textContent = newValue
})

還有一點要實現(xiàn)v-model的雙向綁定

不僅要通過修改數(shù)據(jù)來觸發(fā)更新視圖,還得為node添加 input 事件 改變 data數(shù)據(jù)中的屬性

來達到雙向綁定的效果

7.測試下自己寫的

到了目前為止 響應式 和 雙向綁定 都基本實現(xiàn)了 那么來寫個例子測試下

<body>
  <div id="app">
    {{msg}} <br />
    {{age}} <br />
    <div v-text="msg"></div>
    <input v-model="msg" type="text" />
  </div>
  <script src="./js/dep.js"></script>
  <script src="./js/watcher.js"></script>
  <script src="./js/compiler.js"></script>
  <script src="./js/observer.js"></script>
  <script src="./js/vue.js"></script>
  <script>
    let vm = new Vue({
      el: '#app',
      data: {
        msg: '123',
        age: 21,
      },
    })
  </script>
</body>

8.五個文件代碼

這里直接把5個文件個代碼貼出來 上面有的地方省略了,下面是完整的方便大家閱讀

vue.js

/* vue.js */

class Vue {
  constructor(options) {
    // 獲取到傳入的對象 沒有默認為空對象
    this.$options = options || {}
    // 獲取 el
    this.$el =
      typeof options.el === 'string'
        ? document.querySelector(options.el)
        : options.el
    // 獲取 data
    this.$data = options.data || {}
    // 調(diào)用 _proxyData 處理 data中的屬性
    this._proxyData(this.$data)
    // 使用 Obsever 把data中的數(shù)據(jù)轉(zhuǎn)為響應式
    new Observer(this.$data)
    // 編譯模板
    new Compiler(this)
  }
  // 把data 中的屬性注冊到 Vue
  _proxyData(data) {
    Object.keys(data).forEach((key) => {
      // 進行數(shù)據(jù)劫持
      // 把每個data的屬性 到添加到 Vue 轉(zhuǎn)化為 getter setter方法
      Object.defineProperty(this, key, {
        // 設(shè)置可以枚舉
        enumerable: true,
        // 設(shè)置可以配置
        configurable: true,
        // 獲取數(shù)據(jù)
        get() {
          return data[key]
        },
        // 設(shè)置數(shù)據(jù)
        set(newValue) {
          // 判斷新值和舊值是否相等
          if (newValue === data[key]) return
          // 設(shè)置新值
          data[key] = newValue
        },
      })
    })
  }
}

obsever.js

/* observer.js */

class Observer {
  constructor(data) {
    // 用來遍歷 data
    this.walk(data)
  }
  // 遍歷 data 轉(zhuǎn)為響應式
  walk(data) {
    // 判斷 data是否為空 和 對象
    if (!data || typeof data !== 'object') return
    // 遍歷 data
    Object.keys(data).forEach((key) => {
      // 轉(zhuǎn)為響應式
      this.defineReactive(data, key, data[key])
    })
  }
  // 轉(zhuǎn)為響應式
  // 要注意的 和vue.js 寫的不同的是
  // vue.js中是將 屬性給了 Vue 轉(zhuǎn)為 getter setter
  // 這里是 將data中的屬性轉(zhuǎn)為getter setter
  defineReactive(obj, key, value) {
    // 如果是對象類型的 也調(diào)用walk 變成響應式,不是對象類型的直接在walk會被return
    this.walk(value)
    // 保存一下 this
    const self = this
    // 創(chuàng)建 Dep 對象
    let dep = new Dep()
    Object.defineProperty(obj, key, {
      // 設(shè)置可枚舉
      enumerable: true,
      // 設(shè)置可配置
      configurable: true,

      // 獲取值
      get() {
        // 在這里添加觀察者對象 Dep.target 表示觀察者
        Dep.target && dep.addSub(Dep.target)
        return value
      },
      // 設(shè)置值
      set(newValue) {
        // 判斷舊值和新值是否相等
        if (newValue === value) return
        // 設(shè)置新值
        value = newValue
        // 賦值的話如果是newValue是對象,對象里面的屬性也應該設(shè)置為響應式的
        self.walk(newValue)
        // 觸發(fā)通知 更新視圖
        dep.notify()
      },
    })
  }
}

compiler.js

/* compiler.js */

class Compiler {
  // vm 指 Vue 實例
  constructor(vm) {
    // 拿到 vm
    this.vm = vm
    // 拿到 el
    this.el = vm.$el
    // 編譯模板
    this.compile(this.el)
  }
  // 編譯模板
  compile(el) {
    // 獲取子節(jié)點 如果使用 forEach遍歷就把偽數(shù)組轉(zhuǎn)為真的數(shù)組
    let childNodes = [...el.childNodes]
    childNodes.forEach((node) => {
      // 根據(jù)不同的節(jié)點類型進行編譯
      // 文本類型的節(jié)點
      if (this.isTextNode(node)) {
        // 編譯文本節(jié)點
        this.compileText(node)
      } else if (this.isElementNode(node)) {
        //元素節(jié)點
        this.compileElement(node)
      }
      // 判斷是否還存在子節(jié)點考慮遞歸
      if (node.childNodes && node.childNodes.length) {
        // 繼續(xù)遞歸編譯模板
        this.compile(node)
      }
    })
  }
  // 編譯文本節(jié)點(簡單的實現(xiàn))
  compileText(node) {
    // 核心思想利用把正則表達式把{{}}去掉找到里面的變量
    // 再去Vue找這個變量賦值給node.textContent
    let reg = /\{\{(.+?)\}\}/
    // 獲取節(jié)點的文本內(nèi)容
    let val = node.textContent
    // 判斷是否有 {{}}
    if (reg.test(val)) {
      // 獲取分組一  也就是 {{}} 里面的內(nèi)容 去除前后空格
      let key = RegExp.$1.trim()
      // 進行替換再賦值給node
      node.textContent = val.replace(reg, this.vm[key])
      // 創(chuàng)建觀察者
      new Watcher(this.vm, key, (newValue) => {
        node.textContent = newValue
      })
    }
  }
  // 編譯元素節(jié)點這里只處理指令
  compileElement(node) {
    // 獲取到元素節(jié)點上面的所有屬性進行遍歷
    ![...node.attributes].forEach((attr) => {
      // 獲取屬性名
      let attrName = attr.name
      // 判斷是否是 v- 開頭的指令
      if (this.isDirective(attrName)) {
        // 除去 v- 方便操作
        attrName = attrName.substr(2)
        // 獲取 指令的值就是  v-text = "msg"  中msg
        // msg 作為 key 去Vue 找這個變量
        let key = attr.value
        // 指令操作 執(zhí)行指令方法
        // vue指令很多為了避免大量個 if判斷這里就寫個 uapdate 方法
        this.update(node, key, attrName)
      }
    })
  }
  // 添加指令方法 并且執(zhí)行
  update(node, key, attrName) {
    // 比如添加 textUpdater 就是用來處理 v-text 方法
    // 我們應該就內(nèi)置一個 textUpdater 方法進行調(diào)用
    // 加個后綴加什么無所謂但是要定義相應的方法
    let updateFn = this[attrName + 'Updater']
    // 如果存在這個內(nèi)置方法 就可以調(diào)用了
    updateFn && updateFn.call(this, node, key, this.vm[key])
  }
  // 提前寫好 相應的指定方法比如這個 v-text
  // 使用的時候 和 Vue 的一樣
  textUpdater(node, key, value) {
    node.textContent = value
    // 創(chuàng)建觀察者
    new Watcher(this.vm, key, (newValue) => {
      node.textContent = newValue
    })
  }
  // v-model
  modelUpdater(node, key, value) {
    node.value = value
    // 創(chuàng)建觀察者
    new Watcher(this.vm, key, (newValue) => {
      node.value = newValue
    })
    // 這里實現(xiàn)雙向綁定
    node.addEventListener('input', () => {
      this.vm[key] = node.value
    })
  }

  // 判斷元素的屬性是否是 vue 指令
  isDirective(attr) {
    return attr.startsWith('v-')
  }
  // 判斷是否是元素節(jié)點
  isElementNode(node) {
    return node.nodeType === 1
  }
  // 判斷是否是 文本 節(jié)點
  isTextNode(node) {
    return node.nodeType === 3
  }
}

dep.js

/* dep.js */

class Dep {
  constructor() {
    // 存儲觀察者
    this.subs = []
  }
  // 添加觀察者
  addSub(sub) {
    // 判斷觀察者是否存在 和 是否擁有update方法
    if (sub && sub.update) {
      this.subs.push(sub)
    }
  }
  // 通知方法
  notify() {
    // 觸發(fā)每個觀察者的更新方法
    this.subs.forEach((sub) => {
      sub.update()
    })
  }
}

watcher.js

/* watcher.js */

class Watcher {
  constructor(vm, key, cb) {
    // vm 是 Vue 實例
    this.vm = vm
    // key 是 data 中的屬性
    this.key = key
    // cb 回調(diào)函數(shù) 更新視圖的具體方法
    this.cb = cb
    // 把觀察者的存放在 Dep.target
    Dep.target = this
    // 舊數(shù)據(jù) 更新視圖的時候要進行比較
    // 還有一點就是 vm[key] 這個時候就觸發(fā)了 get 方法
    // 之前在 get 把 觀察者 通過dep.addSub(Dep.target) 添加到了 dep.subs中
    this.oldValue = vm[key]
    // Dep.target 就不用存在了 因為上面的操作已經(jīng)存好了
    Dep.target = null
  }
  // 觀察者中的必備方法 用來更新視圖
  update() {
    // 獲取新值
    let newValue = this.vm[this.key]
    // 比較舊值和新值
    if (newValue === this.oldValue) return
    // 調(diào)用具體的更新方法
    this.cb(newValue)
  }
}

以上就是關(guān)于“VUE響應式原理實例代碼分析”這篇文章的內(nèi)容,相信大家都有了一定的了解,希望小編分享的內(nèi)容對大家有幫助,若想了解更多相關(guān)的知識內(nèi)容,請關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

vue
AI