在Vue3中,因为reactive创立的响应式对象是通过Proxy来实现的,所以传入数据不能为根底类型,所以 ref 对象是对reactive不反对的数据的一个补充。

refreactive 中还有一个重要的工作就是收集、触发依赖,那么依赖是什么呢?怎么收集触发?一起来看一下吧:

咱们先来看一下 ref 的源码实现:

export function ref(value?: unknown) {  return createRef(value, false)}export function shallowRef(value?: unknown) {  return createRef(value, true)}const toReactive = (value) => isObject(value) ? reactive(value) : value;function createRef(rawValue: unknown, shallow: boolean) {  // 如果是ref则间接返回  if (isRef(rawValue)) {    return rawValue  }  return new RefImpl(rawValue, shallow)}class RefImpl<T> {  private _value: T  // 寄存 raw 原始值  private _rawValue: T  // 寄存依赖  public dep?: Dep = undefined  public readonly __v_isRef = true  constructor(value: T, public readonly __v_isShallow: boolean) {    // toRaw 拿到value的原始值    this._rawValue = __v_isShallow ? value : toRaw(value)    // 如果不是shallowRef,应用 reactive 转成响应式对象    this._value = __v_isShallow ? value : toReactive(value)  }  // getter拦截器  get value() {    // 收集依赖    trackRefValue(this)    return this._value  }  // setter拦截器  set value(newVal) {    // 如果是须要深度响应的则获取 入参的raw    newVal = this.__v_isShallow ? newVal : toRaw(newVal)    // 新值与旧值是否扭转    if (hasChanged(newVal, this._rawValue)) {      this._rawValue = newVal      // 更新value 如果是深刻创立并且是对象的话 还须要转化为reactive代理      this._value = this.__v_isShallow ? newVal : toReactive(newVal)      // 触发依赖      triggerRefValue(this, newVal)    }  }}

RefImpl 采纳ES6类的写法,蕴含 getset,其实大家能够用 webpack 等打包工具打包成 ES5 的代码,发现其实就是 Object.defineProperty

能够看到,shallowRefref 都调用了 createRef,只是传入的参数不同。当应用 shallowRef 时,不会调用 toReactive 去将对象转换为响应式,由此可见,shallowRef对象只反对对value值的响应式,ref对象反对对value深度响应式,ref.value.a.b.c中的批改都能被拦挡,举个:

<template>    <p>{{ refData.a }}</p>    <p>{{ shallowRefData.a }}</p>    <button @click="handleChange">change</button></template>let refData = ref({  a: 'ref'})let shallowRefData = shallowRef({  a: 'shallowRef'})const handleChange = () => {  refData.value.a = "ref1"  shallowRefData.value.a = "shallowRef1"}

当咱们点击按钮批改数据后,界面上的 refData.a 的值会变为 ref1,而 shallowRefData.a 应该会不发生变化,但其实在这个例子里,shallowRefData.a 在视图上也会发生变化的,因为批改 refData.a 时候,触发了setter函数,内会去调用 triggerRefValue(this, newVal) 从而触发了 视图更新,所以shallow的最新数据也会被更新到了视图上 (把 refData.value.a = "ref1" 去掉它就不会变了)。

ref 里最要害的还是trackRefValuetriggerRefValue,负责收集触发依赖。

如何收集依赖:

function trackRefValue(ref) {    // 判断是否须要收集依赖    // shouldTrack 全局变量,代表以后是否须要 track 收集依赖    // activeEffect 全局变量,代表以后的副作用对象 ReactiveEffect    if (shouldTrack && activeEffect) {        ref = toRaw(ref);        {            // 如果没有 dep 属性,则初始化 dep,dep 是一个 Set<ReactiveEffect>,存储副作用函数            // trackEffects 收集依赖            trackEffects(ref.dep || (ref.dep = createDep()), {                target: ref,                type: "get",                key: 'value'            });        }    }}

为什么要判断 shouldTrackactiveEffect,因为在Vue3中有些时候不须要收集依赖:

  • 当没有 effect 包裹时,比方定义了一个ref变量,但没有任何中央应用到,这时候就没有依赖,activeEffect 为 undefined,就不须要收集依赖了
  • 比方在数组的一些会扭转本身长度的办法里,也不应该收集依赖,容易造成死循环,此时 shouldTrack 为 false

*依赖是什么?

ref.dep 用于贮存 依赖 (副作用对象),ref 被批改时就会触发,那么依赖是什么呢?依赖就是 ReactiveEffect

为什么要收集依赖(副作用对象),因为在Vue3中,一个响应式变量的变动,往往会触发一些副作用,比方视图更新、计算属性变动等等,须要在响应式变量变动时去触发其它一些副作用函数。

在我看来 ReactiveEffect 其实就和 Vue2 中的 Watcher 的作用差不多,我之前写的《Vue源码学习-响应式原理》里做过阐明:

class ReactiveEffect {    constructor(fn, scheduler = null, scope) {        // 传入一个副作用函数        this.fn = fn;        this.scheduler = scheduler;        this.active = true;        // 存储 Dep 对象,如下面的 ref.dep        // 用于在触发依赖后, ref.dep.delete(effect),双向删除依赖)        this.deps = [];        this.parent = undefined;        recordEffectScope(this, scope);    }    run() {        // 如果以后effect曾经被stop        if (!this.active) {            return this.fn();        }        let parent = activeEffect;        let lastShouldTrack = shouldTrack;        while (parent) {            if (parent === this) {                return;            }            parent = parent.parent;        }        try {            // 保留上一个 activeEffect            this.parent = activeEffect;            activeEffect = this;            shouldTrack = true;            // trackOpBit: 依据深度生成 trackOpBit            trackOpBit = 1 << ++effectTrackDepth;            // 如果不超过最大嵌套深度,应用优化计划            if (effectTrackDepth <= maxMarkerBits) {                // 标记所有的 dep 为 was                initDepMarkers(this);            }            // 否则应用降级计划            else {                cleanupEffect(this);            }            // 执行过程中从新收集依赖标记新的 dep 为 new            return this.fn();        }        finally {            if (effectTrackDepth <= maxMarkerBits) {                // 优化计划:删除生效的依赖                finalizeDepMarkers(this);            }            // 嵌套深度自 + 重置操作的位数            trackOpBit = 1 << --effectTrackDepth;            // 复原上一个 activeEffect            activeEffect = this.parent;            shouldTrack = lastShouldTrack;            this.parent = undefined;            if (this.deferStop) {                this.stop();            }        }    }}

ReactiveEffect 是副作用对象,它就是被收集依赖的理论对象,一个响应式变量能够有多个依赖,其中最次要的就是 run 办法,外面有两套计划,当 effect 嵌套次数不超过最大嵌套次数的时候,应用优化计划,否则应用降级计划。

降级计划:
function cleanupEffect(effect) {    const { deps } = effect;    if (deps.length) {        for (let i = 0; i < deps.length; i++) {            // 从 ref.dep 中删除 ReactiveEffect            deps[i].delete(effect);        }        deps.length = 0;    }}

这个很简略,删除全副依赖,而后从新收集。在各个 dep 中,删除该 ReactiveEffect 对象,而后执行 this.fn()(副作用函数) 时,当获取响应式变量触发 getter 时,又会从新收集依赖。之所以要先删除而后从新收集,是因为随着响应式变量的变动,收集到的依赖前后可能不一样。

const toggle = ref(false)const visible = ref('show')effect(() = {  if (toggle.value) {    console.log(visible.value)  } else {    console.log('xxxxxxxxxxx')  }})toggle.value = true
  • 当 toggle 为 true 时,toggle、visible 都能收集到依赖
  • 当 toggle 为 false 时,只有visible 能够收集到依赖
优化计划:

全副删除,再从新收集,显著太耗费性能了,很多依赖其实是不须要被删除的,所以优化计划的做法是:

// 响应式变量上都有一个 dep 用来保留依赖const createDep = (effects) => {    const dep = new Set(effects);    dep.w = 0;    dep.n = 0;    return dep;};
  1. 执行副作用函数前,给 ReactiveEffect 依赖的响应式变量,加上 w(was的意思) 标记。
  2. 执行 this.fn(),track 从新收集依赖时,给 ReactiveEffect 的每个依赖,加上 n(new的意思) 标记。
  3. 最初,对有 w 然而没有 n 的依赖进行删除。

其实就是一个筛选的过程,咱们当初来第一步,如何加上 was 标记:

// 在 ReactiveEffect 的 run 办法里if (effectTrackDepth <= maxMarkerBits) {    initDepMarkers(this);}const initDepMarkers = ({ deps }) => {    if (deps.length) {        for (let i = 0; i < deps.length; i++) {            deps[i].w |= trackOpBit;        }    }};

这里应用了位运算,快捷高效。trackOpBit是什么呢?代表以后嵌套深度(effect能够嵌套),在Vue3中有一个全局变量 effectTrackDepth

// 全局变量 嵌套深度 let effectTrackDepth = 0;// 在 ReactiveEffect 的 run 办法里// 每次执行 effect 副作用函数前,全局变量嵌套深度会自增1trackOpBit = 1 << ++effectTrackDepth// 执行完副作用函数后会自减trackOpBit = 1 << --effectTrackDepth;

当深度为 1 时,trackOpBit为 2(二进制:00000010),这样执行 deps[i].w |= trackOpBit 时,操作的是第二位,所以第一位是用不到的。

为什么Vue3中嵌套深度最大是 30 ?

1 << 30// 0100 0000 0000 0000 0000 0000 0000 0000// 10737418241 << 31// 1000 0000 0000 0000 0000 0000 0000 0000// -2147483648 溢出

因为js中位运算是以32位带符号的整数进行运算的,最右边一位是符号位,所以可用的正数最多只能到30位。

能够看到,在执行副作用函数之前,应用 deps[i].w |= trackOpBit,对依赖在不同深度是否被依赖( w )进行标记,而后执行 this.fn(),从新收集依赖,下面说到收集依赖调用 trackRefValue 办法,该办法内会调用 trackEffects

function trackEffects(dep, debuggerEventExtraInfo) {    let shouldTrack = false;    if (effectTrackDepth <= maxMarkerBits) {        // 查看是否记录过以后依赖        if (!newTracked(dep)) {            dep.n |= trackOpBit;            // 如果 w 在以后深度有值,阐明effect之前曾经收集过            // 不是新增依赖,不须要再次收集            shouldTrack = !wasTracked(dep);        }    }    else {        shouldTrack = !dep.has(activeEffect);    }    if (shouldTrack) {        // dep增加以后正在应用的effect        dep.add(activeEffect);         // effect的deps也记录以后dep 双向援用        activeEffect.deps.push(dep);    }}

能够看到再从新收集依赖的时候,应用 dep.n |= trackOpBit 对依赖在不同深度是否被依赖( n )进行标记,这里还用到两个工具函数:

const wasTracked = (dep) => (dep.w & trackOpBit) > 0;const newTracked = (dep) => (dep.n & trackOpBit) > 0;

应用 wasTracked 和 newTracked,判断 dep 是否在以后深度被标记。比方判断依赖在深度 1 时 (trackOpBit第二位是1) 是否被标记,采纳按位与:

最初,如果曾经超过最大深度,因为采纳降级计划,是全副删除而后从新收集的,所以必定是最新的,所以只须要把 trackOpBit 复原,复原上一个 activeEffect:

finally {    if (effectTrackDepth <= maxMarkerBits) {        // 优化计划:删除生效的依赖        finalizeDepMarkers(this);    }    trackOpBit = 1 << --effectTrackDepth;    // 复原上一个 activeEffect    activeEffect = this.parent;    shouldTrack = lastShouldTrack;    this.parent = undefined;    if (this.deferStop) {        this.stop();    }}

如果没超过最大深度,就像之前说的把生效的依赖删除掉,而后更新一下deps的程序:

const finalizeDepMarkers = (effect) => {    const { deps } = effect;    if (deps.length) {        let ptr = 0;        for (let i = 0; i < deps.length; i++) {            const dep = deps[i];            // 把有 w 没有 n 的删除            if (wasTracked(dep) && !newTracked(dep)) {                dep.delete(effect);            }            else {                // 更新deps,因为有的可能会被删掉                // 所以要把后面空的补上,用 ptr 独自管制下标                 deps[ptr++] = dep;            }            // 与非,复原到进入时的状态            dep.w &= ~trackOpBit;            dep.n &= ~trackOpBit;        }        deps.length = ptr;    }};

举个简略的,了解起来可能简略点,有两个组件,一个父组件,一个子组件,子组件接管父组件传递的 toggle 参数显示在界面上,toggle 还管制着 visible 的显示,点击按钮切换 toggle 的值:

// Parent<script setup lang="ts">const toggle = ref(true)const visible = ref('show')const handleChange = () => {  toggle.value = false}</script><template>  <div>    <p v-if="toggle">{{ visible }}</p>    <p v-else>xxxxxxxxxxx</p>    <button @click="handleChange">change</button>    <Child :toggle="toggle" />  </div></template>
// Child<script setup lang="ts">const props = defineProps({  toggle: {    type: Boolean,  },});</script><template>  <p>{{ toggle }}</p></template>

第一次渲染,因为toggle 默认为 true,咱们能够收集到 togglevisible 的依赖,

  1. Parent 组件, 执行 run 办法中的 initDepMarkers 办法,首次进入,还未收集依赖,ReactiveEffectdeps 长度为0,跳过。
  2. 执行 run 办法中的 this.fn,从新收集依赖,触发 trackEffects:

    • toggle 的 dep = {n: 2, w: 0}shouldTrack 为 true,收集依赖。
    • visible 的 dep = {n: 2, w: 0}shouldTrack 为 true,收集依赖。
  3. 进入 Child 组件,执行 run 办法中的 initDepMarkers 办法,首次进入,还为收集依赖,deps长度为0,跳过。
  4. 执行 run 办法中的 this.fn,从新收集依赖,触发 trackEffects:

    • toggle 的 dep = {n: 4, w: 0}shouldTrack 为 true,收集依赖。

这样首次进入页面的收集依赖就完结了,而后咱们点击按钮,把 toggle 改为 false:

  1. Parent 组件: 执行 run 办法中的 initDepMarkers 办法,之前在 Parent 组件里收集到了两个变量的依赖,所以将他们 w 标记:

    • toggle 的 dep = {n: 0, w: 2}
    • visible 的 dep = {n: 0, w: 2}
  2. 执行 run 办法中的 this.fn,从新收集依赖,触发 trackEffects:

    • toggle 的 dep = {n: 2, w: 2}shouldTrack 为 false,不必 收集依赖。
    • visible 不显示了,所以没有从新收集到,还是 {n: 0, w: 2}
  3. 进入 Child 组件,执行 run 办法中的 initDepMarkers 办法,之前 收集过 toggle 依赖了,将 toggle 的 w 做标记,toggle 的 dep = {n: 0, w: 4}
  4. 执行 run 办法中的 this.fn,从新收集依赖,触发 trackEffects:

    • toggle 的 dep = {n: 4, w: 4}shouldTrack 为 false,不必收集依赖。

最初发现 visiblew 没有 n,在 finalizeDepMarkers 中删除掉生效依赖。

如何触发依赖:

在一开始讲到的 ref 源码里,能够看到在 setter 时会调用 triggerRefValue 触发依赖:

function triggerRefValue(ref, newVal) {    ref = toRaw(ref);    if (ref.dep) {        {            triggerEffects(ref.dep, {                target: ref,                type: "set",                key: 'value',                newValue: newVal            });        }    }}function triggerEffects(  dep: Dep | ReactiveEffect[]) {  // 循环去取每个依赖的副作用对象 ReactiveEffect  for (const effect of isArray(dep) ? dep : [...dep]) {    // effect !== activeEffect 避免递归,造成死循环    if (effect !== activeEffect || effect.allowRecurse) {      // effect.scheduler能够先不论,ref 和 reactive 都没有      if (effect.scheduler) {        effect.scheduler()      } else {        // 执行 effect 的副作用函数        effect.run()      }    }  }}

触发依赖最终的目标其实就是去执行依赖每个的副作用对象副作用函数,这里的副作用函数可能是执行更新视图、watch数据监听、计算属性等。


我集体再看源码的时候还遇到了一个问题,不晓得大家遇到没有(我看的代码版本算是比拟新v3.2.37),一开始我也是上网看一些源码的解析文章,看到好多解说 effect 这个函数的,先来看看这个办法的源码:

function effect(fn, options) {    if (fn.effect) {        fn = fn.effect.fn;    }    const _effect = new ReactiveEffect(fn);    if (options) {        extend(_effect, options);        if (options.scope)            recordEffectScope(_effect, options.scope);    }    if (!options || !options.lazy) {        _effect.run();    }    const runner = _effect.run.bind(_effect);    runner.effect = _effect;    // 返回一个包装后的函数,执行收集依赖    return runner;}

这个函数看上去挺简略的,创立一个 ReactiveEffect 副作用对象,将用户传入的参数附加到对象上,而后调用 run 办法收集依赖,如果有 lazy 配置不会主动去收集依赖,用户被动执行 effect 包装后的函数,也可能正确的收集依赖。

但我找了一圈,发现源码里一个中央都没调用,于是我就在想是不是以前用到过,当初去掉了,去commit记录里找了一圈,还真找到了:

这次更新把 ReactiveEffect 改为用类来实现,防止不必要时也创立 effect runner,节俭了17%的内存等。

原来的 effect 办法包含了当初的 ReactiveEffect,在视图更新渲染、watch等中央都间接援用了这个办法,但更新后都是间接 new ReactiveEffect,而后去触发 run 办法,不走 effect 了,能够说当初的 ReactiveEffect 类就是之前的 effect 办法 。

export function effect<T = any>(  fn: () => T,  options: ReactiveEffectOptions = EMPTY_OBJ): ReactiveEffect<T> {  const effect = createReactiveEffect(fn, options)  return effect}let uid = 0function createReactiveEffect<T = any>(  fn: () => T,  options: ReactiveEffectOptions): ReactiveEffect<T> {  const effect = function reactiveEffect(): unknown {    if (!effect.active) {      return fn()    }    if (!effectStack.includes(effect)) {      cleanup(effect)      try {        enableTracking()        effectStack.push(effect)        activeEffect = effect        return fn()      } finally {        effectStack.pop()        resetTracking()        const n = effectStack.length        activeEffect = n > 0 ? effectStack[n - 1] : undefined      }    }  } as ReactiveEffect  effect.id = uid++  effect.allowRecurse = !!options.allowRecurse  effect._isEffect = true  effect.active = true  effect.raw = fn  effect.deps = []  effect.options = options  return effect}

结尾

我是周小羊,一个前端萌新,写文章是为了记录本人日常工作遇到的问题和学习的内容,晋升本人,如果您感觉本文对你有用的话,麻烦点个赞激励一下哟~