浅聊一下

前两天写了一个“丐中丐”版的Vue3的呼应式实现原理,现在回想起来感到十分的羞愧,reactive,是我对不住你,再给我一次机会,这次我必定好好来写你…

假如您也和我相同,在预备春招。欢迎加我微信shunwuyu,这儿有几十位专心去大厂的友友能够彼此鼓励,共享信息,模仿面试,共读源码,齐刷算法,手撕面经。来吧,友友们!

开始

在从头手写reactive之前,假如还有不理解Vue3呼应式的掘友们能够先去看我的一篇文章,看完再来阅览本文(到底该用ref仍是reactive???)

首要仍是请上永久18岁的坤坤

<!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拆分

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方针是否现已是一个署理方针,假如我传进来的方针现已是一个署理方针,还要对他进行署理,那不是画蛇添足嘛…

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,假如没有呢,再进行下一步操作

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里边

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有什么区别??)

咱们要搜集这些副作用函数,由于当咱们的呼应式方针进行更新的时分,咱们要通知这些副作用函数调用一次,才干实现对外部数据的呼应式更新…话不多说,来看看怎么搜集副作用函数

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。咱们将搜集来的副作用函数以如下结构贮存

    targetMap = {
        target:{
            key:[Effect1,Effect2]
        }
    }

解释一下,代码中的targetMap便是最外层的结构,用来贮存target和他的key

    targetMap = {
        target: key        
    }

depsMap便是里边的那层结构,用来贮存key和他具有的副作用函数

 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中搜集副作用函数

function createGetter(){
    return function get(target, key,receiver){
        //这个特点究竟还有哪些地方用到了(副作用函数的搜集,computed,watch等等)
        track(target,key)
        return Reflect.get(target, key,receiver);
    }
}

已然搜集完了副作用函数,下一步便是在改动target的时分触发他们了

在署理函数 set 中 对修改了的特点做副作用函数触发

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吗,这怎么搞

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函数中

function createSetter(){
    return function set(target, key, value,receiver) {
        //需求记录下来此刻是哪一个key的值改变了,再去通知其他依靠该值的函数收效,更新浏览器的视图(呼应式)
        //触发被修改的特点身上的副作用函数 依靠搜集(被修改的key在哪些地方被用到了) 发布订阅
        trigger(target,key)
        return Reflect.set(target, key, value,receiver);
    }
}

看看效果

来到最开始的页面上

<!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>

使用咱们创立的副作用函数,来打印一下坤坤的年龄

对不住reactive,这次必定好好手写你

结束

原Vue3源码呢写的是Typescript版别的,为了简化一点点,就写了js版别的reactive…首要我写的上一版太丑恶了,从头加工…

假如您也和我相同,在预备春招。欢迎加我微信shunwuyu,这儿有几十位专心去大厂的友友能够彼此鼓励,共享信息,模仿面试,共读源码,齐刷算法,手撕面经。来吧,友友们!