其实这个问题很多文章都有写,也是面试的高频题目,这里仅仅是记录下本人的了解。
Proxy
和 Object.defineproperty
的区别
Object.defineProperty
只能劫持对象的属性,对于嵌套的对象还须要进行深度的遍历;而Proxy
是间接代理整个对象Object.defineProperty
对新增的属性须要手动的 Observe(应用 $set);Proxy
能够拦挡到对象新增的属性,数组的push
、shift
、splice
也能拦挡到Proxy
具备 13 种拦挡操作,这是defineProperty
不具备的Proxy
兼容性差IE
浏览器不反对很多种Proxy
的办法 目前还没有残缺的polyfill
计划
defineProperty
写法;
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])
})
}
Proxy
的写法:
let proxyObj = new Proxy(data, {get(key) {return data[key]
},
set(key, value) {data[key] = value
}
})
当然还有其余的属性,这里写最简略的。
这两个办法的区别让我想到了事件代理
<ul id="ul">
<li>111</li>
<li>222</li>
<li>333</li>
<li>444</li>
</ul>
如果没有应用事件代理,那么它会给 ul
下的每个 li
绑定事件, 这样写有个问题就是,新增的 li
是没有事件的 ,事件没有一起增加进去。
如果是应用事件代理,那么新增加的子节点也会有事件响应,因为它是通过触发代理节点(父节点 冒泡)来触发事件的
十分相似,这里想要阐明的是:defineProperty
是在自身本人的对象属性上做 getter/setter
, 而Proxy
返回的是一个代理对象,只有批改代理对象才会产生响应式,如果批改原来的对象属性,并不会产生响应式更新.
Object.defineProperty
对数组的解决
查阅 vue
的官网文档 咱们能看到:
Vue 不能检测以下数组的变动:
1、当你利用索引间接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue
2、当你批改数组的长度时,例如:vm.items.length = newLength
对于第一点:
有一些文章间接写
Object.defineProperty
有一个缺点是无奈监听到数组的变动, 导致间接通过数组的下标给数组设置值,不能实时响应
这种说法是 谬误 的,事实上 Object.defineProperty
是能够监听到数组下标的变动,只是在 Vue
的实现中,从性能 / 体验的性价比思考,放弃了这个个性.
对于数组下的索引是能够用getter/setter
的,
然而 vue 为什么没这么做?如果监听索引值,通过 push
或unshift
增加进来的元素的索引还没被劫持,也不是响应式的,须要手动的进行 observe
,通过pop
或shift
删除元素,会删除并更新索引,也能触发响应式,然而数组常常会被遍历,会触发很屡次索引的 getter 性能不是很好。
对于第二点:
MDN:
数组的 length 属性重定义是可能的,然而会受到个别的重定义限度。(length 属性初始为 non-configurable,non-enumerable 以及 writable。对于一个内容不变的数组,扭转其 length 属性的值或者使它变为 non-writable 是可能的。然而扭转其可枚举性和可配置性或者当它是 non-writable 时尝试扭转它的值或是可写性,这两者都是不容许的。)然而,并不是所有的浏览器都容许 Array.length 的重定义。
所以对于数组的 length
, 无奈对它的拜访器属性进行get
和set
, 所以没法进行响应式的更新.
这里留神下有两个概念:索引 和 下标
数组有下标,然而对应的下标可能没有索引值!
arr = [1,2]
arr.length = 5
arr[4] // empty 下标为 4, 值为 empty, 索引值不存在。for..in 不会遍历出索引值不存在的元素
手动赋值 length
为一个更大的值,此时长度会更新,然而对应的索引不会被赋值,也就是对象的属性没有,defineProperty
无奈解决对未知属性的监听,举个例子:length = 5
的数组,未必索引就有 4,这个索引 (属性) 不存在,就没法 setter
了。
数组的索引跟对象的键体现其实是统一的.
vue
对数组进行了独自解决, 对其进行劫持重写,
看一个数组劫持的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
, 并在其下面定义了要劫持的数组,咱们这个只是简略的打印了一句。扭转arr
的原型指向(给 __proto__
赋值),在 arr
操作 push,splice
时会走劫持的办法。vue
的数组劫持实际上是在劫持办法外面增加了 响应式 的逻辑.
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 手动增加响应式 原理
对于对象新增属性 / 数组新增元素, 无奈触发响应式,咱们能够用 vue $set
进行解决
vm.$set(obj,key,value)
对于数组还能应用 splice
办法:
vm.items.splice(indexOfItem, 1, newValue)
然而它们实质是一样的!
set 的实现外围就是:
- 如果是数组,会应用
splice
对元素进行手动observe
- 如果是对象
如果是批改存在的 key, 间接赋值就会触发响应式更新
如果是新增的 key, 就对 key 进行手动observe
- 如果不是响应式的对象(响应式对象有__ob__ 属性)就间接赋值
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
}
参考:
https://www.zhihu.com/questio…
https://www.javascriptc.com/3…
https://juejin.cn/post/684490…