Vue3响应式原理

2020年9月18号vue3.0正式发布,距离今天已经过去3个年头了,目前最新的版本是v3.3.4,从最初发布到现在一共经历过3个次要版本的迭代

版本迭代

v3.0.0 One Piece(海贼王) 2020年9月18号发布

  1. 内部模块解耦,新的架构提供了更好的维护性。
  2. 保留了vue2中对option api的支持,同时还引入入了Composition API,旨在解决大规模应用程序中使用 Vue 的痛点,支持类似于 React 钩子的逻辑组合和重用、更灵活的代码组织模式和更可靠的类型推断。
  3. 性能上面也做了很大的改进,初始化速度、更新、内存使用方面都有很大的减少,通过tree-shaking也能很大程度的减小最终的包大小。编译模板时也做了很多优化,静态提升、更新类型标记等
  4. 更好的类型支持、vue3使用TypeScript编写
  5. 新增实验性功能 <script setup>语法糖和<style vars>语法糖
typescript 复制代码
// <style vars> 语法糖
<template>
  <div class="text">hello</div>
</template>

<script>
export default {
  data() {
    return {
      color: 'red'
    }
  }
}
</script>

<style vars="{ color }">
.text {
  color: var(--color);
}
</style>

v3.1.0 Pluto(冥王星) 2021年6月8号发布

  1. 破坏性更新,props 中声明的 key,将一直存在。不管父组件是否传递该 key,这一直是 Vue 2 中的行为,因此被认为是一个修复。
  2. 性能改进,仅在实际更改时触发 $attrs 更新

v3.2.0 Quintessential Quintuplets 典型的五胞胎 2021年8月5号发布

  1. 实验性功能<script setup>语法糖和<style> v-bind语法糖,现在被认为是稳定的
typescript 复制代码
// <style> v-bind 语法糖
<script setup>
import { ref } from 'vue'

const color = ref('red')
</script>

<template>
  <button @click="color = color === 'red' ? 'green' : 'red'">
    Color is: {{ color }}
  </button>
</template>

<style scoped>
button {
  color: v-bind(color);
}
</style>
  1. 新增defineCustomElement用于创建Vue驱动的Web Components
  2. 新增v-memo指令,缓存一个模板的子树,如果绑定数组里的每个值都与最后一次的渲染相同,那么整个子树的更新将被跳过
  3. 新增实验性功能Reactivity Transform(响应式转换),但是今年年初这个实验性功能被废弃了,详见废弃原因
typescript 复制代码
// 响应式转换  编译前
<script setup> 
    import { $ref } from 'vue/macros'
    let count = $ref(0)
    console.log(count)
    function increment() { count++ } 
</script>

// 编译后
<script setup> 
    import { ref } from 'vue'
    let count = ref(0)
    console.log(count.value)
    function increment() { count.value++ }
</script>

v3.3.0 Rurouni Kenshin(浪客剑心) 2023年5月11号发布

  1. 宏定义的类型支持复杂类型及导入,之前 definePropsdefineEmits 的类型参数使用的类型仅限于本地类型(同一文件),并且只支持类型字面量和接口。这是因为 Vue 需要能够分析 props 接口上的属性,以便生成相应的运行时选项。此限制现已在 3.3 中解决。编译器现在可以解析导入的类型,并支持一组有限的复杂类型
typescript 复制代码
<script setup lang="ts">
    import type { Props } from './foo' 
    // 导入 + 交叉类型 
    defineProps<Props & { extraProp?: string }>() 
</script>
  1. 泛型组件,使用 <script setup> 的组件现在可以通过 generic 属性接受通用类型参数
typescript 复制代码
<script setup lang="ts" generic="T">
defineProps<{
  items: T[]
  selected: T
}>()
</script>

3.更友好的 defineEmits类型定义

typescript 复制代码
// BEFORE
const emit = defineEmits<{
  (e: 'foo', id: number): void,
  (e: 'bar', name: string, ...rest: any[]): void
}>()

// AFTER
const emit = defineEmits<{
  foo: [id: number],
  bar: [name: string, ...rest: any[]],
  baz: [name: string, ...rest: [number, boolean]]
}>()
  1. 使用 defineSlots 定义插槽类型
typescript 复制代码
<script setup lang="ts">
defineSlots<{
  default?: (props: { msg: string }) => any;
  item?: (props: { id: number }) => any
}>()
</script>
  1. 新的 defineOptions 宏允许直接在 <script setup> 中声明组件选项,而不需要单独的 <script>
typescript 复制代码
<script setup>
defineOptions({ name: 'componentName', inheritAttrs: false })
</script>
  1. 实验性功能 props 支持解构
js 复制代码
<script setup>
import { watchEffect } from 'vue'

const { msg = 'hello' } = defineProps(['msg'])

watchEffect(() => {
  // msg 将会被试做为依赖进行追踪, 就像访问 props.msg 一样,当 msg 更新副作用函数会重新执行
  console.log(`msg is: ${msg}`)
})
</script>

<template>{{ msg }}</template>
  1. 实验性功能defineModel
typescript 复制代码
<!-- BEFORE -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
console.log(props.modelValue)

function onInput(e) {
  emit('update:modelValue', e.target.value)
}
</script>

<template>
  <input :value="modelValue" @input="onInput" />
</template>

<!-- AFTER -->
<script setup>
const modelValue = defineModel()
console.log(modelValue.value)
</script>

<template>
  <input v-model="modelValue" />
</template>

vue3响应式过程

vue3由三个核心模块组成:

响应式模块(Reactivity Module)

允许我们创建响应式对象并且能够观测对象的变化,当使用对象的副作用函数运行时,这些函数会被追踪(track),当对象发生变化的时候,会触发(trigger)这些函数重新执行

编译模块(Compiler Module)

将Html templates编译成渲染函数,这个过程可能发生在浏览器端,也有可能发生在构建vue的时候,这取决于你使用vue的方式

渲染模块(Renender Module)

包含在网页上渲染组件的三个个不同阶段

  1. 渲染阶段(Render Phase), 调用渲染函数生成虚拟DOM
  2. 挂载阶段(Mount Phase),根据生成的虚拟DOM,并调用DOM API来创建网页
  3. 补丁阶段(Patch Phase),渲染器将旧的虚拟节点和新的虚拟节点进行比较,并且只更新网页中需要变化的部分

图示过程

响应式模块

响应式转换

reactive方法介绍

typescript 复制代码
/**
 * @description: 创建响应式对象
 * @param target 被代理的对象
 * @param isReadonly 是否只读
 * @param baseHandlers Object Array的捕获器
 * @param collectionHandlers Map Set的捕获器
 * @param proxyMap 一个weakMap 用于存储targe和Proxy的对应关系map
 * @return 代理对象
 */
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  if (!isObject(target)) {  // val !== null && typeof val === 'object'
    // 如果不是对象, 直接返回传入的值
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  
  // 如果需要代理的对象已经是Proxy 直接返回
  // 除了 在一个Proxy方法上 调用readonly方法
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // 如果对象已被代理 直接返回对应的Proxy
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // 只有 Object Array | Map Set WeakMap WeakSet类型的数据可以观测
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  
  // 关键一步 生成代理对象
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}

这个方法基本还算是比较容易理解的, 但是最终生成代理对象的时候,有一个判断,Object ArrayMap Set WeakMap WeakSet 用了不同的捕获器handler对象。

baseHandlers

get 捕获器

用于拦截对象的读取属性操作

typescript 复制代码
  get(target: Target, key: string | symbol, receiver: object) {
    // 响应式对象有两种模式,分别是readonly(只读)和shallow(浅层代理)
    const isReadonly = this._isReadonly,
      shallow = this._shallow
      
    // 处理一些特殊属性的访问 这些属性主要是通过api访问
    if (key === ReactiveFlags.IS_REACTIVE) {

      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (key === ReactiveFlags.IS_SHALLOW) {
      return shallow
    } else if (
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
          ? shallowReactiveMap
          : reactiveMap
        ).get(target)
    ) {
      return target
    }

    const targetIsArray = isArray(target)

    if (!isReadonly) {
      if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
        return Reflect.get(arrayInstrumentations, key, receiver)
      }
      if (key === 'hasOwnProperty') {
        return hasOwnProperty
      }
    }

    const res = Reflect.get(target, key, receiver)

    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }

    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }

    if (shallow) {
      return res
    }

    if (isRef(res)) {
      // ref unwrapping - skip unwrap for Array + integer key.
      return targetIsArray && isIntegerKey(key) ? res : res.value
    }

    if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }

get捕获器中第一步是处理一些特殊属性的访问

typescript 复制代码
    if (key === ReactiveFlags.IS_REACTIVE) {
      // 调用 isReactive()
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      // 调用 isReadonly()
      return isReadonly
    } else if (key === ReactiveFlags.IS_SHALLOW) {
      // 调用 isShallow()
      return shallow
    } else if (
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
          ? shallowReactiveMap
          : reactiveMap
        ).get(target)
    ) {
      // 调用 toRaw() 返回代理的原始对象
      return target
    }

get捕获器中第二步是针对数组做特殊的处理

typescript 复制代码
 if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
        return Reflect.get(arrayInstrumentations, key, receiver)
 }

为什么要对数组上的方法includesindexOflastIndexOf做这些特殊处理呢,我们看一段简单的示例代码就懂了

js 复制代码
const obj = {};

function myReactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver);
      if (typeof res === "object" && res !== null) {
        return myReactive(obj);
      }
      return res;
    },
  });
}
const arr = myReactive([obj]);

console.log(arr.includes(obj)); // false
console.log(arr.indexOf(obj)); // -1
console.log(arr.lastIndexOf(obj)); // -1

代码执行后分别打印了false、-1、-1,这是为什么呢?当调用includesindexOflastIndexOf这些方法时,会遍历arr,遍历arr的过程取到的是被Proxy代理过后的对象,如果拿这个Proxy对象和obj原始对象比较,肯定找不到,所以需要重写这三个方法。

pushpopshiftunshiftsplice这些方法为什么要特殊处理呢?仔细看这几个方法的执行,都会改变数组的长度。以push为例,我们查看ECMAScriptpush的执行流程说明:

在第二步中会读取数组的length属性,在第六步会设置length属性。我们知道在属性的读取过程中会进行依赖的收集,在属性的修改过程中会触发依赖执行。如果按照这样的逻辑会发生什么问题呢?我们还是以一个例子说明:

js 复制代码
const arr = reactive([])
watchEffect(() => {
  //立即运行, 且每次依赖更新时都会重新执行
  arr.push(1)
})

当向arr中进行push操作,首先读取到arr.length,将length对应的依赖effect收集起来,由于push操作会设置length,所以在设置length的过程中会触发length的依赖,执行watchEffect里面的函数,又会调用arr.push操作,这样就会造成一个死循环。

为了解决这两个问题,需要重写这几个方法。

arrayInstrumentations

typescript 复制代码
const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations()

// 对数组特殊处理的部分
function createArrayInstrumentations() {
  const instrumentations: Record<string, Function> = {}
  ;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
    instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
      const arr = toRaw(this) as any
      for (let i = 0, l = this.length; i < l; i++) {
        // 每个索引都需要进行收集依赖
        track(arr, TrackOpTypes.GET, i + '')
      }
      // 在原始对象上调用方法
      const res = arr[key](...args)
      if (res === -1 || res === false) {
        // 如果没有找到,可能参数中有响应对象,将参数转为原始对象,再调用方法
        return arr[key](...args.map(toRaw))
      } else {
        return res
      }
    }
  })

  ;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
    instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
      // 暂停依赖收集 
      // 因为push等操作是修改数组的,所以在push过程中不进行依赖的收集是合理的,只要它能够触       发依赖就可以
      pauseTracking()
      const res = (toRaw(this) as any)[key].apply(this, args)
      // 恢复依赖收集
      resetTracking()
      return res
    }
  })
  return instrumentations
}

我们在回到回到get捕获器中

typescript 复制代码
// 获取 取值操作的结果值
const res = Reflect.get(target, key, receiver);

if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
  // 如果key是Symbol内置的值或者 key在 `__proto__,__v_isRef,__isVue`里面 直接返回结果值
  return res;
}

if (!isReadonly) {
  // 如果不是只读的, 收集依赖, 因为对于只读的响应式数据,是无法对其进行修改的,所以收集它的依赖时没有用的
  track(target, TrackOpTypes.GET, key);
}

if (shallow) {
  // 如果是浅层响应式,不做深层级的转换 直接返回结果值   比如使用shallowReactive方法转换的响应式对象
  return res;
}

if (isRef(res)) {
  // 如果是结果值是ref类型
  // 如果ref是作为响应式数组中的元素直接返回ref,否则解包ref的value值
  return targetIsArray && isIntegerKey(key) ? res : res.value;
}

if (isObject(res)) {
  // 如果res是Object,进行深层响应式处理。 
  return isReadonly ? readonly(res) : reactive(res);
}

return res;

从最后的代码我们可以看出,vue3是懒惰式的创建响应式对象,只有访问对应的key,才会继续创建响应式对象,否则不用创建。

set 捕获器
typescript 复制代码
set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    let oldValue = (target as any)[key]
    if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
      return false
    }
    if (!this._shallow) {
      if (!isShallow(value) && !isReadonly(value)) {
        oldValue = toRaw(oldValue)
        value = toRaw(value)
      }
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }

    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }

set拦截器中首先获取旧值。如果旧值是只读的ref类型,而新的值不是ref,则返回false,不允许修改

js 复制代码
let oldValue = (target as any)[key]
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
   return false
}

// 比如这种情况
const state = reactive({a: readonly(ref(1))})
state.a = 2 // 这种操作是不允许的

接着判断是不是浅层代理

js 复制代码
if (!this._shallow) {
  // 新值不是浅层响应式且不是只读,新旧值取其对应的原始值
  if (!isShallow(value) && !isReadonly(value)) {
    oldValue = toRaw(oldValue)
    value = toRaw(value)
  }
  if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
    // 如果target不是数组并且旧值是ref类型,新值不是ref类型,直接修改oldValue.value为value
    oldValue.value = value  // 相当于直接装箱  在get中有拆箱操作
    return true
  }
} else {
  // in shallow mode, objects are set as-is regardless of reactive or not
  // 如果是浅层响应式,对象按原样设置
}

为什么要取新旧的原始值呢?我猜测是避免下面这种情况触发多次watchEffect回调

js 复制代码
const obj1 = {};
const obj2 = { a: reactive(obj1) };
const state = reactive(obj2);

watchEffect(() => {
  console.log("state.a", state.a);
});
state.a = obj1;

接下来就是调用Reflect.set进行赋值。然后触发依赖。

js 复制代码
// 判断操作的key是否存在
const hadKey =
isArray(target) && isIntegerKey(key)
  ? Number(key) < target.length
  : hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
// 如果是原型链上面的修改,就不触发更新
if (target === toRaw(receiver)) {
if (!hadKey) {
  // 新增值
  trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) { // Object.is(value, oldValue)
  // 如果是修改值 做新旧值的比较
  trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
delete捕获器

用于拦截对对象属性的 delete 操作

typescript 复制代码
deleteProperty(target: object, key: string | symbol): boolean {
    // 目标对象是否有对应的key
    const hadKey = hasOwn(target, key)
    // 旧值
    const oldValue = (target as any)[key]
    const result = Reflect.deleteProperty(target, key)
    if (result && hadKey) {
      // 删除成功且目标对象否有对应的key 触发依赖收集
      trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
    }
    return result
}
has捕获器

用于拦截in 操作符的代理方法。

typescript 复制代码
has(target: object, key: string | symbol): boolean {
    const result = Reflect.has(target, key)
    if (!isSymbol(key) || !builtInSymbols.has(key)) {
      // key不是symbol或者不是内置的Symbol 触发依赖收集
      track(target, TrackOpTypes.HAS, key)
    }
    return result
}
ownKeys捕获器

用于拦截Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Object.keys() 等操作

typescript 复制代码
ownKeys(target: object): (string | symbol)[] {
    track(
      target,
      TrackOpTypes.ITERATE, // 迭代类型
      isArray(target) ? 'length' : ITERATE_KEY
    )
    return Reflect.ownKeys(target)
}

collectionHandlers

为什么要对Map等类型数据写单独的捕获器函数呢?因为proxy本身也有一定的局限性

js 复制代码
const map = new Map([[1, "男"]]);
const proxyMap = new Proxy(map, {
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver);
  },
});

console.log(proxyMap.get(1)); // 报错TypeError: Method Map.prototype.get called on incompatible receiver [object Object]

具体原因我们可以参考一下Proxy 的局限性,这跟内置对象(例如 Map、Set、Date、Promise)的内部机制有关,他们的内部所有的数据存储在一个"internal slots"中。当我们访问 Set.prototype.add 其实就是通过内部的 this 来访问该方法的,但是数据代理的时候this=proxy,但是 proxy 并没有相应的"internal slots"这个东西,所以会报错。

接下来我们看下collectionHandlers的代码定义

typescript 复制代码
// 这个是collectionHandlers定义的入口,我们看到这里只定义了get捕获器
const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: createInstrumentationGetter(false, false)
}

// get捕获器的具体实现
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
  // 和baseHandler一样,他也有两种模式isReadonly(只读)和shallow(浅层代理)
  const instrumentations = shallow
    ? isReadonly
      ? shallowReadonlyInstrumentations
      : shallowInstrumentations
    : isReadonly
    ? readonlyInstrumentations
    : mutableInstrumentations

  return (
    target: CollectionTypes,
    key: string | symbol,
    receiver: CollectionTypes
  ) => {
    // 对一些内部key值判断
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (key === ReactiveFlags.RAW) {
      return target
    }

    //如果是 get has add set delete clear forEach 的方法调用,或者是获取size,那么改为调用mutabelInstrumentations里的相关方法
    return Reflect.get(
      hasOwn(instrumentations, key) && key in target
        ? instrumentations
        : target,
      key,
      receiver
    )
  }
}

看一下mutabelInstrumentations的里面定义的方法

MapSet的get
typescript 复制代码
function get(
  target: MapTypes,
  key: unknown,
  isReadonly = false,
  isShallow = false
) {
  /** 
  * 1. 针对readonly(reactive(new Map()))的情况
  * target获取的是代理对象,而rawTarget的是Map对象
  * 2. 针对reactive(new Map())的情况
  * target和rawTarget都是指向Map对象
  */
  target = (target as any)[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
  // 获取原始的key,这里是因为Map中也可以使用对象作为key,所以对象也可能是响应式的
  const rawKey = toRaw(key)
  if (!isReadonly) {
    // 非只读模式
    // 若key为代理对象,对key进行依赖收集
    if (hasChanged(key, rawKey)) {
      track(rawTarget, TrackOpTypes.GET, key)
    }
    // 对rawKey进行依赖收集
    track(rawTarget, TrackOpTypes.GET, rawKey)
  }
  // 获取Map原型链上的has方法用于判断获取成员是否存在于Map对象上
  const { has } = getProto(rawTarget)
  // 根据不同的代理模式赋值wrap,当前就是toReactive
  const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
  if (has.call(rawTarget, key)) {
    // 转换目标值为响应式对象
    return wrap(target.get(key))
  } else if (has.call(rawTarget, rawKey)) {
    // 转换目标值为响应式对象
    return wrap(target.get(rawKey))
  } else if (target !== rawTarget) {
     /**
     * 针对readonly(reactive(new Map())),即使没有匹配的键值对,也要跟踪对响应式对象某键的依赖信息
     * const state = reactive(new Map())
     * const readonlyState = readonly(state)
     * 
     * effect(() => {
     *  console.log(readonlyState.get('foo'))
     * })
     * // 打印 undefined
     * state.set('foo', 1)
     * // 打印 1
     */
    target.get(key)
  }
}
MapSethas方法
typescript 复制代码
function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {
  const target = (this as any)[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
  // 获取原始的key,这里是因为Map中也可以使用对象作为key,所以对象也可能是响应式的
  const rawKey = toRaw(key)
  if (!isReadonly) {
    // 非只读模式
    // 若key为代理对象,对key进行依赖收集
    if (hasChanged(key, rawKey)) {
      track(rawTarget, TrackOpTypes.GET, key)
    }
    // 对rawKey进行依赖收集
    track(rawTarget, TrackOpTypes.GET, rawKey)
  }
  return key === rawKey
    ? target.has(key)
    : target.has(key) || target.has(rawKey)  // 如果key为代理对象,判断有key或者有rawKey
}
MapSetsize属性
typescript 复制代码
function size(target: IterableCollections, isReadonly = false) {
  target = (target as any)[ReactiveFlags.RAW]
  // 跟踪ITERATE_KEY即所有修改size的操作均会触发访问size属性的副作用函数
  !isReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
  return Reflect.get(target, 'size', target)
}
Setadd方法
typescript 复制代码
function add(this: SetTypes, value: unknown) {
  value = toRaw(value)
  const target = toRaw(this)
  const proto = getProto(target)
  const hadKey = proto.has.call(target, value)
  if (!hadKey) {
    // 如果不存在当前数据,说明是新增的,触发响应式依赖
    trigger(target, TriggerOpTypes.ADD, value, value)
  }
  return this
}
Mapset方法
typescript 复制代码
function set(this: MapTypes, key: unknown, value: unknown) {
  value = toRaw(value)
  const target = toRaw(this)
  // 获取原型上面的方法
  const { has, get } = getProto(target)

  let hadKey = has.call(target, key)
  if (!hadKey) {
    key = toRaw(key)
    hadKey = has.call(target, key)
  } else if (__DEV__) {
    // 开发环境检查目标数据上面是否同时存在rawKey和key
    checkIdentityKeys(target, has, key)
  }

  const oldValue = get.call(target, key)
  target.set(key, value)
  if (!hadKey) {
    // 如果Map不存在对应的key,说明是新增键值数据
    trigger(target, TriggerOpTypes.ADD, key, value)
  } else if (hasChanged(value, oldValue)) {
    // 如果新旧值不同,代表有修改,触发响应式依赖
    trigger(target, TriggerOpTypes.SET, key, value, oldValue)
  }
  return this
}
MapSet的delete方法
typescript 复制代码
function deleteEntry(this: CollectionTypes, key: unknown) {
  const target = toRaw(this)
  // 获取原型上面的方法
  const { has, get } = getProto(target)
  // 检测key是否存在
  let hadKey = has.call(target, key)
  if (!hadKey) {
    key = toRaw(key)
    hadKey = has.call(target, key)
  } else if (__DEV__) {
    // 开发环境检查目标数据上面是否同时存在rawKey和key
    checkIdentityKeys(target, has, key)
  }

  const oldValue = get ? get.call(target, key) : undefined
  // forward the operation before queueing reactions
  const result = target.delete(key)
  if (hadKey) {
    // 如果存在集合中key,触发响应式依赖
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}
MapSetclear方法
typescript 复制代码
function clear(this: IterableCollections) {
  const target = toRaw(this)
  const hadItems = target.size !== 0
  const oldTarget = __DEV__
    ? isMap(target)
      ? new Map(target)
      : new Set(target)
    : undefined
  // forward the operation before queueing reactions
  const result = target.clear()
  if (hadItems) {
    // size不为0的情况下触发响应式依赖
    trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget)
  }
  return result
}
MapSetforEach方法
typescript 复制代码
function createForEach(isReadonly: boolean, isShallow: boolean) {
  return function forEach(
    this: IterableCollections,
    callback: Function,
    thisArg?: unknown
  ) {
    const observed = this as any
    const target = observed[ReactiveFlags.RAW]
    const rawTarget = toRaw(target)
    // 根据不同的代理模式赋值wrap,当前就是toReactive
    const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
    // 不是只读模式就收集ITERATE_KEY依赖,Map/Set对象元素个数发生变化则触发响应式依赖
    !isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
    return target.forEach((value: unknown, key: unknown) => {
      // 将key和value都转换为代理对象
      return callback.call(thisArg, wrap(value), wrap(key), observed)
    })
  }
}
迭代器对象相关方法

迭代器主要是对集合中的迭代方法进行处理即: ['keys', 'values', 'entries', Symbol.iterator]

typescript 复制代码
const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
iteratorMethods.forEach(method => {
mutableInstrumentations[method as string] = createIterableMethod(
  method,
  false,
  false
)

function createIterableMethod(
  method: string | symbol,
  isReadonly: boolean,
  isShallow: boolean
) {
  return function (
    this: IterableCollections,
    ...args: unknown[]
  ): Iterable & Iterator {
    const target = (this as any)[ReactiveFlags.RAW]
    const rawTarget = toRaw(target)
    const targetIsMap = isMap(rawTarget)
    // 如果是entries方法,或者是map的迭代方法,isPair为true
    const isPair =
      method === 'entries' || (method === Symbol.iterator && targetIsMap)
     /**
     * 当调用的是Map对象的keys方法,即副作用函数只依赖Map对象的键而没有依赖值。
     * 避免修改值的时候执行了不必要的依赖更新,在trigger函数中有判断。
     */
    const isKeyOnly = method === 'keys' && targetIsMap
    const innerIterator = target[method](...args)
    // 根据不同的代理模式赋值wrap,当前就是toReactive
    const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
    // 不是只读模式,就收集迭代依赖依赖
    !isReadonly &&
      track(
        rawTarget,
        TrackOpTypes.ITERATE,
        isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
      )
    // 返回包装过的迭代器,这个迭代器返回的值也是被代理的过的
    // 迭代器的值 是触发真实的迭代器获得的
    return {
      // iterator protocol
      next() {
        const { value, done } = innerIterator.next()
        return done
          ? { value, done }
          : {
              value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
              done
            }
      },
      // 可迭代协议
      [Symbol.iterator]() {
        return this
      }
    }
  }
}

副作用函数

前面vue3已经简单的了解到vue3是如何将普通对象转换为响应式对象的,接下来来研究下副作用函数,了解一个类ReactiveEffectReactiveEffect 对象会在这几种场景下创建:

  • computed(计算属性)
  • watch (侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数)
  • watchEffect (立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行)
  • render (页面渲染) 接下来我们看下ReactiveEffect的定义
typescript 复制代码
// 当前递归跟踪的effectshen深度
let effectTrackDepth = 0
// 当前活跃的effect
let activeEffect: ReactiveEffect | undefined
// 一个二进制变量,表示当前 ReactivEffect 的层级
let trackOpBit = 1
// 表示是否需要收集依赖
let shouldTrack = true
/**
 * 按位跟踪标记最多支持 30 级递归
 * 选择该值是为了使现代 JS 引擎能够在所有平台上使用 SMI(small integer 小整数).
 * 当递归深度更大时,回退到使用完全清理.
 */
const maxMarkerBits = 30

class ReactiveEffect<T = any> {
  // 是否活跃
  active = true
  // dep 数组,在响应式对象收集依赖时也会将对应的依赖项添加到这个数组中
  deps: Dep[] = []
  // 父ReactiveEffect 的实例
  parent: ReactiveEffect | undefined = undefined
  
  // 创建后可能会附加的属性,如果是 computed 则指向 ComputedRefImpl
  computed?: ComputedRefImpl<T>
  
  // 是否允许递归,会被外部更改
  allowRecurse?: boolean
  
  // 延迟停止
  private deferStop?: boolean
  
  // 停止事件
  onStop?: () => void
  // dev only
  onTrack?: (event: DebuggerEvent) => void
  // dev only
  onTrigger?: (event: DebuggerEvent) => void

  constructor(
    // 参数赋值给fn
    public fn: () => T,
    // 参数赋值给scheduler
    public scheduler: EffectScheduler | null = null,
    /**
    * 副作用的作用域, 可以捕获其中所创建的响应式副作用,  
    * 这样捕获到的副作用可以在组件卸载的时候一起处理
    */
    scope?: EffectScope
  ) {
    // 记录副作用作用域
    // 参照 effectScope可以看的明白(https://cn.vuejs.org/api/reactivity-advanced.html#effectscope)
    recordEffectScope(this, scope)
  }
  
  // run方法,执行目标函数
  run() {
    if (!this.active) {
      // 如果是非激活状态,直接执行传入的fn函数并返回其结果
      return this.fn()
    }
    // 赋值activeEffect给parent变量
    let parent: ReactiveEffect | undefined = activeEffect
    let lastShouldTrack = shouldTrack
    while (parent) {
      if (parent === this) {
        // 如果找到parent===this 直接return,跳出函数执行
        return
      }
      parent = parent.parent
    }
    try {
      // 将当前活跃activeEffect 赋值给this.parent
      this.parent = activeEffect
      // 将当前当前活跃activeEffect指向自己
      activeEffect = this
      // shouldTrack设置为true
      shouldTrack = true
      
      // 定义当前的 ReactiveEffect 层级
      trackOpBit = 1 << ++effectTrackDepth
      if (effectTrackDepth <= maxMarkerBits) {
        // 标记所有的 dep 为 was(表示过去的依赖)
        initDepMarkers(this)
      } else {
        // 降级方案,删除所有的依赖,再重新收集
        cleanupEffect(this)
      }
      // 执行目标副作用函数
      return this.fn()
    } finally {
      if (effectTrackDepth <= maxMarkerBits) {
        // 重置标记,删除旧的依赖
        finalizeDepMarkers(this)
      }
      
      // 退出当前层级
      trackOpBit = 1 << --effectTrackDepth
      // 将当前活跃实例修改为this.parent
      activeEffect = this.parent
      shouldTrack = lastShouldTrack
      // 将this.parent 赋值为undefined
      this.parent = undefined
      
      // 延时停止,这个标志是在 stop 方法中设置的
      if (this.deferStop) {
        this.stop()
      }
    }
  }

  stop() {
    // 延迟停止,需要执行完当前的副作用函数之后再停止
    if (activeEffect === this) { 
      // 在 run 方法中会判断 deferStop 的值,如果为 true,就会执行 stop 方法
      this.deferStop = true
    } else if (this.active) {
      // 清除所有的依赖追踪标记
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
      // 将active设为false
      this.active = false
    }
  }
}

看下initDepMarkers的代码实现

typescript 复制代码
const initDepMarkers = ({ deps }: ReactiveEffect) => {
  if (deps.length) {
    // 当前作用有依赖
    for (let i = 0; i < deps.length; i++) {
      // 标记依赖在当前深度被追踪,w代表was(过去的)
      deps[i].w |= trackOpBit // set was tracked
    }
  }
}

看下cleanupEffect的代码实现 zhuanlan.zhihu.com/p/461159820

typescript 复制代码
function cleanupEffect(effect: ReactiveEffect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      // 在各个 dep 中,删除该effect对象
      deps[i].delete(effect)
    }
    // 清空依赖数组
    deps.length = 0
  }
}

看下finalizeDepMarkers的代码实现

typescript 复制代码
// 当前深度是否是旧依赖
const wasTracked = (dep: Dep): boolean => (dep.w & trackOpBit) > 0

// 当前深度是否是新依赖
const newTracked = (dep: Dep): boolean => (dep.n & trackOpBit) > 0

const finalizeDepMarkers = (effect: ReactiveEffect) => {
  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
      }
      // 清理 was 和 new 标记,将它们对应深度的 bit,置为 0
      dep.w &= ~trackOpBit
      dep.n &= ~trackOpBit
    }
    deps.length = ptr
  }
}

依赖收集

在看依赖收集函数track前我们先了解一个vue3内部对象targetMap targetMap是vue3响应式一个核心的对象,主要存储了响应式对象和副作用函数的关系。

typescript 复制代码
// weakMap key是响应式对象, value是一个map
const targetMap = new WeakMap<object, KeyToDepMap>()
// map key是响应式对象的key  value是依赖这个key的所有effect
type KeyToDepMap = Map<any, Dep>
// set结构, 保证effect不重复
type Dep = Set<ReactiveEffect> & TrackedMarkers
type TrackedMarkers = {
  /**
   * wasTracked
   */
  w: number // 之前被收集
  /**
   * newTracked // 之后被收集
   */
  n: number
}

看下track的定义

typescript 复制代码
/**
 *
 * 检查当前正在运行哪个effect并将其记录为 dep
 * 记录所有依赖响应式熟悉的effect
 *
 * @param target - 响应式对象.
 * @param type - 定义访问响应式对象属性的类型.
 * @param key - 响应式对象属性的标识符
 */
function track(target: object, type: TrackOpTypes, key: unknown) {
  if (shouldTrack && activeEffect) {
    // 允许依赖收集 且当前activeEffect有值
    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 = __DEV__
      ? { effect: activeEffect, target, type, key }
      : undefined

    trackEffects(dep, eventInfo)
  }
}

function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: 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 (__DEV__ && activeEffect!.onTrack) {
      activeEffect!.onTrack(
        extend(
          {
            effect: activeEffect!
          },
          debuggerEventExtraInfo!
        )
      )
    }
  }
}

派发更新

vue3派发更新的核心是通过track函数实现的,我们来看下函数的实现

typescript 复制代码
/**
 * Finds all deps associated with the target (or a specific property) and
 * triggers the effects stored within.
 *
 * @param target - 响应式对象.
 * @param type - 定义触发副作用函数的操作类型 主要有四种类型:set、add、delete、clear
 * @param key - 响应式对象属性的标识符
 */
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // 如果没有收集过依赖,直接退出函数
    return
  }
  
  // 需要触发的依赖数组
  let deps: (Dep | undefined)[] = []
  if (type === TriggerOpTypes.CLEAR) {
    // 如果是map或set的clear操作,直接触发所有的副作用函数
    deps = [...depsMap.values()]
  } else if (key === 'length' && isArray(target)) {
    // 如果是数组的length
    const newLength = Number(newValue)
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= newLength) {
        // 触发length的依赖和下标大于新值的依赖
        deps.push(dep)
      }
    })
  } else {
    if (key !== void 0) {
      // 触发对应key的依赖
      deps.push(depsMap.get(key))
    }

    switch (type) {
      case TriggerOpTypes.ADD:
        if (!isArray(target)) {
          // 非数组 触发依赖迭代的副作用
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            // 触发依赖map key的迭代依赖
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // 数组添加新属性 且属性是number类型,这时会导致length变化
          deps.push(depsMap.get('length'))
        }
        break
      case TriggerOpTypes.DELETE:
        // 删除操作
        if (!isArray(target)) {
          // 非数组 触发依赖迭代的副作用
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
             // 触发依赖map key迭代的副作用
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        // 数组情况下 删除操作 没有触发对应的依赖
        break
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          // 触发不依赖map key的迭代依赖
          deps.push(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }

  const eventInfo = __DEV__
    ? { target, type, key, newValue, oldValue, oldTarget }
    : undefined

  // 触发所有要执行的副作用函数
  if (deps.length === 1) {
    if (deps[0]) {
      if (__DEV__) {
        triggerEffects(deps[0], eventInfo)
      } else {
        triggerEffects(deps[0])
      }
    }
  } else {
    const effects: ReactiveEffect[] = []
    for (const dep of deps) {
      if (dep) {
        effects.push(...dep)
      }
    }
    if (__DEV__) {
      triggerEffects(createDep(effects), eventInfo)
    } else {
      triggerEffects(createDep(effects))
    }
  }
}

triggerEffects代码实现

typescript 复制代码
function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  const effects = isArray(dep) ? dep : [...dep]
  for (const effect of effects) {
    if (effect.computed) {
      // 确保computed的副作用最先执行,避免在其他副作用执行时引用的是旧的计算结果
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
  for (const effect of effects) {
    if (!effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
}

triggerEffect代码实现

typescript 复制代码
/**
 * @param effect 副作用函数
 * @param debuggerEventExtraInfo 调试信息
 */
function triggerEffect(
  effect: ReactiveEffect,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  if (effect !== activeEffect || effect.allowRecurse) {
    // 判断当前执行的activeEffect和要执行的effect是不是一个,避免死循环
    // 比如避免在watchEffect里面执行count++类似的操作导致死循环
    if (__DEV__ && effect.onTrigger) {
      effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
    }
    if (effect.scheduler) {
      // 如果传入了scheduler调度函数,就执行scheduler调度函数
      effect.scheduler()
    } else {
      // 否则直接执行effect的run方法
      effect.run()
    }
  }
}

接下来我们看下具体的调度函数
computed的调度函数

typescript 复制代码
class ComputedRefImpl{
  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean,
    isSSR: boolean
  ) {
    this.effect = new ReactiveEffect(getter, () => {
      // computed 对应的调度函数
      if (!this._dirty) {
        // 重置_dirty标识
        this._dirty = true
        // 
        triggerRefValue(this)
      }
    })
    this.effect.computed = this
    this.effect.active = this._cacheable = !isSSR
    this[ReactiveFlags.IS_READONLY] = isReadonly
  }
  
   get value() {
    const self = toRaw(this)
    trackRefValue(self)
    if (self._dirty || !self._cacheable) {
      // 值被污染,重新计算 
      self._dirty = false
      // 调用run方法
      self._value = self.effect.run()!
    }
    return self._value
  }

  set value(newValue: T) {
    this._setter(newValue)
  }
}

function triggerRefValue(ref: RefBase<any>, newVal?: any) {
  ref = toRaw(ref)
  const dep = ref.dep
  if (dep) {
    if (__DEV__) {
      triggerEffects(dep, {
        target: ref,
        type: TriggerOpTypes.SET,
        key: 'value',
        newValue: newVal
      })
    } else {
      // 触发依赖它的副作用函数
      triggerEffects(dep)
    }
  }
}

watch的调度函数

typescript 复制代码
let scheduler: EffectScheduler
if (flush === 'sync') {
// 同步执行
scheduler = job as any // the scheduler function gets called directly
} else if (flush === 'post') {
// 等其他副作用执行完后再执行(Vue 更新之后,可以访问更新后的dom)
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// default: 'pre'
job.pre = true
if (instance) job.id = instance.uid
scheduler = () => queueJob(job)
}

const effect = new ReactiveEffect(getter, scheduler)

渲染函数的调度函数

typescript 复制代码
const effect = (instance.effect = new ReactiveEffect(
  componentUpdateFn,
  () => queueJob(update), // 组件渲染的调度函数
  instance.scope // 用于组件执行时访问到的effect, 方便后续一起停止,所以内部的副作用必须都是同步的,异步注册的需要自己手动停止
))
// update最终就是执行run方法
const update: SchedulerJob = (instance.update = () => effect.run())

queueJob代码实现

typescript 复制代码
// 是否正在清空
let isFlushing = false
// 清空任务待执行
let isFlushPending = false

// 任务队列
const queue: SchedulerJob[] = []
// 当前执行的任务在任务队列里面的下标
let flushIndex = 0
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
let currentFlushPromise: Promise<void> | null = null

// 将任务放到执行队列中
function queueJob(job: SchedulerJob) {
  // 重复任务搜索 用Array.includes()方法,默认从当前执行的任务下标开始搜索,避免任务递归触发。
  // 如果任务是watch的callback,就从当前执行的任务下标+1开始搜索,目的是允许他递归触发
  if (
    !queue.length ||
    !queue.includes(
      job,
      isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
    )
  ) {
    if (job.id == null) {
      // 没有id的任务放到队尾
      queue.push(job)
    } else {
       // 否则将任务插入到合适的位置
      queue.splice(findInsertionIndex(job.id), 0, job)
    }
    // 清空任务队列
    queueFlush()
  }
}

// 使用二分查找,根据id,在目标队列里面寻找合适的插入位置,确保任务id递增
function findInsertionIndex(id: number) {
  let start = flushIndex + 1
  let end = queue.length

  while (start < end) {
    const middle = (start + end) >>> 1
    const middleJobId = getId(queue[middle])
    middleJobId < id ? (start = middle + 1) : (end = middle)
  }

  return start
}

function queueFlush() {
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    // 在下一个微任务里面清空任务, 记录当前微任务
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}

function nextTick<T = void, R = void>(
  this: T,
  fn?: (this: T) => R
): Promise<Awaited<R>> {
  const p = currentFlushPromise || resolvedPromise
  // 确保其他的任务在当前队列清空后执行
  return fn ? p.then(this ? fn.bind(this) : fn) : p
}

// 清空任务函数
function flushJobs(seen?: CountMap) {
  isFlushPending = false
  isFlushing = true

  // 给任务任务排序
  // 1. 确保任务执行先父后子
  // 2. 父组件执行更新时,如果子组件已经卸载,可以跳过子组件的更新
  queue.sort(comparator)


  try {
    for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
      const job = queue[flushIndex]
      if (job && job.active !== false) {
        // 任务active标识不为false(在组件卸载的时候这个值会被设置为fasle)
        // 如果任务的active
        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)
    }
  }

总结

vue2vue3响应式部分共同点

  1. 基本实现思路差不多,都是对数据做响应式转换,然后在数据被访问的时候收集依赖,在数据变化的时候触发更新 vue2vue3响应式部分不同同点
  2. vue3有更友好的typescript支持,可以看到整个vue3的代码库也是用typescript编写
  3. vue3优化了系统模块结构,开发人员可以只导入需要的API,从而减少最终的包体积大小
  4. vue3使用Proxy取代Object.defineProperty做数据操作劫持,并且增加对Map和Set数据结构的响应式处理
  5. vue3是惰性的创建响应式,深层属性只有对应的数据被使用到才会继续创建响应式代理,而vue2在数据创建开始就递归的完成所有深层的响应式代理
  6. vue3可以更快更准确的更新,例如在vue2中如果对象新增属性使用this.$set是会触发所有用到这个对象的依赖更新,而在vue3中控制了只会触发用到具体属性的地方更新
相关推荐
糊涂涂是个小盆友2 小时前
前端 - 使用uniapp+vue搭建前端项目(app端)
前端·vue.js·uni-app
开心工作室_kaic5 小时前
ssm111基于MVC的舞蹈网站的设计与实现+vue(论文+源码)_kaic
前端·vue.js·mvc
bug爱好者6 小时前
如何解决sourcetree 一打开就闪退问题
前端·javascript·vue.js
迂 幵6 小时前
vue el-table 超出隐藏移入弹窗显示
javascript·vue.js·elementui
上趣工作室6 小时前
vue2在el-dialog打开的时候使该el-dialog中的某个输入框获得焦点方法总结
前端·javascript·vue.js
家里有只小肥猫6 小时前
el-tree 父节点隐藏
前端·javascript·vue.js
_xaboy8 小时前
开源项目低代码表单设计器FcDesigner扩展自定义的容器组件.例如col
vue.js·低代码·开源·动态表单·formcreate·低代码表单·可视化表单设计器
_xaboy8 小时前
开源项目低代码表单设计器FcDesigner扩展自定义组件
vue.js·低代码·开源·动态表单·formcreate·可视化表单设计器
mez_Blog8 小时前
Vue之插槽(slot)
前端·javascript·vue.js·前端框架·插槽
爱睡D小猪8 小时前
vue文本高亮处理
前端·javascript·vue.js