1、响应式系统介绍
-
Proxy 对象实现属性监听
-
多层属性嵌套,在访问属性过程中处理下一级属性
-
默认监听动态添加的属性
-
默认监听属性的删除操作
-
默认监听数组索引和 length 属性
-
可以作为单独的模块引用
核心方法:
- reactive / ref / toRefs / computed
- effect
- track
- trigger
2、Proxy对象基本使用
语法:
const p = new Proxy(target, handler)
const jiayin = new Proxy(data, {
// target 目标对象
// property 被获取的属性名
// receiver Proxy或者继承Proxy的对象
get(target, key, receiver) {
console.log("get---", key, receiver);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log("set---", key, value, receiver);
return Reflect.set(target, key, receiver); // 在严格模式下,若set方法返回false,则会抛出一个 TypeError 异常。
},
deleteProperty(target, key) {
console.log("delete---", target, key);
return Reflect.deleteProperty(target, key);
},
});
-
set
和deleteProperty
需要返回布尔类型的值,严格模式下,如果返回 false 的话会出现 Type Error 的异常; -
如果 handler 没有设置任何拦截,那就等同于直接通向原对象
var proxy = new Proxy({}, {})
; -
关于 Reflect,一开始有疑问,为什么不直接在 Proxy 内获取到值,非要多此一举使用 Reflect。后来我的理解是,Reflect 对象的方法与 proxy 对象的方法一一对应,且 Reflect 对象用起来更好,所以采用,并不是 proxy 的用不了,也不知道这个理解是否正确。
Reflect 对象有 4 个意义:
- 操作对象出现报错时返回false,而不是报错
- 和 Proxy 一一对应
- 规范化,标准化,函数式(最好删除元素啥的都用这个 API, 不要用 delete obj.key)
- 代替 Object 上的工具函数
-
Proxy 和 Reflect 中使用的 receiver
【这个问题依然没有搞懂!!!,不知道 receiver 到底有什么用】
Proxy 中 receiver —— Proxy 或者继承 Proxy 的对象
Reflect 中 receiver —— 如果 target 对象中设置了 getter, getter 中的 this 指向 receiver
const obj = { name: "kkk", get foo() { console.log("this-->", this); // 这个this指向receiver return this.bar; }, }; const proxy = new Proxy(obj, { get(target, key, receiver) { if (key === "bar") { return "value - bar"; } return Reflect.get(target, key, receiver); }, }); console.log(proxy.foo); // value - bar
3、reactive实现
前置知识:如何判断对象是否有某个属性
(1)点( . )或者方括号( [ ] )
- 如果不存在,返回 undefined (这里的**“不存在”指的是对象自身和原型链上都不存在**,如果原型链有该属性,则会返回原型链上的属性值)
- 局限:如果值本身为 undefined,这个方法就不能判断了。
(2)in 运算符
key in obj
如果指定的属性在指定的对象或其原型链中,则 in 运算符返回 true, 值为 undefined 也可以判断- 局限:无法区分是自身属性还是原型链上的属性。在只需要判断自身属性是否存在时,这种方式就不适用了
(3)Reflect.has(target, propertyKey)
和 in 运算符 的功能完全相同
(4)hasOwnProperty
返回一个布尔值,指示对象自身属性中是否具有指定的属性,忽略掉那些从原型链上继承到的属性;
局限:JavaScript 并没有保护 hasOwnProperty 这个属性名,因此,当某个对象可能自有一个占用该属性名的属性时,就需要使用外部的 hasOwnProperty 获得正确的结果
// 举例:foo 对象占用了 hasOwnProperty, 且始终返回 false var foo = { hasOwnProperty: function () { return false; }, bar: "Here be dragons", }; foo.hasOwnProperty("bar"); // 始终返回 false Reflect.has(foo, 'hasOwnProperty') // true // 如果担心这种情况, // 可以直接使用原型链上真正的 hasOwnProperty 方法 ({}.hasOwnProperty.call(foo, "bar")); // true // 也可以使用 Object 原型上的 hasOwnProperty 属性 // 只有这种方法不会重建对象 Object.prototype.hasOwnProperty.call(foo, "bar"); // true
总结:
最好使用 Object.prototype.hasOwnProperty.call(foo, "bar")
, 这样可以保证用的是当前对象原型上的 hasOwnProperty 方法,不会使用自身的了。
vue3中如果使用 Reflect.has(target, key)
会导致问题,就是 delete obj.hasOwnProperty
的时候,尽管target对象没有 hasOwnProperty 这个属性,也会执行
代码实现
-
新建一个 reactive 函数,接收参数 data
-
几个工具函数:
- 判断 data 是否为对象
const isObject = value => typeof value === 'object' || value === null
- 嵌套对象,判断是否需要递归
const convert = target => isObject(target) ? reactive(target) : target
- 判断对象中是否拥有该属性
const hasOwn = (target, key) => Object.prototype.hasOwnProperty.call(target, key)
- 判断 data 是否为对象
-
创建拦截器对象 handler, 设置 get / set / deleteProperty
-
返回 Proxy 对象
// 判断是否是对象
const isObject = (value) => typeof value === "object" || value === null;
// 如果对象是嵌套状态,就递归处理
const convert = (target) => (isObject(target) ? reactive(target) : target);
// 判断对象中是否有该属性
// const hasOwnProperty = (target, key) => target.hasOwnProperty(key) // 这个方法并不好
const hasOwn = (target, key) =>
Object.prototype.hasOwnProperty.call(target, key);
export const reactive = (data) => {
if (!isObject(data)) return data;
const handler = {
get(target, key, receiver) {
// 收集依赖
console.log("get--", key);
const res = Reflect.get(target, key, receiver);
return convert(res);
},
set(target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver); // 获取旧值与新值对比
let res = true; // 定义返回的布尔变量
if (oldValue !== value) {
console.log("set--", key);
res = Reflect.set(target, key, value, receiver);
// 触发更新
}
return res;
},
deleteProperty(target, key) {
const hasKey = hasOwn(target, key);
const res = Reflect.deleteProperty(target, key);
if (hasKey && res) {
// 触发更新
console.log("delete---", key);
}
return res;
},
};
return new Proxy(data, handler);
};
Vue3比Vue2性能更好的一个点:
- Vue2是在一开始就递归遍历了所有层级数据;
- Vue3则是在 get 的时候才遍历的;
4、ref 实现
前置知识:js 中的 get、set 方法
/*
- obj中的name是数据属性
- get、set后的age属性是访问器属性
访问器属性:当外部js给age赋值时走的时setter函数,当外部js获取age时 走的getter函数,setter和getter是隐藏函数,会取我们写在age后边的函数
*/
let obj = {
name: 1,
get age() {
return 22;
},
set age(value) {
console.log("set");
obj.name = value;
},
};
代码实现
ref 使基本数据类型变成响应式,主要是因为存储在 value 属性中,变成了一个引用类型数据
-
判断传入的是否是 ref 创建的对象,如果是直接返回;
-
传入的不是 ref 创建的对象,内部调用 reactive 创建响应式数据;
-
最后创建一个有 value 属性的对象,value属性的值为第二步处理后的值;
// ... 这里的代码接 reactive 节的代码 export const ref = (raw) => { // 1. 判断 raw 是否是 ref 创建的对象,如果是,直接返回 if(isObject(raw) && raw.__v_isRef) // 2. 传入的是普通数据,进行处理 // 普通对象 -> 被 reactive 处理为响应式对象 // 基本数据 -> 直接返回为基本数据 let reactiveData = convert(raw) // 3. 创建一个有 value 属性的对象返回 const r = { __v_isRef: true, // 注意,这个一定要有 get value() { console.log('ref--get-->>>', reactiveData); // todo:收集依赖 return reactiveData }, set value(newValue) { console.log('ref--set-->>>', reactiveData, newValue); if(newValue !== reactiveData) { raw = newValue reactiveData = convert(raw) // todo:触发更新 } } } return r }
5、reactive vs ref
ref 可以把基本数据类型数据,转成响应式对象;
ref 返回的对象,重新赋值成对象也是响应式的;
reacive 返回的对象,重新赋值丢失响应式;
reactive 返回的对象不可以直接解构,需要使用 toRefs ;
6、toRefs
- toRefs 函数接收一个 reactive 返回的响应式对象,也就是 Proxy 对象
- 把传入的对象的所有属性转成一个类似 ref 返回的对象,注意对 array 和其他数据类型的处理
- 把转换后后的对象挂载到一个新的对象上返回
export const toRefs = (proxy) => {
// 这一步不好理解,对对象和其他数据类型的处理
let ret = proxy instanceof Array ? new Array(proxy.length) : {}
for (const key in proxy) {
ret[key] = toProxyRef(proxy, key)
}
return ret
}
// 将属性转成类似 ref 返回的对象
const toProxyRef = (proxy, key) => {
let r = {
__v_isRef: true,
get value() {
// 注意:这里不需要数据劫持和更新,因为本来就是响应式的数据了,不需要劫持
return proxy[key]
},
set value(newValue) {
proxy[key] = newValue
}
}
return r
}
7、computed
computed内部使用的是effect实现(还未完全明白)
export const computed = (getter) => {
const result = ref()
effect(() => (result.value = getter()))
return result
}