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