面试说我不知道响应式reactive原理,我怒了

面试问我reactive和ref的区别,我没有深入了解,面试官说我不知道reactive,我怒了,一怒之下,我写了这个文章。

无论是 reactive 还是 ref,它们都能将普通的 JavaScript 数据转换为 Vue 的响应式数据,使得当数据发生变化时,相关的视图会自动更新。那为什么reactive只能处理引用类型?ref能黑白通吃。

看过源码可能不理解,干脆手写一个普通版本的reactive试试。

手写过程

创建reactive函数

reactive的作用是把数据变成响应式,所以在创建reactive函数时需要传递第一个参数target(其他参数后面提到)。

js 复制代码
export function reactive(target){  //target是引用类型
   return createReactiveObject(target,reactiveMap,mutableHandlers)
}

为了保持代码的优雅,将逻辑放在createReactiveObject函数里面。

创建响应式函数createReactiveObject

在这个函数里面:

  1. 首先就得判断数据是否是引用类型,还得注意null使用typeof会判断为object。
  2. 对象是否已经被代理成为响应式对象。
  3. 执行代理操作。
  4. 存储代理过的响应式对象。
    • export const reactiveMap = new WeakMap() 。WeakMap 是一种特殊的 Map 数据结构,它的键只能是对象,并且不会阻止被引用的对象被垃圾回收。这个特性使得 WeakMap 在存储对象的同时不会造成内存泄漏。源码使用也是该数据结构。

使用export是为了将reactiveMap作为第二个参数传递到createReactiveObject,更加优雅。

js 复制代码
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处理成响应式)  Proxy不支持原始类型
    const proxy =  new Proxy(target,proxyHandlers) //第二个参数的作用:当target被读取值,设置值等操作会触发的函数

    // 往proxyMap 增加 proxy,把已经代理过的对象缓存起来
    proxyMap.set(target,proxy)
    return proxy
}

代理操作

创建mutableHandlers函数

当我们把数据变成响应式,这个时候我们还得考虑一下reactive涉及到的副作用函数(effect),数据更新,副作用函数也会更新

mutableHandlers函数涉及到第三个参数。当target被v8引擎读取,操作后会触发该函数。

js 复制代码
export const mutableHandlers = {
    get,                    读取使用的target
    set                     更新target
    }
    
    const get = createGetter();
    const set = createSetter();

get

在get里面进行依赖收集,当数据没有被使用时不会被收集,get只读取使用的属性,依赖的收集后面提到。

js 复制代码
function createGetter(){
    return function get(target,key,receiver){
        console.log('读取');
        const res = Reflect.get(target,key,receiver)

        // 依赖收集
        // 在set之前收集这个属性究竟还有哪些地方用到了(副作用函数的收集)
        track(target,key)

        return res
    }
}

set

在set里面更新数据并触发副作用函数

js 复制代码
function createSetter(){
    return function set(target,key,value,receiver) {
        console.log('更新',key,value);
        const res = Reflect.set(target,key,value,receiver) // target[key] = value
        
        //记录此时是哪个key值变更,再去通知依赖该值的函数生效   更新浏览器的视图
        //触发被修改的属性身上添加副(附加 watch,computed)作用函数   触发收集(被修改的key在哪些地方用到了)发布订阅
        trigger(target,key)
        
        return res
    }
}

副作用函数

effect函数如下

js 复制代码
 effect(
         ()=>{
             console.log(state.count+ 'jin' + state.name);
         },
         {
           lazy:false,
           scheduler:()=>{ //当调度器函数执行的时候,副作用函数不再触发
           console.log('调度器')
         }
         })

副作用函数收集器

创建一个effect副作用函数收集器

js 复制代码
let activeEffect = null //得是一个副作用函数


export function effect(fn,options ={}){
    const effectFn = ()=>{
        try {
            activeEffect = effectFn  //保证activeEffect是一个函数
            return fn()
        } finally {
            activeEffect = null
        }
    }
    if (!options.lazy){ //false才会触发
        effectFn()
    }
    effectFn.scheduler = options.scheduler //调度器函数
    return effectFn
}

effect 函数用来创建一个副作用函数。该函数接收两个参数:fnoptions,其中 fn 是副作用函数本身,options 是一个可选的配置对象,用来指定一些额外的选项。

在函数内部,首先定义了一个名为 effectFn 的函数,它的作用是执行副作用函数。在 effectFn 中,首先将当前函数赋值给全局变量 activeEffect,以便在后续的响应式数据访问中记录(收集)当前正在执行的副作用函数。然后执行副作用函数 fn() 并返回其结果。最后,将 activeEffect 重置为 null,以便在下一次副作用函数执行时重新记录当前函数。

track收集依赖响应式数据函数

这里就是实现get操作里面的依赖收集(副作用函数收集),将初次读取到的数据收集,它在 targetMap 中创建或更新一个映射关系,将属性 key 与副作用函数 activeEffect 关联起来。

js 复制代码
const targetMap = new WeakMap();
export function track(target,key){

    let depsMap = targetMap.get(target)
    if(!depsMap){ //初次读取到值 收集effect
        depsMap = new Map()
        targetMap.set(target,depsMap )
    }
    //是否做过依赖收集
    let deps = depsMap.get(key); 
    if(!deps){ //还未添加过effect,说明创建的desMap为空
        deps = new Set()
    }
    if(!deps.has(activeEffect) && activeEffect){
        //存入一个effect函数
        deps.add(activeEffect)
    }
   // track函数更新 depsMap中属性key 的依赖集合,将最新的 deps设置为属性 key的值
    depsMap.set(key,deps)
}

trigger触发依赖响应式数据函数

当我们将所有副作用函数收集后,响应式数据的更新就要触发依赖响应式数据的函数。 更新数据后,在set里面就会触发该函数,通过找到更新的数据的key,将所有依赖该数据的副作用函数执行并更新。

js 复制代码
export function trigger(target,key){
    //将targetMap里面的每个副作用函数赋给depsMap
    const depsMap = targetMap.get(target)
    if(!depsMap){ //当前对象中所有的key都没有副作用函数(从来没有使用过)
        return
    }
    const deps = depsMap.get(key)
    if(!deps){ //这个属性没有依赖
        return
    }
    deps.forEach(effectFn=>{
        //判断调度器是否存在
        if(effectFn.scheduler){
            effectFn.scheduler()
        }else{
            effectFn()//将该属性上的所有的副作用函数全部触发
        
        }
        //即使有调度器函数,副作用函数也会被执行
        effectFn()  
    })
}

在响应式函数createReactiveObject里面target被操作就会触发代理操作,执行mutableHandlers函数的get和set,将每个变更后的target和它的key传给trigger,以此实现将全部副作用函数触发。

最后

假如您也和我一样,在准备春招。欢迎加我微信shunwuyu,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!

相关推荐
前端大卫6 分钟前
Vue3 + Element-Plus 自定义虚拟表格滚动实现方案【附源码】
前端
却尘22 分钟前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare23 分钟前
浅浅看一下设计模式
前端
Lee川26 分钟前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix1 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人1 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl1 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人1 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼1 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端