Object.defineProperty()
实现数据响应Proxy
什么是代理呢,能够理解为在对象以前设置一个“拦截”,当该对象被访问的时候,都必须通过这层拦截。意味着你能够在这层拦截中进行各类操做。好比你能够在这层拦截中对原对象进行处理,返回你想返回的数据结构。html
ES6 原生提供 Proxy 构造函数,MDN上的解释为:Proxy 对象用于定义基本操做的自定义行为(如属性查找,赋值,枚举,函数调用等)。vue
咱们先来看看怎么使用。react
const p = new Proxy(target, handler);
target
: 所要拦截的目标对象(能够是任何类型的对象,包括原生数组,函数,甚至另外一个代理)handler
:一个对象,定义要拦截的行为const p = new Proxy({}, { get(target, propKey) { return '哈哈,你被我拦截了'; } }); console.log(p.name); // 哈哈,你被我拦截了
注意Proxy是用来操做对象的。代理的目的是为了拓展对象的能力。git
再看一个例子
咱们能够实现一个功能:不容许外部修改对象的name属性。es6
const p = new Proxy({}, { set(target, propKey, value) { if (propKey === 'name') { throw new TypeError('name属性不容许修改'); } // 不是 name 属性,直接保存 target[propKey] = value; } }); p.name = 'proxy'; // TypeError: name属性不容许修改 p.a = 111; console.log(p.a); // 111
babel是用来转换语法的,像新增的API(好比Array.from, Array.prototype.includes )咱们须要安装额外的包来进行支持,好比 [core-js/stable]() 和 [regenerator-runtime/runtime]() (PS:babel 7.x 以后@babel/polyfill已不推荐使用),而后还有一些API(String#normalize、Proxy、fetch等)
core-js
中是暂时没有提供 polyfill,具体的可查看官方文档
core-js#missing-polyfills。
Proxy
支持的拦截操做一共 13 种,详细的能够查看 MDN。github
递归遍历data中的数据,使用 Object.defineProperty()劫持 getter和setter,在getter中作数据依赖收集处理,在setter中 监听数据的变化,并通知订阅当前数据的地方。
部分源码 src/core/observer/index.js#L156-L193, 版本为 2.6.11 以下面试
let childOb = !shallow && observe(val) // 对 data中的数据进行深度遍历,给对象的每一个属性添加响应式 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { // 进行依赖收集 dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { // 是数组则须要对每个成员都进行依赖收集,若是数组的成员仍是数组,则递归。 dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } if (getter && !setter) return if (setter) { setter.call(obj, newVal) } else { val = newVal } // 新的值须要从新进行observe,保证数据响应式 childOb = !shallow && observe(newVal) // 将数据变化通知全部的观察者 dep.notify() } })
这么作有什么问题呢?数组
newProperty
,当前新加的这个属性并无加入vue检测数据更新的机制(由于是在初始化以后添加的)。vue.$set
是能让vue知道你添加了属性, 它会给你作处理,$set
内部也是经过调用Object.defineProperty()
去处理的vue3.0还未正式发布,不过vue-next 的相关代码已经开源出来了,目前处于Alpha版本。babel
为何使用 Proxy 能够解决上面的问题呢?主要是由于Proxy是拦截对象,对对象
进行一个"拦截",外界对该对象的访问,都必须先经过这层拦截。不管访问对象的什么属性,以前定义的仍是新增的,它都会走到拦截中,数据结构
下面分别用Object.defineProperty()
和 Proxy
实现一个简单的数据响应
使用Object.defineProperty()
实现:
class Observer { constructor(data) { // 遍历参数data的属性,给添加到this上 for(let key of Object.keys(data)) { if(typeof data[key] === 'object') { data[key] = new Observer(data[key]); } Object.defineProperty(this, key, { enumerable: true, configurable: true, get() { console.log('你访问了' + key); return data[key]; // 中括号法能够用变量做为属性名,而点方法不能够; }, set(newVal) { console.log('你设置了' + key); console.log('新的' + key + '=' + newVal); if(newVal === data[key]) { return; } data[key] = newVal; } }) } } } const obj = { name: 'app', age: '18', a: { b: 1, c: 2, }, } const app = new Observer(obj); app.age = 20; console.log(app.age); app.newPropKey = '新属性'; console.log(app.newPropKey);
上面代码的执行结果为
// 修改 obj原有的属性 age的输出 你设置了age 新的age=20 你访问了age 20 // 设置新属性的输出 新属性
能够看到,给对象新增一个属性,内部并无监听到,新增的属性须要手动再次使用Object.defineProperty()
进行监听。
这就是为何 vue 2.x
中 检测不到对象属性的添加和删除的缘由,内部提供的$set
就是经过调用Object.defineProperty()
去处理的。
下面咱们使用 Proxy
替代 Object.defineProperty()
实现
const obj = { name: 'app', age: '18', a: { b: 1, c: 2, }, } const p = new Proxy(obj, { get(target, propKey, receiver) { console.log('你访问了' + propKey); return Reflect.get(target, propKey, receiver); }, set(target, propKey, value, receiver) { console.log('你设置了' + propKey); console.log('新的' + propKey + '=' + value); Reflect.set(target, propKey, value, receiver); } }); p.age = '20'; console.log(p.age); p.newPropKey = '新属性'; console.log(p.newPropKey);
能够看到下面输出
// 修改原对象的age属性 你设置了age 新的age=20 你访问了age 20 // 设置新的属性 你设置了newPropKey 新的newPropKey=新属性 你访问了newPropKey 新属性
能够看到,新增的属性,并不须要从新添加响应式处理,由于 Proxy
是对对象的操做,只要你访问对象,就会走到 Proxy
的逻辑中。
Reflect(ES6引入) 是一个内置的对象,它提供拦截 JavaScript 操做的方法。将Object对象一些明显属于语言内部方法(好比Object.defineProperty()
)放到Reflect
对象上。修改某些Object方法的返回结果,让其变得更合理。让Object操做都变成函数行为。具体内容查看 MDN
除了即将发布的 vue 3.0
以外,还有哪些库使用了Proxy
呢?
都是使用到了对对象进行读写拦截,在读写中作一些额外的判断和操做。
Proxy
是用来操做对象的,拓展对象的能力。Object.defineProperty()
是对对象属性进行操做。vue2.x
使用 Object.defineProperty()
实现数据的响应式,可是因为 Object.defineProperty()
是对对象属性的操做,因此须要对对象进行深度遍历去对属性进行操做。vue3.0
用 Proxy
是对对象进行拦截操做,不管是对对象作什么样的操做都会走到 Proxy 的处理逻辑中vue3.0
、dobjs/dob
、immer
等库目前都使用到了 Proxy
,对对象进行读写拦截,作一些额外的处理。