乐趣区

关于前端:Vuejs关于响应式部分的优化

Vue 3 正式公布距今曾经快一年了,置信很多小伙伴曾经在生产环境用上了 Vue 3 了。现在,Vue.js 3.2 曾经正式公布,而这次 minor 版本的降级次要体现在源码层级的优化,对于用户的应用层面来说其实变动并不大。其中一个吸引我的点是晋升了响应式的性能:

More efficient ref implementation (~260% faster read / ~50% faster write)
~40% faster dependency tracking
~17% less memory usage

翻译过去就是 ref API 的读效率晋升约为 260%,写效率晋升约为 50%,依赖收集的效率晋升约为 40%,同时还缩小了约 17% 的内存应用。

这几乎就是一个吊炸天的优化啊,因为要晓得响应式零碎是 Vue.js 的外围实现之一,对它的优化就意味着对所有应用 Vue.js 开发的 App 的性能优化。

这次 basvanmeurs 提出的响应式性能优化真的让尤大叫苦不迭,不仅仅是大大晋升了 Vue 3 的运行时性能,还因为这么外围的代码能来自社区的奉献,这就意味着 Vue 3 受到越来越多的人关注;一些能力强的开发人员参加到外围代码的奉献,能够让 Vue 3 走的更远更好。

咱们晓得,相比于 Vue 2,Vue 3 做了多方面的优化,其中一部分是数据响应式的实现由 Object.defineProperty API 改成了 Proxy API。

既然 Proxy 慢,为啥 Vue 3 还是抉择了它来实现数据响应式呢?因为 Proxy 实质上是对某个对象的劫持,这样它不仅仅能够监听对象某个属性值的变动,还能够监听对象属性的新增和删除;而 Object.defineProperty 是给对象的某个已存在的属性增加对应的 getter 和 setter,所以它只能监听这个属性值的变动,而不能去监听对象属性的新增和删除。

而响应式在性能方面的优化其实是体现在把嵌套层级较深的对象变成响应式的场景。在 Vue 2 的实现中,在组件初始化阶段把数据变成响应式时,遇到子属性依然是对象的状况,会递归执行 Object.defineProperty 定义子对象的响应式;而在 Vue 3 的实现中,只有在对象属性被拜访的时候才会判断子属性的类型来决定要不要递归执行 reactive,这其实是一种延时定义子对象响应式的实现,在性能上会有肯定的晋升。

因而,相比于 Vue 2,Vue 3 的确在响应式实现局部做了肯定的优化,但实际上成果是无限的。而 Vue.js 3.2 这次在响应式性能方面的优化,是真的做到了质的飞跃,接下来咱们就来上点硬菜,从源码层面剖析具体做了哪些优化,以及这些优化背地带来的技术层面的思考。

响应式实现原理
所谓响应式,就是当咱们批改数据后,能够主动做某些事件;对应到组件的渲染,就是批改数据后,能主动触发组件的从新渲染。

Vue 3 实现响应式,实质上是通过 Proxy API 劫持了数据对象的读写,当咱们拜访数据时,会触发 getter 执行依赖收集;批改数据时,会触发 setter 派发告诉。

接下来,咱们简略剖析一下依赖收集和派发告诉的实现(Vue.js 3.2 之前的版本)。

依赖收集
首先来看依赖收集的过程,外围就是在拜访响应式数据的时候,触发 getter 函数,进而执行 track 函数收集依赖:

let shouldTrack = true
// 以后激活的 effect
let activeEffect
// 原始数据对象 map
const targetMap = new WeakMap()
function track(target, type, key) {if (!shouldTrack || activeEffect === undefined) {return}
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    // 每个 target 对应一个 depsMap
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    // 每个 key 对应一个 dep 汇合
    depsMap.set(key, (dep = new Set()))
  }
  if (!dep.has(activeEffect)) {
    // 收集以后激活的 effect 作为依赖
    dep.add(activeEffect)
   // 以后激活的 effect 收集 dep 汇合作为依赖
    activeEffect.deps.push(dep)
  }
}

剖析这个函数的实现前,咱们先想一下要收集的依赖是什么,咱们的目标是实现响应式,就是当数据变动的时候能够主动做一些事件,比方执行某些函数,所以咱们收集的依赖就是数据变动后执行的副作用函数。

track 函数领有三个参数,其中 target 示意原始数据;type 示意这次依赖收集的类型;key 示意拜访的属性。

track 函数内部创立了全局的 targetMap 作为原始数据对象的 Map,它的键是 target,值是 depsMap,作为依赖的 Map;这个 depsMap 的键是 target 的 key,值是 dep 汇合,dep 汇合中存储的是依赖的副作用函数。为了不便了解,能够通过下图示意它们之间的关系:

因而每次执行 track 函数,就是把以后激活的副作用函数 activeEffect 作为依赖,而后收集到 target 相干的 depsMap 对应 key 下的依赖汇合 dep 中。

派发告诉
派发告诉产生在数据更新的阶段,外围就是在批改响应式数据时,触发 setter 函数,进而执行 trigger 函数派发告诉:

const targetMap = new WeakMap()
function trigger(target, type, key) {
  // 通过 targetMap 拿到 target 对应的依赖汇合
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // 没有依赖,间接返回
    return
  }
  // 创立运行的 effects 汇合
  const effects = new Set()
  // 增加 effects 的函数
  const add = (effectsToAdd) => {if (effectsToAdd) {
      effectsToAdd.forEach(effect => {effects.add(effect)
      })
    }
  }
  // SET | ADD | DELETE 操作之一,增加对应的 effects
  if (key !== void 0) {add(depsMap.get(key))
  }
  const run = (effect) => {
    // 调度执行
    if (effect.options.scheduler) {effect.options.scheduler(effect)
    }
    else {
      // 间接运行
      effect()}
  }
  // 遍历执行 effects
  effects.forEach(run)
}

trigger 函数领有三个参数,其中 target 示意指标原始对象;type 示意更新的类型;key 示意要批改的属性。

trigger 函数 次要做了四件事件:

从 targetMap 中拿到 target 对应的依赖汇合 depsMap;

创立运行的 effects 汇合;

依据 key 从 depsMap 中找到对应的 effect 增加到 effects 汇合;

遍历 effects 执行相干的副作用函数。

因而每次执行 trigger 函数,就是依据 target 和 key,从 targetMap 中找到相干的所有副作用函数遍历执行一遍。

在形容依赖收集和派发告诉的过程中,咱们都提到了一个词:副作用函数,依赖收集过程中咱们把 activeEffect(以后激活副作用函数)作为依赖收集,它又是什么?接下来咱们来看一下副作用函数的庐山真面目。

副作用函数
那么,什么是副作用函数,在介绍它之前,咱们先回顾一下响应式的原始需要,即咱们批改了数据就能主动做某些事件,举个简略的例子:

import {reactive} from 'vue'
const counter = reactive({num: 0})
function logCount() {console.log(counter.num)
}
function count() {counter.num++}
logCount()
count()

咱们定义了响应式对象 counter,而后在 logCount 中拜访了 counter.num,咱们心愿在执行 count 函数批改 counter.num 值的时候,能主动执行 logCount 函数。

按咱们之前对依赖收集过程的剖析,如果 logCount 是 activeEffect 的话,那么就能够实现需求,但显然是做不到的,因为代码在执行到 console.log(counter.num) 这一行的时候,它对本人在 logCount 函数中的运行是无所不知的。

那么该怎么办呢?其实只有咱们运行 logCount 函数前,把 logCount 赋值给 activeEffect 就好了:

`activeEffect = logCount
logCount()`

顺着这个思路,咱们能够利用高阶函数的思维,对 logCount 做一层封装:

function wrapper(fn) {const wrapped = function(...args) {
    activeEffect = fn
    fn(...args)
  }
  return wrapped
}
const wrappedLog = wrapper(logCount)
wrappedLog()

wrapper 自身也是一个函数,它承受 fn 作为参数,返回一个新的函数 wrapped,而后保护一个全局变量 activeEffect,当 wrapped 执行的时候,把 activeEffect 设置为 fn,而后执行 fn 即可。

这样当咱们执行 wrappedLog 后,再去批改 counter.num,就会主动执行 logCount 函数了。

实际上 Vue 3 就是采纳相似的做法,在它外部就有一个 effect 副作用函数,咱们来看一下它的实现:

// 全局 effect 栈
const effectStack = []
// 以后激活的 effect
let activeEffect
function effect(fn, options = EMPTY_OBJ) {if (isEffect(fn)) {
    // 如果 fn 曾经是一个 effect 函数了,则指向原始函数
    fn = fn.raw
  }
  // 创立一个 wrapper,它是一个响应式的副作用的函数
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    // lazy 配置,计算属性会用到,非 lazy 则间接执行一次
    effect()}
  return effect
}
function createReactiveEffect(fn, options) {const effect = function reactiveEffect() {if (!effect.active) {
      // 非激活状态,则判断如果非调度执行,则间接执行原始函数。return options.scheduler ? undefined : fn()}
    if (!effectStack.includes(effect)) {
      // 清空 effect 援用的依赖
      cleanup(effect)
      try {
        // 开启全局 shouldTrack,容许依赖收集
        enableTracking()
        // 压栈
        effectStack.push(effect)
        activeEffect = effect
        // 执行原始函数
        return fn()}
      finally {
        // 出栈
        effectStack.pop()
        // 复原 shouldTrack 开启之前的状态
        resetTracking()
        // 指向栈最初一个 effect
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  }
  effect.id = uid++
  // 标识是一个 effect 函数
  effect._isEffect = true
  // effect 本身的状态
  effect.active = true
  // 包装的原始函数
  effect.raw = fn
  // effect 对应的依赖,双向指针,依赖蕴含对 effect 的援用,effect 也蕴含对依赖的援用
  effect.deps = []
  // effect 的相干配置
  effect.options = options
  return effect
}

联合上述代码来看,effect 外部通过执行 createReactiveEffect 函数去创立一个新的 effect 函数,前端培训为了和内部的 effect 函数辨别,咱们把它称作 reactiveEffect 函数,并且还给它增加了一些额定属性(我在正文中都有表明)。另外,effect 函数还反对传入一个配置参数以反对更多的 feature,这里就不开展了。

reactiveEffect 函数就是响应式的副作用函数,当执行 trigger 过程派发告诉的时候,执行的 effect 就是它。

按咱们之前的剖析,reactiveEffect 函数只须要做两件事件:让全局的 activeEffect 指向它,而后执行被包装的原始函数 fn。

但实际上它的实现要更简单一些,首先它会判断 effect 的状态是否是 active,这其实是一种管制伎俩,容许在非 active 状态且非调度执行状况,则间接执行原始函数 fn 并返回。

接着判断 effectStack 中是否蕴含 effect,如果没有就把 effect 压入栈内。之前咱们提到,只有设置 activeEffect = effect 即可,那么这里为什么要设计一个栈的构造呢?

其实是思考到以下这样一个嵌套 effect 的场景:

import {reactive} from 'vue' 
import {effect} from '@vue/reactivity' 
const counter = reactive({ 
  num: 0, 
  num2: 0 
}) 
function logCount() {effect(logCount2) 
  console.log('num:', counter.num) 
} 
function count() {counter.num++} 
function logCount2() {console.log('num2:', counter.num2) 
} 
effect(logCount) 
count()

咱们每次执行 effect 函数时,如果仅仅把 reactiveEffect 函数赋值给 activeEffect,那么针对这种嵌套场景,执行完 effect(logCount2) 后,activeEffect 还是 effect(logCount2) 返回的 reactiveEffect 函数,这样后续拜访 counter.num 的时候,依赖收集对应的 activeEffect 就不对了,此时咱们内部执行 count 函数批改 counter.num 后执行的便不是 logCount 函数,而是 logCount2 函数,最终输入的后果如下:

num2: 0
num: 0
num2: 0

而咱们冀望的后果应该如下:

num2: 0
num: 0
num2: 0
num: 1

因而针对嵌套 effect 的场景,咱们不能简略地赋值 activeEffect,应该思考到函数的执行自身就是一种入栈出栈操作,因而咱们也能够设计一个 effectStack,这样每次进入 reactiveEffect 函数就先把它入栈,而后 activeEffect 指向这个 reactiveEffect 函数,接着在 fn 执行结束后出栈,再把 activeEffect 指向 effectStack 最初一个元素,也就是外层 effect 函数对应的 reactiveEffect。

这里咱们还留神到一个细节,在入栈前会执行 cleanup 函数清空 reactiveEffect 函数对应的依赖。在执行 track 函数的时候,除了收集以后激活的 effect 作为依赖,还通过 activeEffect.deps.push(dep) 把 dep 作为 activeEffect 的依赖,这样在 cleanup 的时候咱们就能够找到 effect 对应的 dep 了,而后把 effect 从这些 dep 中删除。cleanup 函数的代码如下所示:

function cleanup(effect) {const { deps} = effect
  if (deps.length) {for (let i = 0; i < deps.length; i++) {deps[i].delete(effect)
    }
    deps.length = 0
  }
}

为什么须要 cleanup 呢?如果遇到这种场景:

<template>
  <div v-if="state.showMsg">
    {{state.msg}}
  </div>
  <div v-else>
    {{Math.random()}}
  </div>
  <button @click="toggle">Toggle Msg</button>
  <button @click="switchView">Switch View</button>
</template>
<script>
  import {reactive} from 'vue'

  export default {setup() {
      const state = reactive({
        msg: 'Hello World',
        showMsg: true
      })

      function toggle() {state.msg = state.msg === 'Hello World' ? 'Hello Vue' : 'Hello World'}

      function switchView() {state.showMsg = !state.showMsg}

      return {
        toggle,
        switchView,
        state
      }
    }
  }
</script>

联合代码能够晓得,这个组件的视图会依据 showMsg 变量的管制显示 msg 或者一个随机数,当咱们点击 Switch View 的按钮时,就会批改这个变量值。

假如没有 cleanup,在第一次渲染模板的时候,activeEffect 是组件的副作用渲染函数,因为模板 render 的时候拜访了 state.msg,所以会执行依赖收集,把副作用渲染函数作为 state.msg 的依赖,咱们把它称作 render effect。而后咱们点击 Switch View 按钮,视图切换为显示随机数,此时咱们再点击 Toggle Msg 按钮,因为批改了 state.msg 就会派发告诉,找到了 render effect 并执行,就又触发了组件的从新渲染。

但这个行为实际上并不合乎预期,因为当咱们点击 Switch View 按钮,视图切换为显示随机数的时候,也会触发组件的从新渲染,但这个时候视图并没有渲染 state.msg,所以对它的改变并不应该影响组件的从新渲染。

因而在组件的 render effect 执行之前,如果通过 cleanup 清理依赖,咱们就能够删除之前 state.msg 收集的 render effect 依赖。这样当咱们批改 state.msg 时,因为曾经没有依赖了就不会触发组件的从新渲染,合乎预期。

响应式实现的优化
后面剖析了响应式实现原理,看上去所有都很 OK,那么这外面还有哪些能够值得优化的点呢?

依赖收集的优化
目前每次副作用函数执行,都须要先执行 cleanup 革除依赖,而后在副作用函数执行的过程中从新收集依赖,这个过程牵涉到大量对 Set 汇合的增加和删除操作。在许多场景下,依赖关系是很少扭转的,因而这里存在肯定的优化空间。

为了缩小汇合的增加删除操作,咱们须要标识每个依赖汇合的状态,比方它是不是新收集的,还是曾经被收集过的。

所以这里须要给汇合 dep 增加两个属性:

export const createDep = (effects) => {const dep = new Set(effects)
  dep.w = 0
  dep.n = 0
  return dep
}

其中 w 示意是否曾经被收集,n 示意是否新收集。

而后设计几个全局变量,effectTrackDepth、trackOpBit、maxMarkerBits。

其中 effectTrackDepth 示意递归嵌套执行 effect 函数的深度;trackOpBit 用于标识依赖收集的状态;maxMarkerBits 示意最大标记的位数。

接下来看它们的利用:

function effect(fn, options) {if (fn.effect) {fn = fn.effect.fn}
  // 创立 _effect 实例 
  const _effect = new ReactiveEffect(fn)
  if (options) {
    // 拷贝 options 中的属性到 _effect 中
    extend(_effect, options)
    if (options.scope)
      // effectScope 相干解决逻辑
      recordEffectScope(_effect, options.scope)
  }
  if (!options || !options.lazy) {
    // 立刻执行
    _effect.run()}
  // 绑定 run 函数,作为 effect runner
  const runner = _effect.run.bind(_effect)
  // runner 中保留对 _effect 的援用
  runner.effect = _effect
  return runner
}

class ReactiveEffect {constructor(fn, scheduler = null, scope) {
    this.fn = fn
    this.scheduler = scheduler
    this.active = true
    // effect 存储相干的 deps 依赖
    this.deps = []
    // effectScope 相干解决逻辑
    recordEffectScope(this, scope)
  }
  run() {if (!this.active) {return this.fn()
    }
    if (!effectStack.includes(this)) {
      try {
        // 压栈
        effectStack.push((activeEffect = this))
        enableTracking()
        // 依据递归的深度记录位数
        trackOpBit = 1 << ++effectTrackDepth
        // 超过 maxMarkerBits 则 trackOpBit 的计算会超过最大整形的位数,降级为 cleanupEffect
        if (effectTrackDepth <= maxMarkerBits) {
          // 给依赖打标记
          initDepMarkers(this)
        }
        else {cleanupEffect(this)
        }
        return this.fn()}
      finally {if (effectTrackDepth <= maxMarkerBits) {
          // 实现依赖标记
          finalizeDepMarkers(this)
        }
        // 复原到上一级
        trackOpBit = 1 << --effectTrackDepth
        resetTracking()
        // 出栈
        effectStack.pop()
        const n = effectStack.length
        // 指向栈最初一个 effect
        activeEffect = n > 0 ? effectStack[n - 1] : undefined
      }
    }
  }
  stop() {if (this.active) {cleanupEffect(this)
      if (this.onStop) {this.onStop()
      }
      this.active = false
    }
  }
}

能够看到,effect 函数的实现做了肯定的批改和调整,外部应用 ReactiveEffect 类创立了一个 _effect 实例,并且函数返回的 runner 指向的是 ReactiveEffect 类的 run 办法。

也就是执行副作用函数 effect 函数时,实际上执行的就是这个 run 函数。

当 run 函数执行的时候,咱们留神到 cleanup 函数不再默认执行,在封装的函数 fn 执行前,首先执行 trackOpBit = 1 << ++effectTrackDepth 记录 trackOpBit,而后比照递归深度是否超过了 maxMarkerBits,如果超过(通常状况下不会)则依然执行老的 cleanup 逻辑,如果没超过则执行 initDepMarkers 给依赖打标记,来看它的实现:

const initDepMarkers = ({deps}) => {if (deps.length) {for (let i = 0; i < deps.length; i++) {deps[i].w |= trackOpBit // 标记依赖曾经被收集
    }
  }
}

initDepMarkers 函数实现很简略,遍历 _effect 实例中的 deps 属性,给每个 dep 的 w 属性标记为 trackOpBit 的值。

接下来会执行 fn 函数,在就是副作用函数封装的函数,比方针对组件渲染,fn 就是组件渲染函数。

当 fn 函数执行时候,会拜访到响应式数据,就会触发它们的 getter,进而执行 track 函数执行依赖收集。相应的,依赖收集的过程也做了一些调整:

function track(target, type, key) {if (!isTracking()) {return}
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    // 每个 target 对应一个 depsMap
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    // 每个 key 对应一个 dep 汇合
    depsMap.set(key, (dep = createDep()))
  }
  const eventInfo = (process.env.NODE_ENV !== 'production')
    ? {effect: activeEffect, target, type, key}
    : undefined
  trackEffects(dep, eventInfo)
}

function trackEffects(dep, debuggerEventExtraInfo) {
  let shouldTrack = false
  if (effectTrackDepth <= maxMarkerBits) {if (!newTracked(dep)) {
      // 标记为新依赖
      dep.n |= trackOpBit 
      // 如果依赖曾经被收集,则不须要再次收集
      shouldTrack = !wasTracked(dep)
    }
  }
  else {
    // cleanup 模式
    shouldTrack = !dep.has(activeEffect)
  }
  if (shouldTrack) {
    // 收集以后激活的 effect 作为依赖
    dep.add(activeEffect)
    // 以后激活的 effect 收集 dep 汇合作为依赖
    activeEffect.deps.push(dep)
    if ((process.env.NODE_ENV !== 'production') && activeEffect.onTrack) {
      activeEffect.onTrack(Object.assign({effect: activeEffect}, debuggerEventExtraInfo))
    }
  }
}

咱们发现,当创立 dep 的时候,是通过执行 createDep 办法实现的,此外,在 dep 把前激活的 effect 作为依赖收集前,会判断这个 dep 是否曾经被收集,如果曾经被收集,则不须要再次收集了。此外,这里还会判断这 dep 是不是新的依赖,如果不是,则标记为新的。

接下来,咱们再来看 fn 执行完后的逻辑:

finally {if (effectTrackDepth <= maxMarkerBits) {
    // 实现依赖标记
    finalizeDepMarkers(this)
  }
  // 复原到上一级
  trackOpBit = 1 << --effectTrackDepth
  resetTracking()
  // 出栈
  effectStack.pop()
  const n = effectStack.length
  // 指向栈最初一个 effect
  activeEffect = n > 0 ? effectStack[n - 1] : undefined
}

在满足依赖标记的条件下,须要执行 finalizeDepMarkers 实现依赖标记,来看它的实现:

const finalizeDepMarkers = (effect) => {const { deps} = effect
  if (deps.length) {
    let ptr = 0
    for (let i = 0; i < deps.length; i++) {const dep = deps[i]
      // 已经被收集过但不是新的依赖,须要删除
      if (wasTracked(dep) && !newTracked(dep)) {dep.delete(effect)
      }
      else {deps[ptr++] = dep
      }
      // 清空状态
      dep.w &= ~trackOpBit
      dep.n &= ~trackOpBit
    }
    deps.length = ptr
  }
}

finalizeDepMarkers 次要做的事件就是找到那些已经被收集过然而新的一轮依赖收集没有被收集的依赖,从 deps 中移除。这其实就是解决后面举的须要 cleanup 的场景:在新的组件渲染过程中没有拜访到的响应式对象,那么它的变动不应该触发组件的从新渲染。

以上就实现了依赖收集局部的优化,能够看到相比于之前每次执行 effect 函数都须要先清空依赖,再增加依赖的过程,当初的实现会在每次执行 effect 包裹的函数前标记依赖的状态,过程中对于曾经收集的依赖不会反复收集,执行完 effect 函数还会移除掉已被收集然而新的一轮依赖收集中没有被收集的依赖。

优化后对于 dep 依赖汇合的操作就缩小了,天然也就优化了性能。

响应式 API 的优化
响应式 API 的优化次要体现在对 ref、computed 等 API 的优化。

以 ref API 为例,来看看它优化前的实现:

function ref(value) {return createRef(value)
}

const convert = (val) => isObject(val) ? reactive(val) : val

function createRef(rawValue, shallow = false) {if (isRef(rawValue)) {
    // 如果传入的就是一个 ref,那么返回本身即可,解决嵌套 ref 的状况。return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

class RefImpl {constructor(_rawValue, _shallow = false) {
    this._rawValue = _rawValue
    this._shallow = _shallow
    this.__v_isRef = true
    // 非 shallow 的状况,如果它的值是对象或者数组,则递归响应式
    this._value = _shallow ? _rawValue : convert(_rawValue)
  }
  get value() {
    // 给 value 属性增加 getter,并做依赖收集
    track(toRaw(this), 'get' /* GET */, 'value')
    return this._value
  }
  set value(newVal) {
    // 给 value 属性增加 setter
    if (hasChanged(toRaw(newVal), this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      // 派发告诉
      trigger(toRaw(this), 'set' /* SET */, 'value', newVal)
    }
  }
}

ref 函数返回了 createRef 函数执行的返回值,而在 createRef 外部,首先解决了嵌套 ref 的状况,如果传入的 rawValue 也是个 ref,那么间接返回 rawValue;接着返回 RefImpl 对象的实例。

而 RefImpl 外部的实现,次要是劫持它的实例 value 属性的 getter 和 setter。

当拜访一个 ref 对象的 value 属性,会触发 getter 执行 track 函数做依赖收集而后返回它的值;当批改一个 ref 对象的 value 值,则会触发 setter 设置新值并且执行 trigger 函数派发告诉,如果新值 newVal 是对象或者数组类型,那么把它转换成一个 reactive 对象。

接下来,咱们再来看 Vue.js 3.2 对于这部分的实现相干的改变:

class RefImpl {constructor(value, _shallow = false) {
    this._shallow = _shallow
    this.dep = undefined
    this.__v_isRef = true
    this._rawValue = _shallow ? value : toRaw(value)
    this._value = _shallow ? value : convert(value)
  }
  get value() {trackRefValue(this)
    return this._value
  }
  set value(newVal) {newVal = this._shallow ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      triggerRefValue(this, newVal)
    }
  }
}

次要改变局部就是对 ref 对象的 value 属性执行依赖收集和派发告诉的逻辑。

在 Vue.js 3.2 版本的 ref 的实现中,对于依赖收集局部,由原先的 track 函数改成了 trackRefValue,来看它的实现:


function trackRefValue(ref) {if (isTracking()) {ref = toRaw(ref)
    if (!ref.dep) {ref.dep = createDep()
    }
    if ((process.env.NODE_ENV !== 'production')) {
      trackEffects(ref.dep, {
        target: ref,
        type: "get" /* GET */,
        key: 'value'
      })
    }
    else {trackEffects(ref.dep)
    }
  }
}

能够看到这里间接把 ref 的相干依赖保留到 dep 属性中,而在 track 函数的实现中,会把依赖保留到全局的 targetMap 中:

let depsMap = targetMap.get(target)
if (!depsMap) {
  // 每个 target 对应一个 depsMap
  targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
  // 每个 key 对应一个 dep 汇合
  depsMap.set(key, (dep = createDep()))
}

显然,track 函数外部可能须要做屡次判断和设置逻辑,而把依赖保留到 ref 对象的 dep 属性中则省去了这一系列的判断和设置,从而优化性能。

相应的,ref 的实现对于派发告诉局部,由原先的 trigger 函数改成了 triggerRefValue,来看它的实现:

function triggerRefValue(ref, newVal) {ref = toRaw(ref)
  if (ref.dep) {if ((process.env.NODE_ENV !== 'production')) {
      triggerEffects(ref.dep, {
        target: ref,
        type: "set" /* SET */,
        key: 'value',
        newValue: newVal
      })
    }
    else {triggerEffects(ref.dep)
    }
  }
}

function triggerEffects(dep, debuggerEventExtraInfo) {for (const effect of isArray(dep) ? dep : [...dep]) {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()
      }
    }
  }
}

因为间接从 ref 属性中就拿到了它所有的依赖且遍历执行,不须要执行 trigger 函数一些额定的逻辑,因而在性能上也失去了晋升。

trackOpBit 的设计
仔细的你可能会发现,标记依赖的 trackOpBit,在每次计算时采纳了左移的运算符 trackOpBit = 1 << ++effectTrackDepth;并且在赋值的时候,应用了或运算:

deps[i].w |= trackOpBit
dep.n |= trackOpBit

那么为什么这么设计呢?因为 effect 的执行可能会有递归的状况,通过这种形式就能够记录每个层级的依赖标记状况。

在判断某个 dep 是否曾经被依赖收集的时候,应用了 wasTracked 函数:

const wasTracked = (dep) => (dep.w & trackOpBit) > 0
复制代码
通过与运算的后果是否大于 0 来判断,这就要求依赖被收集时嵌套的层级要匹配。举个例子,假如此时 dep.w 的值是 2,阐明它是在第一层执行 effect 函数时创立的,然而这时候曾经执行了嵌套在第二层的 effect 函数,trackOpBit 左移两位变成了 4,2 & 4 的值是 0,那么 wasTracked 函数返回值为 false,阐明须要收集这个依赖。显然,这个需要是正当的。

能够看到,如果没有 trackOpBit 位运算的设计,你就很难去解决不同嵌套层级的依赖标记,这个设计也体现了 basvanmeurs 大佬十分扎实的计算机根底功力。

退出移动版