Vue源码探索之知识小储备 ——01.Object.defineProperty VS proxy

写在前面

  • 知识就是力量。 ——安琪拉
  • 知识就是力量,但更重要的是运用知识的技能。 —— 培根
  • 本文主要介绍了vue实现响应式数据/双向绑定原理的基础API的使用方法及拓展知识。
  • vue2 基于 Object.defineProperty,vue3 基于 proxy。本文做了详细描述和对比。

响应式数据/双向绑定原理

Vue 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据。其中,View变化更新Data,可以通过事件监听的方式来实现,所以 Vue数据双向绑定的工作主要是如何根据Data变化更新View。
简而言之, Vue要实现数据双向绑定,需要做三件事情:

  1. 检测到Data变化(Observer)
  2. 追踪收集依赖,通知变更(Dep)
  3. 更新View视图(Watcher)

本文先来讲解vue实现数据双向绑定的第一步:如何检测到Data的变化。

假设我们有如下这样的一个data对象

let obj = {
    a: 1, 
    b: 2,
    c: ['排序', '递归']
};

我们修改obj.a = 3, 那怎样才可以监听到这个改变呢?我们很容易想到大名鼎鼎的Object.defineProperty()。Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

1.Object.defineProperty()

Object.defineProperty(obj, prop, descriptor)
参数:
【obj】: 要定义属性的对象。
【prop】: 要定义或修改的属性的名称或 Symbol 。
【descriptor】: 要定义或修改的属性描述符。

对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。存取描述符是由 getter 函数和 setter 函数所描述的属性。一个描述符只能是这两者其中之一;不能同时是两者

1.1数据描述符

数据描述符是一个具有值的属性,该值可能是可写的,也可能是不可写的。

它具有以下可选的键值:

configurable:表示该属性能否通过delete删除,能否修改属性的特性或者能否修改访问器属性,默认为false。

enumerable:表示该属性是否可以枚举,即可否通过for..in访问属性。默认为false。

value:表示该属性的值。可以是任何有效的Javascript 值(数值,对象。函数等)。默认为undefined。

writable:表示该属性的值是否可写,默认为false。

【栗子】:

    console.log(Object.getOwnPropertyDescriptor(obj, 'a'));
    result: { value: 2, writable: true, enumerable: true, configurable: true }

1.2存取描述符

存取描述符还具有以下可选键值:

get:  
属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。  
执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。  
该函数的返回值会被用作属性的值。

set:  
属性的 setter 函数,如果没有 setter,则为 undefined。  
当属性值被修改时,会调用此函数。  
该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。

显然,我们只需要利用对象属性描述符里面的存取描述符,就能够检测到对象的改变。

【栗子】:

let _value = obj.a;
Object.defineProperty(obj, 'a', {
   get() {
       console.log('you get a number');
       return _value;
   },
   set(newVal){
       console.log('you set a number');
       _value = newVal;
   }
});

console.log(obj.a);
obj.a = 2;
console.log(obj.a);


// 打印结果
you get a number
1
you set a number
you get a number
2

我们成功检测到了obj对象中a的改变,vue2.x正式基于这一特性实现了数据双向绑定的Observer部分。趁热打铁,再多试一次,享受Object.defineProperty的乐趣吧~

【Do it again】:

let _value = obj.c;
Object.defineProperty(obj, 'c', {
   get() {
       console.log('you get the array');
       return _value;
   },
   set(newVal){
       console.log('you change the array');
       _value = newVal;
   }
});

console.log(obj.c);
obj.c.push('二分查找');
console.log(obj.c);

// 打印结果
you get the array
[ '排序', '递归' ]
you get the array
you get the array
[ '排序', '递归', '二分查找' ]

OMG!怎么没有打印set里的日志,可是数组确实追加进去了。难道Object.defineProperty不能监听数组的变化?没错,确实是这样,因此vue2.x在实现数组的响应式时,它使用了一些hack,把无法监听数组的情况通过 重写数组 的部分方法(push,pop, shif, unshift, splice, sort, reverse)来实现响应式,这也只限制在数组的这七个方法,其他数组方法及数组的使用则无法检测到,例如如下两种使用方式:

data.c[index] = newValue;
data.c.length--;

2.数组hack

  1. 复制一份数组的原型方法,防止污染
const oldArrayProperty = Array.prototype;
const arrayProto = Object.create(oldArrayProperty);
  1. 遍历,将arrayProto对象上的方法转换为观察者对象
const methodsToPatch = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];  
methodsToPatch.forEach(methodName => {
    // 往arrayProto对象中加入methodsToPatch中的属性,并执行原生的methodsToPatch对应的方法
    Object.defineProperty(arrayProto, methodName, {
        value () {
            //监听数组变更
            console.log('you change the array');
            oldArrayProperty[methodName].apply(this, arguments)
        }
    })
})
  1. 让数组继承arrayProto中的方法
obj.c.__proto__ = arrayProto;

好啦,见证奇迹的时刻:

console.log(obj.c);
obj.c.push('二叉分法');
console.log(obj.c);

结果:
[ '排序', '递归' ]
you change the array
[ '排序', '递归', '二叉分法' ]

一个简单的数组hack就实现了,vue2.x也正是基于这种数组方法重写的原理,监听数组变更,从而实现数组类型数据的双向绑定的。

vue2.x在实现数据响应式时,需要区别对待数组和对象,因此在vue3.0,尤大大和他的团队弃用了这种方式,选择使用es6的新特性 ProxyReflect 来实现双向绑定。

3.Proxy

vue3.0基于Proxy来做数据劫持代理,可以支持原生数组的响应式。还可以直接支持新增和删除属性,使用起来十分简洁明了。

【栗子】:

var obProxy = new Proxy(ob, {
    get(target, key, receiver) {
        console.log('you get:' + key);
        return target[key];
    },
    set(target, key, value, receiver){
        console.log('you set:' + key);
        // target[key] = value;
        // Reflect.set(target, key, value)等同于上面的target[key] = value;
        return Reflect.set(target, key, value);
    }
});

我们来分别打印一下

console.log(obj.a);
obj.a = 666;
console.log(obj.a);

结果:
1
666
不进入上面的get和set方法

console.log(obProxy.a);
obProxy.a = 2;
console.log(obProxy.a);

结果:
you get:a
1
you set:a
you get:a
2

结果印证:proxy是去创建一个新的代理对象,所以在改变原对象obj时,上述中的get与set中的console均未打印。

4. 对比:为什么vue3改用proxy实现数据响应式?

  1. Proxy 可以直接监听对象而非属性。
  2. Proxy 可以直接监听数组的变化。
  3. Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的。
  4. Object.defineProperty 只能遍历对象属性实现监听,而Proxy 返回的是一个新对象,我们可以只操作新的对象达到目,大大减少了遍历带来的性能开销。
  5. Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;

The end~

参考文档:MDN web docs


下期预告:proxy详解及妙用

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354