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

相关推荐
轻口味20 分钟前
【每日学点鸿蒙知识】AVCodec、SmartPerf工具、web组件加载、监听键盘的显示隐藏、Asset Store Kit
前端·华为·harmonyos
alikami22 分钟前
【若依】用 post 请求传 json 格式的数据下载文件
前端·javascript·json
吃杠碰小鸡1 小时前
lodash常用函数
前端·javascript
emoji1111111 小时前
前端对页面数据进行缓存
开发语言·前端·javascript
泰伦闲鱼1 小时前
nestjs:GET REQUEST 缓存问题
服务器·前端·缓存·node.js·nestjs
m0_748250031 小时前
Web 第一次作业 初探html 使用VSCode工具开发
前端·html
一个处女座的程序猿O(∩_∩)O1 小时前
vue3 如何使用 mounted
前端·javascript·vue.js
m0_748235951 小时前
web复习(三)
前端
迷糊的『迷』1 小时前
vue-axios+springboot实现文件流下载
vue.js·spring boot
web135085886351 小时前
uniapp小程序使用webview 嵌套 vue 项目
vue.js·小程序·uni-app