关于vue3的响应式原理相关的知识,在我们前端的面试中是会被经常问到的问题,因此特意了解了一下它的实现思路到底是什么。vue3它的响应式原理底层是依赖了ES6提供的Proxy和Reflect对象,此外还有WeakMap对象,所以在学习原理之前,我们需要先知道它们到底是什么。
一、Proxy对象
基本用法
Proxy对象可以理解为是替其他对象进行操作的代理对象,在操作目标对象之前进行拦截。外界对某个对象进行操作之前,都要经过Proxy对象这层拦截。创建Proxy对象的基本语法:
js
const objProxy = new Proxy(obj, handler)
// obj是被代理对象,handler是拦截器,对目标对象的操作可以在handler中处理
来段代码理解一下,首先创建一个userInfo对象:
js
const userInfo = {
username: 'abc',
age: 18
}
我们要访问这个对象属性值,直接userInfo.username
来访问,如果给这个对象设置一个代理对象,通过这个代理对象也获取到userInfo属性:
js
const userInfo = {
username: 'abc',
age: 18
}
const userProxy = new Proxy(userInfo, {
get: function(target, key, receiver) {
console.log('get')
return target[key]
},
set: function(target, key, value, receiver) {
console.log('set')
target[key] = value
}
})
console.log(userProxy.username) // 打印结果:abc
userProxy.username = '123'
console.log(userProxy.username) // 打印结果:123
也就是说,当获取对象属性值时会调用proxy对象的getter,设置属性值时会调用setter。通过设置这样一层拦截,可以实现在对属性值进行存取时进行额外的操作。
关于getter中的参数:
target
:实际操作的目标对象,在我们的示例中是userInfokey
:操作的属性receiver
:当前的代理对象,在我们的示例中是userProxy,这个参数的作用在下面讲到Reflect时才能直接应用到
关于setter中的参数:
value
:要设置的新值- 其余参数跟getter中的一样
Proxy捕获器
Proxy一共有13个捕获器(上面的get和set就是其中两个),所有的捕获器都是可选的,如果没有定义某个捕获器,会保留对象的默认行为。13个捕获器分别为:
handler.get(target, key, receiver)
:拦截对象属性的读取handler.set(target, key, value, receiver)
:拦截对象属性设置handler.has(target, key)
:拦截key in proxy
的操作,返回一个布尔值handler.deleteProperty(target, key)
:拦截delete proxy[key]
的操作,返回一个布尔值。handler.ownKeys(target)
:拦截Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols(proxy)
,返回目标对象所有自身的属性的属性名handler.getOwnPropertyDescriptor(target, key)
:拦截Object.getOwnPropertyDescriptor(proxy, key)
handler.defineProperty(target, key, propDesc)
:拦截Object.defineProperty(proxy, key, propDesc)
handler.preventExtensions(target)
:拦截Object.preventExtensions(proxy)
,返回一个布尔值handler.getPrototypeOf(target)
:拦截Object.getPrototypeOf(proxy)
,返回一个对象handler.isExtensible(target)
:拦截Object.isExtensible(proxy)
,返回一个布尔值。handler.setPrototypeOf(target, proto)
:拦截Object.setPrototypeOf(proxy, proto)
,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。handler.apply(target, object, args)
:拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
。handler.construct(target, args)
:拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)
。
二、Reflect对象
Reflect主要作用
- 提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法。比如,
Reflect.getPrototypeOf(target)
类似于Object.getPrototypeOf
为什么已经有Objec可以做这些操作了还要Reflect呢?
- 因为早期ECMA规范中没有考虑到对象本身的操作如何设计更加规范,就把这些API都放到了Object身上
- 但是Object作为一个构造函数,这些操作放到它身上不合适
- 新增Reflect对象可以让操作都集中到Reflect对象上 Reflect中常见的方法和Object是一一对应的,因此我们的示例可以改为用Reflect来获取对象的值:
js
const userInfo = {
username: 'abc',
age: 18
}
const userProxy = new Proxy(userInfo, {
get: function(target, key, receiver) {
console.log('get')
return Reflect.get(target, key, receiver)
},
set: function(target, key, value, receiver) {
console.log('set')
Reflect.set(target, key, value, receiver)
}
})
与大多数全局对象不同 Reflect
并非一个构造函数,所以不能通过new 运算符
对其进行调用,或者将 Reflect
对象作为一个函数来调用。Reflect
的所有属性和方法都是静态的(就像Math
对象)。
关于Receiver参数
上面说到Proxy对象时已经有谈到拦截器里的receiver参数,它本身是一个Proxy对象,其主要作用是作为Reflect对象方法的传参,目的是通过receiver来改变里面的this。来看下面的例子:
js
const userInfo = {
_name: 'abc',
get Name() {
return this._name
},
set Name(value) {
this._name = value
}
}
在上面的代码中,获取name
属性时,内部还是通过this来获取_name
,但是Reflect的存在就是为了避免直接操作Object上的属性,所以还是用Proxy对象来访问属性比较合理,就有了receiver,在内部访问私有属性时还是用的代理。比如内部访问this._name
时其实是通过receiver的getter去获取的。
vue3响应式设计
响应式就是当一个变量发生变化的时候,使用到这个变量的地方能够重新执行获取新的值,这就需要监听对象属性的变化。 大概步骤如下:
- 响应式函数的封装
js
let activeReactiveFn = null
function watchFn(fn) {
// 用全局变量暂存依赖执行函数
activeReactiveFn = fn
fn() // 函数在执行的过程过用到响应式对象时,会收集依赖,函数执行完,依赖收集完成,将全局暂存activeReactiveFn置为null
activeReactiveFn = null
}
- 封装Depend类,实现依赖收集和触发通知
js
class Depend {
constructor() {
// 用Set收集依赖防止重复收集
this.reactiveFns = new Set()
}
// 依赖收集
depend() {
if (activeReactiveFn) {
this.reactiveFns.add(activeReactiveFn)
}
}
// 通知变化
notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}
- 创建监听对象变化的方法,并返回代理对象
js
function reactive(obj) {
return new Proxy(obj, {
get: function(target, key, receiver) {
const depend = getDepend(target, key)
// 添加依赖
depend.depend()
return Reflect.get(target, key, receiver)
},
set: function(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
// 触发依赖
const dep = getDepend(target,key)
dep.notify()
}
})
}
- 依赖收集的方法
js
// 寻找依赖
const targetMap = new WeakMap()
function getDepend(obj, key) {
// 根据对象获取对应的对象
let objMap = targetMap.get(obj)
if (!objMap) {
objMap = new Map()
targetMap.set(obj, objMap)
}
// 根据key获取对应的Depend对象
let depend = objMap.get(key)
if (!depend) {
depend = new Depend()
objMap.set(key, depend)
}
return depend
}
测试例子:
js
let userinfo = reactive({
username: '1111',
age: 19
})
watchFn(function() {
console.log('username', userinfo.username)
console.log('age', userinfo.age)
})
watchFn(() => {
console.log('username222', userinfo.username)
})
结果输出;
js
// username 1111
// age 19
// username222 1111
watchFn中传的回调函数模拟量对象属性的依赖,当回调函数执行时,代理对象会捕获到并将依赖函数收集起来 当改变对象属性值时,会响应触发依赖函数重新执行:
js
let userinfo = reactive({
username: '1111',
age: 19
})
watchFn(function() {
console.log('username', userinfo.username)
console.log('age', userinfo.age)
})
watchFn(() => {
console.log('username222', userinfo.username)
})
userinfo.age = 90 // 改变属性值
// 输出
// username 1111
// age 19
// username222 1111
// username 1111
// age 90
以上就是大概模拟了vue3响应式实现原理的思路。