从源码层面深入理解vue3
主要记录在跟随学习vue3源码的过程中遇到的知识点和架构思维。
Reflect 反射
Reflect的作用:可以完成对象的基本操作(直接调用对象的基本方法)。
对象的基本方法:在语言层面就规定好的对象只有这些操作(内部方法,一般来说我们无法访问),但它会允许我们通过间接的方式去使用:
obj.a = 1 // 间接使用了内部的 [[SET]] 方法
Object.getPrototypeOf(obj) // 间接使用了内部的 [[GetPrototypeOf]] 方法
但是间接操作有一定可能导致意料之外的结果,例如:
const obj = {a:1,b:2}
const keys = Object.keys(obj)
console.log(keys)
// 可以拿到 ['a','b']
// 接下来我们往obj中加入一个 不可枚举 的属性
Object.defineProperty(obj,'c',{
value: 3,
enumberable: false, // 不可枚举
})
// obj变成了{a:1,b:2,c:3}
const new_keys = Object.keys(obj)
console.log(new_keys)
// 依旧只拿到 ['a','b']
那!这是为什么呢?原来ES-262源码中是这样介绍,
Object.keys(O)的步骤:
- 将传入的参数O转化为对象obj,执行 ToObject(O)。
- 执行 EnumberableOwnProperties(obj,KEY),得到一个key的集合keyList。(关键)
- 执行 CreateArrayFromList(keyList),创建一个数组 将其返回。
其中最主要的在EnumberableOwnProperties函数中,调用了内部方法[[GetOwnProperty]]得到了对象的每个属性的描述符,然后去判断这个属性描述符是否是可枚举[[Enumberable]]的。只有可枚举的,才会被加到结果(keyList)里面去。
它中间多的判断,在我们平常开发中,并不是我们想要的效果,我们只是真的想要获取它所有的key。这就是为什么在ES6中会开放Reflect给我们,允许直接调用对象的基本方法。那么我们用Reflect来拿到上面那种情况的所有key:
const obj = {a:1,b:2}
Object.defineProperty(obj,'c',{
value: 3,
enumberable: false, // 不可枚举
})
const keys = Reflect.ownKeys(obj) //使用Reflect直接调用对象的基本方法,不需要弯弯绕绕
console.log(keys)
// 结果:['a', 'b', 'c']
拓展-vue3源码中使用Reflect反射解决的问题
先展示一段我们平常常见的代码:
let obj = {
a: 1,
b: 2,
get c(){
console.log(this)
return this.a + this.b
}
}
console.log(obj.c)
// 读取属性c:
// {a:1,b:2,c:[Getter]}
// 3
我们通常不会去考虑这个get c()里面的this是哪里来的,我们理所应当地把它当作就是obj,其实应该从原理上去理解,它是调用了对象的内部方法[[GET]],而它需要传递3个参数 (对象,属性,this是谁),js内部会帮我们传递这些参数 (obj,'c',obj),因此读到的this就是obj的this。
效果等同于 Reflect.get(obj,'c',obj)
如果我们直接使用Reflect.get来获取c,就可以用更高的灵活度,我们可以自由改变这个this。
let obj = {
a: 1,
b: 2,
get c(){
console.log(this)
return this.a + this.b
}
}
console.log(Reflect.get(obj,'c',{a:5,b:6}));
// 由于c中的this被设置为{a:5,b:6},因此打印也改变:
// {a:5,b:6}
// 11
那么它在vue3中的应用: 我们知道vue3实现响应式使用的是Proxy。
let obj = {
a: 1,
b: 2,
get c(){
console.log(this)
return this.a + this.b
}
}
const handler = new Proxy(obj,{
get(target,key,receiver){
console.log('get',key) // 打印一下读的属性
return target[key] // 这里演示不用反射的情况
}
})
handler.c;
// 这时候控制台会打印:
// get c
// {a:1,b:2,c:[Getter]}
重点! 我们看到在get c()中,有调用到a和b。但是它们却没有被拦截到(如果拦截到了,它们也会触发handler(Proxy)中的get打印)。
问题就出在这里的this,因为现在的c中读到的this是默认传入的obj,它不是handler。因此在get c()中间接读到的a和b并不会被拦截。所以需要使用Reflect来解决这个问题:
const handler = new Proxy(obj,{
get(target,key,receiver){
console.log('get',key)
return Reflect.get(target,key,receiver)
}
})
handler.c;
// 这样就可以拦截到a和b了:
// get c
// {a:1,b:2,c:[Getter]}
// get a
// get b
这就是vue3中数据响应式设计的一个细节,那么我们就延伸到数据响应式。
数据响应式 如何实现的?
目标:数据与函数之间建立起某种关系。建立关系的关键呢,是在函数的运行期间,用(读/改)到了这个数据。那么就根据前提条件引出“建立关系”的两个需要做到的点:
- 监听数据的读取和修改
- 如何知晓数据对应的函数
1. 监听数据的读取和修改
js中我们有两种方式可以做到:Object.defineProperty和Proxy。Proxy是ES6才有的,因此旧的浏览器是不兼容的(兼容性差一丢丢),但是相比之下Proxy的优势实在太明显了它的拦截范围更广,而且现在市面上绝大部分浏览器都支持Proxy了。
由于Object.defineProperty和Proxy都要求数据目标是对象,因此我们说的监听数据的读取和修改其实就是监听对象的读取和修改。
在vue中,它就封装了一个方法用来标记某个对象与某个函数之间进行了关联,称之为reactive,并且称被监听过后的对象为响应式数据。
// reactive.js
// 先实现一个雏形
// 此处先引入 2. 如何知晓数据对应的函数 相关方法
import { track,trigger } from './effect.js';
function reactive(target){
return new Proxy(target,{
get(target,key,receiver){
// 依赖收集(我们需要知道是哪些函数在用这个对象的属性)
// vue把依赖收集的方法写在 effect.js 之后在第二个点介绍,这里先直接使用
track(target,key)
return Reflect.get(target,key,receiver)
},
set(target,key,value,receiver){
// 派发更新(用过这个对象属性的函数,都要重新执行一遍:响应)
// vue把派发更新的方法也写在 effect.js 之后在第二个点介绍,这里先直接使用
trigger(target,key)
// 使用Reflect.set()来修改属性,会返回true/false 代表修改 成功/失败
return Reflect.set(target,key,value,receiver)
}
})
}
在上方雏形的基础上,先处理一些更表层一些的问题:
- 一些边界条件(判断传入的数据是否是对象)
- 同一个对象只生成一个代理(用WeakMap来缓存对象是否代理过,WeakMap比起Map更好地触发垃圾回收机制)
// reactive.js
// 先实现一个雏形
import { track,trigger } from './effect.js';
import { isObject } from './utils.js';
const targetMap = new WeakMap()
function reactive(target){
if(!isObject(target)){
return target // 如果不是对象,直接返回
}
if(targetMap.has(target)){
return targetMap.get(target) // 已代理过,直接返回
}
const proxy = new Proxy(target,{
get(target,key,receiver){
track(target,key)
return Reflect.get(target,key,receiver)
},
set(target,key,value,receiver){
trigger(target,key)
return Reflect.set(target,key,value,receiver)
}
})
targetMap.set(target,proxy)
return proxy
}
这里面new Proxy中的配置其实可以单独剥离出去了,创建一个handlers.js用来存放:
// handlers.js
import { track,trigger } from './effect.js';
export const handlers = {
get(target,key,receiver){
track(target,key)
return Reflect.get(target,key,receiver)
},
set(target,key,value,receiver){
trigger(target,key)
return Reflect.set(target,key,value,receiver)
}
}
这样reactive的代码就简化了
// reactive.js
import { handlers } from './handlers.js';
import { isObject } from './utils.js';
const targetMap = new WeakMap()
function reactive(target){
if(!isObject(target)){
return target // 如果不是对象,直接返回
}
if(targetMap.has(target)){
return targetMap.get(target) // 已代理过,直接返回
}
const proxy = new Proxy(target,handlers)
targetMap.set(target,proxy)
return proxy
}
深度代理(雏形)
OK消化好之后,来解决新的情况,如果我用来监听的obj是这样的:(c是一个对象)
let obj = {
a:1,
b:2,
c: {
d: 3
}
}
const objProxy = reactive(obj)
objProxy.c.d
// 这个时候,只能监听到对属性c的读取 而无法读取到对d的读取,
// 因为这里的c返回的是一个对象而不是一个代理
那么就需要在get()的时候,对读取到的值进行判断,如果读到的值是对象的话,需要把它也进行代理:
// handlers.js
import { track,trigger } from './effect.js';
import { isObject } from './utils.js';
import { reactive } from './reactive.js';
export const handlers = {
get(target,key,receiver){
track(target,key)
const result = Reflect.get(target,key,receiver)
if(isObject(result)){
return reactive(result)
}
return result
},
set(target,key,value,receiver){
trigger(target,key)
return Reflect.set(target,key,value,receiver)
}
}