响应式到底是怎么实现的
前言
这是vue3系列源码的第六章,使用的vue3版本是3.2.45
。
推荐
背景
在上一篇文章中,我们看了一下ref
reactive
是如何定义响应式变量的。页面渲染的时候,会触发对应的get
,在这个过程过我们简单看了一下依赖收集的过程。那么这一篇文章,我们就详细的看一下,vue3的响应式原理,我们把响应式变量的get
set
过程详细的看一看。
前置
<template>
<div>{{ aa }}</div>
<div>{{ bb.name }}</div>
<div @click="change">点击</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
const change = () => {
aa.value = '小识'
bb.name = '谭记'
}
const aa = ref('小石')
const bb = reactive({ name: '潭记' })
</script>
这是页面:
这里我们只需要最简单的展示,和最简单的修改,这里同时使用了ref和reactive,也是看一下两者在源码层面的具体区别。
和上一章的单纯展示相比,我们只是多了一个change
的过程。
不过这里为了连贯,我们还是会再看一遍get
的过程,把get
set
的过程一块看看。
get
这里,我们就不再看ref和reactive函数的具体调用过程,在上一篇文章我们详细的看过了,我们这里直接到get
过程。
我们直接到renderComponentRoot
中执行了
result = normalizeVNode( render!.call(proxyToUse, proxyToUse!, renderCache, props, setupState, data, ctx) )
ref
这里的get是对setup执行结果的监听,这样我们在访问ref对象的时候,就可以不加.value
get: (target, key, receiver) => unref(Reflect.get(target, key, receiver))
接下来的get才是真正的对ref对象value
属性的监听:
get value() {
trackRefValue(this);
return this._value;
}
trackRefValue
函数是get的核心
function trackRefValue(ref) {
if (shouldTrack && activeEffect) {
ref = toRaw(ref);
if ((process.env.NODE_ENV !== 'production')) {
trackEffects(ref.dep || (ref.dep = createDep()), {
target: ref,
type: "get" /* TrackOpTypes.GET */,
key: 'value'
});
}
else {
trackEffects(ref.dep || (ref.dep = createDep()));
}
}
}
dep
const createDep = (effects) => {
const dep = new Set(effects);
dep.w = 0;
dep.n = 0;
return dep;
};
createDep函数实际上是返回了一个dep对象,这个dep对象中保存的是aa这个响应式对象的依赖相关的。
dep本身是一个Set
数据结构,这里面就是收集到的依赖。后面当aa的值变动,set
被触发的时候,就要通知Set
里面的每一个元素,去更新视图。
dep同时还有两个属性w
和n
w
属性通常用于表示当前依赖的状态n
属性通常用于表示该依赖的计数
依赖收集
trackEffects函数干的事情就是传说中的依赖收集。
function trackEffects(dep, debuggerEventExtraInfo) {
let shouldTrack = false;
if (effectTrackDepth <= maxMarkerBits) {
if (!newTracked(dep)) {
dep.n |= trackOpBit; // set newly tracked
shouldTrack = !wasTracked(dep);
}
}
else {
// Full cleanup mode.
shouldTrack = !dep.has(activeEffect);
}
if (shouldTrack) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
if ((process.env.NODE_ENV !== 'production') && activeEffect.onTrack) {
activeEffect.onTrack(Object.assign({ effect: activeEffect }, debuggerEventExtraInfo));
}
}
}
它先干了这么几件事:
- dep.n和trackOpBit
按位或
运算,0 | 2 得到 2 - dep.w和trackOpBit
按位与
运算,0 & 2得到 0,shouldTrack取反结果是true
const wasTracked = (dep) => (dep.w & trackOpBit) > 0;
接下来就是最核心的依赖收集部分了。
首先看一下activeEffect
这个对象。
这个对象,我理解的是被通知更新的对象,为什么这么说,我们往下看。
- 执行了
dep.add(activeEffect)
- 执行了
activeEffect.deps.push(dep);
dep和activeEffect对象互相保存了一份。
到这里,依赖收集的工作就结束了,get的流程也走完了。
reactive
reactive的get最终是走到了trackEffects
函数。
我们再看一下activeEffect
这个对象:
它的deps数组已经不是空的了,里面存的正是上一个ref对象生成的dep对象,现在deps又把当前的这个dep对象添加进去,所以这个函数走完之后,它有两个元素。
那么以上就是依赖收集的过程,下面我们看一下派发更新的过程。
我们只要点击一下,就能触发我们定义的change
函数,就会触发set
set
ref
首先会触发RefImpl
对象中对value的set
:
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, newVal);
}
}
hasChanged(newVal, this._rawValue)
首先会判断一下值是否发生了改变- 接着会更新
this._rawValue
- 接着会更新
this._value
的值,这时它会判断newVal
是不是一个对象,如果是对象,会执行reactive(value)
, 所以ref
在处理对象的时候,其实也是调用了reactive
- 最后执行
triggerRefValue(this, newVal)
以下内容涉及到调度系统,不感兴趣的可以直接略过,直接看下面的componentUpdateFn
函数部分。
triggerRefValue
function triggerRefValue(ref, newVal) {
ref = toRaw(ref);
if (ref.dep) {
if ((process.env.NODE_ENV !== 'production')) {
triggerEffects(ref.dep, {
target: ref,
type: "set" /* TriggerOpTypes.SET */,
key: 'value',
newValue: newVal
});
}
else {
triggerEffects(ref.dep);
}
}
}
这个函数先判断dep中是否存在依赖,如果有,那么就执行triggerEffects
函数。
function triggerEffects(dep, debuggerEventExtraInfo) {
// spread into array for stabilization
const effects = isArray(dep) ? dep : [...dep];
for (const effect of effects) {
if (effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo);
}
}
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo);
}
}
}
最终执行了triggerEffect
函数
function triggerEffect(effect, debuggerEventExtraInfo) {
if (effect !== activeEffect || effect.allowRecurse) {
if ((process.env.NODE_ENV !== 'production') && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo));
}
if (effect.scheduler) {
effect.scheduler();
}
else {
effect.run();
}
}
}
这里我们看一下参数:
- effect, 就是我们在get里面提到的
activeEffect
对象
这里直接执行了effect.scheduler()
, 那么这个scheduler到底是什么。
const componentUpdateFn = () => {...} // create reactive effect for rendering
const effect = (instance.effect =
new ReactiveEffect( componentUpdateFn,
() => queueJob(update), instance.scope // track it in component's effect scope
))
const update: SchedulerJob = (instance.update = () => effect.run())
update.id = instance.uid
那么我们就来看一下这个queueJob
函数做了什么。
queueJob
function queueJob(job: SchedulerJob) {
if (
!queue.length ||
!queue.includes(
job,
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
)
) {
if (job.id == null) {
queue.push(job)
} else {
queue.splice(findInsertionIndex(job.id), 0, job)
}
queueFlush()
}
}
这里就是把传入的update
加入到任务队列queue中,接着执行了quequFlush
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
queueFlush
函数用于触发刷新任务队列- 先检查是否正在执行刷新且没有刷新任务挂起
- 如果进入了if语句中,设置isFlushPending为true,表示刷新任务挂起
- 最后利用Promise触发刷新任务
flushJobs
我们再看一下flushJobs
函数
function flushJobs(seen?: CountMap) {
isFlushPending = false
isFlushing = true
...
try {
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
if (job && job.active !== false) {
if (__DEV__ && check(job)) {
continue
}
// console.log(`running:`, job.id)
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
}
}
} finally {
flushIndex = 0
queue.length = 0
flushPostFlushCbs(seen)
isFlushing = false
currentFlushPromise = null
// some postFlushCb queued jobs!
// keep flushing until it drains.
if (queue.length || pendingPostFlushCbs.length) {
flushJobs(seen)
}
}
}
这个函数主要干了:
- 结束任务挂起的状态,开启任务刷新的状态
- 核心是
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
job
的执行,其实也就是effect.run
的执行,也就是componentUpdateFn
函数的执行。
componentUpdateFn
到了这里,其实就比较熟悉了。
其实到这里,我们基本上已经说完了响应式的原理了。后面具体的更新,我们将在下一篇文章中详细了解。
reactive
事情还没完,我们还要看一下reactive对象的set过程。
set会先触发trigger
函数
trigger
function trigger(target, type, key, newValue, oldValue, oldTarget) {
...
if (deps.length === 1) {
if (deps[0]) {
if ((process.env.NODE_ENV !== 'production')) {
triggerEffects(deps[0], eventInfo);
}
else {
triggerEffects(deps[0]);
}
}
}
else {
...
}
}
其实也是触发了triggerEffects
方法。
和ref
没有太大的区别。
那么其实从上一篇文章和这一篇文章来看,ref函数和reactive函数没有本质上的区别,无论是在依赖收集或者更新上,实际上都是调用一样的更新函数。
总结
就让我们总结一下这个响应式的过程:
- 在render的时候,触发get,进行依赖的收集,收集到的其实是
new ReactiveEffect
得到的对象 - set的时候,对收集到的依赖最终通过调用
componentUpdateFn
函数来进行视图的更新。
以上就是响应式的全部内容。
转载自:https://juejin.cn/post/7322288075849318463