通过实现最简reactive,学习Vue3响应式核心

1,744 阅读18分钟

Reactivity 响应式模块

注:(此篇帖子所描述的是响应式的最简核心逻辑部分,与V3代码会有出入,这是因为Vue的代码会需要大量的处理边缘条件!)


好了!开始!!

首先我们需要明白,Vue3与Vue2中响应式核心的不同,最最明显的区别就是:

  1. 在v2中响应式处理采用Object.defineProperty

  2. 在v3中响应式的处理采用了Proxy

    1. 在v2中由于使用defineProperty的形式,在新增属性的时候并不能对状态进行拦截。
    2. 在v3中使用了Proxy 这是浏览器天然支持的API,Vue团队基于此对Vue进行重写,解决了上面的问题,同时不论是性能还是代码的简洁程度上都要明显提升很多

1. 如何使用Vue3的响应式模块?

如果你还没有用过Vue3的响应式部分,别担心几行代码就能描述清楚

//// ref,reactive,computed
// ref -> interface Ref<T> { value: T }
// 传入一个数值可以是基础类型或者复杂类型,使用ref.value获取到我们传入数值
const name = ref('SG')
console.log(name.value) //// SG
name.value = 'PG'

// reactive -> reactive<T extends object>(target: T)
// 传入一个复杂类型,如果是基础类型会被原路返回
const state = reactive({
    name: 'SG',
    age: 18
})
console.log(state.age)
state.age = 21

// computed 
// 有两种传入方式
// 第一种方式传入一个getter函数,但是这个computed不能被修改
// 第二种方式传入一个options对象,需要自己书写getter和setter函数,可以被修改
// 第一种
const name = ref('SG')
const wrapName = computed(() => name.value + '---wrap')
console.log(wrapName.value)
     
// 第二种
const wrapName = computed({
    get: () => name.value + '---wrap',
    set: (v) => v...
})
 
//// 当然还会有一些其他的API,如:
//// shallowReactive、readonly、shallowReadonly
//// 这些我们到了具体的模块再叉开来说

That' all 我们就先写这么多,那么接下来就是如何实现一个最简化的响应式模块了

2. 响应式模块 -- Reactive

再来看下如何使用该模块:

const state = reactive({ name: 'SG' })

Reactive 都做了什么事情?

  1. 将传入的对象变成响应式的Proxy对象
  2. 对修改和获取进行拦截

下面这两条在后面的依赖部分再补齐,我们先只关注前面两部分的内容即可

3. 在获取的时候进行依赖拦截存储依赖

4. 在修改的时候重新执行依赖

构造Proxy

const isObject = v => v !== null && typeof v ='object'
const isArray = Array.isArray
//// 检测是否是一个数字类型
const isIntegerKey = v => parseInt(v) + '' === v 

function reactive(target: object) {
    //// 因为reactive函数不能接收基础类型
    //// 所以基础类型直接返回就好了
    if(!isObject(target)) {
        return target
    }
    
    //// 构造proxy
    //// 定义getter和setter
    const proxy = new Proxy(target, {
        get: (target, key, receiver) => {
            //// 采用 Reflect 做属性的映射
            const res = Reflect.get(target, key, receiver)
                        
            //// 如果获取的属性值还是一个对象的话那就也包裹成响应式的对象
            //// 这是默认的深层代理,不过在后面这部分的代码会有变化,不要担心继续看就好了
            if (isObject(res)) {
                return reactive(res)
            }
            
            return res
        },
        set: (target, key, value, receiver) => {
            //// 拿到旧值: 如果value一致那么就没有更新的必要
            const oldValue = target[key]
            //// 如果是一个数组那么检测更改的索引是否大于当前的数组的大小
            //// 如果是个对象检测是否存在当前的属性
            //// hadKey 变量用来标明这个setter是更新还是添加
            const hadKey = 
                isArray(target) && isIntegerKey(key) 
                    ? Number(key) < target.length 
                    : hasOwn(target, key)
            
            //// 修改属性值
            const res = Reflect.set(target, key, value, receiver)
            
            //// 无论是添加属性还是修改属性,在这里我们触发依赖的重新执行
            if(!hadKey) {
                //// ADD Property
                
            } else {
                //// UPDATE Property
            }
            
            return res
        }
    })             
}

此时抛开我们上面写的依赖不谈,是可以根据我们所传递的内容来创建新的响应式对象的,但是先来解决一点小问题:

  • 如果传递的内容还是一个proxy怎么办?
//// 对于这个问题仅仅需要这里进行改动即可
//// 对我们构造出来的Proxy进行缓存,到时候判断存不存在即可
const targetMap = new Map()
function reactive(target: object) {
    if(!isObject(target)) {
        return target
    }
    
    //// 如果MAP中存在这个则直接返回即可
    const existProxy = targetMap.get(target)
    if(existProxy) {
        return existProxy
    }
    const proxy = new Proxy(target, {
        ...
    })
    
    //// SET VALUE TO MAP
    targetMap.set(target, proxy)
    
    return proxy
}

That’s All,But上面提到的依赖是什么,怎么存储,如何在值变化的时候进行更新?

那么这些就需要在其他的模块中先进行处理后再联动reactive模块

3. 依赖模块 -- effect

effect模块被称为副作用,它是一个函数,因此也可以称为副作用函数。如果使用过React的useEffect这个hook,应该就明白啥叫副作用函数了,说白了就当我所依靠的状态发生变化的时候就要重新执行和这个状态有关的这部分函数。我们所熟知的Vue组件也是会被编译模块编译成一个副作用的,因此才能在状态变更的时候更新视图,当然还会有很多额外的优化处理比如patchFlags之类的,不过我们现在不关心这些!

与V3明显的区别就是React需要手动传递状态数组,而Vue中的副作用函数会自动收集使用到的状态

  1. 依赖是啥时候收集的?

    依赖其实就是副作用函数,一个包含着响应式状态的函数,一个需要在响应式状态变化的时候需要重新执行的函数。

    那么啥时候能检测到这个函数里使用到了我们所需要的状态呢?还记得:我们把reactive模块传递进来的每个对象都转化成了proxy并重写了gettersetter

    OK!!那很简单了,既然我们访问响应式的状态就会触发对应的getter,那么触发getter的时候去收集依赖就是最合适的。

  2. 依赖收集好了那啥时候重新执行呢?

    既然有getter那就有setter,在给我们代理的对象的属性赋值的时候就会触发setter,因此这个时候会去触发依赖的重新执行


好了,那知道了这两点就容易将reactiveeffect部分联系起来了,下面来写一下这部分。

1. 构建出一个effect依赖函数部分

function effect(fn, options) {
    //// 构造出一个effect函数
    const effect = createReactiveEffect()
    
    //// 如果不是lazy则默认执行,这部分的处理会在后面定义computed 函数API有用
    //// 现在只需要记住是默认执行就好
    if(!options.lazy) {
        effect()
    }
    
    return effect
}
  1. effect函数的核心:createReactiveEffect
//// 它需要做什么呢?
//// 他需要执行我们传入的fn
//// 此外还需要将当前执行的副作用函数保存起来
function createReactiveEffect(fn, options) {
    const effect = function reactiveEffect() {
        try{
            //....
            fn()
        } finally{
            // .....
        }
    }
    
    return effect
}

2. 将依赖收集起来

Track函数

首先明确我们上面说的:依赖就是使用了当前状态的副作用函数,我们需要将被使用的状态和副作用函数关联起来,当我们访问状态的时候就会触发getter,因此收集依赖就需要在getter中被调用,收集依赖的函数Vue称它为 track 函数, 先重写一下getter

function reactive(target: object) {
    // ....
    const proxy = new Proxy(target, {
        get: (target, key, receiver) => {
            //// 采用 Reflect 做属性的映射
            const res = Reflect.get(target, key, receiver)
            //// 将当前的对象,访问的方式,访问的key都收集起来
            track(target, 'get', key)
            
            //// 如果还是一个对象的话那就也包裹成响应式的对象
            if (isObject(res)) {
                return reactive(res)
             }
            return res
        }
        // ....
    })
}

好了,那么依赖的收集在getter中就写完了,仅仅加了一行代码而已,那么下面看看track函数是怎么做的吧

这部分需要关联起刚刚我们创建effect函数时候创建的createReactiveEffect函数,没关系我们会以一点点的抛出问题的方式来看如何实现

先写下track函数

不过,在写出track函数之前先来明确这一点:存储依赖的数据结构是什么样子的?

targetMap是存储各个状态对象的一个WeakMap,它的值是一个Map,这个map的值又是一个Set

targetMap的key是存储的源对象,value是存储的一个map,这个map的键是对应的targetMap中对象的键值,这个map的value是一个set,存储的就是这个key相关的副作用函数

听起来就有点绕,不过下面这么解释一下就简单了

const state = reactive({
    name: 'SG'
})
effect(() => {
    console.log(state.name)
})

effect(() => {
    console.log(state.name)
})
  • 我们检测到effect函数使用到了state.name值,这一步会在getter中触发
  • 然后由track函数传递参数:state: {}, 'get', 'name'
  • 存储这些数据以便后续查找,因此targetMap,的key值就是这个state
  • 那么知道了是什么对象了还得知道访问了什么属性吧,而且一个对象不见得只有一个属性。因此我们需要一个数据结构来存储这个键值的集合,因此采用了Map,这个Map的键就是我们访问的key也就是这个name
  • 这个Map的值呢是一个Set用来存储这个副作用的集合,如我们上面在两个effect函数中都访问到了state.name,所以这个Set会用来存储这两个副作用函数

这就解释通了为什么是这样的数据结构


下面来写一下track函数了

const targetMap = new WeakMap()
funciton track(target, type, key) {
    //// 查找state是否在依赖的缓存中
    let depsMap = targetMap.get(target)
    if(!depsMap) {
        //// 如果不存在则set进去
        targetMap.set(target, (depsMap = new Map()))
    }
    
    //// 查找是否key被缓存过
    let deps = depsMap.get(key)
    if(!dep) {
        depsMap.set(key, (deps = new Set()))
    }
    
    //// 如果依赖集合中没有该effect,就将它添加到集合中
    /*# WARN*/
    if(!deps.has( 依赖 )) {
        deps.add( 依赖 )
    }
}

所以问题来了:这个依赖怎么拿到?

我们在effect函数中访问这个状态的时候是不是处于这个函数中呢? 对滴!所以我们在调用这个副作用函数的时候将当前的effect函数给保存为一个变量,然后在track函数中不就可以访问了嘛,而且这也是与状态相对应的!

需要重写一下createReactiveEffect函数

//// 将当前执行的副作用函数保存起来
let activeEffect
function createReactiveEffect(fn, options) {
    const effect = function reactiveEffect() {
        try{
            activeEffect = effect
            fn()
        } 
    }
    
    return effect
}

好了,这下就能在track函数中访问到当前执行的effect函数了

let activeEffect
function createReactiveEffect() {.....}

const targetMap = new WeakMap()
funciton track(target, type, key) {
    //// 如果是undefined 那就表示当前没有在执行的effect自然也不会收集
    if(activeEffect === undefined) return 
    ........
    //// 如果依赖集合中没有该effect,就将它添加到集合中
    /*# WARN*/
    if(!deps.has( activeEffect )) {
        deps.add( activeEffect)
    }
}

But!But!别高兴的太早!如果effect函数嵌套着写那就栓Q了

effect(() => {
    console.log(state.age)
    effect(() => {
        console.log(state.name)
    })
    console.log(state.major)
})

//// 起初的activeEffect指向外层的effect
//// 然后执行内层的effect后当前的activeEffect指向内层effect
//// 内层effect执行完毕要去执行外层的console.log(state.major) 
////     问题来了!当前的activeEffect还是指向内层的effect啊,这就不对了

这怎么办?我们需要能让activeEffect自己退回去找到上层的effect

使用栈结构对activeEffect进行退回的操作

let activeEffect
const effectStack = []
function createReactiveEffect(fn, options) {
    const effect = function reactiveEffect() {
         if(!effectStack.includes(effect)) {
             try{
                effectStack.push(effect)
                activeEffect = effect
                fn()
            } finally {
                //// 执行完毕让顶层的effect出栈
                effectStack.pop()
                //// 将当前的栈内的顶层effect赋给activeEffect
                activeEffect = effectStack[effectStack.length - 1]
            }
        }
    }
            
    return effect
}

这样就能把依赖收集起来,等到合适的时候再触发即可了!track函数到此为止!

3. 触发依赖

至此为止,我们已经收集好了依赖部分,在effect函数中我们只需访问你想要访问的响应式状态即可,系统会自动为你收集起依赖

下面我们就需要在状态改变的时候触发依赖的重执行

Trigger函数

在修改状态的值后我们需要让依赖重新执行,因为我们更新状态的时候就会触发setter,那么我们就在触发setter的时候也顺手把依赖执行了,这个触发依赖重新执行的函数就叫做trigger函数

先重写一下setter

function reactive(target: object) {
    .......
    const proxy = new Proxy(target, {
        set: (target, key, value, receiver) => {
            const oldValue = target[key]
            const hadKey = 
                isArray(target) && isIntegerKey(key) 
                    ? Number(key) < target.length 
                    : hasOwn(target, key)
                                
            //// 修改属性值
            const res = Reflect.set(target, key, value, receiver)
                                
            if(!hadKey) {
                //// ADD Property
                trigger(target, 'add', key, value)
            } else {
                //// UPDATE Property
                trigger(target, 'set', key, value, oldValue)
            }
                                
            return res
        }
    })
                        
}

Trigger函数部分

const targetMap = new WeakMap()
function tigger(target, type, key, value, oldValue) {

    //// 获取对象的键Map
    const depsMap = targetMap.get(target)
    if(!depsMap) return;
                        
    //// 存储待执行的effect                    
    const effects = new Set() 
    
    //// 添加函数到待执行effects的函数
    const add = (effectsAdd) => {
        if(effectsAdd) {
            effectsAdd.forEach(effect => effects.add(effect))
        }
    }
                        
    //// 如果是个数组且修改的是length
    if (key === 'length' && isArray(target)) {
        depsMap.forEach((dep, key) => {
            if (key === 'length' || key >= value) {
                //// 存储进 effects 待执行集合
                add(dep)
            }
        })
    } else {
        if(key !== undefined) {
            //// 将这个key相关的依赖集合放进待执行的effects
            add(depsMap.get(key))
        }
                             
        //// 处理Trigger Type
        switch(type) {
            case 'add': {
                /*
                * 为什么要处理这个?
                *     如果是直接修改数组的长度则会被上面的if所处理
                *     但是如果是修改超过数组长度的索引的元素,如数组arr长度10,执行arr[100] = 1
                *     那么就需要这一部分处理了,但是触发逻辑也是拿到有关length的依赖然后执行即可
                */
                if(isArray(target) && isIntegerKey(key)) {
                    add(depsMap.get('length'))
                }
                break
            }
        }
    }
    //// executor these effects
    effects.forEach(effect => {
        //// 调用执行
        effect()
    })
}

OK!现在一个trigger函数就已经写完了,其实无非就是通过修改的对象找到targetMap依赖缓存中的源对象的value也就是有关键的Map,再利用修改的key找到有关key的依赖集合,然后执行它!

4. 整体流程思考

1. 流程思考

你可能觉得上面一步步引导式的描述有些分散?没关系那我们再整体梳理一下响应式的过程

  1. 首先利用reactive函数对我们传入的内容进行判断

    1. 如果是一个基本类型那么要原样返回,不能对这个做出处理
    2. 如果是一个已经被代理过的对象,那么就返回这个即可,实际上我们会对每个代理过的对象进行缓存,这就可以直接进行判断了
  2. 通过reactive函数我们会得到一个Proxy的响应式对象,后续会围绕着这个Proxygettersetter进行获取和修改的拦截,在本篇帖子中并没有对集合类型做出getter或者setter的操作,可以翻看源码:vuejs/corebaseHandlescollectionHandles部分

  3. 拦截的是副作用函数,也就是effect函数,这也就是我们俗称的依赖。在getter中将依赖与相关的信息通过调用track函数进行缓存,在setter中调用trigger函数将有关的依赖全部执行,这也是v3中的响应式的核心。

  4. Effect副作用函数

    1. effect函数可以传递两个值,第一个是fn也就是需要执行的函数,第二个就是options配置项

      1. options配置项的lazy属性不为true的前提下,fn是默认执行的,至于为什么要有lazy为什么不能全部默认执行这一点在computed函数中是有用的
    2. 在执行effect函数的时候需要将当前的effect函数进行一个保存,这个保存的变量我们称之为activeEffect,这是因为,我们在effect函数里面访问响应式对象的时候会触发getter,然后由getter去触发track函数,track函数去帮我们绑定依赖与对象间的关系

  5. getter缓存副作用

    1. 利用track函数绑定依赖和相关的对象及key

    2. 依赖缓存的数据结构是一个这样的结构

      targetMap: WeakMap<object, Map<string, Set > >

      1. targetMapkey用来存储源对象

      2. targetMapvalue存储的是一个Map

      3. 这个Mapkey是用来存储你所访问的源对象的那个key

        比如源对象是state,你访问的是state的name属性,所以targetMap的key就存state,value所指向的map的其中一个key就是name

      4. 这个Map的value所指向的是一个Set

      5. 这个Set就是依赖于这个源对象的某个属性的所有副作用函数集合

      为什么是一个WeakMap呢?这样的好处是当源对象被销毁了那么这个依赖项也就没有存在的必要性了,由于WeakMap是对目标对象的弱引用,因此源对象销毁,这个依赖项也会被销毁

    3. 利用track函数绑定副作用函数与源对象及key之间的关系

      前面对于这个数据结构的描述就能大概理解这部分是如何做的,就不多赘述了

    4. 所谓的Vue更新是细粒度的更新,这个粒度之细是精确到了每个状态的每个依赖的,这一点就与React不一样,希望有时间能写一篇有关于React更新的帖子

  6. setter执行副作用

    1. 利用trigger函数触发依赖的更新
    2. setter调用trigger 函数传递target,triggerType,key,value,oldValue
    3. 除此之外还需要传递是什么类型:triggerType,判断并传入是增加属性还是修改属性

响应式核心的内容实际上就是利用getter收集依赖利用setter触发依赖,这是一个典型的观察者模式,理解这个我相信你对它的理解会更深一些。

2. 完整代码

我们先对上面的流程做一个完整的代码诠释

  1. reactive部分
const isObject = v => v !== null && typeof v === 'object' 
const isArray = Array.isArray
const isIntergerKey = v => parseInt(v) + '' === v
const hasOwn = (target, v) => Object.prototype.hasOwnProperty.call(target, v)

const reactiveMap = new WeakMap()
function reactive(target) {
    if(!isObject(target)) {
        return target
    }
    
    const existProxy = reactiveMap.get(target)
    if(existProxy) {
        return existProxy
    }
    
    const proxy = new Proxy(target, {
        get: (target, key, receiver) => {
            const res = Reflect.get(target, key, receiver)
            track(target, 'get', key)
            //// 如果还是一个对象的话那就也包裹成响应式的对象
            if (isObject(res)) {
                return reactive(res)
            }
            return res
        },
        set: (target, key, value, receiver) => {
            //// TRIGGER DEP EXECUTOR AND REFERENCE OPERATOR
            const oldValue = target[key]
            const hadKey =
                isArray(target) && isIntegerKey(key) 
                    ? Number(key) < target.length 
                    : hasOwn(target, key)

            const res = Reflect.set(target, key, value, receiver)

            if (!hadKey) {
                //// ADD PROPERTY
                trigger(target, 'add', key, value)
            } else if (hasChange(oldValue, value)) {
                //// CHANGE VALUE
                trigger(target, 'set', key, value, oldValue)
            }

            return res
        }
    })
                    
    reactiveMap.set(target, proxy)
    return proxy
}
  1. effect部分
function effect(fn, options) {
    const effect = createReactiveEffect(fn, options)
    
    if(!options.lazy) {
        effect()
    }
    
    return effect
}

let activeEffect
const effectStack = []
function createReactiveEffect(fn, options) {
    const effect = function reactiveEffect() {
        if (!effectStack.includes(effect)) {
            try {
                activeEffect = effect
                effectStack.push(effect)
                fn()
            } finally {
                effectStack.pop()
                activeEffect = effectStack[effectStack.length - 1]
            }
        }
    }
                    
    return effect
}

const targetMap = new WeakMap()
function track(target, type, key) {
    if(activeEffect === undefined) return 
    
    let depsMap = targetMap.get(target) 
    if(!depsMap) {
        targetMap.set(target, (depsMap = new Map()))
    }
    let deps = depsMap.get(key)
    if(!deps) {
        depsMap.set(key, (deps = new Set()))
    }
    
    if(!deps.has(activeEffect)) {
        deps.add(activeEffect)
    }
}


function trigger(target, type, key, value, oldValue) {
    const depsMap = targetMap.get(target)
    if(!depMap) return 
    
    const effects = new Set()
    const add = (effectAdd) => {
        effectAdd.forEach(effect => effects.add(effect))
    }
                    
    if(key === 'length' && isArray(target)) {
        depsMap.forEach((dep, key) => {
            if(key === 'length' && key >= value) {
                add(dep)
            }
        })
    } else {
        if(key !== undefined) {
            add(depsMap.get(key))
        }
        
        switch (type) {
            case 'add': {
                if (isArray(target) && isIntegerKey(key)) {
                    add(depsMap.get('length'))
                }
                break
            }
        }
    }
                    
    effects.forEach((effect) => {
        if (effect.options.scheduler) {
            effect.options.scheduler(effect)
        } else {
            effect()
        }
    })
}

3. 其他的响应式API

shallowReactive、readonly、shallowReadonly

  1. shallowReactive意译就是浅层的响应式,如果一个对象是深层嵌套的那么只代理最外层的对象,里面的内层对象仍然还是普通对象
  2. readonly意译就是只读的,对象仍然还是响应式的,深层嵌套的对象也依然是可以进行代理的。但是状态不可修改,对于这样的状态我们不需要收集依赖,因为它不可以被修改也就没有收集的必要性
  3. shallowReadonly 意译就是浅层只读的,汇聚了前两个的特性

那么对于这样的API怎么做处理呢?

还是一样的道理,他们和reactive并没什么不同,如果有那就是要不要收集依赖,要不要做深层代理。

其实对于嵌套对象的深层代理这一点,在v2和v3中就不同,v2是默认做递归代理而在v3中是访问了才去做代理,因此性能相比v2要好得多,避免了可能无关的资源消耗

那么来实现以下这三个API

对于实现这三个API的同时,我希望可以将reactive模块进行一个简单的重构,使其更像源码

  1. reactive.ts 文件:掌管reactive函数的创建和其他API的创建
import {
  mutableHandlers,
  readonlyHandlers,
  shallowReactiveHandlers,
  shallowReadonlyHandlers
} from './baseHandlers'

function reactive(target) {
    return createReactiveObject(target, false, mutableHandlers)
}

function shallowReactive(target) {
    return createReactiveObject(target, false, shallowReactiveHandles)
}

function readonly(target) {
    return createReactiveObject(target, true, readonlyHandles)
}

function shallowReadonly(target) {
    return createReactiveObject(target, true, shallowReadonlyHandles)
}

/**
* 上述的四个API都是调用这个createReactiveObject函数创建出响应式的对象
* 这个函数不过就是上面我们对创建过程的代码的一个抽离
* 这四个API本质上做的事情都是一样的,所以都可以利用这个函数通过传递不同的参数来控制逻辑
*/
const reactiveMap = new WeakMap()
const readonlyMap = new WeakMap()
    
//// 参数所代表的意思分别是:源对象,是否为一个只读的代理(readonly、shallowReadonly),getter和setter函数的对象
function createReactiveObject(target, isReadonly, baseHandles) {
    if(!isObject(target)) {
        return target
    }
    
    //// HAS PROXY?
    const existProxy = reactiveMap.get(target)
    if(existProxy) {
        return existProxy 
    }
    
    const proxy = new Proxy(target, baseHandles)
    
    //// 如果是只读的对象,那么就存入只读的map即可
    const proxyMap = isReadonly ? readonlyMap : reactiveMap
    proxyMap.set(target, proxy)
                
    return proxy
}

  1. baseHandles.ts文件:掌管Proxygettersetter的创建
//// 上面的响应式API的创建的核心逻辑其实就在这个文件中
//// 这是掌管proxy的逻辑的核心

const mutableHandles = {
    get: createGetter(),
    set: createSetter()
}
const shallowReactiveHandles = {
    get: createGetter(false, true),
    set: createSetter(true)
}
const readonlyHandles = {
    get: createGetter(true, false),
    set: () => {
        console.log("THIS VALUE CAN'T BE CHANGE BECAUSE IT's READONLY")
    }
}
const shallowReadonlyHandles = {
    get: createGetter(true, true),
    set: () => {
        console.log("THIS VALUE CAN'T BE CHANGE BECAUSE IT's READONLY")
    }
}

function createGetter(isReadonly = false, shallow = false) {
    return function get(target, key, receiver) {
        const res = Reflect.get(target, key, receiver)
                    
        //// 如果不是只读的那就收集依赖
        if(!isReadonly) {
            track(target, 'get', key)
        }
                    
        //// 如果是浅层代理那么到这就够了,即使是多层嵌套也不需要多层代理了
        if(shallow) {
            return res
        }
                    
        //// 如果是多层嵌套的对象,那么对这层对象进行响应式处理
        if(isObject(res)) {
            //// 这就是一个性能优化:
            //// 不像v2那种默认递归全部处理为响应式,v3中是取值的时候进行操作,如果是多层嵌套,此时再处理也不晚
            return isReadonly ? readonly(res) : reactive(res)
        }
                    
        return res
    }
}

function createSetter(isShallow = false) {
    return function set(target, key, value, receiver) {
        const oldValue = target[key]
        
        //// 检查是修改还是新增
        const hadKey = 
            isArray(target) && isIntegerKey(key)
                ? Number(key) < target.length 
                : hasOwn(target, key)
                            
        const res = Reflect.set(target, key, value, receiver)
        if(!hadKey) {
            //// 新增属性 
            trigger(target, 'add', key, value)
        } else { 
            //// 修改属性
            trigger(target, 'set', key, value, oldValue)
        }
                    
        return res
    }
}

这几个API的核心原理都是差不多的,都是调用统一的创建函数通过不同的参数传递创建出了我们想要的结果。

除了上面写的内容之外,我们还有refcomputed函数没有写出,不过对于响应式的部分来说,上述的内容我相信是可以让您明白Vue3的响应式是如何处理的!

That's it, do you get it

写在最后

  1. 上述我们谈论了reactive函数和副作用函数都做了哪些事情

  2. 什么是副作用函数,副作用函数是如何追踪状态的,如何存储副作用

  3. 状态变更是如何触发副作用执行,驱动页面更新的

  4. 最后我们补全了与reactive相关的几个API的创建

待补全

  1. ref函数API相关内容

  2. computed函数API的相关内容

补全这一部分之后(下一篇很快就出!),响应式的核心逻辑差不多就AC了,那么下面会继续跟进Vue3的runtime模块、关注于组件渲染等,抽时间再写

有时间还想在写一下React部分的更新原理,因为二者的实现原理并不相同,也可以抽时间写一下

帖子首发于我的Github:通过实现一个最简的reactive,学习Vue3响应式核心

如果对你有帮助,欢迎点赞、讨论、收藏、勘误!!