我们日常开发过程中经常会用到响应式数据ref
、计算属性computed
和侦听器watch
。我们知道,他们共同点是在数据
发生变化的时候,总会引起一些其他变化:ref
对应的数据变化可能会引起视图变化、计算属性变化或侦听器的事件触发;computed
变化可能可能会引起视图变化;而watch
则是那个监听响应式数据变化的api
。
简言之,一个变化会引起另一个或另几个的变化。接下来我们就一步步探索这几个api
底层的实现以及他们之间千丝万缕的关系。
先来看一个例子:
<template>
<div>count:{{ count }}</div>
<div>hasUnitCount:{{ hasUnitCount }}</div>
<button @click="increaseCount">increaseCount</button>
</template>
<script setup>
import { computed, ref, watch } from "vue";
// 响应式数据ref
const count = ref(1);
// 计算属性computed
const hasUnitCount = computed(() => {
return count.value + "个";
});
// 侦听器watch
watch(count, (newValue, oldValue) => {
console.log(newValue, oldValue);
});
const increaseCount = () => {
count.value = count.value * 2;
};
</script>
以上例子中,我们涵盖了ref
、computed
和watch
三个api
的使用,为了解释方便,我们简单探讨一下经典的发布订阅者模式
。
一、发布订阅模式的定义
有一条古老的街道,
某天,街上开了一家财经报社(发布者)
,张三(订阅者)
去订阅了一年的报纸,并且留下了地址,从此报社每天都会派送报员给张三送报纸,
某天,李四(订阅者)也订阅了报纸,报社的订阅者就变成了 2 个(发布者的 dep 就变成了 2)。
某天,街上又开了一家体育报社(发布者)
,张三(订阅者)
又订阅了一年的报纸,此时,张三 所依赖的发布者就变成了 2 个(订阅者的 deps 就变成了 2)。
故事很短, 但可以就发布订阅者有一个概括性的认识。发布者
是消息的发出者,订阅者
是消息的接收者。一个发布者可以被多个订阅者订阅,一个订阅者也可以订阅多个发布者的消息。
上图可以清晰的看出依赖收集和派发更新时数据或信息的流向。vue3
的响应式原理就是以发布订阅者模式
为核心设计模式实现的,下面开始讨论vue3
中的发布订阅者。
二、vue3
中的发布者和订阅者
1、ref
-发布者
// ref函数
function ref(value) {
return createRef(value, false);
}
// createRef函数
function createRef(rawValue, shallow) {
// 如果是ref,直接返回
if (isRef(rawValue)) {
return rawValue;
}
// 创建RefImpl实例
return new RefImpl(rawValue, shallow);
}
// 定义RefImpl类(包含get和set函数)
class RefImpl {
constructor(value, __v_isShallow) {
this.__v_isShallow = __v_isShallow;
this.dep = void 0;
this.__v_isRef = true;
this._rawValue = __v_isShallow ? value : toRaw(value);
this._value = __v_isShallow ? value : toReactive(value);
}
get value() {
// 依赖收集:收集订阅者
trackRefValue(this);
return this._value;
}
set value(newVal) {
const useDirectValue =
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal);
newVal = useDirectValue ? newVal : toRaw(newVal);
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal;
this._value = useDirectValue ? newVal : toReactive(newVal);
// 派发更新:将数据变化的消息发送给订阅者
triggerRefValue(this, 4, newVal);
}
}
}
以上是响应式 apiref
的基本逻辑,最终是实例化了RefImpl
,当访问到该数据时,会通过trackRefValue
来实现依赖收集,当修改其数据时,又会通过triggerRefValue
的方式进行派发更新。
computed
和watch
都可以以ref
对应的响应式数据的变化作为变化依据,所以,我们可以认为ref
对应的数据家是一个发布者
。
2、computed
-即是发布者,又是订阅者
// 定义computed入口
const computed = (getterOrOptions, debugOptions) => {
const c = computed$1(getterOrOptions, debugOptions, isInSSRComponentSetup);
if (!!(process.env.NODE_ENV !== "production")) {
const i = getCurrentInstance();
if (i && i.appContext.config.warnRecursiveComputed) {
c._warnRecursive = true;
}
}
return c;
};
// 这个computed就是以上的computed$1
function computed(getterOrOptions, debugOptions, isSSR = false) {
let getter;
let setter;
const onlyGetter = isFunction(getterOrOptions);
if (onlyGetter) {
getter = getterOrOptions;
setter = !!(process.env.NODE_ENV !== "production")
? () => {
warn("Write operation failed: computed value is readonly");
}
: NOOP;
} else {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}
const cRef = new ComputedRefImpl(
getter,
setter,
onlyGetter || !setter,
isSSR
);
if (!!(process.env.NODE_ENV !== "production") && debugOptions && !isSSR) {
cRef.effect.onTrack = debugOptions.onTrack;
cRef.effect.onTrigger = debugOptions.onTrigger;
}
return cRef;
}
// 定义ComputedRefImpl类(包含get和set函数)
class ComputedRefImpl {
constructor(getter, _setter, isReadonly, isSSR) {
this.getter = getter;
this._setter = _setter;
this.dep = void 0;
this.__v_isRef = true;
this["__v_isReadonly"] = false;
// 创建ReactiveEffect实例
this.effect = new ReactiveEffect(
() => getter(this._value),
() => triggerRefValue(this, this.effect._dirtyLevel === 2 ? 2 : 3)
);
this.effect.computed = this;
this.effect.active = this._cacheable = !isSSR;
this["__v_isReadonly"] = isReadonly;
}
get value() {
const self = toRaw(this);
if (
(!self._cacheable || self.effect.dirty) &&
hasChanged(self._value, (self._value = self.effect.run()))
) {
// 派发更新:将数据变化的消息发送给订阅者
triggerRefValue(self, 4);
}
// 依赖收集:收集订阅者
trackRefValue(self);
if (self.effect._dirtyLevel >= 2) {
if (!!(process.env.NODE_ENV !== "production") && this._warnRecursive) {
warn(COMPUTED_SIDE_EFFECT_WARN, `getter: `, this.getter);
}
// 派发更新:将数据变化的消息发送给订阅者
triggerRefValue(self, 2);
}
return self._value;
}
set value(newValue) {
this._setter(newValue);
}
get _dirty() {
return this.effect.dirty;
}
set _dirty(v) {
this.effect.dirty = v;
}
}
以上是计算属性 apicomputed
的基本逻辑,最终是实例化了ComputedRefImpl
,当访问到该计算属性时,会通过trackRefValue
来实现依赖收集,当其依赖的数据变化时,又会通过triggerRefValue
的方式进行派发更新。
计算属性
对应的数据变化会引起视图
的变化,我们可以认为计算属性
对应的数据是发布者
。
它自身又实现了ReactiveEffect
的实例effect
,也称之为computed-effect
,它是可以被收集的。所以,我们称computed
也是订阅者
。
3、watch
-订阅者
function watch(source, cb, options) {
// 如果cb不是函数,控制台打印警告
if (!!(process.env.NODE_ENV !== "production") && !isFunction(cb)) {
warn$1(
`\`watch(fn, options?)\` signature has been moved to a separate API. Use \`watchEffect(fn, options?)\` instead. \`watch\` now only supports \`watch(source, cb, options?) signature.`
);
}
return doWatch(source, cb, options);
}
function doWatch(
source,
cb,
{ immediate, deep, flush, once, onTrack, onTrigger } = EMPTY_OBJ
) {
let getter;
let forceTrigger = false;
let isMultiSource = false;
// 1、通过source获取getter
// source是ref
if (isRef(source)) {
getter = () => source.value;
forceTrigger = isShallow(source);
// source是reactive
} else if (isReactive(source)) {
getter = () => reactiveGetter(source);
forceTrigger = true;
// source是数组
} else if (isArray(source)) {
isMultiSource = true;
forceTrigger = source.some((s) => isReactive(s) || isShallow(s));
getter = () =>
source.map((s) => {
if (isRef(s)) {
return s.value;
} else if (isReactive(s)) {
return reactiveGetter(s);
} else if (isFunction(s)) {
return callWithErrorHandling(s, instance, 2);
} else {
!!(process.env.NODE_ENV !== "production") && warnInvalidSource(s);
}
});
// source是函数
} else if (isFunction(source)) {
if (cb) {
getter = () => callWithErrorHandling(source, instance, 2);
} else {
getter = () => {
if (cleanup) {
cleanup();
}
return callWithAsyncErrorHandling(source, instance, 3, [onCleanup]);
};
}
// 否则为空函数
} else {
getter = NOOP;
!!(process.env.NODE_ENV !== "production") && warnInvalidSource(source);
}
if (cb && deep) {
const baseGetter = getter;
getter = () => traverse(baseGetter());
}
let cleanup;
let onCleanup = (fn) => {
cleanup = effect.onStop = () => {
callWithErrorHandling(fn, instance, 4);
cleanup = effect.onStop = void 0;
};
};
let oldValue = isMultiSource
? new Array(source.length).fill(INITIAL_WATCHER_VALUE)
: INITIAL_WATCHER_VALUE;
// 2、定义scheduler
const job = () => {
if (!effect.active || !effect.dirty) {
return;
}
if (cb) {
const newValue = effect.run();
if (
deep ||
forceTrigger ||
(isMultiSource
? newValue.some((v, i) => hasChanged(v, oldValue[i]))
: hasChanged(newValue, oldValue)) ||
false
) {
if (cleanup) {
cleanup();
}
callWithAsyncErrorHandling(cb, instance, 3, [
newValue,
// pass undefined as the old value when it's changed for the first time
oldValue === INITIAL_WATCHER_VALUE
? void 0
: isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
? []
: oldValue,
onCleanup,
]);
oldValue = newValue;
}
} else {
effect.run();
}
};
job.allowRecurse = !!cb;
let scheduler;
if (flush === "sync") {
scheduler = job;
} else if (flush === "post") {
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense);
} else {
// 用于后续事件优先级的排序(在后续流程中用到,pre表示watch在job.id相同的时候有更高的执行优先级)
job.pre = true;
if (instance) job.id = instance.uid;
scheduler = () => queueJob(job);
}
// 3、创建ReactiveEffect实例
const effect = new ReactiveEffect(getter, NOOP, scheduler);
const scope = getCurrentScope();
const unwatch = () => {
effect.stop();
if (scope) {
remove(scope.effects, effect);
}
};
// 4、执行job/effect.run()
if (cb) {
if (immediate) {
job();
} else {
// 当前例子中执行effect.run()
oldValue = effect.run();
}
}
// 5、返回unwatch
return unwatch;
}
这里主要根据条件获取getter
和scheduler
,然后通过const effect = new ReactiveEffect(getter, NOOP, scheduler)
创建侦听器effect
,也称之为watch-effect
,它是可以被收集的。所以,我们称watch
也是订阅者
。
4、render
-订阅者
这里不得不提一个渲染对应的effect
,先称其为render-effect
,我们省略其他逻辑,主要列出其流程代码:
const componentUpdateFn = () => {
// 获取实例的vnode
instance.subTree = renderComponentRoot(instance);
// 渲染实例的视图
patch(null, subTree, container, anchor, instance, parentSuspense, namespace);
};
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
NOOP,
() => queueJob(update),
instance.scope
));
视图会根据响应式数据ref
和computed
的变化而更新,所以,它也是名副其实的订阅者
。
5、小结
先根据以上内容对ref
、computed
、watch
和render
做个对比。
可以看出,ref
仅仅作为发布者,可以对computed
、watch
和render
进行收集。computed
比较特殊,它既可以作为发布者被render
订阅,也可作为订阅者订阅ref
消息或数据。watch
和render
的特点一致,都只能作为订阅者来订阅其他发布者的消息或数据。
三、vue3
中发布者收集订阅者
vue3
的详细流程可以参考寻找 vue3 源码脉络图。这里只介绍与本例有关的主要流程:
这里首先介绍主要的ReactiveEffect
:
// 定义当前激活状态的effect
let activeEffect;
// 定义ReactiveEffect类
class ReactiveEffect {
constructor(fn, trigger, scheduler, scope) {
this.fn = fn;
this.trigger = trigger;
this.scheduler = scheduler;
this.active = true;
this.deps = [];
this._dirtyLevel = 4;
this._trackId = 0;
this._runnings = 0;
this._shouldSchedule = false;
this._depsLength = 0;
recordEffectScope(this, scope);
}
get dirty() {
if (this._dirtyLevel === 2 || this._dirtyLevel === 3) {
this._dirtyLevel = 1;
pauseTracking();
for (let i = 0; i < this._depsLength; i++) {
const dep = this.deps[i];
if (dep.computed) {
triggerComputed(dep.computed);
if (this._dirtyLevel >= 4) {
break;
}
}
}
if (this._dirtyLevel === 1) {
this._dirtyLevel = 0;
}
resetTracking();
}
return this._dirtyLevel >= 4;
}
set dirty(v) {
this._dirtyLevel = v ? 4 : 0;
}
run() {
this._dirtyLevel = 0;
if (!this.active) {
return this.fn();
}
let lastShouldTrack = shouldTrack;
// 记录上一个activeEffect
let lastEffect = activeEffect;
try {
shouldTrack = true;
// 将当前effect赋值给activeEffect,在执行fn时,必要的话可以对当前的activeEffect进行收集
activeEffect = this;
this._runnings++;
preCleanupEffect(this);
// 执行fn
return this.fn();
} finally {
postCleanupEffect(this);
this._runnings--;
// 将effect重置为上一次的activeEffect
activeEffect = lastEffect;
shouldTrack = lastShouldTrack;
}
}
stop() {
var _a;
if (this.active) {
preCleanupEffect(this);
postCleanupEffect(this);
(_a = this.onStop) == null ? void 0 : _a.call(this);
this.active = false;
}
}
}
以上代码中主要关注run
函数,它执行fn
的过程中,可能会访问到响应式apiref
或computed
对应的数据,也就访问到了其get
函数,进而触发trackRefValue
依赖收集的过程。
这里在执行fn
之前,执行了activeEffect = this
,所以,如果收集的话,就是收集的其自身effect
。
computed
、watch
和render
中都是以该ReactiveEffect
为共同类,又根据其自身不同的特定实例化各自对应的effect
。
再看依赖收集函数:
function trackRefValue(ref2) {
var _a;
if (shouldTrack && activeEffect) {
ref2 = toRaw(ref2);
trackEffect(
activeEffect,
(_a = ref2.dep) != null
? _a
: (ref2.dep = createDep(
() => (ref2.dep = void 0),
ref2 instanceof ComputedRefImpl ? ref2 : void 0
)),
!!(process.env.NODE_ENV !== "production")
? {
target: ref2,
type: "get",
key: "value",
}
: void 0
);
}
}
function trackEffect(effect2, dep, debuggerEventExtraInfo) {
var _a;
if (dep.get(effect2) !== effect2._trackId) {
dep.set(effect2, effect2._trackId);
const oldDep = effect2.deps[effect2._depsLength];
if (oldDep !== dep) {
if (oldDep) {
cleanupDepEffect(oldDep, effect2);
}
effect2.deps[effect2._depsLength++] = dep;
} else {
effect2._depsLength++;
}
if (!!(process.env.NODE_ENV !== "production")) {
(_a = effect2.onTrack) == null
? void 0
: _a.call(effect2, extend({ effect: effect2 }, debuggerEventExtraInfo));
}
}
}
trackRefValue
中将activeEffect
和ref2.dep
作为参数传入。
trackEffect
中将当前激活的effect2
作为key
,effect2._trackId
作为value
收集到dep
中。同时,通过effect2.deps[effect2._depsLength++] = dep
的方式将dep
记录到deps
中去。
至此,响应式数据ref
和各个effect
之间的关系完成建立。发布者收集了effect
,通过dep
记录各个订阅者的信息;订阅者也将其订阅的发布者信息记录在deps
中。
1、setup
阶段
setupComponent(instance)
--> setupStatefulComponent(instance, isSSR)
--> setup
。
在这个流程中:
实例化了计算属性的ComputedRefImpl
,并且为其实例化了activeEffect
--computed-effect
。
实例化了watch-effect
,并且完成了ref
对于watch-effect
的收集。
2、setupRenderEffect
阶段
实例化了渲染的activeEffect
--render-effect
3、subTree
阶段
执行const subTree = instance.subTree = renderComponentRoot(instance)
获取组件的vnode
阶段,访问到了ref
和computed
对应的数据,在这个流程中:
完成了RefImpl
对于render-effect
的收集。
完成了RefImpl
对于computed-effect
的收集。
完成了ComputedRefImpl
对于render-effect
的收集。
篇幅有限,如果需要了解响应式依赖收集和派发更新的详细流程,可以移步:
响应式数据
详情可查看vue3响应式原理:reactive。
计算属性
详情可查看vue3响应式原理:可被收集/也可被收集的computed。
侦听器
详情可查看vue3响应式原理:被监听的watch。
四、vue3
中发布者通知订阅者
当我们点击例子中的按钮,触发数据变化时,会让视图重新渲染。
const increaseCount = () => {
count.value++;
};
修改count.value
会触发RefImpl
的get
函数:
function set(newVal) {
const useDirectValue =
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal);
newVal = useDirectValue ? newVal : toRaw(newVal);
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal;
this._value = useDirectValue ? newVal : toReactive(newVal);
triggerRefValue(this, 4, newVal);
}
}
function triggerRefValue(ref2, dirtyLevel = 4, newVal) {
ref2 = toRaw(ref2);
const dep = ref2.dep;
// 如果有收集到的dep,执行triggerEffects
if (dep) {
triggerEffects(
dep,
dirtyLevel,
!!(process.env.NODE_ENV !== "production")
? {
target: ref2,
type: "set",
key: "value",
newValue: newVal,
}
: void 0
);
}
}
function triggerEffects(dep, dirtyLevel, debuggerEventExtraInfo) {
var _a;
pauseScheduling();
// 以dep的key值为数组遍历执行trigger或queueEffectSchedulers.push(effect2.scheduler)函数
for (const effect2 of dep.keys()) {
let tracking;
if (
effect2._dirtyLevel < dirtyLevel &&
(tracking != null
? tracking
: (tracking = dep.get(effect2) === effect2._trackId))
) {
effect2._shouldSchedule ||
(effect2._shouldSchedule = effect2._dirtyLevel === 0);
effect2._dirtyLevel = dirtyLevel;
}
if (
effect2._shouldSchedule &&
(tracking != null
? tracking
: (tracking = dep.get(effect2) === effect2._trackId))
) {
if (!!(process.env.NODE_ENV !== "production")) {
(_a = effect2.onTrigger) == null
? void 0
: _a.call(
effect2,
extend({ effect: effect2 }, debuggerEventExtraInfo)
);
}
// 触发effet2的派发更新
effect2.trigger();
if (
(!effect2._runnings || effect2.allowRecurse) &&
effect2._dirtyLevel !== 2
) {
effect2._shouldSchedule = false;
if (effect2.scheduler) {
// 如果有计划中的函数scheduler,推入到queueEffectSchedulers中去
queueEffectSchedulers.push(effect2.scheduler);
}
}
}
}
resetScheduling();
}
// 执行完事件派发更新后
function resetScheduling() {
pauseScheduleStack--;
while (!pauseScheduleStack && queueEffectSchedulers.length) {
queueEffectSchedulers.shift()();
}
}
最终会执行到effect2.trigger()
,也可能执行到queueEffectSchedulers.push(effect2.scheduler)
的流程,至于后续的流程请自行断点调试。如果有疑问,可以评论区留言,我再补充完善。
这里主要介绍了
ref
、computed
、watch
和render
之间的关系,它们可以简单的归为发布者或订阅者,关系是收集和被收集,信息或数据流向是指向收集和通知的方向,整个响应式体系围绕着发布订阅者模式展开。