1
1
2
- ### Proxy
2
+ # ES6的代理模式 | Proxy
3
3
4
4
> 定义: 用于定义基本操作的自定义行为
5
5
6
- 由于 ` proxy ` 修改的是程序默认形为,就形同于在编程语言层面上做修改,属于元编程(` meta ` ` programming ` )
6
+ ` proxy ` 修改的是程序默认形为,就形同于在编程语言层面上做修改,属于元编程(` meta ` ` programming ` )
7
7
8
- ** 元编程(英语:Metaprogramming** ,又译超编程,是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作
8
+ - ** 元编程(英语:Metaprogramming** ,又译超编程,是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作
9
9
10
10
一段代码来理解元编程
11
11
``` bash
@@ -17,18 +17,32 @@ for ((I=1; I<=1024; I++)) do
17
17
done
18
18
chmod +x program
19
19
```
20
- 这段程序每执行一次能帮我们生成一个名为program的文件,文件内容为1024行` echo ` ,如果我们手动来写1024行代码,显然就很低效
20
+ 这段程序每执行一次能帮我们生成一个名为program的文件,文件内容为1024行` echo ` ,如果我们手动来写1024行代码,效率显然低效
21
21
22
- 元编程优点:与手工编写全部代码相比,程序员可以获得更高的工作效率,或者给与程序更大的灵活度去处理新的情形而无需重新编译
22
+ ** 元编程优点** :与手工编写全部代码相比,程序员可以获得更高的工作效率,或者给与程序更大的灵活度去处理新的情形而无需重新编译
23
23
24
- ` proxy ` 音译为代理,在操作目标对象前架设一层代理,将所有本该我们手动编写的程序交由代理来处理,通俗点可以理解为生活的代购,中介服务,所有的行为都不会直接触达目标对象
24
+ ` proxy ` 译为代理,可以理解为在操作目标对象前架设一层代理,将所有本该我们手动编写的程序交由代理来处理,生活中也有许许多多的“proxy”, 如代购,中介,因为他们所有的行为都不会直接触达到目标对象
25
+
26
+ ## 正文
27
+
28
+ 本篇文章作为 ` Vue3 ` 源码系列前置篇章之一,` Proxy ` 的科普文,跟` Vue3 ` 并没有绝对关系,但是当你静下心读完了前置篇章,再去读后续的源码系列,感受定会截然不同
29
+
30
+ 前置篇章包含
31
+ - 为什么要学习源码
32
+ - 认识Typescript
33
+ - 理解函数式编程
34
+ - 搞明白Proxy
35
+ - 摸清楚Set、Map、WeakSet、WeakMap
36
+
37
+ 下来将介绍 ` Proxy ` 的基本使用
38
+
39
+ ## 语法
40
+
41
+ - target 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理
42
+ - handler 一个通常以函数作为属性的对象,用来定制拦截行为
25
43
26
- ### 语法
27
44
``` js
28
- /
29
- * target 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理
30
- * handler 一个通常以函数作为属性的对象,用来定制拦截行为
31
- */
45
+
32
46
const proxy = new Proxy (target, handle)
33
47
```
34
48
@@ -48,7 +62,7 @@ origin.b // undefined
48
62
```
49
63
上方代码我们给一个空对象的get架设了一层代理,所有` get ` 操作都会直接返回我们定制的数字10,需要注意的是,代理只会对` proxy ` 对象生效,如上方的` origin ` 就没有任何效果
50
64
51
- ### Handler 对象常用的方法
65
+ ## Handler 对象常用的方法
52
66
53
67
| 方法 | 描述 |
54
68
| ---- | ---- |
@@ -63,14 +77,14 @@ origin.b // undefined
63
77
64
78
下面挑` handler.get ` 重点讲一下,其它方法的使用也都大同小异,不同的是参数的区别
65
79
66
- #### handler.get
80
+ ### handler.get
67
81
68
- get我们在上面例子已经体验过了,这里再详细介绍一下,方法用于代理目标对象的属性读取操作
82
+ ` get ` 我们在上面例子已经体验过了,现在详细介绍一下,用于代理目标对象的属性读取操作
69
83
70
84
授受三个参数 ` get(target, propKey, ?receiver) `
71
85
- target 目标对象
72
86
- propkey 属性名
73
- - receiver Proxy实例本身
87
+ - receiver Proxy 实例本身
74
88
75
89
** 举个例子**
76
90
``` js
@@ -116,7 +130,7 @@ const p = new Proxy(obj, {
116
130
p .a // Uncaught TypeError: 'get' on proxy: property 'a' is a read-only and non-configurable..
117
131
```
118
132
119
- ### 可撤消的Proxy
133
+ ## 可撤消的Proxy
120
134
121
135
` proxy ` 有一个唯一的静态方法,` Proxy.revocable(target, handler) `
122
136
@@ -138,12 +152,15 @@ revoke() // 取值完成对proxy进行封闭,撤消代理
138
152
proxy .name // TypeError: Revoked
139
153
```
140
154
141
- ### Proxy的应用场景
155
+ ## Proxy的应用场景
142
156
143
157
` Proxy ` 的应用范围很广,下方列举几个典型的应用场景
144
- - ** 校验器**
145
158
146
- 想要一个` number ` ,拿回来的却是` string ` ,头大不大?下面我们使用` Proxy ` 实现一个逻辑分离的数据格式验证器,嗯,真香!
159
+ ### ** 校验器**
160
+
161
+ 想要一个` number ` ,拿回来的却是` string ` ,惊不惊喜?意不意外?下面我们使用` Proxy ` 实现一个逻辑分离的数据格式验证器
162
+
163
+ 嗯,真香!
147
164
148
165
``` js
149
166
const target = {
@@ -183,11 +200,9 @@ proxy._id = 22 // Uncaught Error: Cannot set _id to 22. Invalid type
183
200
184
201
```
185
202
203
+ ### 私有属性
186
204
187
-
188
- - ** 防止对象的内部属性(私有属性)被外部读写**
189
-
190
- 在日常编写代码的过程中,我们想定义一些私有属性,通常是在团队中进行约定,大家按照约定在变量名之前添加下划线 _ 或者其它格式来表明这是一个私有属性,但我们不能保证他能真私‘私有化’,下方示例使用Proxy轻松实现私有属性拦截
205
+ 在日常编写代码的过程中,我们想定义一些私有属性,通常是在团队中进行约定,大家按照约定在变量名之前添加下划线 _ 或者其它格式来表明这是一个私有属性,但我们不能保证他能真私‘私有化’,下面使用Proxy轻松实现私有属性拦截
191
206
192
207
``` js
193
208
const target = {
@@ -215,24 +230,238 @@ proxy._id // Uncaught Error: _id is restricted
215
230
proxy ._id = ' 1025' // Uncaught Error: _id is restricted
216
231
```
217
232
218
- ` Proxy ` 的使用场景还有很多很多,这里也不再一一列举,只要你需要在某一个动作的生命周期内做一些特定的处理,那么` Proxy ` 都是适合你的
233
+ ` Proxy ` 使用场景还有很多很多,不再一一列举,如果你需要在某一个动作的生命周期内做一些特定的处理,那么` Proxy ` 都是适合的
234
+
235
+ ## 为什么要用Proxy重构
236
+
237
+ 在 ` Proxy ` 之前,` JavaScript ` 中就提供过 ` Object.defineProperty ` ,允许对对象的 ` getter/setter ` 进行拦截
238
+
239
+ Vue3.0之前的双向绑定是由 ` defineProperty ` 实现, 在3.0重构为 ` Proxy ` ,那么两者的区别究竟在哪里呢?
240
+
241
+ 首先我们再来回顾一下它的定义
242
+
243
+ > Object.defineProperty() 方法会直接在一个** 对象上** 定义一个** 新属性** ,或者修改一个对象的现有属性,并返回此对象
244
+
245
+ 上面给两个词划了重点,** 对象上** ,** 属性** ,我们可以理解为是针对对象上的某一个属性做处理的
246
+
247
+ ** 语法**
248
+
249
+ - obj 要定义属性的对象
250
+ - prop 要定义或修改的属性的名称或 Symbol
251
+ - descriptor 要定义或修改的属性描述符
252
+
253
+ ``` js
254
+ Object .defineProperty (obj, prop, descriptor)
255
+ ```
256
+
257
+ 举个例子
258
+ ``` js
259
+ const obj = {}
260
+ Object .defineProperty (obj, " a" , {
261
+ value : 1 ,
262
+ writable : false , // 是否可写
263
+ configurable : false , // 是否可配置
264
+ enumerable : false // 是否可枚举
265
+ })
266
+
267
+ // 上面给了三个false, 下面的相关操作就很容易理解了
268
+ obj .a = 2 // 无效
269
+ delete obj .a // 无效
270
+ for (key in obj){
271
+ console .log (key) // 无效
272
+ }
273
+ ```
274
+
275
+ ### ** Vue中的defineProperty**
276
+
277
+ Vue3之前的双向绑定都是通过 ` defineProperty ` 的 ` getter,setter ` 来实现的,我们先来体验一下 ` getter,setter `
278
+ ``` js
279
+ const obj = {};
280
+ Object .defineProperty (obj, ' a' , {
281
+ set (val ) {
282
+ console .log (` 开始设置新值: ${ val} ` )
283
+ },
284
+ get () {
285
+ console .log (` 开始读取属性` )
286
+ return 1 ;
287
+ },
288
+ writable : true
289
+ })
290
+
291
+ obj .a = 2 // 开始设置新值: 2
292
+ obj .a // 开始获取属性
293
+ ```
294
+
295
+ 看到这里,我相信有些同学已经想到了实现双向绑定背后的流程了,其实很简单嘛,只要我们观察到对象属性的变更,再去通知更新视图就好了
296
+
297
+ 我们摘抄一段 Vue 源码中的核心实现验证一下,这一部分一笔代过,不是本文重点
298
+ ``` js
299
+ // 源码位置:https://github.com/vuejs/vue/blob/ef56410a2c/src/core/observer/index.js#L135
300
+ // ...
301
+ Object .defineProperty (obj, key, {
302
+ enumerable: true ,
303
+ configurable: true ,
304
+ get : function reactiveGetter () {
305
+ // ...
306
+ if (Dep .target ) {
307
+ // 收集依赖
308
+ dep .depend ()
309
+ }
310
+ return value
311
+ },
312
+ set : function reactiveSetter (newVal ) {
313
+ // ...
314
+ // 通知视图更新
315
+ dep .notify ()
316
+ }
317
+ })
318
+ ```
319
+
320
+ ### ** 对象新增属性为什么不更新**
321
+
322
+ 这个问题用过Vue的同学应该有超过95%比例遇到过
323
+ ``` js
324
+ data () {
325
+ return {
326
+ obj: {
327
+ a: 1
328
+ }
329
+ }
330
+ }
331
+
332
+ methods: {
333
+ update () {
334
+ this .obj .b = 2
335
+ }
336
+ }
337
+ ```
338
+
339
+ 上面的伪代码,当我们执行 ` update ` 更新 ` obj ` 时,我们预期视图是要随之更新的,实际是并不会
340
+
341
+ 这个其实很好理解,我们先要明白 ` vue ` 中 ` data init ` 的时机,` data init ` 是在生命周期 ` created ` 之前的操作,会对 ` data ` 绑定一个观察者 ` Observer ` ,之后 ` data ` 中的字段更新都会通知依赖收集器` Dep ` 触发视图更新
342
+
343
+ 然后我们回到 ` defineProperty ` 本身,是对** 对象上的属性** 做操作,而非对象本身
344
+
345
+ 一句话来说就是,在 ` Observer data ` 时,新增属性并不存在,自然就不会有 ` getter, setter ` ,也就解释了为什么新增视图不更新,解决有很多种,` Vue ` 提供的全局` $set ` 本质也是给新增的属性手动 ` observer `
346
+
347
+ ``` js
348
+ // 源码位置 https://github.com/vuejs/vue/blob/dev/src/core/observer/index.js#L201
349
+ function set (target : Array < any > | Object , key : any , val : any ): any {
350
+ // ....
351
+ if (! ob) {
352
+ target[key] = val
353
+ return val
354
+ }
355
+ defineReactive (ob .value , key, val)
356
+ ob .dep .notify ()
357
+ return val
358
+ }
359
+ ```
360
+
361
+ ### 数组变异
362
+
363
+ > 由于 JavaScript 的限制,Vue 不能检测以下数组的变动: 当你利用索引直接设置一个数组项时,例如:vm.items[ indexOfItem] = newValue
364
+
365
+ 先来看一段代码
366
+
367
+ ``` js
368
+ var vm = new Vue ({
369
+ data: {
370
+ items: [' 1' , ' 2' , ' 3' ]
371
+ }
372
+ })
373
+ vm .items [1 ] = ' 4' // 视图并未更新
374
+ ```
375
+ 文档已经做出了解释,但并不是` defineProperty ` 的锅,而是尤大在设计上对性能的权衡,下面这段代码可以验证
376
+ ``` js
377
+ function defineReactive (data , key , val ) {
378
+ Object .defineProperty (data, key, {
379
+ enumerable: true ,
380
+ configurable: true ,
381
+ get : function defineGet () {
382
+ console .log (` get key: ${ key} val: ${ val} ` );
383
+ return val;
384
+ },
385
+ set : function defineSet (newVal ) {
386
+ console .log (` set key: ${ key} val: ${ newVal} ` );
387
+ val = newVal;
388
+ }
389
+ })
390
+ }
391
+
392
+ function observe (data ) {
393
+ Object .keys (data).forEach (function (key ) {
394
+ defineReactive (data, key, data[key]);
395
+ })
396
+ }
397
+
398
+ let test = [1 , 2 , 3 ];
399
+
400
+ observe (test);
401
+
402
+ test[0 ] = 4 // set key: 0 val: 4
403
+ ```
404
+
405
+ 虽然说索引变更不是 ` defineProperty ` 的锅,但新增索引的确是 ` defineProperty ` 做不到的,所以就有了数组的变异方法
406
+
407
+ 能看到这里,大概也能猜到内部实现了,还是跟` $set ` 一样,手动 ` observer ` ,下面我们验证一下
408
+
409
+ ``` js
410
+ const methodsToPatch = [
411
+ ' push' ,
412
+ ' pop' ,
413
+ ' shift' ,
414
+ ' unshift' ,
415
+ ' splice' ,
416
+ ' sort' ,
417
+ ' reverse'
418
+ ]
419
+
420
+ methodsToPatch .forEach (function (method ) {
421
+ // 缓存原生数组
422
+ const original = arrayProto[method]
423
+ // def使用Object.defineProperty重新定义属性
424
+ def (arrayMethods, method, function mutator (... args ) {
425
+ const result = original .apply (this , args) // 调用原生数组的方法
426
+
427
+ const ob = this .__ob__ // ob就是observe实例observe才能响应式
428
+ let inserted
429
+ switch (method) {
430
+ // push和unshift方法会增加数组的索引,但是新增的索引位需要手动observe的
431
+ case ' push' :
432
+ case ' unshift' :
433
+ inserted = args
434
+ break
435
+ // 同理,splice的第三个参数,为新增的值,也需要手动observe
436
+ case ' splice' :
437
+ inserted = args .slice (2 )
438
+ break
439
+ }
440
+ // 其余的方法都是在原有的索引上更新,初始化的时候已经observe过了
441
+ if (inserted) ob .observeArray (inserted)
442
+ // dep通知所有的订阅者触发回调
443
+ ob .dep .notify ()
444
+ return result
445
+ })
446
+ })
447
+ ```
448
+ ### 对比
449
+ 一个优秀的开源框架本身就是一个不断打碎重朔的过程,上面做了些许铺垫,现在我们简要总结一下
450
+
451
+ - ` Proxy ` 作为新标准将受到浏览器厂商重点持续的性能优化
219
452
220
- ### Proxy 与 Object.defineProperty
221
- 在 ` Proxy ` 出现之前,` JavaScript ` 中就提供过 ` Object.defineProperty ` ,允许对对象的 ` getter/setter ` 进行拦截
453
+ - ` Proxy ` 能观察的类型比 ` defineProperty ` 更丰富
222
454
223
- 大家熟悉的Vue框架双向绑定的实现也由 ` defineProperty ` 重构为 ` Proxy ` ,那么两者的区别在究竟在哪里呢?
455
+ - ` Proxy ` 不兼容IE,也没有 ` polyfill ` , ` defineProperty ` 能支持到IE9
224
456
225
- 请看下面这张图,图中所示的都是 ` defineProperty ` 现存的问题,而 ` Proxy ` 均已完美支持
457
+ - ` Object.definedProperty ` 是劫持对象的属性,新增元素需要再次 ` definedProperty ` 。而 ` Proxy ` 劫持的是整个对象,不需要做特殊处理
226
458
227
- ![ image.png ] ( https://static.vue-js.com/5289d760-c675-11ea-ae44-f5d67be454e7.png )
459
+ - 使用 ` defineProperty ` 时,我们修改原来的 ` obj ` 对象就可以触发拦截,而使用 ` proxy ` ,就必须修改代理对象,即 ` Proxy ` 的实例才可以触发拦截
228
460
229
- ### 兼容
230
- 除了IE,其它浏览器最新版本均已支持
231
461
232
- ![ image.png] ( https://static.vue-js.com/9a145b00-c675-11ea-ae44-f5d67be454e7.png )
233
462
234
- 参考:
463
+ ## 参考文献
235
464
- [ https://zh.wikipedia.org/wiki/ ] ( https://zh.wikipedia.org/wiki/ )
236
465
- [ https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy ] ( https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy )
237
466
- [ https://es6.ruanyifeng.com/#docs/proxy#Proxy-revocable ] ( https://es6.ruanyifeng.com/#docs/proxy#Proxy-revocable )
238
- - [ https://juejin.im/post/5e78d908f265da57340267f7#heading-2 ] ( https://juejin.im/post/5e78d908f265da57340267f7#heading-2 )
467
+ - [ https://youngzhang08.github.io/ ] ( https://youngzhang08.github.io/ )
0 commit comments