手搓vue3中ref和reactive(小白丐版)

前言

最近小管在学Vue3,于是对Vue3中的响应式很感兴趣。在了解Vue3中ref和reactive的实现原理后,今天,小管将手把手带你手搓ref和reactive。当然,由于小管是个小白,所以用js实现一个丐中丐版。

知道定义后,直接!开干!

reactive

先说reactive的基本定义:reactive 只能用于对象、数组和 Map/Set 等引用类型 ,不能直接用于基本数据类型。它会返回一个深度响应式的对象

reactive.js

我们先来创建一个文件夹reactivity,再创建一个reactive.js

  1. 抛出一个函数reactive(),并传入形参target(接受一个对象),这个函数返回一个创建一个响应式对象createReactiveObject(target)函数。

reactive.js

js 复制代码
export function reactive(target){
    return createReactiveObject(target)
}
function createReactiveObject(target){

}
  1. createReactiveObject(target)函数里面的逻辑
  • 先判断传入的target是不是一个对象,如果不是,立即返回
  • 创建一个proxy代理这个target,易知当我们创建一个proxy,应该传入目标对象(target)和处理程序对象(handler),我们将这个处理程序对象命名为proxyHandlers
  • 则追根溯源,上面的createReactiveObject()也传入一个proxyHandlers,官方命名为mutableHandlers,为代码维护,这个mutableHandlers处理程序对象将写在另外一个文件 'baseHandlers.js里,再引入到本reactive.js
  • 返回这个proxy
js 复制代码
import { mutableHandlers } from './baseHandlers.js';

export function reactive(target){
    return createReactiveObject(target,mutableHandlers)

}

function createReactiveObject(target,proxyHandlers){
    if(typeof target !== 'object'){
        return target
    }
    
    const proxy = new Proxy(target,proxyHandlers)
    
    return proxy

}
  1. 创建一个 WeakMap()对象
  • 定义一个弱引用对象reactiveMap,将代理完的对象存入reactiveMap,为方便代码维护,直接把reactiveMap传入createReactiveObject()中,
  • 将原对象和代理完的对象传入proxyMap
  • 定义一个开关变量existingProxy ,用proxyMap中的get方法查看传入的target是否在proxyMap,如果存在,立即返回。
js 复制代码
import { mutableHandlers } from './baseHandlers.js';

export const reactiveMap = new WeakMap();//是弱版本map,

export function reactive(target){
    return createReactiveObject(target,mutableHandlers,reactiveMap)

}

function createReactiveObject(target,proxyHandlers,proxyMap){
    if(typeof target !== 'object'){
        return target
    }

    const existingProxy = proxyMap.get(target)

    if(existingProxy){
        return existingProxy
    }
    
    
    const proxy = new Proxy(target,proxyHandlers)
    proxyMap.set(target,proxy)
    return proxy

}

baseHandlers.js

下面我们来写mutableHandlers这个对象里面的逻辑,这个对象就是Proxy中的那个处理程序对象,自带13种方法,下面我列出最常见的五种:

  1. get(target, property, receiver)
  2. set(target, property, value, receiver)
  3. has(target, property)
  4. deleteProperty(target, property)
  5. ownKeys(target) ......

于是我们创建函数createGetter()和函数 createSetter(),getset为这两个函数返回的值,getset作为mutableHandlers中的方法

baseHandlers.js:

js 复制代码
const get = createGetter()
const set = createSetter()

function createGetter(){

}

function createSetter(){

}

export const mutableHandlers ={ 
    get,
    set
}
createGetter()
  • createGetter()将会返回一个函数体 ,返回的这个函数体 会接收三个Proxy赋予它的参数target, key, receiver
  • 返回的函数体 会在被读取值的时候触发,所以返回的将是target[key],这里我们用Reflect.get(target, key,receiver),将读取出的值存在res中,返回这个res
js 复制代码
function createGetter(){
    return function(target, key, receiver){
        const res =Reflect.get(target, key,receiver)
        
        return res
    }

}

接着我们思考一个问题,如果传入的是一个对象呢?Proxy只能代理一层,所以我们就得继续让下一层也变为响应式。如果下下一层又是对象呢?我们就会运用递归。所以我们用一个函数来判断一个值是不是对象,为方便维护,写一个工具函数文件夹shared,并创建一个文件index.js,抛出一个函数判断一个值是否是对象:

index.js:

js 复制代码
export function isObject(obj) {
    return typeof obj === 'object' && obj!== null;
}

将这个函数引入原来的baseHandlers.js

  • 运用isObject判断res是否是一个对象,如果是,递归调用reactive
js 复制代码
function createGetter(){
    return function(target, key, receiver){
        const res =Reflect.get(target, key,receiver)

        if(isObject(res)){
            return reactive(res)
        }
        return res
    }
}
createSetter()

同理:

  • createSetter()将会返回一个函数体 ,返回的这个函数体 会接收四个Proxy赋予它的参数target, key, value, receiver
  • 返回的函数体 会在被读取值的时候触发,所以返回的将是target[key],这里我们用Reflect.set(target, key, value, receiver),将读取出的值存在res中,返回这个res
js 复制代码
function createSetter(){
    return function(target, key,value, receiver){
        const res = Reflect.set(target, key, value, receiver)
        return res
    }

}
tracktrigger
  • 当我们读取到响应式对象的属性时,我们就应该收集与之相关的如组件渲染、计算属性等副作用函数.这样,等我们修改这响应式对象的属性时,与之相关联的副作用函数就可以从被我们收集的地方取出来再次修改。副作用函数(如组件渲染、计算属性)与响应式数据之间的关联关系 称之为依赖
  • 收集相关副作用函数的行为就叫收集依赖
  • 更新相关副作用函数的行为就叫触发依赖(依赖更新)
  1. 将收集依赖的函数称为 track,传入三个参数target,'get', key(第二个参数为操作类型)
  2. 将更新依赖的函数称为 trigger,传入三个参数target,'set', key

为方便维护,将这两个函数,写在effect.js文件中,再引入baseHandlers.js

baseHandlers.js我们就写完了,完整代码如下:

js 复制代码
import { isObject } from "../shared/index.js"
import { reactive } from "./reactive.js"
import { track, trigger} from "./effect.js"

const get = createGetter()
const set = createSetter()
function createGetter(){
    return function(target, key, receiver){
        const res =Reflect.get(target, key,receiver)
        // 在值初次被读取时就要进行依赖收集
        track(target,'get', key)

        if(isObject(res)){
            return reactive(res)
        }
        return res
    }
}

function createSetter(){
    return function(target, key,value, receiver){
        const res = Reflect.set(target, key, value, receiver)
        //触发依赖(更新)
        trigger(target,'set',key)
        return res
    }
}
export const mutableHandlers ={ 
    get,
    set
}

effect.js

下面我们来处理effect.js,里面要抛出两个函数 track()trigger()分别传入三个参数(target,type,key)

  • 定义并抛出两个函数track()trigger()
  • 声明一个常量 targetMap,并将其初始化为一个 WeakMap 实例。用于存储响应式对象及其属性对应的依赖集合 ,注意,targetMapkey就为传入的响应式对象target(可以用来判断这个对象是否已经存到targetMap中)。再拿target这个对象中的属性的key再做key,这个key所对应的值将是一个数组,里面存放着和这个key相关的副作用函数。

伪代码如下:

js 复制代码
 targetMap = {
        target:{
            key:[effect1,effect2,...]
        }
        
    }

代码如下:

js 复制代码
const targetMap = new WeakMap();

//依赖收集
export function track(target,type,key){
}

//更新依赖
export function trigger(target,type,key){

}
副作用函数

为方便理解,我们先定义一个副作用函数effect

  • 声明一个全局变量 activeEffect,并初始化为 nullactiveEffect 用于存储当前正在执行的副作用函数、
  • 定义了个 effect 函数,其主要作用是将传入的副作用函数 fn 进行包装,在执行 fn 之前将其赋值给 activeEffect 变量,以便在 fn 执行过程中进行依赖收集
js 复制代码
let activeEffect = null // 副作用函数

export function effect(fn,options={}){
    const effectFn = ()=>{
        try{
            activeEffect=fn
            return fn()
        }catch(error){
            console.log(error);           
        }
    }
    effectFn()
}
track()

现在来写track(),也就是收集依赖里面的逻辑,我们传入(target,type,key),分别是目标对象,类型,还有传入的目标对象中的属性。

  • 先声明一个depsMap,判断targetMap中是否已经有了目标对象,相当于一个开关变量

  • 检查 depsMap 是否为 undefined,如果是,则说明 target 对象还没有存入depsMap。 此时,创建一个新的 Map 对象作为 depsMap,并将其与 target 对象关联起来,存储到 targetMap 中。

  • depsMap 中获取与 key 属性关联的依赖集合(deps)。

  • 检查 deps 是否为 undefined,如果是,则说明 key 属性还没有对应的依赖集合。此时,创建一个新的 Set 对象(用Set可以保证值为唯一)作为 deps,用于存储与该属性相关的副作用函数。

  • 检查 deps 集合中是否已经包含当前的活跃副作用函数(activeEffect),并且 activeEffect 不为 nullundefined。 如果 deps 中不包含 activeEffect,则将 activeEffect 添加到 deps 集合中。

  • 将更新后的 deps 集合存储到 depsMap 中,与 key 属性关联起来。

js 复制代码
//依赖收集
export function track(target,type,key){
    let depsMap= targetMap.get(target);
    if(!depsMap){
        targetMap.set(target,depsMap= new Map());
    }
    let deps= depsMap.get(key);
    if(!deps){
        deps = new Set()
    }
    if(!deps.has(activeEffect) && activeEffect){
        deps.add(activeEffect)
    }
    depsMap.set(key,deps)   
}
trigger()

收集完依赖我们就要考虑在每一次响应式对象改变的时候,它的依赖也要进行变更,同理

  • 先判断targetMap中是否已经有目标对象,如果没有,直接返回
  • 如果有,就判断传入的key是否在targetMap中,如果没有,又直接返回
  • 如果还是有,此时就该调用那个存放着与之相关的所有副作用函数,这里用forEach实现
js 复制代码
//触发依赖
export function trigger(target,type,key){
    const depsMap= targetMap.get(target);
    if(!depsMap) return
    const deps = depsMap.get(key) 
    
    if(!deps) return
    deps.forEach(effect=>{
        effect()
    })
}

effect.js文件中的完整代码就为

js 复制代码
const targetMap = new WeakMap();
let activeEffect = null // 副作用函数

export function effect(fn,options={}){
    const effectFn = ()=>{
        try{
            activeEffect=fn
            return fn()
        }catch(error){
            console.log(error);           
        }
    }
    effectFn()
}

//依赖收集
export function track(target,type,key){
    let depsMap= targetMap.get(target);
    if(!depsMap){
        targetMap.set(target,depsMap= new Map());
    }
    let deps= depsMap.get(key);
    if(!deps){
        deps = new Set()
    }
    if(!deps.has(activeEffect) && activeEffect){
        deps.add(activeEffect)
    }
    depsMap.set(key,deps)   
}

//触发依赖
export function trigger(target,type,key){
    const depsMap= targetMap.get(target); 
    if(!depsMap) return
    const deps = depsMap.get(key) 
    
    if(!deps) return
    deps.forEach(effect=>{
        effect()
    })
}

以上就为小管手搓的reactive完整代码!

ref

讲完reactive我们就来谈谈ref,你可能会好奇,为什么先讲reavtive而不是ref。别急,我们这就来谈谈为什么。

首先,我们知道reactive 只能用于对象、数组和 Map/Set 等引用类型 ,不能直接用于基本数据类型。所以ref的出现就填补了基本数据类型不能变成响应式这一缺点。

我们来看它的 定义ref 主要用于创建一个响应式的引用,它可以包装任何类型的值,包括基本数据类型(如 numberstringboolean 等)和对象、数组等引用类型。

所以当需要响应的值为对象或者数组等引用类型,在ref的源码里,直接用reactive就可以,这就是我们为什么要先讲reactive的实现原理的原因。

  • @/shared 模块导入isObject,用于判断一个值是否为对象。
  • ./reactive.js 模块导入 reactive,用于创建响应式对象。
  • 创建 ref 函数,它接收一个值 val,并返回一个 RefImp 类的实例。这个实例就是一个响应式引用。
  • 创建RefImp类,里面有构造函数constructor,接收一个val,为传入的那个应该被变为响应式的值
  • 创建一个 convert 函数, 接收一个值 val 作为参数。 使用 isObject 函数判断 val 是否为对象,如果是对象,则使用 reactive 函数将其转换为响应式对象并返回;否则直接返回 val
js 复制代码
import { isObject } from "@/shared";
import { reactive } from "./reactive.js";

export function ref(val){
    return new RefImp(val);
}

class RefImp {
    constructor(val){
        
    }

}

function convert(val){
    return isObject(val) ? reactive(val):val
}

RefImp

下面来写RefImp类中的逻辑

  • 首先用convert 函数,判断构造函数constructor中传入的val是否是一个对象,如果是,直接用reactive让其变为响应式的对象并将结果赋值给 this._val。若不是,直接将val赋值给 this._val
  • 用类中一个特殊的关键字get 定义一个方法value,返回这个this._val。当访问 ref 实例的 value 属性时会自动调用。
js 复制代码
export function ref(val){
    return new RefImp(val);
}

class RefImp {
    constructor(val){
        if(convert(val)){
            this._val = reactive(val)
        }
        this._val = val;
    }

    get value(){  
    }  

}

function convert(val){
    return isObject(val) ? reactive(val):val
}

get 关键字定义一个特殊的方法,这个方法在访问对象的某个属性时会自动调用。也就是说不用打那个(),将这个方法直接当成属性调用了,比如如果我们声明一个nref返回的一个RefImp实例对象,如果没有get关键字,我们就需要用()调用value

js 复制代码
class RefImp {
    constructor(val){
        if(convert(val)){
            this._val = reactive(val)
        }
        this._val = val;
    }

     value(){  
    }  
}
const n = ref(1)
console.log(n.vaule())//如果没有get关键字

class RefImp {
    constructor(val){
        if(convert(val)){
            this._val = reactive(val)
        }
        this._val = val;
    }
    get value(){  
    }  
}
consle.log(n.value)
  • reactive原理一样,当我们首次读取到这个ref 实例的 value时,我们也开始依赖收集,于是我们直接调用前面写好的track()
js 复制代码
 get value(){ 
        track(this,'get','value')
        return this._val;
    }  
  • 接着用一个set关键字,它与get相似,用 set 关键字定义一个特殊的方法,在给对象的某个属性赋值时,这个方法会自动被调用,我们也写成value
  • val改变的时候,执行set定义的value方法,首先传入新的值将原本的this._val改变,然后,在value方法里调用已经写好的trigger()方法,完成更新依赖
js 复制代码
 set value(newVal){
        if(this._val!==newVal){
            this._val=newVal
            trigger(this,'set','value')
        }

大功告成,ref就这么些东西,完整代码如下:

js 复制代码
import { isObject } from "@/shared";
import { reactive } from "./reactive.js";
import {track,trigger} from "./effect.js"


export function ref(val){
    return new RefImp(val);
}

class RefImp {
    constructor(val){
        if(convert(val)){
            this._val = reactive(val)
        }
        this._val = val;
    }

    get value(){ 
        track(this,'get','value')
        return this._val;
    }  
    
    set value(newVal){
        if(this._val!==newVal){ref和reactive
            this._val=newVal
            trigger(this,'set','value')
        }  
   }
}

function convert(val){
    return isObject(val) ? reactive(val):val
}
const n = ref(1)

写在最后

恭喜你看到了这里,其实refreactive也没有我们想象的那么复杂,无非是一层又一层的方法。去繁化简,其实很多时候只是需要我们耐心一点点,其实人生也是同样的道理吧,再耐心一点点,可能事情也没我们想的那么难。(我又在说点奇怪的话了)

ok,今天就聊到这,我是编程小白小管,我们下次聊。

ps:完整代码可以点击这里

相关推荐
焦糖酒drunksweet2 分钟前
认识Event Loop【1】
前端
Georgewu2 分钟前
【HarmonyOS Next】鸿蒙加固方案调研和分析
前端·面试·harmonyos
夜寒花碎5 分钟前
前端事件循环
前端·javascript·面试
用户4192559999625 分钟前
mk-计算机视觉—YOLO+Transfomer多场景目标检测实战
前端
大霸王龙8 分钟前
去除HTML有序列表(ol)编号的多种解决方案
前端·html
没头发的卓卓8 分钟前
学会SSL/TLS,在面试官面前化身歪嘴龙王!
前端
阿常1111 分钟前
uni-app基础拓展
前端·javascript·uni-app
壹贰叁肆伍上山打老虎11 分钟前
突发奇想,写了一个有意思的函数,一个有趣的 JavaScript 函数:将数组分割成多维块
前端·javascript
bbb16912 分钟前
react源码分析 setStatae究竟是同步任务还是异步任务
前端·javascript·react.js
言兴13 分钟前
你知道吗?JavaScript中的事件循环机制
前端·javascript