Object.defineProperty实现响应式
首先需要知道是Object.defineProperty
只能监听对象,并且这个对象不是指对象类型(数组也是对象类型),而是Object
构造器对象,也就是{}
。
结构分为三个部分:
- updateView:更新视图的函数
- defineReactive:监听对象数据改变的函数
- observer:分解数据每一项属性,以便进行深度监听
监听对象
Vue2.x实现响应式视图更新,大概流程就是下面这样:
//设置简单的函数表示视图更新
function updateView() {
console.log("视图更新")
}
// 分解对象属性函数
function observer(target) {
if (typeof target !== "object" || target === null) {
// 不是对象或数组
return target
}
// 监听target每一个属性
for (let key in target) {
defineReactive(target, key, target[key])
}
}
function defineReactive(target, key, value) {
// 深度监听:如果value是对象就继续分解
observer(value)
// 核心API:Object.defineProperty()
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue !== value) {
// 设置新值也要监听是否是对象和数组
observer(newValue)
// 设置新值
// 注意value一直在闭包中,此处设置完后再get时也是会获取到value的值
value = newValue
// 触发更新视图
updateView()
}
},
})
}
由于JavaScript
的限制,Vue
里的数据不能够动态添加根级响应式 property。也就是说必须在初始化实例前声明所有根级响应式 property,哪怕只是一个空值,不然是不会触发视图更新虽然值已经变了。
大家可以通过上面的代码运行一下。👆
监听数组
为了能够监听数组,Vue2重写了部分数组方法去实现视图更新,但是这部分和Object.defineProperty
就没有关系了。
👇大概实现是这样实现的:
//设置简单的函数表示视图更新
function updateView() {
console.log("视图更新")
}
// 重新定义数组原型
const oldArrayProperty = Array.prototype
//这样新增方法也不会影响到Array原型
const arrPrototype = Object.create(oldArrayProperty)
// 假设添加了这些方法,那么如果数组调用这些方法就会触发视图更新
;["push", "pop", "unshift", "shift", "splice"].forEach(
(method) =>
(arrPrototype[method] = function () {
// 如果调用以上的方法就触发视图更新
updateView()
oldArrayProperty[method].call(this, ...arguments)
})
)
// 分解对象属性函数
function observer(target) {
if (typeof target !== "object" || target === null) {
// 不是对象或数组
return target
}
// 为了不污染全局Array原型:需要重新定义数组原型
if (Array.isArray(target)) {
target.__proto__ = arrPrototype
}
// 重新定义各个属性
for (let key in target) {
defineReactive(target, key, target[key])
}
}
因为Object.defineProperty
的限制,Vue2中对数组的操作非常有限制。
Vue 不能检测以下数组的变动:
- 当你利用索引直接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue
- 当你修改数组的长度时,例如:
vm.items.length = newLength
官方推荐最好用splice
方法对数组进行增删操作就是因为内部改写方法时改写了splice
方法。或者使用Vue.set()|vm.$set()
强行将数据添加到响应式里。
Proxy和Reflect实现响应式
在贴上代码之前必须要好好讲一下Proxy
,感觉很多人都对Proxy
有误解,以为Proxy
只是ES6语法用来代替ES5语法中Object.defineProperty
的语法糖,这种想法是大错特错的。Proxy
和Object.defineProperty
是两个完全不同的东西。所以Object.defineProperty
也不能代替Proxy
。
Proxy
Proxy 可以理解为:在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。
先上代码看看基本操作👇
var arr = [1,2]
var arrProxy = new Proxy(
arr,
{
get(target, propKey) {
console.log(`getting ${propKey}!`)
},
set(target, propKey, value) {
console.log(`setting ${propKey}!`)
},
}
)
//设置值
arrProxy[0] = 'change' //setting 0!
//读取值
arrProxy[1] //getting 1!
上面的代码对arr
数组架设了一层拦截,重定义了属性的读取(get
)和设置(set
)行为。
作为构造函数,Proxy
接受两个参数:
- 第一个参数是所要代理的目标对象(上例是一个
arr
对象),即如果没有Proxy
的介入,操作原来要访问的就是这个arr
对象。这里的对象是指对象类型(数组也是对象类型)。 - 第二个参数是一个配置对象
handler
,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。比如,上面代码中,配置对象有一个get
方法,用来拦截对目标对象属性的访问请求。get
方法的两个参数分别是目标对象和所要访问的属性。
注意: 要使Proxy起作用,必须针对Proxy实例(上例是arrProxy对象)进行操作,而不是针对目标对象(上例是arr对象)进行操作。
下面是 Proxy 支持的拦截操作一览,一共 13 种。
- get(target, propKey, receiver):拦截对象属性的读取,比如
proxy.foo
和proxy['foo']
。 - set(target, propKey, value, receiver):拦截对象属性的设置,比如
proxy.foo = v
或proxy['foo'] = v
,返回一个布尔值。 - has(target, propKey):拦截
propKey in proxy
的操作,返回一个布尔值。 - deleteProperty(target, propKey):拦截
delete proxy[propKey]
的操作,返回一个布尔值。 - ownKeys(target):拦截
Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols(proxy)
、Object.keys(proxy)
、for...in
循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()
的返回结果仅包括目标对象自身的可遍历属性。 - getOwnPropertyDescriptor(target, propKey):拦截
Object.getOwnPropertyDescriptor(proxy, propKey)
,返回属性的描述对象。 - defineProperty(target, propKey, propDesc):拦截
Object.defineProperty(proxy, propKey, propDesc)
、Object.defineProperties(proxy, propDescs)
,返回一个布尔值。 - preventExtensions(target):拦截
Object.preventExtensions(proxy)
,返回一个布尔值。 - getPrototypeOf(target):拦截
Object.getPrototypeOf(proxy)
,返回一个对象。 - isExtensible(target):拦截
Object.isExtensible(proxy)
,返回一个布尔值。 - setPrototypeOf(target, proto):拦截
Object.setPrototypeOf(proxy, proto)
,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。 - apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如
proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
。 - construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如
new proxy(...args)
。
可以看出Proxy
不仅可以实现Object.defineProperties
的功能,还有其他的操作也可以拦截。
我有关Proxy
的内容基本都是参考阮一峰的ES6教程,讲的特别好,大家可以看看。
Reflect
说完Proxy
就必须要说一说Reflect
这个ES6新增的API。Reflect
对象和Proxy
对象一样也是用来操作对象的,但是Reflect
对象的设计目的有重大的意义。
-
将
Object
对象的一些明显属于语言内部的方法(比如Object.defineProperty
),放到Reflect
对象上。现阶段,某些方法同时在Object
和Reflect
对象上部署,未来的新方法将只部署在Reflect
对象上。也就是说,从Reflect
对象上可以拿到语言内部的方法。这样做的好处就是使Object
类更纯粹,JavaScript
更像一门语言,Object
更像一个类,而不是什么乱七八糟的方法都往Object
上塞。 -
修改某些
Object
方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)
在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)
则会返回false
。// 老写法 try { Object.defineProperty(target, property, attributes); // success } catch (e) { // failure } // 新写法 if (Reflect.defineProperty(target, property, attributes)) { // success } else { // failure }
-
让
Object
操作都变成函数行为。某些Object
操作是命令式,比如name in obj
和delete obj[name]
,而Reflect.has(obj, name)
和Reflect.deleteProperty(obj, name)
让它们变成了函数行为。// 老写法 'assign' in Object // true // 新写法 Reflect.has(Object, 'assign') // true
-
(核心)
Reflect
对象的方法与Proxy
对象的方法一一对应,只要是Proxy
对象的方法,就能在Reflect
对象上找到对应的方法。这就让Proxy
对象可以方便地调用对应的Reflect
方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy
怎么修改默认行为,你总可以在Reflect
上获取默认行为。
Reflect
对象一共有 13 个静态方法。
- Reflect.apply(target, thisArg, args)
- Reflect.construct(target, args)
- Reflect.get(target, name, receiver)
- Reflect.set(target, name, value, receiver)
- Reflect.defineProperty(target, name, desc)
- Reflect.deleteProperty(target, name)
- Reflect.has(target, name)
- Reflect.ownKeys(target)
- Reflect.isExtensible(target)
- Reflect.preventExtensions(target)
- Reflect.getOwnPropertyDescriptor(target, name)
- Reflect.getPrototypeOf(target)
- Reflect.setPrototypeOf(target, prototype)
上面这些方法的作用,大部分与Object
对象的同名方法的作用都是相同的,而且它与Proxy
对象的方法是一一对应的。
我们来改写之前proxy
的例子🌰,加上Reflect
:
var arr = [1,2]
var arrProxy = new Proxy(
arr,
{
get(target, propKey, receiver) {
console.log(`getting ${propKey}!`)
return Reflect.get(target, propKey, receiver)
},
set(target, propKey, value, receiver) {
console.log(`setting ${propKey}!`)
return Reflect.set(target, propKey, receiver)
},
}
)
//设置值
arrProxy[0] = 'change' //setting 0! change
//读取值
arrProxy[1] //getting 1! 2
上面的代码中,每一个Proxy
对象的拦截操作(get
、set
),内部都调用对应的Reflect
方法,保证原生行为能够正常执行。Reflect
在Proxy
中调用还有一个重要的原因是recevier
参数。
来,看一个🌰来说明recevier
参数作用👇
let p = {
a: "a",
}
let handler = {
set(target, key, value, receiver) {
console.log("set")
Reflect.set(target, key, value, receiver)
},
defineProperty(target, key, attribute) {
console.log("defineProperty")
Reflect.defineProperty(target, key, attribute)
},
}
let obj = new Proxy(p, handler)
obj.a = "A"
// set
// defineProperty
上面代码中,Proxy.set
拦截里面使用了Reflect.set
,而且传入了receiver
,导致触发Proxy.defineProperty
拦截。
这是因为Proxy.set
的receiver
参数总是指向当前的 Proxy
实例(即上例的obj
),而Reflect.set
一旦传入receiver
,就会将属性赋值到receiver
上面(即obj
),导致触发defineProperty
拦截。如果Reflect.set
没有传入receiver
,那么就不会触发defineProperty
拦截。
所以receiver
的作用就是让Proxy
里的对象操作都指向当前的Proxy
实例,这样就能拦截到所有对实例的操作。(嗯,非常严谨无懈可击:+1:
Proxy对比Object.defineProperty
看到这里,我相信大家已经明白了Object.defineProperties
和Proxy
的区别。但是我还是要再啰嗦一句Proxy
并不是Object.defineProperties
的语法糖!!!!
babel
通过@babel/polyfill
(corejs
和re-generator
)将ES6语法转译为大部分浏览器都支持的ES5语法。原理说得简单点就是一些ES6的特性可以用ES5代替,只不过用ES5写会很麻烦或者不直观。
比如我们看一段代码👇:
// ES6定义类
class Person{
constructor(name){
this.name = name
}
sayName(){
console.log(this.name)
}
}
这个定义类的方法为了让它能够被大部分浏览器识别,我们一般会用babel
将它转译:
// 通过babel的polyfill后
function Person(name){
this.name = name
}
Person.prototype.sayName = function(){
console.log(this.name)
}
转译后的ES5代码和之前的ES6版本代码实现的功能完全一致。我们其实也可以说class
定义类的方法是通过function
定义类(构造函数)的语法糖。
但是Proxy
是不能通过babel
转译的,因为在ES5中完全没有一种语法可以模拟出Proxy
的特性。因此Vue3.x
版本没有办法兼任一些低版本浏览器。
响应式代码
function reactive(target = {}) {
if (typeof target !== "object" || target == null) {
return target
}
// 代理配置
const proxyConf = {
get(target, key, receiver) {
//只监听对象本身(非原型)属性
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
//如果是本身的属性就监听,如果是对象原型的属性就不监听
console.log("get", key)
}
const result = Reflect.get(target, key, receiver)
//(惰性)深度监听-->提升性能
return reactive(result)
},
set(target, key, val, receiver) {
// 重复的数据不处理
if (val === target[key]) {
return true
}
// 监听是否是新增的key
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log("已有的key", key)
} else {
console.log("新增的key", key)
}
const result = Reflect.set(target, key, val, receiver)
console.log("set", key, val)
return result //通过return的值可以看出是否设置成功
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
console.log("delete property", key)
return result //是否删除成功
},
}
// 生成代理对象
const observed = new Proxy(target, proxyConf)
return observed
}
Vue3的响应式逻辑如何一步一步构造出来的,我放在另一篇博文Vue3响应式实现逻辑。
结语
ok,在最后我想说一些话。
有很多人觉得Vue3为了性能舍弃了部分浏览器是得不偿失的行为。但是,我并不觉得。
首先,Vue3现目前还在ra
阶段,到真正发布估计还要一段时间。而且并不是Vue3发布了,我们做项目就必须用3,中间应该是漫长的Vue2.x到3的过渡时期。随着时间的拉长,我相信会有越来越多浏览器支持Proxy
属性,我们也会抛弃对越来越古老的浏览器版本的兼容。
其次不能因为现目前部分浏览器的不兼容就不去考虑Vue的性能提升,技术的推动是慢慢迭代的。要是都以现状做导向的话,互联网也不会发展得这么快。
嗯,就这样吧。
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: Liqiuyue 原文链接:https://juejin.im/post/6855129007412346888