Skip to content

Commit de56667

Browse files
author
febobo
committed
update proxy
1 parent bea9d9d commit de56667

File tree

1 file changed

+263
-34
lines changed

1 file changed

+263
-34
lines changed

docs/es6/index.md

+263-34
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11

2-
### Proxy
2+
# ES6的代理模式 | Proxy
33

44
> 定义: 用于定义基本操作的自定义行为
55
6-
由于`proxy`修改的是程序默认形为,就形同于在编程语言层面上做修改,属于元编程(`meta` `programming`)
6+
`proxy`修改的是程序默认形为,就形同于在编程语言层面上做修改,属于元编程(`meta` `programming`)
77

8-
**元编程(英语:Metaprogramming**,又译超编程,是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作
8+
- **元编程(英语:Metaprogramming**,又译超编程,是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作
99

1010
一段代码来理解元编程
1111
```bash
@@ -17,18 +17,32 @@ for ((I=1; I<=1024; I++)) do
1717
done
1818
chmod +x program
1919
```
20-
这段程序每执行一次能帮我们生成一个名为program的文件,文件内容为1024行`echo`,如果我们手动来写1024行代码,显然就很低效
20+
这段程序每执行一次能帮我们生成一个名为program的文件,文件内容为1024行`echo`,如果我们手动来写1024行代码,效率显然低效
2121

22-
元编程优点:与手工编写全部代码相比,程序员可以获得更高的工作效率,或者给与程序更大的灵活度去处理新的情形而无需重新编译
22+
**元编程优点**:与手工编写全部代码相比,程序员可以获得更高的工作效率,或者给与程序更大的灵活度去处理新的情形而无需重新编译
2323

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 一个通常以函数作为属性的对象,用来定制拦截行为
2543

26-
### 语法
2744
```js
28-
/
29-
* target 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理
30-
* handler 一个通常以函数作为属性的对象,用来定制拦截行为
31-
*/
45+
3246
const proxy = new Proxy(target, handle)
3347
```
3448

@@ -48,7 +62,7 @@ origin.b // undefined
4862
```
4963
上方代码我们给一个空对象的get架设了一层代理,所有`get`操作都会直接返回我们定制的数字10,需要注意的是,代理只会对`proxy`对象生效,如上方的`origin`就没有任何效果
5064

51-
### Handler 对象常用的方法
65+
## Handler 对象常用的方法
5266

5367
| 方法 | 描述 |
5468
| ---- | ---- |
@@ -63,14 +77,14 @@ origin.b // undefined
6377

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

66-
#### handler.get
80+
### handler.get
6781

68-
get我们在上面例子已经体验过了,这里再详细介绍一下,方法用于代理目标对象的属性读取操作
82+
`get`我们在上面例子已经体验过了,现在详细介绍一下,用于代理目标对象的属性读取操作
6983

7084
授受三个参数 `get(target, propKey, ?receiver) `
7185
- target 目标对象
7286
- propkey 属性名
73-
- receiver Proxy实例本身
87+
- receiver Proxy 实例本身
7488

7589
**举个例子**
7690
```js
@@ -116,7 +130,7 @@ const p = new Proxy(obj, {
116130
p.a // Uncaught TypeError: 'get' on proxy: property 'a' is a read-only and non-configurable..
117131
```
118132

119-
### 可撤消的Proxy
133+
## 可撤消的Proxy
120134

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

@@ -138,12 +152,15 @@ revoke() // 取值完成对proxy进行封闭,撤消代理
138152
proxy.name // TypeError: Revoked
139153
```
140154

141-
### Proxy的应用场景
155+
## Proxy的应用场景
142156

143157
`Proxy`的应用范围很广,下方列举几个典型的应用场景
144-
- **校验器**
145158

146-
想要一个`number`,拿回来的却是`string`,头大不大?下面我们使用`Proxy`实现一个逻辑分离的数据格式验证器,嗯,真香!
159+
### **校验器**
160+
161+
想要一个`number`,拿回来的却是`string`,惊不惊喜?意不意外?下面我们使用`Proxy`实现一个逻辑分离的数据格式验证器
162+
163+
嗯,真香!
147164

148165
```js
149166
const target = {
@@ -183,11 +200,9 @@ proxy._id = 22 // Uncaught Error: Cannot set _id to 22. Invalid type
183200

184201
```
185202

203+
### 私有属性
186204

187-
188-
- **防止对象的内部属性(私有属性)被外部读写**
189-
190-
在日常编写代码的过程中,我们想定义一些私有属性,通常是在团队中进行约定,大家按照约定在变量名之前添加下划线 _ 或者其它格式来表明这是一个私有属性,但我们不能保证他能真私‘私有化’,下方示例使用Proxy轻松实现私有属性拦截
205+
在日常编写代码的过程中,我们想定义一些私有属性,通常是在团队中进行约定,大家按照约定在变量名之前添加下划线 _ 或者其它格式来表明这是一个私有属性,但我们不能保证他能真私‘私有化’,下面使用Proxy轻松实现私有属性拦截
191206

192207
```js
193208
const target = {
@@ -215,24 +230,238 @@ proxy._id // Uncaught Error: _id is restricted
215230
proxy._id = '1025' // Uncaught Error: _id is restricted
216231
```
217232

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` 作为新标准将受到浏览器厂商重点持续的性能优化
219452

220-
### Proxy 与 Object.defineProperty
221-
`Proxy` 出现之前,`JavaScript` 中就提供过 `Object.defineProperty`,允许对对象的 `getter/setter` 进行拦截
453+
- `Proxy` 能观察的类型比 `defineProperty` 更丰富
222454

223-
大家熟悉的Vue框架双向绑定的实现也由`defineProperty`重构为`Proxy`,那么两者的区别在究竟在哪里呢?
455+
- `Proxy` 不兼容IE,也没有 `polyfill`, `defineProperty` 能支持到IE9
224456

225-
请看下面这张图,图中所示的都是`defineProperty`现存的问题,而`Proxy`均已完美支持
457+
- `Object.definedProperty` 是劫持对象的属性,新增元素需要再次 `definedProperty`。而 `Proxy` 劫持的是整个对象,不需要做特殊处理
226458

227-
![image.png](https://static.vue-js.com/5289d760-c675-11ea-ae44-f5d67be454e7.png)
459+
- 使用 `defineProperty` 时,我们修改原来的 `obj` 对象就可以触发拦截,而使用 `proxy`,就必须修改代理对象,即 `Proxy` 的实例才可以触发拦截
228460

229-
### 兼容
230-
除了IE,其它浏览器最新版本均已支持
231461

232-
![image.png](https://static.vue-js.com/9a145b00-c675-11ea-ae44-f5d67be454e7.png)
233462

234-
参考:
463+
## 参考文献
235464
- [https://zh.wikipedia.org/wiki/](https://zh.wikipedia.org/wiki/)
236465
- [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)
237466
- [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

Comments
 (0)