數據響應式基礎

Object.defineProperty()

Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回此對象。它接收三個參數

  • obj:需要被定義新屬性或改變屬性的對象

  • prop:即將被定義或修改的屬性名稱

  • descriptor:即將被定義或修改的屬性的描述符,分爲數據描述符和存取描述符。

    • 數據描述符:就是descriptor是一個具體的值,或者是一個擁有value屬性的對象:
    Object.defineProperty(obj, 'data', 'abc')
    Object.defineProperty(obj, 'data', {
    	value: 'abc',
    	writable: false
    })
    
    • 存取描述符:就是屬性的getter和setter函數
    let value
    Object.defineProperty(obj, 'data', {
    	get () {
    		return obj.data
    	},
    	set (newVal) {
    		value = newVal
    	}
    })
    
  
此外,還有兩種描述符共享的兩個鍵值:
  
  - configurable:當且僅當該屬性的 configurable 鍵值爲 true 時,該屬性的描述符才能夠被改變,同時該屬性也能從對應的對象上被刪除。默認爲false。
- enumerable:當且僅當該屬性的 enumerable 鍵值爲 true 時,該屬性纔會出現在對象的枚舉屬性中。默認爲false。
  
  關於Object.defineProperty()方法的更多介紹,[請點擊這裏](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty)
  
#### 實現數據響應式

- Object的響應式

我們先實現一個最簡單的頁面:

這裏面的obj.foo每一秒都會發生改變,觸發setter函數,在setter函數裏面我們就能更新視圖了,這裏我們就實現了一個簡單的頁面

```html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div id="app"></div>
  <script>
    const app = document.getElementById('app')
    function definedReactive(obj, key, val) {
      Object.defineProperty(obj, key, {
        get() {
          console.log('get', key, val)
          return val
        },
        set(newVal) {
          if (newVal !== val) {
            console.log('set', key, newVal)
            val = newVal
            // 使用視圖的更新函數
            update()
          }
        }
      })
    }

    function update() {
      app.innerText = obj.foo
    }
    const obj = {}
    definedReactive(obj, 'foo', '')
    obj.foo = new Date().toLocaleTimeString()
    setInterval(() => {
      obj.foo = new Date().toLocaleTimeString()
    }, 1000)
  </script>
</body>

</html>

但是,很明顯,這個obj的結構非常簡單,一個key,一個value,那對於obj={foo: ‘foo’, baz: ‘baz’}這樣有多個key的對象,這樣處理明顯就不夠了,我們需要遍歷每個key,對每一個key都做響應式處理

  • defineReactive.js
// 遍歷每個key
function observe (obj) {
  if (!obj || typeof obj !== 'object') {
    return
  }
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
}

// 給每個key都設置getter和setter
function defineReactive (obj, key, val) {
  Object.defineProperty(obj, key, {
    get () {
      console.log('get', key, val)
      return val
    },
    set (newVal) {
      if (newVal !== val) {
        console.log('set', key, newVal)
        val = newVal
      }
    }
  })
}

let obj = { foo: 'foo', baz: 'baz' }
observe(obj)

obj.foo // get foo foo
obj.foo = 'fooooooooo' //set foo fooooooooo

對於更復雜一點的對象,obj={foo: ‘foo’, baz: {a: 1}},我們就需要使用遞歸去完成響應式處理了

// 遍歷每個key
function observe (obj) {
  // 如果obj只是個普通值, 就結束遞歸
  if (!obj || typeof obj !== 'object') {
    return
  }
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
}

// 給每個key都設置getter和setter
function defineReactive (obj, key, val) {
  // 如果val也是個object,那就需要遞歸遍歷它,判斷條件已經卸載observe()裏面了,所以不需要加判斷
  observe(val)
  Object.defineProperty(obj, key, {
    get () {
      console.log('get', key, val)
      return val
    },
    set (newVal) {
      if (newVal !== val) {
        // 對於每一個newval,也得對它進行遞歸
        observe(newVal)
        console.log('set', key, newVal)
        val = newVal
      }
    }
  })
}

let obj = { foo: 'foo', bar: 'bar', baz: { a: 1 } }
observe(obj)

obj.baz.a // get baz { a: [Getter/Setter] }  get a 1
obj.baz.a = { b: 1 } // get baz { a: [Getter/Setter] }  set a { b: [Getter/Setter] }
  • 以上是對Object類型的變量做響應式處理,對於Array類型,我們需要做另外的處理

js中,能改變原數組的方法只有七個,分別是push()、pop()、shift()、unshift()、sort()、reverse()、splice(),所以,我們只需要重寫這七個方法,讓這些方法擁有通知更新的功能,就能實現數組的響應式。

// 1、拿出數組的原型
// 2、克隆數組的原型,不然所有數組都變了
// 3、修改七個方法
// 4、覆蓋需要響應式處理的數組的原型的這七個方法

const arrayProto = Array.prototype
const newProto = Object.create(arrayProto)
const methods = ['push', 'pop', 'shift', 'unshift', 'sort', 'reverse', 'splice']
methods.forEach(method => {
  newProto[method] = function () {
    // 執行這個方法原有的功能
    arrayProto[method].apply(this, arguments)
    // 這裏添加一個更新通知,並重新對arr做響應式處理
    observe(this)
    console.log(method + '執行了!')
  }
})

修改一下原來的observe函數

// 遍歷每個key
function observe (obj) {
  // 判斷值是否object,不是就結束遞歸
  if (!obj || typeof obj !== 'object') {
    return
  }
  // 判斷obj是否是數組
  if (Array.isArray(obj)) {
    // 替換掉arr原型上原有的七個方法,這樣寫能避免改變arrayProto的值
    obj.__proto__ = Object.assign({}, arrayProto, newProto)
    // 對每一個值進行遍歷,如果這個值是object,則需要做響應式處理
    for (let i = 0; i < obj.length; i++) {
      observe(obj[i])
    }
  } else {
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key])
    })
  }
}

做一下測試

let arr = [1, 2, 3]
observe(arr)
arr.push({ c: 4 }) // push執行了!
console.log(arr[3]) // { c: [Getter/Setter] }
arr[3].c = 5 // set c 5

點擊這裏查看關於Object.create()方法的詳細介紹

點擊這裏查看關於Object.assign()方法的詳細介紹

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