前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >实现一个简化版的Vue3数据侦测

实现一个简化版的Vue3数据侦测

作者头像
w候人兮猗
发布2020-07-01 15:29:28
5310
发布2020-07-01 15:29:28
举报

Contents

前言

距离国庆假期尤大发布vue3前瞻版本发布已经有一个月的时间,大家都知道在vue2x版本中的响应式数据更新是用的defineProperty这个API。

vue2中,针对ObjectArray两种数据类型采用了两种不同的处理方式。

对于Object类型,通过Object.defineProperty通过getter/setter递归侦测所有对象的key,实现深度侦测

对于Array类型,通过拦截Array原型上的几个操作实现了对数组的响应式,但是存在一些问题。

总之,通过defineProperty这种方式存在一定的性能问题

为了解决这个问题,从很早之前vue3就计划将采用ES6 Proxy代理的方式实现数据的响应式。(IE不支持这个API,所以vue3也不支持IE11了,垃圾IE)

关于Proxy

可以先查看MDN Proxy详细用法。

这里主要讲一下基本语法

代码语言:javascript
复制
const obj = new Proxy(target,{
    // 获取对象属性会走这里
    get(target, key, receiver){},
    // 修改对象属性会走这里
    set(target, key, value, receiver){},
    // 删除对象上的方法会走这里
    deleteProperty(target,key){} 
})

尝试使用一下Proxy这个API,尝试几种用法,发现一些问题

– 代理普通对象

代码语言:javascript
复制
const obj = {
  name: 'ahwgs',
  age: 22,
}
const res = new Proxy(obj, {
  // 获取对象属性会走这里
  get(target, key, receiver) {
    console.log('get', target, key)
    return target[key]
  },
  // 修改对象属性会走这里
  set(target, key, value, receiver) {
    console.log('set', target[key])
    target[key] = value
    return true
  },
  // 删除对象上的方法会走这里
  deleteProperty(target, key) {
    console.log('deleteProperty', target[key])
  },
})

const n = res.name
res.age = 23
console.log(obj)
// get { name: 'ahwgs', age: 22 } name
// set 22
// { name: 'ahwgs', age: 23 }
  • 代理数组
代码语言:javascript
复制
// const obj = {
//   name: 'ahwgs',
//   age: 22,
// }
const obj = [1, 2, 3]
const res = new Proxy(obj, {
  // 获取对象属性会走这里
  get(target, key, receiver) {
    console.log('get', target, key)
    return target[key]
  },
  // 修改对象属性会走这里
  set(target, key, value, receiver) {
    console.log('set', target[key])
    target[key] = value
    return true
  },
  // 删除对象上的方法会走这里
  deleteProperty(target, key) {
    console.log('deleteProperty', target[key])
  },
})

res.push(4)
console.log(obj)
// get [ 1, 2, 3 ] push
// get [ 1, 2, 3 ] length
// set undefined
// set 4
// [ 1, 2, 3, 4 ]

代理数组的时候发现了一个问题,get调用的两次,一次是push一次是length这两个都是数组自身的属性

那么vue3中是如何解决这个问题的呢?

  • 代理深层次对象
代码语言:javascript
复制
const obj = {
  name: 'ahwgs',
  age: 22,
  arr: [1, 2, 3],
}
const res = new Proxy(obj, {
  // 获取对象属性会走这里
  get(target, key, receiver) {
    console.log('get', target, key)
    return target[key]
  },
  // 修改对象属性会走这里
  set(target, key, value, receiver) {
    console.log('set', target, key)
    target[key] = value
    return true
  },
  // 删除对象上的方法会走这里
  deleteProperty(target, key) {
    console.log('deleteProperty', target[key])
  },
})

res.arr.push(4)
console.log(obj)
// get { name: 'ahwgs', age: 22, arr: [ 1, 2, 3 ] } arr
// { name: 'ahwgs', age: 22, arr: [ 1, 2, 3, 4 ] }

发现并没有执行set逻辑,并没有代理到第二层级的对象,那么vue中是如何做到深层次的代理的呢?

解决问题

上面的代码我们遇到了两个问题:

  • 多次触发了get/set
  • 无法代理深层级的对象

我们手写一个简单的vue3尝试解决上面这些问题,具体看下述代码:

代码语言:javascript
复制
const toProxy = new WeakMap() // 存放的是代理后的对象
const toRaw = new WeakMap() // 存放的是代理前的对象

function isObject(target) {
  // 这里为什么!==null 因为typeof null =object 这是js的一个bug
  return typeof target === 'object' && target !== null;
}

// 模拟UI更新
function trigger() {
  console.log('UI更新了!!');
}

// 判断key是否是val的私有属性
function hasOwn(val, key) {
  const { hasOwnProperty } = Object.prototype
  return hasOwnProperty.call(val, key)
}

// 数据代理
// target是要代理的对象,vue中data()return的那个对象
function reactive(target) {
  // 先判断如果不是对象 不需要做代理 直接返回
  if (!isObject(target)) return target;

  // 如果代理表中已经存在 就不需要再次代理 直接返回已存在的代理对象
  const proxy = toProxy.get(target)
  if (proxy) return proxy
  // 如果传入的对象被代理过
  if (toRaw.has(target)) return target

  const handler = {
    set(tar, key, value, receiver) {
      // 触发更新
      // 如果触发的是私有属性的话才去更新视图 用以解决类似于数组操作中多次set的问题
      if (hasOwn(target, key)) {
        trigger()
      }
      // 这里使用ES6 Reflect 为Proxy设置一些属性
      // 用于简化自定义的一些方法
      return Reflect.set(tar, key, value, receiver)
    },
    get(tar, key, receiver) {
      const res = Reflect.get(tar, key, receiver)
      // 判断当前修改的值是否是否是对象 如果是对象的话 递归再次代理 解决深层级代理的问题
      if (isObject(tar[key])) {
        return reactive(res)
      }
      return res
    },
    deleteProperty(tar, key) {
      return Reflect.deleteProperty(tar, key)
    },
  }

  // 被代理的对象
  const observed = new Proxy(target, handler)

  // 将代理过的对象 放入缓存中
  // 防止代理过的对象再次被代理
  // WeekMap因为的key是弱引用关系,涉及到垃圾回收机制,要比Map的效率高
  toProxy.set(target, observed) // 源对象 : 代理后的结果
  toRaw.set(observed, target) //
  return observed
}


const data = {
  name: 'ahwgs',
  age: 22,
  list: [1, 2, 3],
}
let user = reactive(data)
user = reactive(data)
user = reactive(data)
user.list.push(4)

针对上面的几个问题做以下解释:

– 多次触发了get/set

通过hasOwn这个方法,判断当前修改的属性是否是私有属性,如果是的话才去更新视图。

对于这一点,源码中是这样做的:

代码语言:javascript
复制
 // 判断是否有
  const hadKey = hasOwn(target, key)
  const result = Reflect.set(target, key, value, receiver)
  // don't trigger if target is something up in the prototype chain of original
  if (target === toRaw(receiver)) {
    /* istanbul ignore else */
    if (__DEV__) {
      const extraInfo = { oldValue, newValue: value }
      if (!hadKey) {
        trigger(target, OperationTypes.ADD, key, extraInfo)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, OperationTypes.SET, key, extraInfo)
      }
    } else {
      if (!hadKey) {
        trigger(target, OperationTypes.ADD, key)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, OperationTypes.SET, key)
      }
    }
  }

判断要setkey是否是存在的,如果是存在的就去更新视图(trigger方法),如果不是的话往视图中新增

  • 无法代理深层级的对象

通过在get方法中判断当前的值是否是对象,如果是对象的话再去代理一次,做一个递归的操作

对于源码中是这样的:

代码语言:javascript
复制
const res = Reflect.get(target, key, receiver)
    if (isSymbol(key) && builtInSymbols.has(key)) {
      return res
    }
    if (isRef(res)) {
      return res.value
    }
    track(target, OperationTypes.GET, key)
    return isObject(res)
      ? isReadonly
        ? // need to lazy access readonly and reactive here to avoid
          // circular dependency
          readonly(res)
        : reactive(res)
      : res
  }

总结

  • 整体是通过ES6 Proxy这个新特性去实现的响应式,并且还通过WeakWap去缓存的整个代理数据的保存,提高响应式数据的性能
  • 简单版是这么简单处理的,但是源码中对每一个细节处理的都很细致,并且结构分明,具体可以查看https://github.com/vuejs/vue-next/tree/master/packages/reactivity/src

关于

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019年11月6日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 关于Proxy
  • 解决问题
  • 总结
  • 关于
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档