如题,这篇文章是我在阅读源码的过程中做的笔记,发布到掘金有两个目的:
- 记录自己的学习过程,方便以后查阅;
- 通过与掘友们讨论,反查自己对框架实现原理理解是否到位,查漏补缺。
vue2和vue3的响应式实现核心都使用了发布订阅设计模式
下面是ref实现响应式对象的流程介绍
下面开始分析源码
ref 函数入口
export function ref(value?: unknown) {
// 调用createRef创建响应式对象
return createRef(value, false)
}
function createRef(rawValue: unknown, shallow: boolean) {
// 判断value是否已经是一个响应式对象了,如果是,则直接返回
if (isRef(rawValue)) return rawValue
// 实例化 RefImpl 创建响应式对象
return new RefImpl(rawValue, shallow)
}
从上面代码可以看出创建响应式对象的核心在于 class RefImpl ,下面开始分析这个 class
创建响应式对象
class RefImpl<T> {
// 储存 ref 函数创建的值
private _value: T
// 没有被代理的原始数据
private _rawValue: T
// 是一个Set集合,这个 ref 所有的订阅者都会存在这个集合里面
public dep?: Dep = undefined
// 一个标记
public readonly __v_isRef = true
constructor(value: T, public readonly __v_isShallow: boolean) {
// 根据一个 Vue 创建的代理返回其原始对象。
this._rawValue = __v_isShallow ? value : toRaw(value)
// toReactive 内部会通过 reactive 将 value 转换成一个响应式对象。
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
// 添加订阅者
trackRefValue(this)
// 返回对应ref变量的值
return this._value
}
set value(newVal) {
newVal = this.__v_isShallow ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
// 更新 value
this._rawValue = newVal
this._value = this.__v_isShallow ? newVal : toReactive(newVal)
// 通知订阅者更新
triggerRefValue(this, newVal)
}
}
}
- get value: 我们在 template 上面使用 {{ xxx }} 或者在外部(比如 setup 内部)使用 xxx.value 都会通过它来取值,同时会在这方法里面添加订阅者。
- set value: 在执行 xxx.value = yyy 时触发,除了在这个方法内部更新value,还会通知对应订阅者更新。
下面开始分析 RefImpl 是怎么添加订阅和通知订阅更新的
添加订阅(也叫做依赖收集)
export function trackRefValue(ref: RefBase<any>) {
// 需要订阅并且存在订阅者
if (shouldTrack && activeEffect) {
ref = toRaw(ref)
trackEffects(ref.dep || (ref.dep = createDep()))
}
}
- shouldTrack 用来判断是否需要添加订阅,初始化时为 true
- activeEffect 订阅者对象(会在组件渲染、计算属性、watch监听这三种情况下创建)。
// ref.dep 内部结构
export type Dep = Set<ReactiveEffect> & {w:number,n:number}
// 订阅者层级的标记
let effectTrackDepth = 0
// 当前
let trackOpBit = 1
// 最大标记的比特位
const maxMarkerBits = 30
export function trackEffects(
dep: Dep,
// debug 配置,不需要关心
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
let shouldTrack = false
// 判断是否超出依赖存储上限,一般情况下不会超过这个值
if (effectTrackDepth <= maxMarkerBits) {
// 性能优化,是否需要添加这个新的依赖
if (!newTracked(dep)) {
dep.n |= trackOpBit // set newly tracked
shouldTrack = !wasTracked(dep)
}
} else {
shouldTrack = !dep.has(activeEffect!)
}
// 添加依赖
if (shouldTrack) {
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
}
}
变量介绍:
- dep 用来添加订阅者的集合。
- 关于 dep.n dep.w 源码中的解释是: wasTracked 和 newTracked 维护多个级别的效果跟踪递归的状态。 每个级别使用一位来定义是否跟踪依赖项。
- sholudTrack 是否需要添加订阅者。
通知订阅者(触发更新)
触发更新阶段相对于依赖收集结点逻辑会清晰一些,主要就是遍历 ref.dep 通知对应的订阅者执行对应的代码。
set value(newVal) {
newVal = this.__v_isShallow ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
// 更新 value
this._rawValue = newVal
this._value = this.__v_isShallow ? newVal : toReactive(newVal)
// 通知订阅者更新
triggerRefValue(this, newVal)
}
}
从上面代码可以看出来,set value 首先会更新 ref 对象内部的值,紧接着会去通知订ref的订阅者们更新。
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
// 将订阅者集合转换成数组
const effects = isArray(dep) ? dep : [...dep]
// 下面两个 for 循环用来通知每个订阅者更新
for (const effect of effects) {
if (effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
for (const effect of effects) {
if (!effect.computed) {
// 通知计算属性订阅者更新
triggerEffect(effect, debuggerEventExtraInfo)
}
}
}
// 执行对应订阅者对象面的更新方法。
function triggerEffect(
effect: ReactiveEffect,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
if (effect !== activeEffect || effect.allowRecurse) {
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
总结:
通过上面的分析我们能看出来,ref 实现响应式对象的核心就是发布订阅模式,所以只要我们能够好好的去理解,学习vue的原理也不是那么困难了。
第一次把自己阅读源码的理解以这种形式记录下来,还是没什么经验,如果在文中某些点我的表达或者理解有误,请掘友们不吝赐教,我会积极更正错误
附上我很喜欢的一句诗:长风破浪会有时,直挂云帆济沧海