vue3响应式原理到这里已经结束,接下来要准备runtime-core的源码学习了,这里总结了前面所学的reactivity里面的核心知识,内容围绕流程图来讲,希望大家跟我一样可以有所收获, 这里的流程图可能不太清晰,要高清的可以私信我备注流程图
流程图
这份流程图是我对响应式原理知识点的手绘图,对自己学习的知识点的理解,如果有不完善的地方,请大家可以指出来,补充一下,也希望大家多多包涵
核心逻辑
依赖收集与触发
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的响应式源码之后,我发现源码其实离我们并不远,以前觉得很高大上的东西,自己顺着思路写一遍后,其实理解起来并不困难,也会赞叹尤大团队的厉害。看到这里的小伙伴希望给个赞,希望大家共同努力,加油创作!