从数组的响应式看下一代响应式数据

随着MVVM框架的广泛使用响应式数据已经变得耳熟能详,现在谈响应式数据好像有点炒冷饭的意思,对!没错!不过这次炒的是蛋炒饭,而且还是加火腿肠的那种。之前看过几个框架的响应式数据实现,貌似都对数组(Array)做了特殊处理,咋的啦,有这么特别吗?看来是时候反思和总结一波了。

如何实现响应式数据

首先来简单实现一个响应式对象。easy! ES5在Object对象中新增了defineProperties和defineProperty两个方法用来对对象的属性进行描述,其中的setter和getter这一对CP就是实现响应式对象的关键,先来看一段简单的代码

const person = {
    name:"wyy",
    age:"18"
}
Object.defineProperty(person,name,{
    get(){
        console.log(`没错,我就是人见人爱,车见爆胎的wyy`);
        return "wyy"
    },
    set(newVal){
        console.log('俺老王,行不更名,坐不改姓')
        return
    }
})

没错就是这么简单,这是目前普遍的实现方式。当然了,这段代码只是响应式的一部分,在get中进行依赖搜集,在set中通知收集来的依赖进行相应的处理,然后再搭配上高大上的观察者模式,这样响应式系统才基本成型,大部分响应式数据的实现都是这个路数,比如看过Vue2.x的响应式数据源码会发现几个关键字Observe,Watcher,Dep,notify,从字面意思来看大概也就是这么个路数,这里就不展开说了,毕竟掘金上个个都是人才,上面有不下50篇文章分析vue的响应式数据,篇篇精彩。

好了分析结束。wait…wait…wait…标题好像是谈数组的响应式,跑题了

实现一个响应式数组

下面来说所数组,没记错的话数组也是继承自Object,它算一种特殊的对象,那么数组能用Object.defineProperty来实现响应式吗?不说了,打开VSCode就是干

const arr = ["2019","云","栖","音","乐","节"];
arr.forEach((val,index)=>{
    Object.defineProperty(arr,index,{
        set(newVal){
            console.log("来了老弟");
        },
        get(){
            console.log("小老弟,来呀,快活呀");
            return val;
        }
    })
})
let index = arr[1];
arr[0] = "2050";

没毛病,和上面的效果一样儿一样儿的

上面这段代码,有没有人没看懂,我假装你们都不懂,贴张图


不,不,不是这张,是这张


通过观察数组的结构发现其实数组也是一个key-value键值对集合,只是key是数字罢了,那自然也可以通过属性描述的方式来实现访问和赋值的拦截了。一切貌似都是那么合理,那么Vue是这么干的吗?翻开源码一顿看后发现

import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(function (method) {
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    ob.dep.notify()
    return result
  })
})

实在不想贴源码了,太占篇幅了。你没有看错,Vue的响应式系统中并没有通过属性描述的方式处理数组,而对数组对象上的几常用方法进行了代理,只有当我们调用数组的这7个方法的时候才会触发视图的更新,这也解释了为什么我们在Vue项目中通过改变数组索引的方式改变数值是不会触发视图更新的。那么问题来了,为什么不使用属性描述的方式呢?

下面接着上面实现的响应式数组看个问题,直接贴图


前面的数组长度是6,这里我们直接给索引为7的位置赋值,会发现属性访问和赋值并没有被拦截。这是因为,当我们在数组中新增一个元素的时候,新增的元素是设置set和get属性的,所以索引位置的访问和赋值就不能被拦截了,这个好理解,即便是一个普通的key-value对象新增一个key后也需要对该key重新进行属性描述(defineProperty),所以貌似重新通过Object.definePropert一下就好了,这就完了吗?再来看张图


当我们通过上面这种方式改变数组长度的时候,发现之前设置的访问和赋值拦截都不起作用了,纳尼!其实也没什么,就算一个普通的key-value对象,你把对应的key删除掉,结果也一样。

再来看一张图:

瞬间数组的长度就变成16,中间的位置使用空元素填充。这个时候我们改变这些新增位置值的时候,也是不会被拦截的。

从的上面的操作总结一下,通过Object.defineProperty描述数组为什么不合适

  1. 数组和普通对象的区别在于,js中的数组太"多变"了,就像…。比如:arr.length=0,可以瞬间清空一个数组;arr[100]=1有可以瞬间将一个数组的长度变为100,等等骚操作。对于一个普通对象,我们一般只会改变key对应的value值,而不会连key都改变了,当key改变时我们只需要在拦截器中将重新赋值的value使用Object.defineProperty给它加上拦截器就行了。但是数组就不一样了,因为他的多变,我们需要重新将整个数组对象的所有key递归的加上拦截器并且我们还要穷举每一种数组变化的可能,这样势必就会带来性能开销问题,有的人会觉得这点性能开销算个x呀,但是性能问题都是由小变大的,如果数组对象中存的数据量大而且操作频繁的时候,这就是一个大问题。React16.x在就因为在优化textNode的时候,移除了一些无意义的标签,性能据说都提升了多少个percent。
  2. 数组在应用中经常会被操作,但是通常'push','pop','shift','unshift','splice','sort','reverse'这7中方法,基本上也能达到我们的目的。因此如果为了使数组具备通过value的改变就能触发视图更新的能力,而使用Object.defineProperty就显得代价太大了。

这大概这也是Vue设计之初考虑到的。那么数组的响应式实现只能是这种阉割版吗?接着往下看。

Proxy和Reflect实现响应式数组

有一天在React项目中玩弄mobx的时候,突然发现在mobx中通过改变数组索引也能触发视图的改变,该死的好奇心让我辗转反侧,难于入眠,于是乎装模作样的去看了一下它的实现,不看不知道一看又发现一对CPProxy和Reflet,真香。讲真,要是不是mobx我还真想不起他两的使用场景了。

Proxy和Reflect是ES2015新增的类,字面意思已经很明白了代理,反射,目的就是用他们来生成一个代理对象。这里不展开讲这两个类本身的使用了,扔一个链接,没映象或概念模糊的可以去看看ES的黑科技Proxy和Reflect,简单来说这两家伙就是Object.defineProperty的加强版。下面用Proxy和Reflect来将前面的代码重构一下。
下面用Proxy和Reflect来重写一下上面的代码

const arr = ["2019","云","栖","音","乐","节"];
let ProxyArray = new Proxy(arr,{
    get:function(target, name, value, receiver) {
        console.log("来了老弟")
        return Reflect.get(target,name);
    },
    set: function(target, name, value, receiver) {
       console.log("来啊,快活呀")
       Reflect.set(target,name, value, receiver);;
    }
 })
 const index = ProxyArray[0];
 ProxyArray[0]="2050"

效果一样一样的,而且我们不用去遍历每个key给它加上属性描述器了,还有个大大的好处就是通过代理对象无论我们怎么改变数组,数组的变化和访问我们都能观察到,大家可以去试试看。

当然好的东西也是有缺点的,就是兼容性问题,因为这是ES6的新特性,而且是没办hack成ES5,所以使用的时候你懂的…。

下一代响应式数据的实现

从上的文章中有没有闻出点味道,没错是他就是她,Proxy和Reflect,这对CP会作为猪脚闪亮登场了,微软都放弃IE拥抱chromiun了,还考虑那么多兼容问题干嘛。
所以在以后响应式数据的实现可能就变成下面这样了

const defineReactiveProxyData = data => new Proxy(data,{
        get: function(data, key){
            //....依赖搜集
            return Reflect.get(data, key);
        },
        set: function(data, key, newVal){
            //通知下发
            if(typeof newVal === 'object'){ // 如果是object,递归设置代理
                return Reflect.set(data, key, defineReactiveProxyData(newVal));
            }
            return Reflect.set(data, key, newVal);
        }
    })

通过Proxy和Reflect完美的解决了之前数组响应式实现遇到的问题,同时也不用再做那么多深层的遍历了,是不是爽歪歪。

最后再扔一个vue3.0的源码链接,有兴趣的可以去看看Vue3.0中响应式数据部分的实现。

总结

其实讲了这么多,就是想将大家的目光从Object.defineProperty吸引到Proxy和Rflect上来,毕竟ES2019都出来了,Proxy和Rflect也该拿出来用用了,不要某天某个时刻被问起ES6的时候只记得let,const,箭头函数之类的。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值