响应式到底是怎么实现的

前言

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

推荐

createApp都发生了什么

mount都发生了什么

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

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

ref reactive是怎么实现的

背景

在上一篇文章中,我们看了一下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同时还有两个属性wn

  • 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到底是什么。

这里就要追溯到我们之前的文章:
页面到底是从什么时候开始渲染的这一章里面有这么一段代码,这里面的() => queueJob(update)就是传入进去成为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

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

页面到底是从什么时候开始渲染的这一章里,我们提到过,页面的渲染就是执行了effect.run函数也就是执行了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 作者:小识谭记

(0)
上一篇 2024年1月11日 上午10:53
下一篇 2024年1月11日 上午11:03

相关推荐

发表回复

登录后才能评论