数据响应式基础

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()方法的详细介绍

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