对不起reactive,这次一定好好手写你

浅聊一下

前两天写了一个"丐中丐"版的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分三步走

  1. 用Proxy代理对象
  2. 在代理函数 get 中 对使用了的属性做副作用函数收集
  3. 在代理函数 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 中 对使用了的属性做副作用函数收集

什么是副作用函数?

副作用函数是指在执行过程中会产生额外的影响,而不仅仅是返回一个值的函数。也就是说,可能改变外部属性的函数就叫副作用函数,例如computedwatch就是副作用函数,不知道computedwatch的掘友也可以去看看我的文章(都要春招了,还不知道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拥有的副作用函数的数组

  1. let depsMap = targetMap.get(target);:从 targetMap 中获取目标对象 target 对应的依赖映射。
  2. if (!depsMap):检查是否存在 depsMap,如果不存在,则说明之前没有跟踪过这个目标对象的依赖关系。
  3. targetMap.set(target, depsMap = new Map()):如果不存在 depsMap,则创建一个新的 Map 对象,并将它与目标对象 target 关联起来,作为该对象的依赖映射。
  4. let deps = depsMap.get(key);:从依赖映射中获取目标属性 key 对应的依赖集合。
  5. if (!deps):检查是否存在依赖集合 deps,如果不存在,则说明之前没有跟踪过这个目标属性的依赖关系。
  6. depsMap.set(key, deps = new Set());:如果不存在依赖集合 deps,则创建一个新的 Set 对象,并将它与目标属性 key 关联起来,作为该属性的依赖集合。
  7. if (!deps.has(activeEffect) && activeEffect):检查当前活动的副作用函数 activeEffect 是否已经存在于依赖集合中,如果不存在且当前有活动的副作用函数,就将其添加到依赖集合中。
  8. deps.add(activeEffect);:将当前活动的副作用函数 activeEffect 添加到依赖集合中,建立依赖关系。
  9. 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;
}
  1. export function effect(fn, options = {}):这是一个导出的函数 effect,它接受两个参数:fn(要执行的函数)和 options(选项对象,包含一些额外的配置参数)。
  2. const effectFn = () => { ... }:定义了一个内部函数 effectFn,它是实际执行副作用的函数体。这个函数体内部有一个 try...finally 块,用来确保在执行完 fn 后,将 activeEffect 重新置为 null,以防止副作用函数被错误地嵌套调用。
  3. try { activeEffect = effectFn; return fn(); } finally { activeEffect = null; }:在 try 块中,将 activeEffect 设置为当前的 effectFn,然后调用传入的函数 fn,并返回其执行结果。在 finally 块中,将 activeEffect 重新置为 null,确保不会影响其他副作用函数。
  4. if (!options.lazy) { effectFn(); }:如果在 options 中没有设置 lazy 属性或者 lazy 属性为假值,则立即执行 effectFn,否则将延迟执行。
  5. 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,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!

相关推荐
喵叔哟16 分钟前
重构代码之取消临时字段
java·前端·重构
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解1 小时前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
man20171 小时前
【2024最新】基于springboot+vue的闲一品交易平台lw+ppt
vue.js·spring boot·后端
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶21361 小时前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js