对 Object.defineProperty 的理解
仅以此文记录自己对
Object.defineProperty()
方法的理解,源起题目监听 data 变化的核心 API 是什么?
听君一席话
首先是方法的描述:
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
通俗易懂,来自MDN
参数部分,参数有三:
- target,要修改的目标对象;
- prop, 要修改的属性;
- descriptor, 要定义或修改的属性描述符;
前两个好理解,参数 3 如何理解呢?
对象中目前存在两种属性描述符,数据描述符和存取描述符;
这两种属性描述符共享以下三个参数:
configurable,值为 true 或 false,为 true 时属性才能被修改或删除,从 config 这个词就可以理解,默认为 false;
enumerable,值为 true 或 false,为 true 时属性才能被枚举,还可以直接写成属性对应的值,默认为 false;
writable,值为 true 或 false,为 true 时属性才能被修改,也就是 enumerable 才能直接写属性对应的值,默认为 false;
同时,writable 还有两个可选的键值 get 和 set
- get,属性的 getter 函数,访问属性时调用此方法,函数的返回值就是属性的值,默认为 undefined;
- set, 属性的 setter 函数,修改属性时调用此方法,接受一个参数,也就是新的属性值,默认为 undefined;
默认情况下,直接由
Object.defineProperty()
定义的属性是不可修改的,因为 configurable 属性默认是 false,如要修改,需要显示的定义 configurable 属性为 true 才行;接上一条,注意:是直接由
Object.defineProperty()
定义的属性默认不能修改,所以,对象上已存在的(不是由Object.defineProperty()
定义的)属性默认是可以被修改的;
今天的主角是vue 是如何实现 data 响应式的?
,所以,不要过分纠结此方法的细节,否则会深陷其中而顾此失彼,感兴趣的话可以抽空通读细读文档;
正题
先来一道开胃菜,不借助框架如何实现下面对象的响应式?
1 | const data = { |
预期调用:
1 | observer(data); |
预期输出:
首先定义一个方法,提示视图更新:
1 | function updataView(key, value, newValue) { |
然后定义主方法:
1 | function observer(target) { |
然后实现核心方法
1 | function defineReactive(target, key, value) { |
加个菜
如果对象是这样的呢?
1 | const data = { |
是不是想到了,在遍历目标对象的属性时加个判断,然后递归调用,没错!
1 | function observer(target) { |
输出:
菜中菜
如果调用时是这样呢?
1 | data.age = 20; |
很简单,在 set 函数中也使用深度监听(递归调用):
1 | function defineReactive(target, key, value) { |
输出:
基本类型和对象的响应式已经差不多了,那么数组的响应式是如何实现的呢,往下看;
Object.defineProperty()
方法的不足
第 1 条确实是Object.defineProperty()
方法的不足,但下面两条其实Object.defineProperty()
是可以实现的,但基于性能或其他考虑,Vue 并没有使用
- 深度监听时,如果对象嵌套层级很深,需要递归到底,一次性计算量很大;
- 无法监听新增和删除属性:
- 原因:
由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。
来自官方文档; - 可以使用
Vue.set()
和Vue.delete()
实现对象的增删;
- 原因:
- 无法监听数组长度的变化,以及直接使用索引修改数组内容
数组的响应式
接着上面的代码,首先重写数组原型:
1 | // 重新定义数组原型 |
然后在主函数中处理数组:
1 | function observer(target) { |
输入:
1 | const data = { |
输出:
undefined 是因为数组在触发视图更新时没有传递参数;