实现双向绑定Object.defineProperty与proxy的VS

入坑互联网

共 5558字,需浏览 12分钟

 · 2022-01-11

关注 入坑互联网 ,回复“加群

加入我们一起学习,天天进步


实现双向绑定的方法有很多,KnockoutJS基于观察者模式的双向绑定,Ember基于数据模型的双向绑定,Angular基于脏检查的双向绑定,本篇文章我们重点讲面试中常见的基于数据劫持的双向绑定。

常见的基于数据劫持的双向绑定有两种实现,一个是目前Vue在用的Object.defineProperty,另一个是ES2015中新增的Proxy,而Vue的作者宣称将在Vue3.0版本后加入Proxy从而代替Object.defineProperty,通过本文你也可以知道为什么Vue未来会选择Proxy。

(严格来讲Proxy应该被称为『代理』而非『劫持』,不过由于作用有很多相似之处,我们在下文中就不再做区分,统一叫『劫持』。)

什么是数据劫持

指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果。

比较典型的是 Object.defineProperty() 和 ES2015 中新增的 Proxy 对象。数据劫持最著名的应用当属双向绑定,这也是一个已经被讨论烂了的面试必考题。例如 Vue 2.x 使用的是 Object.defineProperty()(Vue 在 3.x 版本之后改用 Proxy 进行实现)。

Object.defineProperty

ES5 提供了 Object.defineProperty 方法,该方法可以在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。

语法:

Object.defineProperty(obj, prop, descriptor)

参数:

obj: 要在其上定义属性的对象。

prop:  要定义或修改的属性的名称。

descriptor: 将被定义或修改的属性的描述符。

来个例子:

// 这是将要被劫持的对象const data = {  name: '',};
function say(name) { if (name === '孙红雷') { console.log('给大家推荐一款超好玩的游戏'); } else if (name === '绿茶婊') { console.log('绿茶婊,撩妹属性爆表'); } else { console.log('来做我的兄弟'); }}
// 遍历对象,对其属性值进行劫持Object.keys(data).forEach(function(key) { Object.defineProperty(data, key, { //当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false。 enumerable: true, //当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,也能够被删除。默认为 false。 configurable: true, get: function() { console.log('get'); }, set: function(newVal) { // 当属性值发生变化时我们可以进行额外操作 console.log(`大家好,我系${newVal}`); say(newVal); }, });});
data.name = '绿茶婊';//大家好,我系绿茶婊//绿茶婊,撩妹属性爆表

Proxy

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。

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

我们来看看它的语法:

var proxy = new Proxy(target, handler);

proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。

var proxy = new Proxy({}, {    get: function(obj, prop) {        console.log('设置 get 操作')        return obj[prop];    },    set: function(obj, prop, value) {        console.log('设置 set 操作')        obj[prop] = value;    }});
proxy.time = 35; // 设置 set 操作
console.log(proxy.time); // 设置 get 操作 // 35

除了 get 和 set 之外,proxy 可以拦截多达 13 种操作,比如 has(target, propKey),可以拦截 propKey in proxy 的操作,返回一个布尔值。

// 使用 has 方法隐藏某些属性,不被 in 运算符发现var handler = {  has (target, key) {    if (key[0] === '_') {      return false;    }    return key in target;  }};var target = { _prop: 'foo', prop: 'foo' };var proxy = new Proxy(target, handler);console.log('_prop' in proxy); // false

又比如说 apply 方法拦截函数的调用、call 和 apply 操作。

apply 方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组,不过这里我们简单演示一下:

var target = function () { return 'I am the target'; };var handler = {  apply: function () {    return 'I am the proxy';  }};
var p = new Proxy(target, handler);
p();// "I am the proxy"

又比如说 ownKeys 方法可以拦截对象自身属性的读取操作。具体来说,拦截以下操作:

Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Object.keys()

下面的例子是拦截第一个字符为下划线的属性名,不让它被 for of 遍历到。

let target = {  _bar: 'foo',  _prop: 'bar',  prop: 'baz'};
let handler = { ownKeys (target) { return Reflect.ownKeys(target).filter(key => key[0] !== '_'); }};
let proxy = new Proxy(target, handler);for (let key of Object.keys(proxy)) { console.log(target[key]);}// "baz"

我们使用 proxy 再来写一下 watch 函数。使用效果如下:

(function() {    var root = this;
function watch(target, func) {
var proxy = new Proxy(target, { get: function(target, prop) { return target[prop]; }, set: function(target, prop, value) { target[prop] = value; func(prop, value); } });
if(target[name]) proxy[name] = value; return proxy; }
this.watch = watch;})()
var obj = { value: 1}
var newObj = watch(obj, function(key, newvalue) { if (key == 'value') document.getElementById('container').innerHTML = newvalue;})
document.getElementById('button').addEventListener("click", function() { newObj.value += 1});

我们也可以发现,使用 defineProperty 和 proxy 的区别,当使用 defineProperty,我们修改原来的 obj 对象就可以触发拦截,而使用 proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截。

vue3为什么用proxy?舍弃Object.defineProperty

Object.defineProperty缺点:

①不能监听数组的变化

数组的以下几个方法不会触发 set:

push、pop、shift、unshift、splice、sort、reverse;

Vue 把这些方法定义为变异方法 (mutation method),指的是会修改原来数组的方法。与之对应则是非变异方法 (non-mutating method),例如 filter, concat, slice 等,它们都不会修改原始数组,而会返回一个新的数组。

let arr = [1,2,3]let obj = {}Object.defineProperty(obj, 'arr', {  get () {    console.log('get arr')    return arr  },  set (newVal) {    console.log('set', newVal)    arr = newVal  }})obj.arr.push(4) // 只会打印 get arr, 不会打印 setobj.arr = [1,2,3,4] // 这个能正常 set

②必须遍历对象的每个属性

使用 Object.defineProperty() 多数要配合 Object.keys() 和遍历,于是多了一层嵌套。

Object.keys(obj).forEach(key => {  Object.defineProperty(obj, key, {    // ...  })})

③必须深层遍历嵌套的对象

如果是这一类嵌套对象,那就必须逐层遍历,直到把每个对象的每个属性都调用 Object.defineProperty() 为止。Vue 的源码中就能找到这样的逻辑 (叫做 walk 方法)。

let obj = {  info: {    name: 'eason'  }}

针对Object.defineProperty有这些缺点,为什么用proxy?

1.proxy可以直接监听数组的变化;

2.proxy可以监听对象而非属性.它在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

3.Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改。

4.Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具备的。

当然,Proxy的劣势就是兼容性问题,而且无法用polyfill磨平,因此Vue的作者才声明需要等到下个大版本(3.0)才能用Proxy重写。

❤️ 看完三件事

如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:

  1. 点赞,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓)。
  2. 关注公众号「入坑互联网」,不定期分享原创知识。
  3. 也看看其它文章

如何做到优秀,好难!

面试题联盟之CSS篇

年底前端面试及答案-html/css

年底面试之es6总结

年底面试之JavaScript总结(用心收集)

- END -


结伴同行前端路



浏览 24
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报