Vue3为什么比Vue2快—Proxy实现响应式

1,724 阅读2分钟

Vue3为什么比Vue2快的原因,主要有一下几点:

  • Proxy实现响应式
  • PatchFlag
  • hoistStatic
  • CacheHandler
  • Tree-shaking

由于篇幅有限,接下来分开讲,本篇先说一下Proxy实现响应式。

Object.defineProperty缺点

先回顾一下Vue2.x的Object.defineProperty的缺点

  • 深度监听需要一次性递归。初始化阶段和Set阶段都需要深度监听
  • 无法监听新增属性和删除属性。如果想实现监听,新增属性需要使用Vue.set,删除属性需要 使用Vue.delete
  • 无法原生监听数组,需要特殊处理

Proxy基本使用

/**
 * 创建响应式数据
 * @param {Object|Array} data
 * @returns
 */
function createProxyData (data) {
  return new Proxy(data, {
    get (target, key, receiver) {
      console.log(`触发get key=${key}`)
      const result = Reflect.get(target, key, receiver)
      console.log(`get 结果: ${result}`)
      return result
    },
    set (target, key, value, receiver) {
      // 返回Boolean值
      console.log(`触发set key=${key},value=${value}`)
      const result = Reflect.set(target, key, value, receiver)
      console.log(`set 结果: ${result}`)
      // 返回结果 成功true,失败false
      return result
    },
    deleteProperty (target, key) {
      console.log(`触发 deleteProperty key=${key}`)
      const result = Reflect.deleteProperty(target, key)
      console.log(`deleteProperty 结果: ${result}`)
      return result
    }
  })
}

下面我们通过示例开一下输出结果

对象响应式

创建对象的响应式

const proxyData = createProxyData({ name: '太凉', phone: '186****8080' })
获取对象属性
const proxyData = createProxyData({ name: '太凉', phone: '186****8080' })
console.log(proxyData.name) //获取name值

输出结果

触发get key=name
get 结果: 太凉
太凉
设置对象属性
const proxyData = createProxyData({ name: '太凉', phone: '186****8080' })
proxyData.age = 20 // 触发set

输出结果:

触发set key=age,value=20
set 结果: true
删除对象属性
const proxyData = createProxyData({ name: '太凉', phone: '186****8080' })
delete proxyData.phone

输出结果:

触发 deleteProperty key=phone
deleteProperty 结果: true

对象的响应式很简单,通过简单的代码就可以实现,下面看一下 数组的响应式

数组的响应式

创建数组的响应式

const proxyArray = createProxyData(['a', 'b'])
新增值
const proxyArray = createProxyData(['a', 'b'])
proxyArray.push('c')

输出结果:

触发get key=push
get 结果: function push() { [native code] }
触发get key=length
get 结果: 2
触发set key=2,value=c
set 结果: true
触发set key=length,value=3
set 结果: true

看到输出其结果,是不是很奇怪,只是调用了一下push('c') 触发了两次get,两次set。我们分析一下:

  • 触发get key=push : 调用 push方法会触发. 需要忽略掉
  • 触发get key=length:对数组设置值的时候,或获取当前 length长度
  • 触发set key=2,value=c : 设置 2 位置上的值为 'c'
  • 触发set key=length,value=3 : 更新数组的长度
优化1:忽略数组原型方法
function createProxyData (data) {
  return new Proxy(data, {
    get (target, key, receiver) {
      /**新增1:start **/
      //忽略 push,pop,....等原型上的属性,只对本身的属性进行监听
      if (Reflect.ownKeys(target).includes(key)) {
        // 监听
        console.log(`触发get key=${key}`)
      }
      /**新增1:end **/
      const result = Reflect.get(target, key, receiver)
      return result
    },
    set (target, key, value, receiver) {
      /**新增2: start**/
      // 不重复处理数据
      const oldValue = target[key]
      if (oldValue === value) {
        return true
      }
      /**新增2:end **/
      console.log(`触发set key=${key},value=${value}`)
      const result = Reflect.set(target, key, value, receiver)
      console.log(`set 结果: ${result}`)
      // 返回结果 成功true,失败false
      return result
    },
    deleteProperty (target, key) {
      console.log(`触发 deleteProperty key=${key}`)
      const result = Reflect.deleteProperty(target, key)
      console.log(`deleteProperty 结果: ${result}`)
      return result
    }
  })
}

const proxyArray = createProxyData(['a', 'b'])
proxyArray.push('c')

输出结果:

触发get key=length
触发set key=2,value=c
set 结果: true

Proxy实现响应式

根据上线的分析结果可以看到,通过Proxy很容易实现 对象的响应式。实现数组的响应式,需要注意两点:

  • 忽略 push,pop,....等原型上的属性,只对本身的属性进行监听
  • 不重复处理数据

基于以上内容,下面用Proxy实现简单的响应式

// 创建响应式
function reactive (target = {}) {
  if (typeof target !== 'object' || target == null) {
    // 不是对象或数组,则返回
    return target
  }

  // 代理配置
  const proxyHanlder = {
    get (target, key, receiver) {
      // 忽略 push,pop,....等原型上的属性,只对本身的属性进行监听
      if (Reflect.ownKeys(target).includes(key)) {
        console.log('get', key) // 监听
      }
      const result = Reflect.get(target, key, receiver)

      // 深度监听
      return reactive(result)
    },
    set (target, key, val, receiver) {
      // 重复的数据,不处理
      if (val === target[key]) {
        return true
      }

      const ownKeys = Reflect.ownKeys(target)
      if (ownKeys.includes(key)) {
        console.log('已有的 属性', key)
      } else {
        console.log('新增的 属性', key)
      }

      const result = Reflect.set(target, key, val, receiver)
      console.log('set', key, val)
      return result
    },
    deleteProperty (target, key) {
      const result = Reflect.deleteProperty(target, key)
      console.log('delete property', key)
      return result
    }
  }

  // 生成新对象
  const proxyData = new Proxy(target, proxyHanlder)
  return proxyData
}

测试一下:

const data = {
  name: 'TaiLiang',
  age: 18,
  address: {
    province: '北京'
  }
}

const proxyData = reactive(data)
console.log(proxyData)

Vue3比Vue2块的原因之一

Proxy实现的响应式优点

  • 深度监听层级只到下一级,不会一直到底,性能好
  • 可以监听 新增属性和删除属性
  • 可以监听 原生数组变化

通过以上三点,可以看到Proxy能避开Object.defineProperty的缺点,但是Proxy也有缺点,无法兼容所有浏览器,无法做polyfill

参考资料