数据劫持Proxy之apply

1,080 阅读3分钟

概述

所谓proxy,字面意义是代理。而有关于代理的名词我们应该听过颇多,例如:代理模式、ip代理等。同时,proxy在Vue,React,Angular等mvvm前端框架的运用中,我们最为耳熟的应该是数据绑定,这使得我们不需要手动进行Dom操作也能实现数据更新。对于ES6中的proxy代理,我们可以简单理解为在目标对象前设置一层拦截,所有外界的访问都必须经过这层拦截,才能对目标对象进行操作,而我们可以在拦截层对操作进行过滤和改写。

Proxy中的劫持操作

对于Proxy中的数据劫持操作,我们使用最多的应该是get和set了。其实除了这两项常用操作,proxy还有一些其他处理方式:

图1

Proxy代理方法——apply

提到apply,想必大家会联想到修改this指向的apply方法。其实,proxy的apply内部实现也蕴含了这样的操作。以下是从最近一次的学习示例中,来聊聊proxy方法代理的apply。
首先,上个栗子!此例本来是想每秒执行一次回调,但是输出结果却不如人愿。

    function asyncQue(cb) {
      setTimeout(() => {
        cb()
      },1000)
    }  
    const cb = () => {
      let now = new Date().getTime()
      console.log(now)
    }
    
    asyncQue(cb)
    asyncQue(cb)
    asyncQue(cb)

图2
为什么会这样呢?其实,大家应该了解setTimeout并不能准时运行。此例中,在主线程每次调用时,遇到了setTimeout,都是先将回调函数加入event table中,然后开始计时,1秒到达就加入event queue。只有主线程栈空,才会开始执行,而三次调用几乎是同时的,所以执行时间几乎相同。
接着,借用这个例子来聊聊apply,并捋顺回调执行。既然是代理方法,那么我们先创建一个Proxy对象,而目标对象就是我们的asyncQue方法咯,此时会返回一个代理方法。
图3
然后,为handle对象添加上apply,apply接收三个参数,一是目标对象target,二是this指向_this,三是目标对象接收的参数args,而且args为数组,无需扩展运算符'...'
图4
接着,要将回调捋顺,可以借助我们的Promise。首先,定义个全局promise。在每次调用时then会返回一个pending状态的Promise对象,会修改那个全局promise,用作下一次调用时resolve的promise。而只有上一个回调执行结束resolve后,promise状态改变为resolved时,下一个promise的then方法回调才会执行。
剩下的就是恢复目标函数默认行为。怎么恢复呢?我们有和proxy紧密联系的Reflect,通过Reflect的apply静态方法,我们可以完成目标对象的执行。其实,这里的apply确实和修改this指向的apply相像,他接收三个参数,一是要执行的目标对象,二是要绑定的this对象,三是类数组参数。

    let promise //全局
    let asyncQueProxy = new Proxy(asyncQue,{
      apply(target,_this,args) {
        promise = Promise.resolve(promise).then(() => {
          console.log(target)
          // console.log(args)
          // .then 返回一个promise
          // 上一个promise resolve时,下一个才能执行.then
          // Promise.resolve 接收一个promise则返回该promise
          return new Promise((resolve,reject) => {
            // 恢复他的默认行为
            Reflect.apply(target,_this,[() => {
              args[0]()
              resolve()
            }])
          })
        })
        console.log(promise)
      }
    })

    asyncQueProxy(cb)
    asyncQueProxy(cb)
    asyncQueProxy(cb)

最后,通过代理对象调用结果我们可以看到,三次回调间隔都是1秒。

图5