你了解vue3.0响应式数据怎么实现吗?

从 Proxy 提及

什么是Proxy

proxy翻译过来的意思就是”代理“,ES6对Proxy的定位就是target对象(原对象)的基础上经过handler增长一层”拦截“,返回一个新的代理对象,以后全部在Proxy中被拦截的属性,均可以定制化一些新的流程在上面,先看一个最简单的例子javascript

const target = {}; // 要被代理的原对象
// 用于描述代理过程的handler
const handler = {
  getfunction (target, key, receiver{
    console.log(`getting ${key}!`);
    return Reflect.get(target, key, receiver);
  },
  setfunction (target, key, value, receiver{
    console.log(`setting ${key}!`);
    return Reflect.set(target, key, value, receiver);
  }
}
// obj就是一个被新的代理对象
const obj = new Proxy(target, handler);
obj.a = 1 // setting a!
console.log(obj.a) // getting a!
复制代码

上面的例子中咱们在target对象上架设了一层handler,其中拦截了针对target的get和set,而后咱们就能够在get和set中间作一些额外的操做了html

注意1:对Proxy对象的赋值操做也会影响到原对象target,同时对target的操做也会影响Proxy,不过直接操做原对象的话不会触发拦截的内容~vue

obj.a = 1// setting a!
console.log(target.a) // 1 不会打印 "getting a!"
复制代码

注意2:若是handler中没有任何拦截上的处理,那么对代理对象的操做会直接通向原对象java

const target = {};
const handler = {};
const obj = new Proxy(target, handler);
obj.a = 1;
console.log(target.a) // 1
复制代码

既然proxy也是一个对象,那么它就能够作为原型对象,因此咱们把obj的原型指向到proxy上后,发现对obj的操做会找到原型上的代理对象,若是obj本身有a属性,则不会触发proxy上的get,这个应该很好理解git

const target = {};
const obj = {};
const handler = {
    getfunction(target, key){
            console.log(`get ${key} from ${JSON.stringify(target)}`);
            return Reflect.get(target, key);
    }
}
const proxy = new Proxy(target, handler);
Object.setPrototypeOf(obj, proxy);
proxy.a = 1;
obj.b = 1
console.log(obj.a) // get a from {"a": 1}   1
console.log(obj.b) // 1
复制代码

ES6的Proxy实现了对哪些属性的拦截?

经过上面的例子了解了Proxy的原理后,咱们来看下ES6目前实现了哪些属性的拦截,以及他们分别能够作什么? 下面是 Proxy 支持的拦截操做一览,一共 13 种es6

  1. get(target, propKey, receiver):拦截对象属性的读取,好比proxy.foo和proxy['foo'];
  2. set(target, propKey, value, receiver):拦截对象属性的设置,好比proxy.foo = v或proxy['foo'] = v,返回一个布尔值;
  3. has(target, propKey):拦截propKey in proxy的操做,返回一个布尔值。
  4. deleteProperty(target, propKey):拦截delete proxy[propKey]的操做,返回一个布尔值;
  5. ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for…in循环,返回一个数组。该方法返回目标对象全部自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性;
  6. getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象;
  7. defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值;
  8. preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值;
  9. getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象;
  10. isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值;
  11. setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。若是目标对象是函数,那么还有两种额外操做能够拦截;
  12. apply(target, object, args):拦截 Proxy 实例做为函数调用的操做,好比proxy(…args)、proxy.call(object, …args)、proxy.apply(…);
  13. construct(target, args):拦截 Proxy 实例做为构造函数调用的操做,好比new proxy(…args);

以上是目前es6支持的proxy,具体的用法不作赘述,有兴趣的能够到阮一峰老师的es6入门去研究每种的具体用法,其实思想都是同样的,只是每种对应了一些不一样的功能~github

实际场景中 Proxy 能够作什么?

实现私有变量

js的语法中没有private这个关键字来修饰私有变量,因此基本上全部的class的属性都是能够被访问的,可是在有些场景下咱们须要使用到私有变量,如今业界的一些作法都是使用”_变量名“来”约定“这是一个私有变量,可是若是哪天被别人从外部改掉的话,咱们仍是没有办法阻止的,然而,当Proxy出现后,咱们能够用代理来处理这种场景,看代码:数据库

const obj = {
    _name'nanjin',
    age19,
    getName() => {
        return this._name;
    },
    setName(newName) => {
        this._name = newName;
    }
}

const proxyObj = obj => new Proxy(obj, {
    get(target, key) => {
        if(key.startsWith('_')){
            throw new Error(`${key} is private key, please use get${key}`)
        }
        return Reflect.get(target, key);
    },
    set(target, key, newVal) => {
        if(key.startsWith('_')){
            throw new Error(`${key} is private key, please use set${key}`)
        }
        return Reflect.set(target, key, newVal);
    }
})

const newObj = proxyObj(obj);
console.log(newObj._name) // Uncaught Error: _name is private key, please use get_name
newObj._name = 'newname'// Uncaught Error: _name is private key, please use set_name
console.log(newObj.age) // 19
console.log(newObj.getName()) // nanjin
复制代码

可见,经过proxyObj方法,咱们能够实现把任何一个对象都过滤一次,而后返回新的代理对象,被处理的对象会把全部_开头的变量给拦截掉,更进一步,若是有用过mobx的同窗会发现mobx里面的store中的对象都是相似于这样的设计模式

有handler 和 target,说明mobx自己也是用了代理模式,同时加上Decorator函数,在这里就至关于把proxyObj使用装饰器的方式来实现,Proxy + Decorator 就是mobx的核心原理啦~api

vue响应式数据实现

VUE的双向绑定涉及到模板编译,响应式数据,订阅者模式等等,有兴趣的能够看这里,由于这篇文章的主题是proxy,所以咱们着重介绍一下数据响应式的过程。

2.x版本

在当前的vue2.x的版本中,在data中声名一个obj后,vue会利用Object.defineProperty来递归的给data中的数据加上get和set,而后每次set的时候,加入额外的逻辑。来触发对应模板视图的更新,看下伪代码:

const defineReactiveData = data => {
    Object.keys(data).forEach(key => {
        let value = data[key];
        Object.defineProperty(data, key, {
         get : function(){
            console.log(`getting ${key}`)
            return value;
         },
         set : function(newValue){
            console.log(`setting ${key}`)
            notify() // 通知相关的模板进行编译
            value = newValue;
         },
         enumerable : true,
         configurable : true
        })
    })
}
复制代码

这个方法能够给data上面的全部属性都加上get和set,固然这只是伪代码,实际场景下咱们还须要考虑若是某个属性仍是对象咱们应该递归下去,来试试:

const data = {
    name: 'nanjing',
    age: 19
}
defineReactiveData(data)
data.name // getting name  'nanjing'
data.name = 'beijing';  // setting name
复制代码

能够看到当咱们get和set触发的时候,已经可以同时触发咱们想要调用的函数拉,Vue双向绑定过程当中,当改变this上的data的时候去更新模板的核心原理就是这个方法,经过它咱们就能在data的某个属性被set的时候,去触发对应模板的更新。

如今咱们在来试试下面的代码:

const data = {
    userIds: ['01','02','03','04','05']
}
defineReactiveData(data);
data.userIds // getting userIds ["01", "02", "03", "04", "05"]
// get 过程是没有问题的,如今咱们尝试给数组中push一个数据
data.userIds.push('06'// getting userIds 
复制代码

what ? setting没有被触发,反而由于取了一次userIds因此触发了一次getting~,
不只如此,不少数组的方法都不会触发setting,好比:push,pop,shift,unshift,splice,sort,reverse这些方法都会改变数组,可是不会触发set,因此Vue为了解决这个问题,从新包装了这些函数,同时当这些方法被调用的时候,手动去触发notify();看下源码:

// 得到数组原型
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
  })
})
复制代码

上面是官方的源码,咱们能够实现一下push的伪代码,为了省事,直接在prototype上下手了~

const push = Array.prototype.push;
Array.prototype.push = function(...args){
    console.log('push is happenning');
    return push.apply(this, args);
}
data.userIds.push('123'// push is happenning
复制代码

经过这种方式,咱们能够监听到这些的变化,可是vue官方文档中有这么一个注意事项

因为 JavaScript 的限制,Vue 不能检测如下变更的数组:

  • 当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue
  • 当你修改数组的长度时,例如:vm.items.length = newLength
    这个最根本的缘由是由于这2种状况下,受制于js自己没法实现监听,因此官方建议用他们本身提供的内置api来实现,咱们也能够理解到这里既不是defineProperty能够处理的,也不是包一层函数就能解决的,这就是2.x版本如今的一个问。

回到这篇文章的主题,vue官方会在3.x的版本中使用proxy来代替defineProperty处理响应式数据的过程,咱们先来模拟一下实现,看看可否解决当前遇到的这些问题;

3.x版本

咱们先来经过proxy实现对data对象的get和set的劫持,并返回一个代理的对象,注意,咱们只关注proxy自己,全部的实现都是伪代码,有兴趣的同窗能够自行完善

const defineReactiveProxyData = data => new Proxy(data, 
    {
        getfunction(data, key){
            console.log(`getting ${key}`)
            return Reflect.get(data, key);
        },
        setfunction(data, key, newVal){
            console.log(`setting ${key}`);
            if(typeof newVal === 'object'){ // 若是是object,递归设置代理
                return Reflect.set(data, key, defineReactiveProxyData(newVal));
            }
            return Reflect.set(data, key, newVal);
        }
    })
const data = {
    name'nanjing',
    age19
};
const vm = defineReactiveProxyData(data);
vm.name // getting name  nanjing
vm.age = 20// setting age  20
复制代码

看起来咱们的代理已经起做用啦,以后只要在setting的时候加上notify()去通知模板进行编译就能够了,而后咱们来尝试设置一个数组看看;

vm.userIds = [1,2,3] //  setting userIds
vm.userIds.push(1);
// getting userIds 由于咱们会先访问一次userids
// getting push 调用了push方法,因此会访问一次push属性
// getting length 数组push的时候 length会变,因此须要先访问原来的length
// setting 3 经过下标设置的,因此set当前的index3
// setting length 改变了数组的长度,因此会set length
// 4 返回新的数组的长度
复制代码

回顾2.x遇到的第一个问题,须要从新包装Array.prototype上的一些方法,使用了proxy后不须要了,解决了~,继续看下一个问题

vm.userIds.length = 2
// getting userIds 先访问
// setting length 在设置
vm.userIds[1] = '123'
// getting userIds 先访问
// setting 1 设置index=1的item
// "123"
复制代码

从上面的例子中咱们能够看到,不论是直接改变数组的length仍是经过某一个下标改变数组的内容,proxy都能拦截到此次变化,这比defineProperty方便太多了,2.x版本中的第二个问题,在proxy中根本不会出现了。

总结1

经过上面的例子和代码,咱们看到Vue的响应模式若是使用proxy会比如今的实现方式要简化和优化不少,很快在即未来临的3.0版本中,你们就能够体验到了。不过由于proxy自己是有兼容性的,好比ie浏览器,因此在低版本的场景下,vue会回退到如今的实现方式。

总结2

回归到proxy自己,设计模式中有一种典型的代理模式,proxy就是js的一种实现,它的好处在于,我能够在不污染自己对象的条件下,生成一个新的代理对象,全部的一些针对性逻辑放到代理对象上去实现,这样我能够由A对象,衍生出B,C,D…每一个的处理过程都不同,从而简化代码的复杂性,提高必定的可读性,好比用proxy实现数据库的ORM就是一种很好的应用,其实代码很简单,关键是要理解背后的思想,同时可以触类旁通~

扩展:

1.Proxy.revocable()

这个方法能够返回一个可取消的代理对象

const obj = {};
const handler = {};
const {proxy, revoke} = Proxy.revocable(obj, handler);
proxy.a = 1
proxy.a // 1
revoke();
proxy.a // Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
复制代码

一旦代理被取消了,就不能再从代理对象访问了

打印proxy 能够看到IsRevoked变为true了

2.代理对象的this问题

由于new Proxy出来的是一个新的对象,因此在若是你在target中有使用this,被代理后的this将指向新的代理对象,而不是原来的对象,这个时候,若是有些函数是原对象独有的,就会出现this指向致使的问题,这种场景下,建议使用bind来强制绑定this

看代码:

const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);

proxy.getDate(); // Uncaught TypeError: this is not a Date object.
复制代码

由于代理后的对象并非一个Date类型的,不具备getDate方法的,因此咱们须要在get的时候,绑定一下this的指向

const target = new Date();
const handler = {
    getfunction(target, key){
        if(typeof target[key] === 'function'){
            return target[key].bind(target) // 强制绑定
            this到原对象
        }
        return Reflect.get(target, key)
    }
};
const proxy = new Proxy(target, handler);

proxy.getDate(); // 6
复制代码

这样就能够正常使用this啦,固然具体的使用还要看具体的场景,灵活运用吧!

伪代码部分都是笔者揣摩写的,若有问题,欢迎指正~