Object.defineProperty是如何实现对数组的监听

6,502 阅读5分钟

关注微信公众号[码不停息]解锁更多优质文章😊

Object.defineProperty简单了解

  • 定义

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

一脸懵逼中...

  • 语法

Object.defineProperty(obj, prop, descriptor)

obj  要定义属性的对象。
prop 要定义或修改的属性的名称
descriptor 要定义或修改的属性描述符

objprop很好理解 比如我们定义一个变量为

const o = {
    name:'公众号码不停息'
}

其中obj指的就是o,prop指的就是o.name 下面我们主要看看descriptor(比较懒直接截图啦😃) 直接看代码吧

let obj = {}
 Object.defineProperty(obj, 'name', {
  configurable: true, // 可删除
  enumerable: true,	//可枚举
  writable: true,	//可修改
  value: '码不停息'
})
需要注意的是:value,writable 和get,set不能同时进行配置

上面的例子还可以写成

let obj = {}
let name = '码不停息'
Object.defineProperty(obj, 'name', {
  configurable: true, // 可删除
  enumerable: true,	//可枚举
  get() {
    return name
  },
  set(newVal) {
    name = newVal
  }
})
obj.name // 码不停息

Object.defineProperty监听对象变化

现在我们有这样一个对象

let obj = {
	name:'码不停息',
    age:18,
    love:['吃饭','睡觉','打豆豆']
}

我们想实现只要是对象里面的任何数据改变,都要跟新视图或者执行我们自己的逻辑,那我们需要怎么办呢?

我们似乎需要解决以下几个问题:

  • obj对象有多个属性,可能需要__循环__添加到Object.defineProperty里面
  • obj的属性也可能是对象或者数组,可能需要__递归__
  • 用户可能给obj赋值新的属性,这种情况可能需要 单独处理

那下面我们就试探性的去解决上面的问题

首先我们先做个实现响应式的函数 defineProperty

function defineProperty(obj, key, val){
  Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        // 读取方法
        console.log('读取', key, '成功')
        return val
      },
      set(newval) {
        // 赋值监听方法
        if (newval === val) return
        observer(newval)
        console.log('监听赋值成功', newval)
        val = newval
        // 可以执行渲染操作
      }
    })
}

其次我们再做个遍历函数observer

function observer(obj) {
  if (typeof obj !== 'object' || obj == null) {
    return
  }
  for (const key in obj) {
    // 给对象中的每一个方法都设置响应式
    defineProperty(obj, key, obj[key])
  }
}

最后的代码结构如下 下面我们测试下看看我们更改数据时能不能做到响应式 可以看到我们的监听函数已经监听到数据的变化了,下面我们把数据弄复杂点 可以出现在我们虽然更改了obj.haha.name的值,但是并没有监听到数据的改变,这是为什么呢?

还记得我们上面说的 obj的属性也可能是对象或者数组,可能需要递归 下面我们来递归一下

这里递归实现非常简单,只需要把`observer`函数在`defineProperty` 重新调用一遍即可,在此判断传过来的`val`是不是一个对象,如果是一个对象在遍历下这个对象进行响应式收集

为了防止用户传进来的值也是一个对象,如用户可能这样使用

obj.name = {
	name:xxx
}

我们在set方法里面也调用下observer

现在我们的响应式是否健壮了许多,下面我们来处理下 用户可能给obj赋值新的属性 的情况,如下所示,用户可能这样给obj赋值

obj.mama = 'xxx'
// 注意 obj开始时并没有 mama 

我们可以粗略的处理下这种情况,就是写个set函数,用户使用set赋新属性,函数里面再调用defineProperty进行依赖收集,如下所示:

function set(obj, key, val) {
  defineProperty(obj, key, val)
}

下面我们来测试下 可以看出通过set定义的新属性也具备响应式了,对此我们对对象的监听已经基本完成

Object.defineProperty实现对数组的监听

Object.defineProperty 是对象的方法监听不到数组的变更的,如下图所示: 那我们怎么来实现对数组的监听呢?答案就是重写Array的原型方法

const orginalProto = Array.prototype;
const arrayProto = Object.create(orginalProto); // 先克隆一份Array的原型出来
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(method => {
  arrayProto[method] = function () {
    // 执行原始操作
    orginalProto[method].apply(this, arguments)
    console.log('监听赋值成功', method)
  }
})

原理就是重写数组的七个原始方法,当使用者执行这些方法时,我们就可以监听到数据的变化,然后做些跟新操作,下面我们在observer中加上关于对数组的判断

function observer(obj) {
  if (typeof obj !== 'object' || obj == null) {
    return
  }
  if (Array.isArray(obj)) {
    // 如果是数组, 重写原型
    obj.__proto__ = arrayProto
    // 传入的数据可能是多维度的,也需要执行响应式
    for (let i = 0; i < obj.length; i++) {
      observer(obj[i])

    }
  } else {
    for (const key in obj) {
      // 给对象中的每一个方法都设置响应式
      defineProperty(obj, key, obj[key])

    }
  }
}

其实我们发现 observer 其实是个递归,最后会把所有的数据都变成响应式,下面我们来测试下

完美!

总结

可以感觉到,在用Object.defineProperty实现数据响应式时我们必须要遍历所有的数据,还需要重写数组的方法,性能消耗也比较大,我们知道Vue2.x就是基于Object.defineProperty实现数据响应式的但新版本的Vue3放弃了Object.defineProperty采用Proxy重写了响应式系统,那Vue3为什么要选择Proxy?Proxy又是如何实现数据拦截的呢?,我们下期分享用Proxy如何实现响应式。