响应式到底是怎么实现的

前言

这是vue3系列源码的第六章,使用的vue3版本是3.2.45

推荐

createApp都发生了什么

mount都发生了什么

页面到底是从什么时候开始渲染的

setup中的内容到底是什么时候执行的

ref reactive是怎么实现的

背景

在上一篇文章中,我们看了一下ref reactive是如何定义响应式变量的。页面渲染的时候,会触发对应的get,在这个过程过我们简单看了一下依赖收集的过程。那么这一篇文章,我们就详细的看一下,vue3的响应式原理,我们把响应式变量的get set过程详细的看一看。

前置

js 复制代码
<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中执行了

js 复制代码
result = normalizeVNode( render!.call(proxyToUse, proxyToUse!, renderCache, props, setupState, data, ctx) )

ref

这里的get是对setup执行结果的监听,这样我们在访问ref对象的时候,就可以不加.value

js 复制代码
get: (target, key, receiver) => unref(Reflect.get(target, key, receiver))

接下来的get才是真正的对ref对象value属性的监听:

js 复制代码
 get value() {
        trackRefValue(this);
        return this._value;
    }

trackRefValue函数是get的核心

js 复制代码
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

js 复制代码
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同时还有两个属性wn

  • w 属性通常用于表示当前依赖的状态
  • n 属性通常用于表示该依赖的计数

依赖收集

trackEffects函数干的事情就是传说中的依赖收集

js 复制代码
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
js 复制代码
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:

js 复制代码
   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

js 复制代码
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函数。

js 复制代码
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函数

js 复制代码
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到底是什么。

这里就要追溯到我们之前的文章: 在页面到底是从什么时候开始渲染的这一章里面有这么一段代码,这里面的() => queueJob(update)就是传入进去成为scheduler属性。

js 复制代码
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

js 复制代码
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

js 复制代码
function queueFlush() {
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}
  • queueFlush函数用于触发刷新任务队列
  • 先检查是否正在执行刷新且没有刷新任务挂起
  • 如果进入了if语句中,设置isFlushPending为true,表示刷新任务挂起
  • 最后利用Promise触发刷新任务

flushJobs

我们再看一下flushJobs函数

js 复制代码
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

到了这里,其实就比较熟悉了。

页面到底是从什么时候开始渲染的这一章里,我们提到过,页面的渲染就是执行了effect.run函数也就是执行了componentUpdateFn函数,这里页面的更新,最终也是调用了这个函数。

其实到这里,我们基本上已经说完了响应式的原理了。后面具体的更新,我们将在下一篇文章中详细了解。

reactive

事情还没完,我们还要看一下reactive对象的set过程。

set会先触发trigger函数

trigger

js 复制代码
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函数来进行视图的更新。

以上就是响应式的全部内容。

相关推荐
weixin_382395233 小时前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__3 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.4 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~5 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华6 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子8 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅8 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni8 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学10 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端