浅聊一下
前两天写了一个"丐中丐"版的Vue3的响应式实现原理,现在回想起来感到十分的羞愧,reactive,是我对不住你😭,再给我一次机会,这次我一定好好来写你...
假如您也和我一样,在准备春招。欢迎加我微信shunwuyu,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!
开始
在重新手写reactive之前,如果还有不懂Vue3响应式的掘友们可以先去看我的一篇文章,看完再来阅读本文(到底该用ref还是reactive???)
首先还是请上永远18岁的坤坤
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script type="module">
import { reactive } from './reactive.js'
const state = reactive({
name:'坤坤',
age:18
})
setInterval(()=>{
state.age++
},1000)
</script>
</body>
</html>
按照reactive的用法,将state变成响应式对象,设置一个定时器,每一秒改变一次坤坤的年龄,不过我这里引用的reactive是自己写的,接下来,我们就来实现这个reactive
实现
其实实现reactive分三步走
- 用Proxy代理对象
- 在代理函数 get 中 对使用了的属性做副作用函数收集
- 在代理函数 set 中 对修改了的属性做副作用函数触发
接下来我们就来一步步完成
用Proxy代理对象
本着一个函数完成一个功能的原则,将reactive拆分
js
export function reactive(target){//将target变成响应式
return createReactiveObject(target,mutableHandlers)
}
export function createReactiveObject(targe,proxyHandlers){//创建响应式函数
//判断target是否是引用类型
if(typeof target !== 'object' || target === null){//不是对象就不给操作
return target
}
}
在下面完成校验你传进来的是不是一个对象,如果不是一个对象,那我就直接返回...为什么一定要是对象类型呢?
Proxy 可以理解成,在目标对象之前架设一层"拦截",外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来"代理"某些操作,可以译为"代理器"。
这就能解释明白了,因为Proxy只能处理对象类型的数据...
言归正传,接下来当我们传入的是一个对象以后,该进行什么操作?判断一下,这个传进来target对象是否已经是一个代理对象,如果我传进来的对象已经是一个代理对象,还要对他进行代理,那不是画蛇添足嘛...
js
export const reactiveMap = new WeakMap();//new Weakmap 对内存的回收更加友好
export function reactive(target){//将target变成响应式
return createReactiveObject(target,reactiveMap,mutableHandlers)
}
export function createReactiveObject(target,proxyMap,proxyHandlers){//创建响应式函数
//判断target是否是引用类型
if(typeof target !== 'object' || target === null){//不是对象就不给操作
return target
}
//该对象是否已经被代理过(已经是响应式对象)
const existingProxy = proxyMap.get(target);
if(existingProxy){
return existingProxy
}
}
定义一个Weakmap对象,用来保存已经代理过的对象,首先判断Weakmap对象中是否含有target,如果含有,就直接返回target的代理对象existingProxy,如果没有呢,再进行下一步操作
js
export const reactiveMap = new WeakMap();//new Weakmap 对内存的回收更加友好
export function reactive(target){//将target变成响应式
return createReactiveObject(target,reactiveMap,mutableHandlers)
}
export function createReactiveObject(target,proxyMap,proxyHandlers){//创建响应式函数
//判断target是否是引用类型
if(typeof target !== 'object' || target === null){//不是对象就不给操作
return target
}
//该对象是否已经被代理过(已经是响应式对象)
const existingProxy = proxyMap.get(target);
if(existingProxy){
return existingProxy
}
//执行代理操作(将target处理成响应式)
const proxy = new Proxy(target,proxyHandlers)
//往proxyMap 增加 proxy,把已经代理过的对象缓存起来
proxyMap.set(target,proxy)
return proxy
}
各种判断完成以后,我们就给传进来的target创建一个Proxy代理对象啦!,并且在创建完成以后,要记得把target和他的代理对象存入我们的reactiveMap中...
在创建Proxy实例对象的时候,不要忘记我们还有一个参数proxyHandlers
,我们对target进行操作时,会被代理对象拦截,而我们要进行的操作,就在proxyHandlers
里面
js
const get = createGetter()
const set = createSetter()
function createGetter(){
return function get(target, key,receiver){
return Reflect.get(target, key,receiver);
}
}
function createSetter(){
return function set(target, key, value,receiver) {
return Reflect.set(target, key, value,receiver);
}
}
export const mutableHandlers = {
get,
set,
}
这里创建了一个 get 和 set 函数,并且写进 mutableHandlers
里
在代理函数 get 中 对使用了的属性做副作用函数收集
什么是副作用函数?
副作用函数是指在执行过程中会产生额外的影响,而不仅仅是返回一个值的函数。也就是说,可能改变外部属性的函数就叫副作用函数,例如computed
和watch
就是副作用函数,不知道computed
和watch
的掘友也可以去看看我的文章(都要春招了,还不知道computed和watch有什么区别??)
我们要收集这些副作用函数,因为当我们的响应式对象进行更新的时候,我们要通知这些副作用函数调用一次,才能实现对外部数据的响应式更新...话不多说,来看看如何收集副作用函数
js
const targetMap = new WeakMap();
let activeEffect = null;//一个副作用函数
export function track(target,key){
// targetMap = {
// target:{
// key:[Effect1,Effect2]
// }
// }
let depsMap = targetMap.get(target);
if (!depsMap){//初次读取到值 收集effect
targetMap.set(target,depsMap = new Map())
}
let deps = depsMap.get(key);
if (!deps){//该属性还未添加过副作用
depsMap.set(key,deps = new Set());
}
if(!deps.has(activeEffect) && activeEffect){
//存入一个effect函数
deps.add(activeEffect);
}
depsMap.set(key,deps);
}
定义了一个activeEffect副作用函数,让他暂时为null。我们将收集来的副作用函数以如下结构储存
js
targetMap = {
target:{
key:[Effect1,Effect2]
}
}
解释一下,代码中的targetMap
就是最外层的结构,用来储存target和他的key
js
targetMap = {
target: key
}
depsMap
就是里面的那层结构,用来储存key和他拥有的副作用函数
js
target:{
key:[Effect1,Effect2]
}
deps
就是key拥有的副作用函数的数组
let depsMap = targetMap.get(target);
:从targetMap
中获取目标对象target
对应的依赖映射。if (!depsMap)
:检查是否存在depsMap
,如果不存在,则说明之前没有跟踪过这个目标对象的依赖关系。targetMap.set(target, depsMap = new Map())
:如果不存在depsMap
,则创建一个新的Map
对象,并将它与目标对象target
关联起来,作为该对象的依赖映射。let deps = depsMap.get(key);
:从依赖映射中获取目标属性key
对应的依赖集合。if (!deps)
:检查是否存在依赖集合deps
,如果不存在,则说明之前没有跟踪过这个目标属性的依赖关系。depsMap.set(key, deps = new Set());
:如果不存在依赖集合deps
,则创建一个新的Set
对象,并将它与目标属性key
关联起来,作为该属性的依赖集合。if (!deps.has(activeEffect) && activeEffect)
:检查当前活动的副作用函数activeEffect
是否已经存在于依赖集合中,如果不存在且当前有活动的副作用函数,就将其添加到依赖集合中。deps.add(activeEffect);
:将当前活动的副作用函数activeEffect
添加到依赖集合中,建立依赖关系。depsMap.set(key, deps);
:将更新后的依赖集合重新关联到目标属性key
上,确保依赖集合的最新状态被保存在依赖映射中。
我们在什么时候调用这个函数呢?get中,当我们要操作这个target的时候,第一步肯定是会触发get函数的,所以我们要在get中收集副作用函数
js
function createGetter(){
return function get(target, key,receiver){
//这个属性究竟还有哪些地方用到了(副作用函数的收集,computed,watch等等)
track(target,key)
return Reflect.get(target, key,receiver);
}
}
既然收集完了副作用函数,下一步就是在改变target的时候触发他们了
在代理函数 set 中 对修改了的属性做副作用函数触发
js
export function trigger(target,key){
const depsMap = targetMap.get(target);
if(!depsMap){//当前对象中所有的key都没有副作用函数(从来没有使用过)
return
}
const deps = depsMap.get(key);
if(!deps){
return
}
deps.forEach(effectFn => {
effectFn()
});
}
这里就很简单了,就是看这个key有没有副作用函数,有就拿出来全部调用一遍
那么我们这里还有一个问题,我们的副作用函数上面定义的不是一个null吗,这怎么搞
js
export function effect(fn,options = {}){ //watch 和 computed 的核心逻辑
const effectFn = ()=>{
try{
activeEffect = effectFn
return fn()
}finally{
activeEffect = null
}
}
if(!options.lazy){
effectFn();
}
return effectFn;
}
export function effect(fn, options = {})
:这是一个导出的函数effect
,它接受两个参数:fn
(要执行的函数)和options
(选项对象,包含一些额外的配置参数)。const effectFn = () => { ... }
:定义了一个内部函数effectFn
,它是实际执行副作用的函数体。这个函数体内部有一个try...finally
块,用来确保在执行完fn
后,将activeEffect
重新置为null
,以防止副作用函数被错误地嵌套调用。try { activeEffect = effectFn; return fn(); } finally { activeEffect = null; }
:在try
块中,将activeEffect
设置为当前的effectFn
,然后调用传入的函数fn
,并返回其执行结果。在finally
块中,将activeEffect
重新置为null
,确保不会影响其他副作用函数。if (!options.lazy) { effectFn(); }
:如果在options
中没有设置lazy
属性或者lazy
属性为假值,则立即执行effectFn
,否则将延迟执行。return effectFn;
:返回创建的副作用函数effectFn
。
副作用函数触发写完了,最后添加到set函数中
js
function createSetter(){
return function set(target, key, value,receiver) {
//需要记录下来此时是哪一个key的值变更了,再去通知其他依赖该值的函数生效,更新浏览器的视图(响应式)
//触发被修改的属性身上的副作用函数 依赖收集(被修改的key在哪些地方被用到了) 发布订阅
trigger(target,key)
return Reflect.set(target, key, value,receiver);
}
}
看看效果
来到最开始的页面上
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script type="module">
import { reactive } from './reactive.js'
import { effect } from './effect.js'
const state = reactive({
name:'坤坤',
age:18
})
console.log(state.age)
effect(
()=>console.log(`${state.name}今年${state.age}岁了`),
{lazy:false}
)
setInterval(()=>{
state.age++
},1000)
</script>
</body>
</html>
使用我们创建的副作用函数,来打印一下坤坤的年龄
结尾
原Vue3源码呢写的是Typescript版本的,为了简化一点点,就写了js版本的reactive...主要我写的上一版太丑陋了,重新加工...
假如您也和我一样,在准备春招。欢迎加我微信shunwuyu,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!