面试说我不知道响应式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,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!

相关推荐
aPurpleBerry7 分钟前
JS常用数组方法 reduce filter find forEach
javascript
GIS程序媛—椰子36 分钟前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
DogEgg_00142 分钟前
前端八股文(一)HTML 持续更新中。。。
前端·html
ZL不懂前端1 小时前
Content Security Policy (CSP)
前端·javascript·面试
乐闻x1 小时前
ESLint 使用教程(一):从零配置 ESLint
javascript·eslint
木舟10091 小时前
ffmpeg重复回听音频流,时长叠加问题
前端
王大锤43911 小时前
golang通用后台管理系统07(后台与若依前端对接)
开发语言·前端·golang
我血条子呢1 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
黎金安1 小时前
前端第二次作业
前端·css·css3
啦啦右一1 小时前
前端 | MYTED单篇TED词汇学习功能优化
前端·学习