你了解vue3.0响应式数据怎么实现吗


Posted in Javascript onJune 07, 2019

从 Proxy 说起

什么是Proxy

proxy翻译过来的意思就是”代理“,ES6对Proxy的定位就是target对象(原对象)的基础上通过handler增加一层”拦截“,返回一个新的代理对象,之后所有在Proxy中被拦截的属性,都可以定制化一些新的流程在上面,先看一个最简单的例子

const target = {}; // 要被代理的原对象
// 用于描述代理过程的handler
const handler = {
 get: function (target, key, receiver) {
  console.log(`getting ${key}!`);
  return Reflect.get(target, key, receiver);
 },
 set: function (target, key, value, receiver) {
  console.log(`setting ${key}!`);
  return Reflect.set(target, key, value, receiver);
 }
}
// obj就是一个被新的代理对象
const obj = new Proxy(target, handler);
obj.a = 1 // setting a!
console.log(obj.a) // getting a!

上面的例子中我们在target对象上架设了一层handler,其中拦截了针对target的get和set,然后我们就可以在get和set中间做一些额外的操作了

注意1:对Proxy对象的赋值操作也会影响到原对象target,同时对target的操作也会影响Proxy,不过直接操作原对象的话不会触发拦截的内容~

obj.a = 1; // setting a!
console.log(target.a) // 1 不会打印 "getting a!"

注意2:如果handler中没有任何拦截上的处理,那么对代理对象的操作会直接通向原对象

const target = {};
const handler = {};
const obj = new Proxy(target, handler);
obj.a = 1;
console.log(target.a) // 1

既然proxy也是一个对象,那么它就可以做为原型对象,所以我们把obj的原型指向到proxy上后,发现对obj的操作会找到原型上的代理对象,如果obj自己有a属性,则不会触发proxy上的get,这个应该很好理解

const target = {};
const obj = {};
const handler = {
  get: function(target, key){
      console.log(`get ${key} from ${JSON.stringify(target)}`);
      return Reflect.get(target, key);
  }
}
const proxy = new Proxy(target, handler);
Object.setPrototypeOf(obj, proxy);
proxy.a = 1;
obj.b = 1
console.log(obj.a) // get a from {"a": 1}  1
console.log(obj.b) // 1

ES6的Proxy实现了对哪些属性的拦截?

通过上面的例子了解了Proxy的原理后,我们来看下ES6目前实现了哪些属性的拦截,以及他们分别可以做什么? 下面是 Proxy 支持的拦截操作一览,一共 13 种

  1. get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy['foo'];
  2. set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值;
  3. has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
  4. deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值;
  5. ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for…in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性;
  6. getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象;
  7. defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值;
  8. preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值;
  9. getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象;
  10. isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值;
  11. setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截;
  12. apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(…args)、proxy.call(object, …args)、proxy.apply(…);
  13. construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(…args);

以上是目前es6支持的proxy,具体的用法不做赘述,有兴趣的可以到阮一峰老师的es6入门去研究每种的具体用法,其实思想都是一样的,只是每种对应了一些不同的功能~

实际场景中 Proxy 可以做什么?

实现私有变量

js的语法中没有private这个关键字来修饰私有变量,所以基本上所有的class的属性都是可以被访问的,但是在有些场景下我们需要使用到私有变量,现在业界的一些做法都是使用”_变量名“来”约定“这是一个私有变量,但是如果哪天被别人从外部改掉的话,我们还是没有办法阻止的,然而,当Proxy出现后,我们可以用代理来处理这种场景,看代码:

const obj = {
  _name: 'nanjin',
  age: 19,
  getName: () => {
    return this._name;
  },
  setName: (newName) => {
    this._name = newName;
  }
}

const proxyObj = obj => new Proxy(obj, {
  get: (target, key) => {
    if(key.startsWith('_')){
      throw new Error(`${key} is private key, please use get${key}`)
    }
    return Reflect.get(target, key);
  },
  set: (target, key, newVal) => {
    if(key.startsWith('_')){
      throw new Error(`${key} is private key, please use set${key}`)
    }
    return Reflect.set(target, key, newVal);
  }
})

const newObj = proxyObj(obj);
console.log(newObj._name) // Uncaught Error: _name is private key, please use get_name
newObj._name = 'newname'; // Uncaught Error: _name is private key, please use set_name
console.log(newObj.age) // 19
console.log(newObj.getName()) // nanjin

可见,通过proxyObj方法,我们可以实现把任何一个对象都过滤一次,然后返回新的代理对象,被处理的对象会把所有_开头的变量给拦截掉,更进一步,如果有用过mobx的同学会发现mobx里面的store中的对象都是类似于这样的

你了解vue3.0响应式数据怎么实现吗

有handler 和 target,说明mobx本身也是用了代理模式,同时加上Decorator函数,在这里就相当于把proxyObj使用装饰器的方式来实现,Proxy + Decorator 就是mobx的核心原理啦~

vue响应式数据实现

VUE的双向绑定涉及到模板编译,响应式数据,订阅者模式等等,有兴趣的可以看这里 ,因为这篇文章的主题是proxy,因此我们着重介绍一下数据响应式的过程。

2.x版本

在当前的vue2.x的版本中,在data中声名一个obj后,vue会利用Object.defineProperty来递归的给data中的数据加上get和set,然后每次set的时候,加入额外的逻辑。来触发对应模板视图的更新,看下伪代码:

const defineReactiveData = data => {
  Object.keys(data).forEach(key => {
    let value = data[key];
    Object.defineProperty(data, key, {
     get : function(){
      console.log(`getting ${key}`)
      return value;
     },
     set : function(newValue){
      console.log(`setting ${key}`)
      notify() // 通知相关的模板进行编译
      value = newValue;
     },
     enumerable : true,
     configurable : true
    })
  })
}

这个方法可以给data上面的所有属性都加上get和set,当然这只是伪代码,实际场景下我们还需要考虑如果某个属性还是对象我们应该递归下去,来试试:

const data = {
  name: 'nanjing',
  age: 19
}
defineReactiveData(data)
data.name // getting name 'nanjing'
data.name = 'beijing'; // setting name

可以看到当我们get和set触发的时候,已经能够同时触发我们想要调用的函数拉,Vue双向绑定过程中,当改变this上的data的时候去更新模板的核心原理就是这个方法,通过它我们就能在data的某个属性被set的时候,去触发对应模板的更新。

现在我们在来试试下面的代码:

const data = {
  userIds: ['01','02','03','04','05']
}
defineReactiveData(data);
data.userIds // getting userIds ["01", "02", "03", "04", "05"]
// get 过程是没有问题的,现在我们尝试给数组中push一个数据
data.userIds.push('06') // getting userIds

what ? setting没有被触发,反而因为取了一次userIds所以触发了一次getting~,

不仅如此,很多数组的方法都不会触发setting,比如:push,pop,shift,unshift,splice,sort,reverse这些方法都会改变数组,但是不会触发set,所以Vue为了解决这个问题,重新包装了这些函数,同时当这些方法被调用的时候,手动去触发notify();看下源码:

// 获得数组原型
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// 重写以下函数
const methodsToPatch = [
 'push',
 'pop',
 'shift',
 'unshift',
 'splice',
 'sort',
 'reverse',
]
methodsToPatch.forEach(function(method) {
 // 缓存原生函数
 const original = arrayProto[method]
 // 重写函数
 def(arrayMethods, method, function mutator(...args) {
  // 先调用原生函数获得结果
  const result = original.apply(this, args)
  const ob = this.__ob__
  let inserted
  // 调用以下几个函数时,监听新数据
  switch (method) {
   case 'push':
   case 'unshift':
    inserted = args
    break
   case 'splice':
    inserted = args.slice(2)
    break
  }
  if (inserted) ob.observeArray(inserted)
  // 手动派发更新
  ob.dep.notify()
  return result
 })
})

上面是官方的源码,我们可以实现一下push的伪代码,为了省事,直接在prototype上下手了~

const push = Array.prototype.push;
Array.prototype.push = function(...args){
  console.log('push is happenning');
  return push.apply(this, args);
}
data.userIds.push('123') // push is happenning

通过这种方式,我们可以监听到这些的变化,但是vue官方文档中有这么一个注意事项

由于 JavaScript 的限制,Vue 不能检测以下变动的数组:

  • 当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue
  • 当你修改数组的长度时,例如:vm.items.length = newLength

这个最根本的原因是因为这2种情况下,受制于js本身无法实现监听,所以官方建议用他们自己提供的内置api来实现,我们也可以理解到这里既不是defineProperty可以处理的,也不是包一层函数就能解决的,这就是2.x版本现在的一个问。 回到这篇文章的主题,vue官方会在3.x的版本中使用proxy来代替defineProperty处理响应式数据的过程,我们先来模拟一下实现,看看能否解决当前遇到的这些问题;

 3.x版本

我们先来通过proxy实现对data对象的get和set的劫持,并返回一个代理的对象,注意,我们只关注proxy本身,所有的实现都是伪代码,有兴趣的同学可以自行完善

const defineReactiveProxyData = data => new Proxy(data, 
  {
    get: function(data, key){
      console.log(`getting ${key}`)
      return Reflect.get(data, key);
    },
    set: function(data, key, newVal){
      console.log(`setting ${key}`);
      if(typeof newVal === 'object'){ // 如果是object,递归设置代理
        return Reflect.set(data, key, defineReactiveProxyData(newVal));
      }
      return Reflect.set(data, key, newVal);
    }
  })
const data = {
  name: 'nanjing',
  age: 19
};
const vm = defineReactiveProxyData(data);
vm.name // getting name nanjing
vm.age = 20; // setting age 20

看起来我们的代理已经起作用啦,之后只要在setting的时候加上notify()去通知模板进行编译就可以了,然后我们来尝试设置一个数组看看;

vm.userIds = [1,2,3] // setting userIds
vm.userIds.push(1);
// getting userIds 因为我们会先访问一次userids
// getting push 调用了push方法,所以会访问一次push属性
// getting length 数组push的时候 length会变,所以需要先访问原来的length
// setting 3 通过下标设置的,所以set当前的index是3
// setting length 改变了数组的长度,所以会set length
// 4 返回新的数组的长度

回顾2.x遇到的第一个问题,需要重新包装Array.prototype上的一些方法,使用了proxy后不需要了,解决了~,继续看下一个问题

vm.userIds.length = 2
// getting userIds 先访问
// setting length 在设置
vm.userIds[1] = '123'
// getting userIds 先访问
// setting 1 设置index=1的item
// "123"

从上面的例子中我们可以看到,不管是直接改变数组的length还是通过某一个下标改变数组的内容,proxy都能拦截到这次变化,这比defineProperty方便太多了,2.x版本中的第二个问题,在proxy中根本不会出现了。

总结1

通过上面的例子和代码,我们看到Vue的响应模式如果使用proxy会比现在的实现方式要简化和优化很多,很快在即将来临的3.0版本中,大家就可以体验到了。不过因为proxy本身是有兼容性的,比如ie浏览器,所以在低版本的场景下,vue会回退到现在的实现方式。

总结2

回归到proxy本身,设计模式中有一种典型的代理模式,proxy就是js的一种实现,它的好处在于,我可以在不污染本身对象的条件下,生成一个新的代理对象,所有的一些针对性逻辑放到代理对象上去实现,这样我可以由A对象,衍生出B,C,D…每个的处理过程都不一样,从而简化代码的复杂性,提升一定的可读性,比如用proxy实现数据库的ORM就是一种很好的应用,其实代码很简单,关键是要理解背后的思想,同时能够举一反三~

扩展: 1.Proxy.revocable()

这个方法可以返回一个可取消的代理对象

const obj = {};
const handler = {};
const {proxy, revoke} = Proxy.revocable(obj, handler);
proxy.a = 1
proxy.a // 1
revoke();
proxy.a // Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked

一旦代理被取消了,就不能再从代理对象访问了

你了解vue3.0响应式数据怎么实现吗

打印proxy 可以看到IsRevoked变为true了

2.代理对象的this问题

因为new Proxy出来的是一个新的对象,所以在如果你在target中有使用this,被代理后的this将指向新的代理对象,而不是原来的对象,这个时候,如果有些函数是原对象独有的,就会出现this指向导致的问题,这种场景下,建议使用bind来强制绑定this

看代码:

const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);

proxy.getDate(); // Uncaught TypeError: this is not a Date object.

因为代理后的对象并不是一个Date类型的,不具有getDate方法的,所以我们需要在get的时候,绑定一下this的指向

const target = new Date();
const handler = {
  get: function(target, key){
    if(typeof target[key] === 'function'){
      return target[key].bind(target) // 强制绑定
      this到原对象
    }
    return Reflect.get(target, key)
  }
};
const proxy = new Proxy(target, handler);

proxy.getDate(); // 6

这样就可以正常使用this啦,当然具体的使用还要看具体的场景,灵活运用吧!

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
js 实现无缝滚动 兼容IE和FF
Jul 15 Javascript
jquery 中多条件选择器,相对选择器,层次选择器的区别
Jul 03 Javascript
javascript实现可改变滚动方向的无缝滚动实例
Jun 17 Javascript
jquery表单验证框架提供的身份证验证方法(示例代码)
Dec 27 Javascript
javascript替换已有元素replaceChild()使用介绍
Apr 03 Javascript
自写的jQuery异步加载数据添加事件
May 15 Javascript
JavaScript fontcolor方法入门实例(按照指定的颜色来显示字符串)
Oct 17 Javascript
jQuery绑定事件监听bind和移除事件监听unbind用法实例详解
Jan 19 Javascript
一道关于JavaScript变量作用域的面试题
Mar 08 Javascript
VC调用javascript的几种方法(推荐)
Aug 09 Javascript
jQuery操作事件完整实例分析
Jan 10 jQuery
关于antd tree 和父子组件之间的传值问题(react 总结)
Jun 02 Javascript
ES6小技巧之代替lodash
Jun 07 #Javascript
vue+element 模态框表格形式的可编辑表单实现
Jun 07 #Javascript
vue-cli3项目展示本地Markdown文件的方法
Jun 07 #Javascript
Element实现表格分页数据选择+全选所有完善批量操作
Jun 07 #Javascript
详解vue项目中实现图片裁剪功能
Jun 07 #Javascript
sortable+element 实现表格行拖拽的方法示例
Jun 07 #Javascript
利用Vue-draggable组件实现Vue项目中表格内容的拖拽排序
Jun 07 #Javascript
You might like
在windows iis5下安装php4.0+mysql之我见
2006/10/09 PHP
使用php发送有附件的电子邮件-(PHPMailer使用的实例分析)
2013/04/26 PHP
PHP数据库万能引擎类adodb配置使用以及实例集锦
2014/06/12 PHP
PHP中exec与system用法区别分析
2014/09/22 PHP
php生成数字字母的验证码图片
2015/07/14 PHP
jQuery源码解读之removeClass()方法分析
2015/02/20 Javascript
JavaScript中调用函数的4种方式代码实例
2015/07/08 Javascript
jQuery实现向下滑出的平滑下拉菜单效果
2015/08/21 Javascript
JavaScript原型及原型链终极详解
2016/01/04 Javascript
NodeJS连接MongoDB数据库时报错的快速解决方法
2016/05/13 NodeJs
jQuery leonaScroll 1.1 自定义滚动条插件(推荐)
2016/09/17 Javascript
Bootstrap基本插件学习笔记之Alert警告框(20)
2016/12/08 Javascript
javascript 秒表计时器实现代码
2017/03/09 Javascript
作为老司机使用 React 总结的 11 个经验教训
2017/04/08 Javascript
windows下vue-cli及webpack搭建安装环境
2017/04/25 Javascript
JavaScript比较两个数组的内容是否相同(推荐)
2017/05/02 Javascript
关于vue-router的beforeEach无限循环的问题解决
2017/09/09 Javascript
Vue中props的使用详解
2018/06/15 Javascript
[57:47]Fnatic vs Winstrike 2018国际邀请赛小组赛BO2 第二场 8.18
2018/08/19 DOTA
基于python 字符编码的理解
2017/09/02 Python
pytorch训练imagenet分类的方法
2018/07/27 Python
python中退出多层循环的方法
2018/11/27 Python
python实现删除列表中某个元素的3种方法
2020/01/15 Python
python 的topk算法实例
2020/04/02 Python
Django 用户登陆访问限制实例 @login_required
2020/05/13 Python
Python离线安装各种库及pip的方法
2020/11/28 Python
买房委托公证书
2014/04/08 职场文书
求职意向书
2014/07/29 职场文书
幼儿园八一建军节活动方案
2014/08/27 职场文书
仓库保管员岗位职责
2015/02/09 职场文书
2015年员工工作表现评语
2015/03/25 职场文书
作息时间调整通知
2015/04/22 职场文书
亲戚关系证明
2015/06/24 职场文书
详解TS数字分隔符和更严格的类属性检查
2021/05/06 Javascript
使用 MybatisPlus 连接 SqlServer 数据库解决 OFFSET 分页问题
2022/04/22 SQL Server
Vue操作Storage本地化存储
2022/04/29 Vue.js