In fact, many articles on this question have been written, and it is also a high-frequency topic for interviews. This is just to record my own understanding.
The difference between Proxy
and Object.defineproperty
Object.defineProperty
can only hijack the properties of the object, and deep traversal is required for nested objects; whileProxy
directly proxy the entire objectObject.defineProperty
requires manual Observe (using $set) for the new attributes;Proxy
can intercept the new attributes of the object, and the array ofpush
,shift
,splice
can also be interceptedProxy
has 13 interception operations, whichdefineProperty
does not haveProxy
poor compatibilityIE
browser does not support very manyProxy
way there is no completepolyfill
program
defineProperty
writing method;
function defineReactive(data, key, value) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function defineGet() {
console.log(`get key: ${key} value: ${value}`)
return value
},
set: function defineSet(newVal) {
console.log(`set key: ${key} value: ${newVal}`)
value = newVal
}
})
}
function observe(data) {
Object.keys(data).forEach(function(key) {
// 递归的getter setter
defineReactive(data, key, data[key])
})
}
The wording of Proxy
let proxyObj = new Proxy(data, {
get(key) {
return data[key]
},
set(key, value) {
data[key] = value
}
})
Of course there are other attributes, the simplest is written here.
The difference between these two methods reminds me of event proxy
<ul id="ul">
<li>111</li>
<li>222</li>
<li>333</li>
<li>444</li>
</ul>
If no event agency, then it will give ul
each under li
binding events, wrote that there is a problem, new li
are no events , no events added to it together.
If you are using an event proxy, then the newly added child node will also have an event response, because it triggers the event by triggering the proxy node (parent node bubbling)
Very similar, what I want to explain here is: defineProperty
getter/setter
on its own object properties, and Proxy
returns a proxy object. Only when the proxy object is modified will the response type occur. If the original object property is modified, it does not Will generate responsive updates.
Object.defineProperty
processing of arrays
Check vue
of official document we can see:
Vue cannot detect changes in the following arrays:
1. When you use the index to directly set an array item, for example:
vm.items[indexOfItem] = newValue
2. When you modify the length of the array, for example:vm.items.length = newLength
For the first point:
Some articles are written directly
Object.defineProperty
has a defect that it is unable to monitor the changes of the array, which causes the array to set the value directly through the subscript of the array, and it cannot respond in real time.
This statement is error . In fact, Object.defineProperty
can monitor the change of the array index, but in Vue
, this feature is abandoned from the consideration of performance/experience.
For the index under the array, getter/setter
can be used,
But why didn't vue do this? If you monitor the index value, push
or unshift
not been hijacked, nor is it responsive, you need to manually perform observe
, pop
or shift
, it will delete and update the index, and it can also trigger the response. , But the array is often traversed, which triggers many index getter performance is not very good.
For the second point:
MDN:
Redefining the length property of an array is possible, but it is subject to general redefinition restrictions. (The length attribute is initially non-configurable, non-enumerable, and writable. For an array with constant content, it is possible to change the value of its length attribute or make it non-writable. But change its enumerability and Configurability or trying to change its value or writeability when it is non-writable, both of which are not allowed.) However, not all browsers allow the redefinition of Array.length.
Therefore, for the array of length
, it is impossible to perform get
and set
accessor attributes, so it cannot be updated responsively.
Note here that there are two concepts: index and subscript
The array has a subscript, but the corresponding subscript may not have an index value!
arr = [1,2]
arr.length = 5
arr[4] // empty 下标为4,值为empty,索引值不存在。 for..in 不会遍历出索引值不存在的元素
Manually assign length
to a larger value. At this time, the length will be updated, but the corresponding index will not be assigned, that is, the attribute of the object is not. defineProperty
cannot handle the monitoring of unknown attributes. For example: the array of length = 5
There is an index of 4. If this index (attribute) does not exist, setter
is impossible.
array is actually the same as the key performance of the object.
vue
the array separately, hijacked it and rewritten it,
Look at an array hijacked demo
:
const arrayProto = Array.prototype
// 以arrayProto为原型的空对象
const arrayMethods = Object.create(arrayProto)
const methodToPatch = ['push', 'splice']
methodToPatch.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args)
console.log('劫持hh')
return result
})
})
function def(obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
configurable: true,
writable: true
})
}
let arr = [1,2,3]
arr.__proto__ = arrayMethods
arr.push(4)
// 输出
// 劫持hh
// 4
arrayMethods
using the array as the prototype, and defined the array to be hijacked on it, we just simply printed a sentence. Change arr
prototype points (to __proto__
assignment), in arr
operation push,splice
method will take the hijacking of time. The array hijacking of vue
actually adds the logic responsive
function mutator(...args) {
// cache original method
const original = arrayProto[method]
// obj key, val, enumerable
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
//eg: push(a) inserted = [a] // 为push的值添加Oberserve响应监听
inserted = args
break
case 'splice':
// eg: splice(start,deleteCount,...items) inserted = [items] // 为新添加的值添加Oberserve响应监听
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
$set manually add responsive principle
For the new attribute of the object/new element of the array, the response type cannot be triggered, we can use vue $set
for processing
vm.$set(obj,key,value)
For arrays, the splice
method can also be used:
vm.items.splice(indexOfItem, 1, newValue)
But they are essentially the same!
The core of the implementation of set is:
- If it is an array, use
splice
the elements manuallyobserve
- If it is an object
If it is to modify the existing key, direct assignment will trigger a responsive update
If it is a new key, manuallyobserve
- If it is not a reactive object (the reactive object has __ob__ attribute), just assign it directly
The internal implementation of set:
export function set (target: Array<any> | Object, key: any, val: any): any {
// 如果 set 函数的第一个参数是 undefined 或 null 或者是原始类型值,那么在非生产环境下会打印警告信息
// 这个api本来就是给对象与数组使用的
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 类似$vm.set(vm.$data.arr, 0, 3)
// 修改数组的长度, 避免索引>数组长度导致splcie()执行有误
target.length = Math.max(target.length, key)
// 利用数组的splice变异方法触发响应式, 这个前面讲过
target.splice(key, 1, val)
return val
}
// target为对象, key在target或者target.prototype上。
// 同时必须不能在 Object.prototype 上
// 直接修改即可, 有兴趣可以看issue: https://github.com/vuejs/vue/issues/6845
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
// 以上都不成立, 即开始给target创建一个全新的属性
// 获取Observer实例
const ob = (target: any).__ob__
// Vue 实例对象拥有 _isVue 属性, 即不允许给Vue 实例对象添加属性
// 也不允许Vue.set/$set 函数为根数据对象(vm.$data)添加属性
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
// target本身就不是响应式数据, 直接赋值
if (!ob) {
target[key] = val
return val
}
// ---->进行响应式处理
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
refer to:
https://www.zhihu.com/question/51520173
https://www.javascriptc.com/3058.html
https://juejin.cn/post/6844903614096343047
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。