本文首发于公众号:前端琐话(qianduansuohua),欢迎关注

前言

4 月 17 日,尤大在微博上宣布 Vue 3.0 beta 版本正式发布。

dc0928a34ea1b68a8e94bf3d9a470f5a.jpeg

在尤大发布的《 Vue3 设计过程》文章中提到之所以重构 Vue 一个考量就是JavaScript新的语言特性在主流浏览器中的支持程度,其中最值得一提的就是Proxy,它为框架提供了拦截对于object的操作的能力。Vue 的一项核心能力就是监听用户定义的状态变化并响应式刷新DOM。Vue 2是通过替换状态对象属性的getter和setter来实现这一特性的。改为Proxy后,可以突破Vue当前的限制,比如无法监听新增属性,还能提供更好的性能表现。

Two key considerations led us to the new major version (and rewrite) of Vue: First, the general availability of new JavaScript language features in mainstream browsers. Second, design and architectural issues in the current codebase that had been exposed over time.

作为一名高级前端猿,我们要知其然,更要知其所以然,那就让我们来看一下到底什么是 Proxy?

什么是 Proxy?

Proxy 这个词翻译过来就是“代理”,用在这里表示由它来“代理”某些操作。
Proxy 会在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可
以对外界的访问进行过滤和改写。

先来看下 proxy 的基本语法

const proxy = new Proxy(target, handler)
  • target :您要代理的原始对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
  • handler :一个对象,定义将拦截哪些操作以及如何重新定义拦截的操作

我们看一个简单的例子:

const person = {
    name: 'muyao',
    age: 27
};

const proxyPerson = new Proxy({}, {
  get: function(target, propKey) {
    return 35;
  }
});

proxy.name // 35
proxy.age // 35
proxy.sex // 35 不存在的属性同样起作用

person.name // muyao 原对象未改变

上面代码中,配置对象有一个get方法,用来拦截对目标对象属性的访问请求。get方法的两个参数分别是目标对象和所要访问的属性。可以看到,由于拦截函数总是返回35,所以访问任何属性都得到35

注意,Proxy 并没有改变原有对象 而是生成一个新的对象,要使得 Proxy 起作用,必须针对 Proxy 实例(上例是 proxyPerson)进行操作,而不是针对目标对象(上例是 person)进行操作

Proxy 支持的拦截操作一共 13 种:

  • get(target, propKey, receiver):拦截对象属性的读取,比如 proxy.fooproxy['foo']
  • set(target, propKey, value, receiver):拦截对象属性的设置,比如 proxy.foo = vproxy['foo'] = v, 返回一个布尔值。
  • has(target, propKey):拦截 propKey in proxy 的操作,返回一个布尔值。
  • deleteProperty(target, propKey):拦截 delete proxy[propKey] 的操作,返回一个布尔值。
  • ownKeys(target):拦截 Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in 循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名。
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
  • getPrototypeOf(target):拦截 Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target):拦截 Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)proxy.call(object, ...args)proxy.apply(...)
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)

为什么要用 Proxy?

vue2 变更检测

Vue2 中是递归遍历 data 中的所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter,在getter 中做数据依赖收集处理,在 setter 中 监听数据的变化,并通知订阅当前数据的地方。

// 对 data中的数据进行深度遍历,给对象的每个属性添加响应式
Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
         // 进行依赖收集
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            // 是数组则需要对每一个成员都进行依赖收集,如果数组的成员还是数组,则递归。
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 新的值需要重新进行observe,保证数据响应式
      childOb = !shallow && observe(newVal)
      // 将数据变化通知所有的观察者
      dep.notify()
    }
  })

但由于 JavaScript 的限制,这种实现有几个问题:

  • 无法检测对象属性的添加或移除,为此我们需要使用 Vue.set 和 Vue.delete 来保证响应系统的运行符合预期
  • 无法监控到数组下标及数组长度的变化,当直接通过数组的下标给数组设置值或者改变数组长度时,不能实时响应
  • 性能问题,当data中数据比较多且层级很深的时候,因为要遍历data中所有的数据并给其设置成响应式的,会导致性能下降

Vue3 改进

Vue3 进行了全新改进,使用 Proxy 代理的作为全新的变更检测,不再使用 Object.defineProperty

在 Vue3 中,可以使用 reactive() 创建一个响应状态

import { reactive } from 'vue'

// reactive state
const state = reactive({
  desc: 'Hello Vue 3!',
  count: 0
});

我们在源码 vue-next/packages/reactivity/src/reactive.ts 文件中看到了如下的实现:

//reactive f => createReactiveObject()
function createReactiveObject(target, toProxy, toRaw, baseHandlers, collectionHandlers) {
  ...
  // 设置拦截器
  const handlers = collectionTypes.has(target.constructor)
      ? collectionHandlers
      : baseHandlers;
  observed = new Proxy(target, handlers);
  ...
  return observed; 
}

下面我们看下 state 经过处理后的情况

可以看到被代理的目标对象 state 设置了 get()、set()、deleteProperty()、has()、ownKeys()这 5 个 handler,一起来看下它们都做了什么

get()

get() 会自动读取响应数据,并进行 track 调用

function createGetter(isReadonly = false, shallow = false) {
  return function get(target, key, receiver) {
    ...
    // 恢复默认行为
    const res = Reflect.get(target, key, receiver)
    ...
    // 调用 track
    !isReadonly && track(target, TrackOpTypes.GET, key)
    return isObject(res)
      ? isReadonly
        ? // need to lazy access readonly and reactive here to avoid
          // circular dependency
          readonly(res)
        : reactive(res)
      : res
}
  

set()

目标对象上不存在的属性设置值时,进行 “添加” 操作,并且会触发 trigger() 来通知响应系统的更新。解决了 Vue 2.x 中无法检测到对象属性的添加的问题

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    ...
    const hadKey = hasOwn(target, key)
    // 恢复默认行为
    const result = Reflect.set(target, key, value, receiver)
    // 如果目标对象在原型链上,不要 trigger
    if (target === toRaw(receiver)) {
      // 如果设置的属性不在目标对象上 就进行 Add 这就解决了 Vue 2.x 中无法检测到对象属性的添加或删除的问题
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

deleteProperty()

关联 delete 操作,当目标对象上的属性被删除时,会触发 trigger() 来通知响应系统的更新。这也解决了 Vue 2.x 中无法检测到对象属性的删除的问题

function deleteProperty(target, key) {
  const hadKey = hasOwn(target, key)
  const oldValue = (target as any)[key]
  const result = Reflect.deleteProperty(target, key)
  
  // 存在属性删除时触发 trigger
  if (result && hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}

has() 和 ownKeys()

这两个 handler 并没有修改默认行为,但是它们都调用 track() 函数,回顾上文可以知道has() 影响 in 操作的,ownKeys() 影响 for...in 及循环

function has(target: object, key: string | symbol): boolean {
  const result = Reflect.has(target, key)
  track(target, TrackOpTypes.HAS, key)
  return result
}

function ownKeys(target: object): (string | number | symbol)[] {
  track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
  return Reflect.ownKeys(target)
}

通过上面的分析,我们可以看到,Vue3 借助 Proxy 的几个 Handler 拦截操作,收集依赖,实现了响应系统核心。

Proxy 还可以做什么?

我们已经看到了 Proxy 在 Vue3 中的应用场景,其实在使用了Proxy后,对象的行为基本上都是可控的,所以我们能拿来做一些之前实现起来比较复杂的事情。

实现访问日志

let api = {
  getUser: function(userId) {
    /* ... */
  },
  setUser: function(userId, config) {
    /* ... */
  }
};
// 打日志
function log(timestamp, method) {
  console.log(`${timestamp} - Logging ${method} request.`);
}
api = new Proxy(api, {
  get: function(target, key, proxy) {
    var value = target[key];
    return function(...arguments) {
      log(new Date(), key); // 打日志
      return Reflect.apply(value, target, arguments);
    };
  }
});
api.getUsers();

校验模块

let numObj = { count: 0, amount: 1234, total: 14 };
numObj = new Proxy(numObj, {
  set(target, key, value, proxy) {
    if (typeof value !== 'number') {
      throw Error('Properties in numObj can only be numbers');
    }
    return Reflect.set(target, key, value, proxy);
  }
});

// 抛出错误,因为 "foo" 不是数值
numObj.count = 'foo';
// 赋值成功
numObj.count = 333;

可以看到 Proxy 可以有很多有趣的应用,大家快快去探索吧!



牧遥
17 声望1 粉丝