当前位置:   article > 正文

Vue3源码-Proxy_vue3proxy源码

vue3proxy源码

语法:

const proxy = new Proxy(target, handle)
  • target 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理
  • handler 一个通常以函数作为属性的对象,用来定制拦截行为

例:

  1. const origon={}
  2. const obj=new Proxy(origon,{
  3. get:function(target,propkey,receiver){
  4. return "10"
  5. }
  6. })
  7. console.log(obj.b);//10
  8. console.log(obj.a);//10
  9. console.log(origon.a);//undefined
  10. console.log(origon.a);//undefined

上方代码我们给一个空对象的get架设了一层代理,所有get操作都会直接返回我们定制的数字10,需要注意的是,代理只会对

Handler 对象常用的方法

方法描述
handler.has()in 操作符的捕捉器。
handler.get()属性读取操作的捕捉器。
handler.set()属性设置操作的捕捉器。
handler.deleteProperty()delete 操作符的捕捉器。
handler.ownKeys()Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。
handler.apply()函数调用操作的捕捉器。
handler.construct()new 操作符的捕捉器

下面挑handler.get重点讲一下,其它方法的使用也都大同小异,不同的是参数的区别

proxy对象生效,如上方的origin就没有任何效果

handler.get

get我们在上面例子已经体验过了,现在详细介绍一下,用于代理目标对象的属性读取操作

授受三个参数 get(target, propKey, ?receiver)

  • target 目标对象
  • propkey 属性名
  • receiver Proxy 实例本身

例:

  1. const preson = {
  2. like: 'vue.js'
  3. }
  4. const obj = new Proxy(preson, {
  5. get: function (target, propkey) {
  6. if (propkey in target) {
  7. return target[propkey]
  8. } else {
  9. throw new ReferenceError('错误')
  10. }
  11. }
  12. })
  13. console.log(obj.like);// vuejs
  14. console.log(obj.text);//错误

注意:

  • 如果要访问的目标属性是不可写以及不可配置的,则返回的值必须与该目标属性的值相同
  • 如果要访问的目标属性没有配置访问方法,即get方法是undefined的,则返回值必须为undefined

如:

  1. const obj = {};
  2. Object.defineProperty(obj, "a", {
  3. configurable: false,
  4. enumerable: false,
  5. value: 10,
  6. writable: false
  7. })
  8. const p = new Proxy(obj, {
  9. get: function(target, prop) {
  10. return 20;
  11. }
  12. })
  13. p.a // Uncaught TypeError: 'get' on proxy: property 'a' is a read-only and non-configurable..

可撤消的Proxy

proxy`有一个唯一的静态方法,`Proxy.revocable(target, handler)

Proxy.revocable()方法可以用来创建一个可撤销的代理对象

该方法的返回值是一个对象,其结构为: {"proxy": proxy, "revoke": revoke}

  • proxy 表示新生成的代理对象本身,和用一般方式 new Proxy(target, handler) 创建的代理对象没什么不同,只是它可以被撤销掉。
  • revoke 撤销方法,调用的时候不需要加任何参数,就可以撤销掉和它一起生成的那个代理对象。

该方法常用于完全封闭对目标对象的访问, 如下示例

  1. const target = { name: 'vuejs'}
  2. const {proxy, revoke} = Proxy.revocable(target, handler)
  3. proxy.name // 正常取值输出 vuejs
  4. revoke() // 取值完成对proxy进行封闭,撤消代理
  5. proxy.name // TypeError: Revoked

Proxy的应用场景

Proxy的应用范围很广,下方列举几个典型的应用场景

#校验器

想要一个number,拿回来的却是string,惊不惊喜?意不意外?下面我们使用Proxy实现一个逻辑分离的数据格式验证器

  1. const target = {
  2. _id: '1024',
  3. name: 'vuejs'
  4. }
  5. const validators = {
  6. name(val) {
  7. return typeof val === 'string';
  8. },
  9. _id(val) {
  10. return typeof val === 'number' && val > 1024;
  11. }
  12. }
  13. const createValidator = (target, validator) => {
  14. return new Proxy(target, {
  15. _validator: validator,
  16. set(target, propkey, value, proxy){
  17. let validator = this._validator[propkey](value)
  18. if(validator){
  19. return Reflect.set(target, propkey, value, proxy)
  20. }else {
  21. throw Error(`Cannot set ${propkey} to ${value}. Invalid type.`)
  22. }
  23. }
  24. })
  25. }
  26. const proxy = createValidator(target, validators)
  27. proxy.name = 'vue-js.com' // vue-js.com
  28. proxy.name = 10086 // Uncaught Error: Cannot set name to 10086. Invalid type.
  29. proxy._id = 1025 // 1025
  30. proxy._id = 22 // Uncaught Error: Cannot set _id to 22. Invalid type

私有属性

在日常编写代码的过程中,我们想定义一些私有属性,通常是在团队中进行约定,大家按照约定在变量名之前添加下划线 _ 或者其它格式来表明这是一个私有属性,但我们不能保证他能真私‘私有化’,下面使用Proxy轻松实现私有属性拦截

  1. const target = {
  2. _id: '123',
  3. name: '张三'
  4. }
  5. const proxy = new Proxy(target, {
  6. get(target, key, proxy) {
  7. if (key[0] === "_") {
  8. throw Error('我是私有属性')
  9. }
  10. return Reflect.get(target, key, proxy)
  11. },
  12. set(target, key, val, proxy) {
  13. if (key[0] === "_") {
  14. throw Error('我是私有属性')
  15. }
  16. return Reflect.set(target, key, val, proxy)
  17. }
  18. })
  19. console.log(proxy.name);//张三
  20. console.log(proxy._id);//Error: 我是私有属性
  21. console.log(proxy.name = '李四');//李四
  22. console.log(proxy._id = 456);//Error: 我是私有属性

Proxy 使用场景还有很多很多,不再一一列举,如果你需要在某一个动作的生命周期内做一些特定的处理,那么Proxy 都是适合的

为什么要用Proxy重构

在 Proxy 之前,JavaScript 中就提供过 Object.defineProperty,允许对对象的 getter/setter 进行拦截

Vue3.0之前的双向绑定是由 defineProperty 实现, 在3.0重构为 Proxy,那么两者的区别究竟在哪里呢?

首先我们再来回顾一下它的定义

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象

上面给两个词划了重点,对象上属性,我们可以理解为是针对对象上的某一个属性做处理的

语法

  • obj 要定义属性的对象
  • prop 要定义或修改的属性的名称或 Symbol
  • descriptor 要定义或修改的属性描述符
Object.defineProperty(obj, prop, descriptor)

举个例子

  1. const obj = {}
  2. Object.defineProperty(obj, "a", {
  3. value : 1,
  4. writable : false, // 是否可写
  5. configurable : false, // 是否可配置
  6. enumerable : false // 是否可枚举
  7. })
  8. // 上面给了三个false, 下面的相关操作就很容易理解了
  9. obj.a = 2 // 无效
  10. delete obj.a // 无效
  11. for(key in obj){
  12. console.log(key) // 无效
  13. }

Vue中的defineProperty

Vue3之前的双向绑定都是通过 defineProperty 的 getter,setter 来实现的,我们先来体验一下 getter,setter

  1. const obj = {};
  2. Object.defineProperty(obj, 'a', {
  3. set(val) {
  4. console.log(`开始设置新值: ${val}`)
  5. },
  6. get() {
  7. console.log(`开始读取属性`)
  8. return 1;
  9. },
  10. writable : true
  11. })
  12. obj.a = 2 // 开始设置新值: 2
  13. obj.a // 开始获取属性

看到这里,我相信有些同学已经想到了实现双向绑定背后的流程了,其实很简单嘛,只要我们观察到对象属性的变更,再去通知更新视图就好了

我们摘抄一段 Vue 源码中的核心实现验证一下,这一部分一笔代过,不是本文重点

  1. // 源码位置:https://github.com/vuejs/vue/blob/ef56410a2c/src/core/observer/index.js#L135
  2. // ...
  3. Object.defineProperty(obj, key, {
  4. enumerable: true,
  5. configurable: true,
  6. get: function reactiveGetter () {
  7. // ...
  8. if (Dep.target) {
  9. // 收集依赖
  10. dep.depend()
  11. }
  12. return value
  13. },
  14. set: function reactiveSetter (newVal) {
  15. // ...
  16. // 通知视图更新
  17. dep.notify()
  18. }
  19. })

#对象新增属性为什么不更新

这个问题用过Vue的同学应该有超过95%比例遇到过

  1. data () {
  2. return {
  3. obj: {
  4. a: 1
  5. }
  6. }
  7. }
  8. methods: {
  9. update () {
  10. this.obj.b = 2
  11. }
  12. }

上面的伪代码,当我们执行 update 更新 obj 时,我们预期视图是要随之更新的,实际是并不会

这个其实很好理解,我们先要明白 vue 中 data init 的时机,data init 是在生命周期 created 之前的操作,会对 data 绑定一个观察者 Observer,之后 data 中的字段更新都会通知依赖收集器Dep触发视图更新

然后我们回到 defineProperty 本身,是对对象上的属性做操作,而非对象本身

一句话来说就是,在 Observer data 时,新增属性并不存在,自然就不会有 getter, setter,也就解释了为什么新增视图不更新,解决有很多种,Vue 提供的全局$set 本质也是给新增的属性手动 observer

  1. function set (target: Array<any> | Object, key: any, val: any): any {
  2. // ....
  3. if (!ob) {
  4. target[key] = val
  5. return val
  6. }
  7. defineReactive(ob.value, key, val)
  8. ob.dep.notify()
  9. return val
  10. }

数组变异

由于 JavaScript 的限制,Vue 不能检测以下数组的变动: 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue

先来看一段代码

  1. var vm = new Vue({
  2. data: {
  3. items: ['1', '2', '3']
  4. }
  5. })
  6. vm.items[1] = '4' // 视图并未更新

文档已经做出了解释,但并不是defineProperty的锅,而是尤大在设计上对性能的权衡,下面这段代码可以验证

  1. function defineReactive(data, key, val) {
  2. Object.defineProperty(data, key, {
  3. enumerable: true,
  4. configurable: true,
  5. get: function defineGet() {
  6. console.log(`get key: ${key} val: ${val}`);
  7. return val;
  8. },
  9. set: function defineSet(newVal) {
  10. console.log(`set key: ${key} val: ${newVal}`);
  11. val = newVal;
  12. }
  13. })
  14. }
  15. function observe(data) {
  16. Object.keys(data).forEach(function(key) {
  17. defineReactive(data, key, data[key]);
  18. })
  19. }
  20. let test = [1, 2, 3];
  21. observe(test);
  22. test[0] = 4 // set key: 0 val: 4

虽然说索引变更不是 defineProperty 的锅,但新增索引的确是 defineProperty 做不到的,所以就有了数组的变异方法

能看到这里,大概也能猜到内部实现了,还是跟$set一样,手动 observer,下面我们验证一下

  1. const methodsToPatch = [
  2. 'push',
  3. 'pop',
  4. 'shift',
  5. 'unshift',
  6. 'splice',
  7. 'sort',
  8. 'reverse'
  9. ]
  10. methodsToPatch.forEach(function (method) {
  11. // 缓存原生数组
  12. const original = arrayProto[method]
  13. // def使用Object.defineProperty重新定义属性
  14. def(arrayMethods, method, function mutator (...args) {
  15. const result = original.apply(this, args) // 调用原生数组的方法
  16. const ob = this.__ob__ // ob就是observe实例observe才能响应式
  17. let inserted
  18. switch (method) {
  19. // push和unshift方法会增加数组的索引,但是新增的索引位需要手动observe的
  20. case 'push':
  21. case 'unshift':
  22. inserted = args
  23. break
  24. // 同理,splice的第三个参数,为新增的值,也需要手动observe
  25. case 'splice':
  26. inserted = args.slice(2)
  27. break
  28. }
  29. // 其余的方法都是在原有的索引上更新,初始化的时候已经observe过了
  30. if (inserted) ob.observeArray(inserted)
  31. // dep通知所有的订阅者触发回调
  32. ob.dep.notify()
  33. return result
  34. })
  35. })

对比

一个优秀的开源框架本身就是一个不断打碎重朔的过程,上面做了些许铺垫,现在我们简要总结一下

  • Proxy 作为新标准将受到浏览器厂商重点持续的性能优化
  • Proxy 能观察的类型比 defineProperty 更丰富
  • Proxy 不兼容IE,也没有 polyfilldefineProperty 能支持到IE9
  • Object.definedProperty 是劫持对象的属性,新增元素需要再次 definedProperty。而 Proxy 劫持的是整个对象,不需要做特殊处理
  • 使用 defineProperty 时,我们修改原来的 obj 对象就可以触发拦截,而使用 proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截
声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号