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

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

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

流程图

image.png

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

核心逻辑

依赖收集与触发

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


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,从而进行依赖的触发与收集


//创建全局变量, 确保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函数


//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进行逻辑处理,这里为什么用这种方式就不说了,可以取看看相关的博客


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中


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,而如果是响应式数据,则需要覆盖掉,


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时,锁已经解开了,就可以进行值更新


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

相关文章
|
27天前
|
缓存 JavaScript UED
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
128 64
|
27天前
|
JavaScript 前端开发 API
Vue 3 中 v-model 与 Vue 2 中 v-model 的区别是什么?
总的来说,Vue 3 中的 `v-model` 在灵活性、与组合式 API 的结合、对自定义组件的支持等方面都有了明显的提升和改进,使其更适应现代前端开发的需求和趋势。但需要注意的是,在迁移过程中可能需要对一些代码进行调整和适配。
108 60
|
2天前
|
JavaScript API 数据处理
vue3使用pinia中的actions,需要调用接口的话
通过上述步骤,您可以在Vue 3中使用Pinia和actions来管理状态并调用API接口。Pinia的简洁设计使得状态管理和异步操作更加直观和易于维护。无论是安装配置、创建Store还是在组件中使用Store,都能轻松实现高效的状态管理和数据处理。
13 3
|
27天前
|
前端开发 JavaScript 测试技术
Vue3中v-model在处理自定义组件双向数据绑定时,如何避免循环引用?
Web 组件化是一种有效的开发方法,可以提高项目的质量、效率和可维护性。在实际项目中,要结合项目的具体情况,合理应用 Web 组件化的理念和技术,实现项目的成功实施和交付。通过不断地探索和实践,将 Web 组件化的优势充分发挥出来,为前端开发领域的发展做出贡献。
32 8
|
27天前
|
存储 JavaScript 数据管理
除了provide/inject,Vue3中还有哪些方式可以避免v-model的循环引用?
需要注意的是,在实际开发中,应根据具体的项目需求和组件结构来选择合适的方式来避免`v-model`的循环引用。同时,要综合考虑代码的可读性、可维护性和性能等因素,以确保系统的稳定和高效运行。
30 1
|
27天前
|
JavaScript
Vue3中使用provide/inject来避免v-model的循环引用
`provide`和`inject`是 Vue 3 中非常有用的特性,在处理一些复杂的组件间通信问题时,可以提供一种灵活的解决方案。通过合理使用它们,可以帮助我们更好地避免`v-model`的循环引用问题,提高代码的质量和可维护性。
36 1
|
27天前
|
JavaScript
在 Vue 3 中,如何使用 v-model 来处理自定义组件的双向数据绑定?
需要注意的是,在实际开发中,根据具体的业务需求和组件设计,可能需要对上述步骤进行适当的调整和优化,以确保双向数据绑定的正确性和稳定性。同时,深入理解 Vue 3 的响应式机制和组件通信原理,将有助于更好地运用 `v-model` 实现自定义组件的双向数据绑定。
|
1月前
|
JavaScript 前端开发 API
从Vue 2到Vue 3的演进
从Vue 2到Vue 3的演进
40 0
|
1月前
|
JavaScript 前端开发 API
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
57 0
|
JavaScript 容器
【Vue源码解析】mustache模板引擎
【Vue源码解析】mustache模板引擎
71 0