我将vue3响应式核心源码手写了一遍,成长了许多

vue3响应式原理到这里已经结束,接下来要准备runtime-core的源码学习了,这里总结了前面所学的reactivity里面的核心知识,内容围绕流程图来讲,希望大家跟我一样可以有所收获, 这里的流程图可能不太清晰,要高清的可以私信我备注流程图

流程图

这份流程图是我对响应式原理知识点的手绘图,对自己学习的知识点的理解,如果有不完善的地方,请大家可以指出来,补充一下,也希望大家多多包涵

核心逻辑

依赖收集与触发

vue3响应式的核心逻辑在于依赖的收集与触发,这个贯穿了整个reactivity的过程,对于reactive,我们会搭配副作用函数effect一起使用,通过effect监听reactive数据的变化,及时的进行依赖的收集以及触发,在effect逻辑中,我们创建了一个ReactiveEffect类用于依赖的收集,触发,执行run函数,stop函数

js 复制代码
export class reactiveEffect {
    //传入effect的函数
    private _fn: any;
    //控制是否stop做清空处理
    active = true
    //收集依赖, 用于将依赖清空实现stop
    deps: any = [];
    //stop之后的回调
    onStop?: () => void
    constructor(fn, public scheduler?: Function) {
        this._fn = fn
        this.scheduler = scheduler
    }
    run() {
        //我们要控制,当我们stop之后停止依赖的收集,所以这里要控制
        if (!this.active) {
            return this._fn()
        }
        //收集依赖
        shouldTrack = true
        activeEffect = this
        const res = this._fn()
        shouldTrack = false
        return res
    }
    stop() {
        if (this.active) {
            clearEffect(this)
            if (this.onStop) {
                this.onStop()
            }
            this.active = false
        }
    }
}

当我们创建一个effect函数时,会实例化出reactiveEffect,从而进行依赖的触发与收集

js 复制代码
//创建全局变量, 确保dep能拿到fn
let activeEffect

export function effect(fn, options: any = {}) {
    const _effect = new reactiveEffect(fn, options.schdeuler)
    extend(_effect, options)
    // Object.assign(_effect, options)
    _effect.run()

    const runner: any = _effect.run.bind(_effect)
    runner._effect = _effect
    return runner
}

对于track函数,我们不仅对dep的取值有写到,也对比如readonly这种不需要收集依赖,做了shouldTrack判断,以及后面对于ref对象我们也同样考虑到它的dep结构,进行了优化,从而写出了trackEffect函数

js 复制代码
//target ------> key ------> dep 结构 weakMap ------> map ------> set
const targetMap = new WeakMap()
//控制依赖是否收集
let shouldTrack = true
export function track(target, key) {
    let depMap = targetMap.get(target)
    //解决初始化不存在的问题
    if (!depMap) {
        depMap = new Map()
        targetMap.set(target, depMap)
    }
    let dep = depMap.get(key)
    //解决初始化不存在的问题
    if (!dep) {
        dep = new Set()
        depMap.set(key, dep)
    }
    // 这里我们用下面的isTrack来优化
    /*如果没有依赖项,就直接返回
    if (!activeEffect) return;
    //如果不要收集依赖,这里直接返回
    if (!shouldTrack) return;*/
    trackEffect(dep)
}
export function trackEffect(dep) {
    if (!isTrack()) return
    //收集依赖, 我们需要拿到fn 如果依赖中存在activeEffect 就直接return 不再收集了
    if (dep.has(activeEffect)) return
    dep.add(activeEffect)
    //这里我们将
    activeEffect.deps.push(dep)
}
export function isTrack() {
    return shouldTrack && activeEffect
}

对于trigger函数,他只有在响应式数据发生改变时才会被调用, 当调用它时,他会触发依赖从而执行effect,进而执行fn 而执行完fn之后,它又会触发get操作,再次进行依赖收集,并且值做到更新

reactive

对于一个reactive来说,我们主要接受的是Object类型的数据,对于非这种的我们会判断它的targetType,然后进行相应地处理,reactive是通过Proxy代理,在handler里面对get set进行逻辑处理,这里为什么用这种方式就不说了,可以取看看相关的博客

js 复制代码
export function reactive(raw: any) {
    return createActiveObject(raw, mutableHandler)
    // return new Proxy(raw, mutableHandler)
}

export function readonly(raw) {
    return createActiveObject(raw, readonlyHandler)
    // return new Proxy(raw, readonlyHandler)
}

export function shallowReadonly(raw) {
    return createActiveObject(raw, shallowReaonlyHandler)
}


function createActiveObject(raw: any, baseHandlers) {
    return new Proxy(raw, baseHandlers)
}

我们的主要逻辑都在baseHandlers里面,对于不同的结构,readonly shallowReadonly,reactive等结构我们将相同的代码抽取出来,封装成公共函数, 流程图可以看到,对于reactive我们的处理逻辑在multableHandlers中

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

const get = createGetter()
const set = createSetter()
const getReadonly = createGetter(true)

const getShallowReadonly = createGetter(true, true)

export const enum reactiveFlags {
    IS_REACTIVE = '__v_isReactive',
    IS_READONLY = '__v_isReadonly'
}
function createGetter(isReadonly = false, isShallow = false) {
    return function get(target, key) {
        if (key === reactiveFlags.IS_REACTIVE) {
            return !isReadonly
        } else if (key === reactiveFlags.IS_READONLY) {
            return isReadonly
        }
        const res = Reflect.get(target, key)
        if (isShallow) {
            return res
        }

        if (isObject(res)) {
            return isReadonly ? readonly(res) : reactive(res)
        }
        if (!isReadonly) {
            track(target, key)
        }
        return res
    }
}

function createSetter() {
    return function set(target, key, value) {
        const res = Reflect.set(target, key, value)
        trigger(target, key)
        return res
    }
}

export const mutableHandler = {
    get,
    set
}
export const readonlyHandler = {
    get: getReadonly,
    set(target, key, value) {
        console.warn(`${key}不能set 因为target 是readonly`, target)
        return true
    }
}
export const shallowReaonlyHandler = extend({}, readonlyHandler,
    { get: getShallowReadonly }
)
export function isReactive(value) {
    //要是我们挂载的对象不是Proxy,那么就会变成undefined所以用!!转换成布尔值
    return !!value[reactiveFlags.IS_REACTIVE]
}

export function isReadonly(value) {
    return !!value[reactiveFlags.IS_READONLY]
}

export function isProxy(value) {
    return isReactive(value) || isReadonly(value)
}

在这个里面我们先看multableHandlers的逻辑,我们通过createGetter以及createSetter对get set逻辑进行封装,在createGetter中,因为有readonly,shallowReadonly等不同逻辑,而在get中又有相同的逻辑,我们对代码进一步抽离,就封装了两个函数,当触发get set时,会调用对应的函数逻辑,并且对深度监听做了对应处理

ref

ref与reactive的区别在于,ref可以监听基本数据类型和引用数据类型,但是ref读取值时要加上.value,对于ref,如果是基本数据结构,它的依赖也就只有一个,所以我们不需要再一层一层获取,可以直接给定一个dep。并且如果传入的数据类型是Object那么我们会将其转化为reactive对象。 在ref中很重要的一点在于,我们要比较修改后的数据类型,如果相比没有改变,就直接返回,不做依赖的触发,如果是值类型,我们就直接赋值给value,而如果是响应式数据,则需要覆盖掉,

js 复制代码
import { isHasChanged, isObject } from "../shared"
import { trackEffect, tiggerEffect, isTrack } from "./effect"
import { reactive } from "./reactive"
class RefImpl {
    private _value
    private _raw
    public dep
    //是否是ref的标识
    public __v_isRef = true
    constructor(value) {
        //保存没有代理前的value
        this._raw = value
        //做对象类型检测
        this._value = isObject(value) ? reactive(value) : value
        this.dep = new Set()
    }
    get value() {
        trackRefValue(this.dep)
        return this._value

    }
    set value(newValue) {
        if (!isHasChanged(this._raw, newValue)) return
        this._raw = newValue
        this._value = newValue
        tiggerEffect(this.dep)

    }
}

export function trackRefValue(dep) {
    if (isTrack()) {
        trackEffect(dep)
    }
}

export function ref(value) {
    return new RefImpl(value)
}


export function isRef(value) {
    return !!value.__v_isRef
}

export function unRef(ref) {
    return isRef(ref) ? ref.value : ref
}
export function ProxyRefs(objectWithRef) {
    return new Proxy(objectWithRef, {
        get(target, key) {
            const res = unRef(Reflect.get(target, key))
            return res
        },
        set(target, key, value) {
            
            if (isRef(target[key]) && !isRef(value)) {
                return target[key].value = value
            } else {
                return Reflect.set(target, key, value)
            }
        }
    })
}

computed

计算属性,它的内部传入一个计算函数,返回值如同ref对象需要.value,因此我们用实例化对象computedRefImpl来处理对应的逻辑,在computed中我们需要对响应式数据进行依赖收集以及触发,所以我们需要借助effect函数来帮我们,因此在内部我们会会创建一个effect用于get set时的逻辑。

在computed中它的几大特性,懒值脏值检查 等,我们也进行了手写实现,这里的脏值检查是我们对于缓存的应用,也就是流程图里的加锁解锁 ,当我们get操作之后,依赖收集完毕,我们锁上,这样再次调用我们就不要再收集依赖,直接从缓存中取值,而这个锁如何解开,我们需要借助set操作,当我们响应式数据的值发生改变时,会重新执行effect,执行fn 然后触发get 进行track, 而我们知道再传入fn的时候,我们也可以传入配置项给effect,其中一个scheduler(调度) 就发挥了作用,当我们将解锁条件写在调度逻辑内时,当我们触发set操作时,会先触发scheduler中的逻辑,然后再执行effect,这样当我们走到get时,锁已经解开了,就可以进行值更新

js 复制代码
import { reactiveEffect } from "./effect"
class computedRefImpl {
    private _getter
    private _dirty = true
    private _value
    private _effect
    constructor(getter) {
        this._getter = getter
        this._effect = new reactiveEffect(getter, () => {
            if (!this._dirty) {
                this._dirty = true
            }
        })

    }
    get value() {
        if (this._dirty) {
            this._dirty = false
            this._value = this._effect.run()
        }
        return this._value
    }
}


export function computed(getter) {
    return new computedRefImpl(getter)
}

到这里我们基本完成了大致的逻辑关系,其中的readonly isReactive isReadonly isRef unRef等工具类函数实现再其他章节里有详细介绍,本章对接流程图里的操作,关于本章没讲到的dep的底层实现,希望大家可以看看其他博客,本章不再说这个知识了

写在最后

在手写了一遍vue3的响应式源码之后,我发现源码其实离我们并不远,以前觉得很高大上的东西,自己顺着思路写一遍后,其实理解起来并不困难,也会赞叹尤大团队的厉害。看到这里的小伙伴希望给个赞,希望大家共同努力,加油创作!

相关推荐
Lupino9 分钟前
被 React “玩弄”的 24 小时:为了修一个不存在的 Bug,我给大模型送了顿火锅钱
前端·react.js
米丘16 分钟前
了解 Javascript 模块化,更好地掌握 Vite 、Webpack、Rollup 等打包工具
前端
Heo17 分钟前
深入 React19 Diff 算法
前端·javascript·面试
滕青山18 分钟前
个人所得税计算器 在线工具核心JS实现
前端·javascript·vue.js
小怪点点19 分钟前
手写promise
前端·promise
国思RDIF框架28 分钟前
RDIFramework.NET Web 敏捷开发框架 V6.3 发布 (.NET8+、Framework 双引擎)
前端
Mintopia29 分钟前
如何在有限的时间里,活出几倍的人生
前端
炫饭第一名30 分钟前
速通Canvas指北🦮——变形、渐变与阴影篇
前端·javascript·程序员
Neptune131 分钟前
让我带你迅速吃透React组件通信:从入门到精通(上篇)
前端·javascript
阿懂在掘金31 分钟前
Vue 表单避坑(一):为什么 v-model 绑定对象属性会偷偷修改父组件数据?
前端·vue.js