<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          vue源碼解讀-數(shù)據(jù)響應(yīng)式原理(通俗易懂)

          共 15439字,需瀏覽 31分鐘

           ·

          2023-10-31 07:50

          一、Object 的變化偵測

          1.1 API 的引入

          1.1.1 Object.defineProperty()

             在vue2.x中,我們經(jīng)常遇到當(dāng)數(shù)據(jù)的值改變之后,該值在頁面上被引用的部分也會更新這種情況。那么今天我們就來解開這神奇的面紗。

             Object.defineProperty()可以用于監(jiān)聽某一對象對應(yīng)的屬性,監(jiān)聽類型主要分為值特性和訪問器特性。它就是vue2.x響應(yīng)式數(shù)據(jù)實現(xiàn)的基本原理。

          1.1.2 值特性的配置

          目標(biāo)屬性是否允許被刪除、遍歷訪問、覆蓋和該屬性的值的配置。


          let data = { }
          Object.defineProperty(data, 'age', {
          configurable: false, // 被刪除時,靜默失敗
          writable: false, // 被重寫時,靜默失敗
          enumerable: false, // 不可以枚通過for/in進(jìn)行枚舉
          value: 23,// 該屬性的值
          })
          console.log(data.age); //23
          delete data.age // 靜默失敗
          console.log(data.age); //23
          data.age=30 // 靜默失敗
          console.log(data.age); //23
          for (const key in data) {
          console.log(key,'key'); // 靜默失敗 age屬性不能被訪問!
          }

          1.1.3 訪問器特性的配置

          目標(biāo)屬性在被訪問和賦值等操作完成之前進(jìn)行劫持。


          let person = {
          _name:'Jone'
          }
          Object.defineProperty(person, 'name', {
          get: () => {
          console.log('GET');
          return person._name
          },
          set: (value) => {
          console.log('SET');
          person._name=value
          }
          })
          console.log(person.name); // 'GET' 'Jone'
          person.name='Mike' // 'SET'

               通過上面的代碼,我們疑惑為什么不配置一個value值屬性而是要借助一個第三者屬性_name完成呢?這個問題官方給出了解釋:

          訪問器屬性不能和值屬性中的(writable和value)同時配置。

               我們深思一下:如果說我為一個屬性即配置了value屬性又為他配置了get訪問器屬性。那么當(dāng)我們訪問該屬性的時候,是以get訪問器為準(zhǔn)還是以value為準(zhǔn)呢?

          1.2 如何實現(xiàn)數(shù)據(jù)的劫持

          如果要對 data 中數(shù)據(jù)進(jìn)行深層次的劫持,我們可以使用 深度優(yōu)先搜索算法 實現(xiàn):

            • 如果當(dāng)前的鍵指向值的類型為基本數(shù)據(jù)類型,則使用 Object.defineProperty() 這一個 API 實現(xiàn)數(shù)據(jù)的劫持。

            • 如果當(dāng)前的鍵指向值的類型為復(fù)雜數(shù)據(jù)類型中的 Object類型,則需要將這個 Object 中的鍵值對進(jìn)行劫持。

          1.3 實現(xiàn)數(shù)據(jù)的劫持

          class Vue {
          constructor(rest) {
          let { data, watch } = rest
          this.$data = typeof data === 'function' ? data() : data
          this.initData(this.$data)
          // 開始遞歸
          this.observe(this.$data)
          }
          }

          function observe(data) {
          new Observer(data)
          }

          class Observer {
          constructor(data) {
          this.walk(data)
          }
          walk(data) {
          for (const key in data) {
          reactive(data, key, data[key])
          }
          }
          }

          function reactive(object, key, val) {
          let isArray = val instanceof Array
          let isObject = val instanceof Object
          // 如果鍵指向的對象為數(shù)組類型,本小節(jié)暫不處理。
          if(isArray) return
          // 如果鍵指向的對象為對象類型,則再對該對象進(jìn)行遞歸。
          if (isObject) {
          return observe(val)
          }

          // 數(shù)據(jù)的劫持操作
          Object.defineProperty(object, key, {
          configurable: true,
          enumerable: true,
          get() {
          return val
          },
          set(value) {
          if (val !== value) {
          val = value
          }
          }
          })
          }

          過上述代碼,我們不難發(fā)現(xiàn):遞歸邏輯在實現(xiàn)的過程中,并不是函數(shù)自身的調(diào)用,而是將三個函數(shù)首位相接完成了遞歸的邏輯。下圖是對三個函數(shù)實現(xiàn)遞歸邏輯的展示:

          1.4 為何需要進(jìn)行依賴收集

          在上文中我們對數(shù)據(jù)實現(xiàn)了劫持操作,如果只是劫持?jǐn)?shù)據(jù)其實并沒有什么作用,因為我們需要的功能是當(dāng)數(shù)據(jù)變化后對引用該數(shù)據(jù)的部分進(jìn)行更新操作,所以我們還需要知道以下兩個內(nèi)容:

          • 響應(yīng)式數(shù)據(jù)在何處被引用(模板還是計算屬性還是其他地方)。

          • 當(dāng)響應(yīng)式數(shù)據(jù)發(fā)生變化后,通知引用該數(shù)據(jù)的部分進(jìn)行更新。

          1.5 在何處收集、在何時通知更新

          1.5.1 何處收集

          舉個例子:

          <template>
          <p>{{ name }}</p>
          </template>

          該模板中引用了響應(yīng)式數(shù)據(jù) name 的值。換句話說:該模板中首先訪問響應(yīng)式數(shù)據(jù) name 屬性的值,再將對應(yīng)的值放在模板對應(yīng)的位置。所以,當(dāng) name 屬性被訪問的時候,在被訪問的位置打上標(biāo)注。換言之:該位置依賴了 name 屬性的值,需要將這部分邏輯收集起來。

          恰巧, API 中 get()訪問器的用處不就是當(dāng)數(shù)據(jù)被訪問時,進(jìn)行攔截操作嗎?所以我們應(yīng)該在 get()函數(shù)中進(jìn)行依賴收集這個動作。

          1.5.2 何時通知更新

          當(dāng)響應(yīng)式數(shù)據(jù)的值發(fā)生變化之后,引用該數(shù)據(jù)的邏輯部分應(yīng)當(dāng)更新。當(dāng)響應(yīng)式數(shù)據(jù)發(fā)生變化時,我們在哪里得知呢?是不是可以在 set()訪問器進(jìn)行通知更新呢?

          1.6 收集依賴的介紹

          經(jīng)過分析,我們知道需要在 get() 函數(shù)中進(jìn)行依賴的收集。那么收集到的依賴存放到哪里呢?我們是不是考慮將依賴存放到一個數(shù)組中或者一個對象中呢?

          function reactive(object, key, val) {
          let dep = []
          Object.defineProperty(object, key, {
          configurable: true,
          enumerable: true,
          get() {
          // 依賴收集處,Dep.target 將他看做依賴。
          dep.push(Dep.target)
          return val
          },
          set(value) {
          if (val !== value) {
          // 依賴的觸發(fā)
          dep.forEach(cb=>cb())
          val = value
          }
          }
          })
          }

          通過上述代碼,我們新增一個數(shù)組dep,用于存放被收集的依賴。值得注意的是,由于 get() 和 set()函數(shù)中均引用了其父級作用域中聲明的變量 dep,形成了閉包。

          但是這樣寫耦合度較低,我們可以封裝一個單獨的Dep類讓它專門負(fù)責(zé)依賴收集。

          class Dep {
          constructor() {
          // 依賴收集的中心
          this.subs = []
          }
          // 依賴的收集
          add() {
          if (Dep.target) {
          this.subs.push(Dep.target)
          }
          }
          // 觸發(fā)依賴對應(yīng)的回調(diào)函數(shù)
          update() {
          let subs = this.subs.slice()
          subs.forEach(watch => {
          // 觸發(fā)依賴的回調(diào)函數(shù)
          watch.run()
          })
          }
          }

          然后我們改造一下依賴收集的動作對應(yīng)的函數(shù)。

          function reactive(object, key, val) {
          let dep = new Dep()
          Object.defineProperty(object, key, {
          configurable: true,
          enumerable: true,
          get() {
          // 依賴收集處
          dep.append()
          return val
          },
          set(value) {
          if (val !== value) {
          // 依賴的觸發(fā)
          dep.update(value)
          val = value
          }
          }
          })
          }

          1.7 依賴的介紹

          1.7.1 依賴是什么

          當(dāng)響應(yīng)式數(shù)據(jù)被訪問的時,收集 。當(dāng)響應(yīng)式數(shù)據(jù)發(fā)生變化時,通知 進(jìn)行更新;這個就是依賴。由于響應(yīng)式數(shù)據(jù)既有可能在模板中被引用,也有可能被引用在 computed 中,所以我們不妨封裝一個類實例,當(dāng)需要收集的時候,直接收集該實例。當(dāng)響應(yīng)式數(shù)據(jù)發(fā)生變化時候,也只通知他一個,再有他通知其他地方進(jìn)行更新。我們?yōu)檫@個實例起個名字吧,叫 Watcher 。

          1.7.2 依賴函數(shù)封裝

            1. 當(dāng)響應(yīng)式數(shù)據(jù)被訪問時,我們需要實例化一個對象,這個對象被收集的目標(biāo)。

            2. 當(dāng)響應(yīng)式數(shù)據(jù)發(fā)生變化時,該實例對象需要通知引用部分進(jìn)行更新。

          // 依賴構(gòu)造函數(shù)
          class Watcher {
          constructor(vm, key, cb) {
          this.vm = vm
          this.key = key
          this.cb = cb
          this.get()
          }
          get() {
          // 依賴收集的對象
          Dep.target = this
          // Object.defineProperty 中的 get 函數(shù)會被調(diào)用。調(diào)用之后,依賴進(jìn)行收集。
          this.vm[this.key]
          Dep.target = undefined
          }
          run() {
          // 通知更新的能力
          // 為了防止this指針出現(xiàn)錯誤,我們重新綁定this指向。
          this.cb.apply(this.vm)
          }
          }

          1.8 模擬實現(xiàn) vue 中的  watch 選項

          在 vue 中,提供了一個 watch 偵聽器選項,它的功能是當(dāng)響應(yīng)式數(shù)據(jù)發(fā)生變化時,執(zhí)行對應(yīng)的回調(diào)函數(shù)。結(jié)合之前的邏輯,我們封裝一個屬于我們的 watch 選項。

          import { arrayProto } from './array.js'

          class Vue {
          constructor(rest) {
          let { data, watch } = rest
          this.$data = typeof data === 'function' ? data() : data
          // 初始化響應(yīng)式數(shù)據(jù)
          this.initData(this.$data)
          for (const key in this.$data) {
          Object.defineProperty(this, key, {
          configurable: true,
          enumerable: true,
          get() {
          return this.$data[key]
          },
          set(value) {
          this.$data[key] = value
          }
          })
          }
          // 初始化所有的偵聽器
          this.initWatch(watch)

          }
          initData = () => {
          observe(this.$data)
          }

          initWatch(watch) {
          for (const key in watch) {
          this.$watch(key, watch[key])
          }
          }
          // 響應(yīng)式數(shù)據(jù)在在偵聽器中被引用
          $watch(key, cb) {
          new Watcher(this, key, cb)
          }
          }
          // 依賴的收集容器
          class Dep {
          constructor() {
          this.deps = []
          }
          // 收集
          append() {
          if (Dep.target) {
          this.deps.push(Dep.target)
          }
          }
          // 觸發(fā)
          update(newValue) {
          let subs = this.deps.slice()
          subs.forEach(watch => {
          watch.run(newValue)
          })

          }
          }

          // 依賴
          class Watcher {
          constructor(vm, key, cb) {
          this.vm = vm
          this.cb = cb
          this.key = key
          this.get()
          }
          get() {
          Dep.target = this
          this.vm[this.key]
          Dep.target = undefined
          }
          run(newValue) {
          // 將變化前和變化后的值傳給對應(yīng)的回調(diào)函數(shù)。
          this.cb.call(this.vm, this.vm[this.key], newValue)
          }
          }
          // 遞歸實現(xiàn)
          export function observe(data) {
          if (typeof data !== 'object') { return }
          new Observer(data)
          }

          class Observer {
          constructor(data) {
          if (Array.isArray(data)) {
          // 數(shù)組需要單獨處理
          } else {
          this.walk(data)
          }
          }
          walk(data) {
          for (const key in data) {
          if (typeof data[key] === 'object') {
          observe(data[key])
          }
          reactive(data, key, data[key])
          }
          }
          }

          function reactive(object, key, val) {
          let dep = new Dep()
          Object.defineProperty(object, key, {
          configurable: true,
          enumerable: true,
          get() {
          // 依賴收集處
          dep.append()
          return val
          },
          set(value) {
          if (val !== value) {
          // 依賴的觸發(fā)
          dep.update(value)
          val = value
          }
          }
          })
          }

          // 測試數(shù)據(jù)
          let vm = new Vue({
          data() {
          return {
          name: 'zs',
          age: 23,
          sex: 'nan',
          hobby: [1, [2], 3, 4]
          }
          },
          watch: {
          age() {
          console.log('age變化了,哈哈哈');
          },
          hobby() {
          console.log('hobby變化了,hobby', this.hobby);
          },
          }
          })
          vm.name = 'll' // name變化了,hhh
          console.log(vm, 'vv');
          vm.$data.hobby.push()

          二、Array 的變化偵測

          2.1 如何實現(xiàn)數(shù)據(jù)的劫持

          如果要對數(shù)組中的每一個元素實現(xiàn)數(shù)據(jù)的劫持,我們依然可以通過 Object.defineProperty() 這個內(nèi)置的API遞歸實現(xiàn)。但是,如果用戶聲明了一個擁有100個元素的數(shù)組,那在對該數(shù)組進(jìn)行遞歸劫持時,是不是會占用大量的內(nèi)存呢?所以,vue在對數(shù)組進(jìn)行劫持操作時,并沒有采用這種方法。

          所謂數(shù)組的劫持,通俗的來說就是當(dāng)數(shù)組中的元素被訪問(或者被修改)時,外界可以感知到。

          當(dāng)數(shù)組中的元素被訪問時,指向數(shù)組的變量名一定會收到通知,所以我們依舊在 get 中實現(xiàn)劫持?jǐn)?shù)組被訪問的操作。那如何實現(xiàn)當(dāng)數(shù)組中的元素被修改時也能被外界感知這一功能呢?我們是不是可以通過劫持能夠改變數(shù)組中元素的實例方法完成呢?

          2.2 數(shù)組的劫持

          經(jīng)過上文得知,我們需要對原型對象中能夠改變數(shù)組中數(shù)據(jù)的7個實例方法進(jìn)行劫持操作。既要實現(xiàn)數(shù)組的劫持,又要完成其對應(yīng)的功能。

          const ArrayProto = Array.prototype
          export const array = Object.create(ArrayProto)
          // 被劫持的 7 個方法
          let methods = [ 'push', 'pop','shift', 'unshift','splice','sort', 'reverse']
          // 對7個方法進(jìn)行劫持操作
          methods.forEach(method => {
          array[method] = function (...arg) {
          // 原實例對象中的7個實例方法的功能也需要進(jìn)行實現(xiàn)。
          let result = ArrayProto[method].apply(this, arg)

          // 對于新增加入的數(shù)據(jù),需要進(jìn)行響應(yīng)式處理
          let data
          switch (method) {
          case 'push':
          case 'unshift':
          data = arg
          break
          case 'splice':
          data = arg[2]
          }
          data && data.forEach(v => {
          observe(v)
          })
          return result
          }
          })

          下圖是對代碼邏輯的詳細(xì)展示:

          2.3 在何處收集、在何時通知更新

          對于 Object 類型而言,當(dāng) Object 的鍵被訪問時,將依賴進(jìn)行收集;當(dāng) Object 的鍵對應(yīng)的值發(fā)生變化時,通知更新。所以我們在 get() 和 set() 函數(shù)中分別進(jìn)行依賴的收集和通知更新。

          那數(shù)組也是如此,在 get() 中進(jìn)行依賴的收集,當(dāng)數(shù)組中的元素發(fā)生變化時通知更新。在元素發(fā)生變化通知更新非常容易理解,為什么依賴收集還是在 get() 中呢?我們舉個例子:

          {
          list: [1, 2, 3, 4, 5, 6]
          }

          對于上面的數(shù)組 list ,如果我們想要得到數(shù)組中的任意一個值,一定是需要經(jīng)過 list 這個 key 的。對不對?所以,我們在獲取數(shù)組 list 中的元素時,對應(yīng)的 get() 一定會被觸發(fā)。若數(shù)組使用我們改寫的那七個實例方法,那么需要在那七個實例方法中進(jìn)行通知更新。

          function reactive(object, key, val) {
          let dep = new Dep()
          Object.defineProperty(object, key, {
          configurable: true,
          enumerable: true,
          get() {
          dep.append()
          // 數(shù)組的依賴收集處
          return val
          },
          set(value) {
          if (val !== value) {
          dep.update(value)
          val = value
          }
          }
          })
          }
          methods.forEach(method => {
          arrayProto[method] = function (...arg) {
          // 數(shù)組原來的方法也必須映射過來
          let result = ArrayProto[method].apply(this, arg)
          let data
          switch (method) {
          case 'push':
          case 'unshift':
          data = arg
          break
          case 'splice':
          data = arg[2]
          }
          data && data.forEach(v => {
          observe(v)
          })
          // 數(shù)組元素發(fā)生變化,通知對應(yīng)的依賴進(jìn)行更新

          }
          })

          2.4 依賴的收集和觸發(fā)

          2.4.1 收集的依賴存在何處

          在 Object 而言,依賴收集中心是放在一個 dep 中,那數(shù)組收集的依賴能不能也放到 dep 中呢?

          function reactive(object, key, val) {
          // 我們維護(hù)的的依賴收集中心,對于 Object 而言。
          let dep = new Dep()
          Object.defineProperty(object, key, {
          configurable: true,
          enumerable: true,
          get() {
          // 收集
          dep.append()
          return val
          },
          set(value) {
          if (val !== value) {
          // 通知更新
          dep.update(value)
          val = value
          }
          }
          })
          }

          由于 Object 的依賴收集和通知更新處于同一個作用域中,利用函數(shù)閉包中的數(shù)據(jù)在函數(shù)運行完畢之后不會被垃圾回收的特性,我們在 get() 函數(shù)和 set() 函數(shù)父級作用域維護(hù)了一個依賴中心(dep)。這樣 dep 就能常駐內(nèi)存。

          經(jīng)過上節(jié)分析,數(shù)組的依賴的收集是在 get() 函數(shù)中,然而數(shù)組的通知更新是在攔截器中進(jìn)行的。所以我們是不是可以仿照 Object 依賴收集和通知更新的方式,在其父作用域維護(hù)一個依賴中心呢?這個位置恰恰在 Observer 類。

          import { arrayProto } from './array.js'
          class Observer {
          constructor(data) {
          // 數(shù)組的依賴中心放在這里是不是更合適呢?
          this.dep = new Dep()
          if (Array.isArray(data)) {
          // 理由1
          // 如果為數(shù)組,則將我們寫好的原型對象覆蓋數(shù)組原來的原型對象。
          // 我們寫好的原型對象中需要訪問到依賴中心。
          data.__proto__ = arrayProto
          } else {
          this.walk(data)
          }
          }
          walk(data) {
          // 省略......
          // 理由2
          // 為 Object 的鍵進(jìn)行劫持,在這個函數(shù)中需要訪問到依賴中心。
          reactive(data, key, data[key])
          // 省略.....
          }
          }

          2.4.2 收集依賴

          經(jīng)過上文分析可以得知,對于數(shù)組而言,依賴的收集應(yīng)該在 get() 函數(shù)中進(jìn)行。觸發(fā)依賴的更新是在 攔截器 方法中進(jìn)行。

          在這里我們需要先思考幾個問題:

            1. 如果該數(shù)據(jù)已經(jīng)被劫持了,需要被二次劫持嗎?這個功能寫在哪里比較合適呢?

            2. 如何知道該數(shù)據(jù)已經(jīng)被劫持了?

          回答:

            1. 肯定不需要。我們可以將判斷目標(biāo)數(shù)據(jù)是否已經(jīng)被劫持這部分邏輯寫在 observe() 函數(shù)中。請思考為什么?因為 observe() 函數(shù)是遞歸實現(xiàn)數(shù)據(jù)劫持的第一個被調(diào)用的函數(shù)。換言之:如果要將某個數(shù)據(jù)實現(xiàn)被劫持,一定需要調(diào)用 observe() 函數(shù)。比如以下代碼。

          methods.forEach(method => {
          arrayProto[method] = function (...arg) {
          // 。。。。。。省略部分代碼
          data && data.forEach(v => {
          // 將新加入數(shù)組的元素遞歸實現(xiàn)劫持。
          observe(v)
          })
          }
          })
            1. 我們可以在被劫持的數(shù)據(jù)中,新增一個唯一的標(biāo)識。請思考應(yīng)該在哪里為數(shù)據(jù)添加這個唯一標(biāo)識呢?應(yīng)該是 Observer 類。因為如果 Observer 類能夠被執(zhí)行,那么 data 一定是復(fù)雜數(shù)據(jù)類型。我們可以先為 data 打上唯一標(biāo)識,然后再對 data 這個復(fù)雜數(shù)據(jù)類型進(jìn)行遍歷。遍歷過程中如果值是復(fù)雜數(shù)據(jù)類型,則值部分的數(shù)據(jù)進(jìn)行遞歸劫持,如果為簡單數(shù)據(jù)類型,則進(jìn)入 reactive() 函數(shù),進(jìn)行劫持操作!

          // 為每一個響應(yīng)式數(shù)據(jù)添加__ob__屬性
          function def(obj, key, value, enumerable) {
          Object.defineProperty(obj, key, {
          value: value,
          enumerable: enumerable || false,
          writable: true,
          configurable: true
          })
          }
          // 遞歸開始函數(shù)
          export function observe(data) {
          // 如果不是復(fù)雜數(shù)據(jù)類型,直接返回
          if (typeof data !== 'object') { return }

          // 目標(biāo)數(shù)據(jù)屬性中,是否存在我們規(guī)定的唯一標(biāo)識 __ob__。
          if (Object.hasOwn(data, '__ob__') && data['__ob__'] instanceof Observer) {
          // 如果存在則不進(jìn)行二次監(jiān)聽
          return
          } else {
          // 如果不存在。說明該數(shù)據(jù)暫未劫持。
          new Observer(data)
          }
          }
          class Observer {
          constructor(data) {
          this.dep=new Dep()
          // 唯一標(biāo)識
          def(data, '__ob__', this)
          if (Array.isArray(data)) {
          data.__proto__ = arrayProto
          }
          }
          walk(data) {
          for (const key in data) {
          // 如果值為復(fù)雜類型,則需要將值部分進(jìn)行遞歸劫持。
          if (typeof data[key] === 'object') {
          observe(data[key])
          }else{
          reactive(data, key, data[key])
          }
          }
          }
          }
          function reactive(object, key, val) {
          // 。。。。。。省略
          // 進(jìn)行劫持操作
          }

          2.4.3 在攔截器中訪問到依賴中心

          經(jīng)過上文的分析,我們不難發(fā)現(xiàn):在 Observer 類中,我們?yōu)楫?dāng)前的復(fù)雜數(shù)據(jù)類型全部新增加一個__ob__的屬性,并且其值為當(dāng)前 Observer 當(dāng)前實例對象。但是,Observer 實例對象中是不是存在一個依賴收集中心呢?這個依賴收集中心是不是為了收集數(shù)組的依賴而設(shè)置的呢?

          因為我們?yōu)槊恳粋€復(fù)雜數(shù)據(jù)類型中新注入了一個能訪問到數(shù)組依賴中心的屬性,所以在攔截器中只要獲取到數(shù)組實例對象的__ob__屬性,就能拿到數(shù)組的依賴中心。

          那問題又來了:我們是不是有需要寫個方法去獲取數(shù)組中的__ob__屬性啊?能不能復(fù)用已有的函數(shù)呢?

          答案是:肯定可以??v觀我們封裝過得所有函數(shù),不難發(fā)現(xiàn),引用__ob__屬性較多的有 observe() 函數(shù)和

          Observer 類。我們是在 Observer 類中對復(fù)雜數(shù)據(jù)類型添加__ob__屬性的,所以我們考慮二次改變observe()

          函數(shù)。在攔截器中,獲取該數(shù)組__ob__屬性指向的對象。

          // 改造
          export function observe(data) {
          if (typeof data !== 'object') {
          return
          }

          let ob
          if (Object.hasOwn(data, "__ob__") && data instanceof Observer) {
          // 如果存在屬性__ob__,則返回其值。
          ob = data['__ob__']
          } else {
          // 當(dāng)然我們也需要考慮到兼容之前的邏輯。如果該復(fù)雜類型沒有屬性__ob__,
          // 那就證明該復(fù)雜類型的數(shù)據(jù)還沒有進(jìn)行響應(yīng)式劫持操作,需要進(jìn)入進(jìn)入下一個
          // 環(huán)節(jié),對復(fù)雜數(shù)據(jù)類型進(jìn)行劫持。
          ob = new Observer(data)
          }
          return ob
          }

          class Observer {
          constructor(data) {
          // data 一定是復(fù)雜數(shù)據(jù)類型
          // 依賴中心
          this.dep = new Dep()
          // 為復(fù)雜數(shù)據(jù)類型手動添加 __ob__ 屬性。
          // 數(shù)組的實例對象中已經(jīng)擁有了依賴中心
          def(data, '__ob__', this)

          if (Array.isArray(data)) {
          // 將被改寫的原型對象覆蓋數(shù)組自帶的原型對象
          data.__proto__ = arrayProto
          }
          // 省略 ......
          }
          }
          // 省略......
          }

          // 我們給每一個響應(yīng)式數(shù)據(jù)添加__ob__屬性,
          export function def(obj, key, value, enumerable) {
          Object.defineProperty(obj, key, {
          value: value,
          enumerable: enumerable || false,
          writable: true,
          configurable: true
          })
          }

          我們通過 def() 函數(shù)對每一個復(fù)雜數(shù)據(jù)類型的數(shù)據(jù)手動添加了一個__ob__屬性,__ob__的值為當(dāng)前 Observer 實例對象,這個實例對象中擁有一個依賴中心 —— dep。從此數(shù)組的實例屬性中增加了一個__ob__。

          在被改寫的 7 個實例方法中,我們可以通過 this 獲取數(shù)組中的 __ob__屬性得到數(shù)組的依賴中心,從而進(jìn)行通知!

          methods.forEach(method => {
          arrayProto[method] = function (...arg) {
          // 數(shù)組原來的方法也必須映射過來
          let result = ArrayProto[method].apply(this, arg)
          let data
          switch (method) {
          case 'push':
          case 'unshift':
          data = arg
          break
          case 'splice':
          data = arg[2]
          }
          data && data.forEach(v => {
          observe(v)
          })
          // 我們把數(shù)組的依賴中心掛在到了數(shù)組實例屬性中,通過 this 可以獲取依賴中心。
          this.__ob__.dep.update(...arg)
          return result
          }
          })

          2.5 偵測數(shù)組中元素的變化

          如果當(dāng)前的元素屬于數(shù)組類型,除了改變其原型對象之外,我們可以通過遍歷該數(shù)組中的元素進(jìn)行深層次的偵測。

          class Observer {
          constructor(data) {
          this.dep = new Dep()
          def(data, '__ob__', this)
          if (Array.isArray(data)) {
          // 改變數(shù)組的原型對象
          data.__proto__ = arrayProto
          // 如果是數(shù)組,則遍歷它。
          this.addressArray(data)
          } else {
          this.walk(data)
          }
          }

          addressArray(data) {
          for (const v of data) {
          // 對數(shù)組中的值進(jìn)行深層度的遞歸
          observe(v)
          }
          }
          // 省略......
          }

          2.6 偵測新增元素的變化

          對于新加入數(shù)組的元素,我們非常有必要對他們進(jìn)行監(jiān)聽。

          methods.forEach(method => {
          arrayProto[method] = function (...arg) {
          // 數(shù)組原來的方法也必須映射過來
          let result = ArrayProto[method].apply(this, arg)
          let data

          switch (method) {
          case 'push':
          case 'unshift':
          data = arg
          break
          case 'splice':
          data = arg[2]
          }
          // data && data.forEach(v => {
          // observe(v)
          // })
          // 修改檢測方式
          this.__ob__.addressArray(arg)
          // 觸發(fā)依賴更新
          this.__ob__.dep.update(...arg)
          return result
          }
          })


          瀏覽 526
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  青娱乐日韩精品 | 国产性爱毛片 | 久久综合久色欧美综合狠狠 | 黑丝高跟后入 | 解释www的含义 |