侧边栏

Vue3源码分析——数据侦测

发布于 | 分类于 源码分析/Vue

Vue3.0发布beta版本了,还是来凑个热闹看看源码。本系列大概会有三篇文章,包括应用整体流程、新的响应式系统、组合式API相关内容。

Vue的一个特点就是数据响应式系统,由于这是一个比较独立的系统,因此我们先从这里开始,看看新的Proxy是如何实现数据依赖收集和通知的。

本文内容有点长,可以按目录分章节阅读。

参考:

预备知识

在学习之前需要了解一些JavaScript预备知识:ReflectProxy

Reflect

Reflect封装一些对象的操作,主要是为了整合之前JS中存在的一些不太规范的地方

感觉Reflect的作用主要是把一些零散的工具函数整合在Reflect这个对象上,如indelete等运算符和Function.prototype.apply等API

一个比较有意思的功能是Reflect.get的第三个参数receiver

js
var a = {
  name: 'a',
  get greet(){
    return 'hello ' + this.name
  }
}

console.log(a.greet); // "hi, I am a"

var receiver = {
  name: 'receiver'
}
// receiver 修改类似于 通过函数的apply修改内部this指向一样,不过这里修改的是访问对象
// 通过这个功能,可以实现新对象借助原对象部分属性和方法的功能
var res = Reflect.get(a, 'greet', receiver); // "hi, I am receiver"
console.log(res)

JavaScript Proxy

为了收集数据依赖、以及在数据变化时更新视图,Vue2通过defineProperty劫持set和get属性访问描述符。

defineProperty存在一些问题,最常见的问题就是无法监听对象以及数组动态添加的属性,即使Vue2重写了数组原型相关方法,但仍旧无法监听到arr[1]=xxx这种形式。

Vue3使用了ES6新增的Proxy接口来替代defineProperty。在本章节主要了解Proxy的基本使用、一些特殊用法和Proxy本身存在的缺点。

Proxy 对象用于定义基本操作的自定义行为

js
const p = new Proxy(target, handler)

其中handler对象是一个容纳一批特定属性的占位符对象,它包含有 Proxy 的各个捕获器trap,如setget`等

一些需要注意的细节

  • set方法应该返回一个布尔值,在严格模式下,如果set方法返回falsish(包括undefined、false等),会抛出异常,这些细节比较麻烦,可以通过Reflect来处理

  • 如果代理对象是数组,当调用pushpop等方法时,不仅会改变数组元素,也会改变length等属性,此时如果代理了set,则会被触发多次。

js
let arr = [100,200,300]
let p = new Proxy(arr, {
  get(target, key) {
    return target[key]
  },
  set(target, key, value, receiver) {
    console.log('set value', key)
    target[key] = value
    return true
  }
})

p.push(400)
// set value 3 第一次data[3] = 400
// set value length 第二次 data.length = 4

这种多次触发handler的特性在某些场景下是冗余的,如果在set后会重新渲染视图,则多次set可能导致多次渲染(在不考虑flushQueue入队列去重的情况下)。

  • proxy只能代理一层
js
let o = {x:{y:100},z:10}
let p = new Proxy(o, {
    get(target, key) {
        console.log('get value', key)
        return target[key]
    },
    set(target, key, value, receiver) {
        console.log('set value', key)
        target[key] = value
        return true
    }
})

p.x.y =100 // 只输出了get value x, 无法监听到set value

因此,当代理对象是多层嵌套结构时,需要开发者自己实现将嵌套的属性对象转换为Proxy对象

js
let handler = {
    get(target, key) {
        console.log('get value', key)
        return target[key]
    },
    set(target, key, value, receiver) {
        console.log('set value', key)
        target[key] = value
        return true
    }
}

let x = {y:100}
let o = {x:new Proxy(x, handler),z:10}
let p = new Proxy(o, handler)

p.x.y =100
// get value x
// set value y

综上就是Proxy存在的一些问题

  • 代理数组时可能存在多次触发trap的情况,需要实现去重
  • 嵌套对象需要手动转换为Proxy,可以通过递归实现

TypeScript

Vue2使用flow进行类型检测,Vue3全面拥抱TypeScript,因此阅读源码需要具备一点TS相关知识,参考之前整理的

lerna

Vue3采用了lerna组织项目源码(目前很多大型项目都采用lerna了),之前写过一篇使用verdaccio和lerna和管理npm包,这里就不过多介绍了。

开发环境

接下来搭建源码开发环境

sh
# 如果下载比较慢的话可以从gitee上面克隆备份仓库
git clone git+https://github.com/vuejs/vue-next.git

# 安装依赖,需要一会
yarn install

# 开始rollup watch
npm run dev

cd packages/vue/examples/

examples下可以查看各种demo代码,这里我们直接选择composition目录下的代码查看新的语法,强烈建议在阅读源码之前先看看composition-api文档,这对了解Vue3中的API设计有非常重要的帮助!!

然后修改一下源码vue/src/index.ts,刷新页面,就可以看见改动后的效果,接下来就开始愉快的源码时间~

rective

文档里面展示了一个关于rective的基本用例

js
const { reactive, watchEffect, computed } = Vue
const state = reactive({
    count: 0
})

function render() {
    document.body.innerHTML = `count is ${state.count}`
}

watchEffect(render) // 初始化会调用一次render

setTimeout(() => {
    state.count = 100 // state.count发生变化,会通过watchEffect重新触发render
}, 1000)

看起来reactive方法会代理返回代理对象,而watchEffect会在对象属性发生变化时重新执行注册的回调函数render

接下来就从这两个方法开始一探究竟

ts
function reactive(target: object) {
  return createReactiveObject(target,false,mutableHandlers,mutableCollectionHandlers)
}

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  // 检测target是否是对象,是否已经设置__v_isReactive、__v_skip、__v_isReadonly等
  // ...
  const observed = new Proxy(
    target,
    // 根据target是否是Set, Map, WeakMap, WeakSet对象来判断使用哪一种handler
    collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers 
  )
  
  return observed
}

先看看普通对象的baseHandlers,也就是传入的mutableHandlers配置项

ts
export const mutableHandlers: ProxyHandler<object> = {
  get: createGetter(),
  set: createSetter(),
  // 下面三个方法均是通过`Reflect`来实现相关操作
  deleteProperty,
  has,
  ownKeys
}

createGetter 收集依赖

ts
const targetMap = new WeakMap<any, KeyToDepMap>()

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: object, key: string | symbol, receiver: object) {
    // ... 根据 key与ReactiveFlags 返回特殊值
    // ... 处理target为数组时的get操作

    const res = Reflect.get(target, key, receiver)
    // ... 处理res是Ref类型的操作
    if (isRef(res)) {
      if (targetIsArray) {
        !isReadonly && track(target, TrackOpTypes.GET, key)
        return res
      } else {
        // ref unwrapping, only for Objects, not for Arrays.
        return res.value
      }
    }
    // 收集依赖
    !isReadonly && track(target, TrackOpTypes.GET, key)
    // 判断属性值类型,递归调用reactive处理,返回新的Proxy
    return isObject(res) ? reactive(res) : res
  }
}

可以看见,只有当触发get的时候,才会检测属性值的类型,然后对属性值进行reactive操作,整体性能就Vue2在初始化时就递归劫持所有get而言,应该有不少提升。

我们知道,在触发get时需要收集依赖,可以看见track就是处理这个工作的

ts
export function track(target: object, type: TrackOpTypes, key: unknown) {
  // 初始化target的依赖列表,通过Map保存,每个依赖可能依赖target某个或某些属性,因此该Map的键值是target的每个属性
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  // 对于每个属性key而言,通过Set保存依赖该key的activeEffect
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  // activeEffect是一个全局变量
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
  }
}

在渲染视图时,在render方法中,会通过get触发track,然后将activeEffect添加到数据属性的依赖列表中,这样就完成了依赖收集

effect 变化的抽象

我们这里接触到了一个新的概念effect,包括前面的effectwatchEffect等方法。类似于Watcher对象,用于封装各种变化。

在示例中的watchEffect注册的回调函数,就可以理解为一个effect。

ts
// 从类型声明可以看出,effect是一个包含如下属性的函数
export interface ReactiveEffect<T = any> {
  (...args: any[]): T
  _isEffect: true
  id: number
  active: boolean
  raw: () => T
  deps: Array<Dep>
  options: ReactiveEffectOptions
}

然后来看看watchEffect的实现

ts
export function watchEffect(
  effect: WatchEffect,
  options?: WatchOptionsBase
): WatchStopHandle {
  return doWatch(effect, null, options)
}

function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect,
  cb: WatchCallback | null,
  { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
  const instance = currentInstance
  // 根据source的类型封装getter,内部执行source的调用
  let getter: () => any = () => {return callWithErrorHandling(source, instance) }

  // 根据 flush字符串初始化调度器,决定何时调用getter
  let scheduler: (job: () => any) => void = job => queuePostRenderEffect(job, instance && instance.suspense)

  // 调用effect注册
  const runner = effect(getter, {
    lazy: true, // 由于下面会直接调用runner,因此lazy传入了true
    computed: true,
    onTrack,
    onTrigger,
    scheduler: applyCb ? () => scheduler(applyCb) : scheduler
  })

  recordInstanceBoundEffect(runner)
  runner()

  // 返回一个取消effect的方法
  return () => {
    stop(runner)
    if (instance) {
      remove(instance.effects!, runner)
    }
  }
}

我们在这里看见了一个与mountComponentinstance.update类似的runner方法,他们实际上都是一个通过effect包装的函数。

ts
const effectStack: ReactiveEffect[] = []
let activeEffect: ReactiveEffect | undefined

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  // 创建effect对象(函数)
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    // 在前面初始化instance.update时会先调用一次 componentEffect,从而完成页面的初始化渲染 
    effect() 
  }
  return effect
}
// 下面这段源码基本是原样copy过来的,没有做删减
function createReactiveEffect<T = any>(
  fn: (...args: any[]) => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(...args: unknown[]): unknown {
    if (!effect.active) {
      return options.scheduler ? undefined : fn(...args)
    }
    if (!effectStack.includes(effect)) {
      cleanup(effect) // 清除effect.deps
      try {
        enableTracking()
        effectStack.push(effect)
        activeEffect = effect // 将全局变量activeEffect 设置为当前运行的effect,然后调用effect
        return fn(...args)
      } finally {
        // finally中的代码始终都能执行
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1] // 将activeEffect重置为上一个effect
      }
    }
  } as ReactiveEffect
  effect.id = uid++
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}

整理一下watchEffect(cb)的流程

  • doWatch中将cb封装在getter中,
  • 调用effect(getter),通过createReactiveEffect返回一个真正的ReactiveEffect函数,赋值给runner
  • 回到doWatch中,直接调用runner执行
    • 在ReactiveEffect运行时会通过effectStack设置当前全局变量activeEffect

activeEffect是一个非常重要的全局变量,在前面的watchEffect(render)中,会在初始化时运行render

ts
function render() {
  document.body.innerHTML = `count is ${state.count}`
}

由于内部方法内部访问了state.count,会触发state代理的get操作,这样就能够在track的时候通过activeEffect访问到封装了render方法的这个effect,这样当state.count发生变化时,才能够再次运行相关的effect,我们修改一下render方法

js
function render() {
  // document.body.innerHTML = `count is ${state.count}`
  console.log('render') // 只有第一次初始化的时候回打印render
  document.body.innerHTML = '123'
}

watchEffect(render)
state.count // 触发一个get,然而并没有activeEffect,因此不会收集到相关的依赖
setTimeout(() => {
  state.count = 100 // state.count更新时也不会触发render
}, 1000)

可以看见,如果在回调中没有触发state.count,则无法正确track到依赖。结合getactiveEffect,可以精确到收集每个属性变化时对应的的effect,这是非常高效的。

另外在watchEffect的源码中可以看见,由于runner是同步执行的,在执行完毕后会将activeEffect进行重置,如果我们在render方法中通过异步的方式访问state.count,也无法正确track依赖。

js
function render() {
  setTimeout(()=>{
    document.body.innerHTML = `count is ${state.count}` // 放在回调里面
  })
}
watchEffect(render)
setTimeout(() => {
  state.count = 100 // 也不会更新视图
}, 1000)

createSetter 通知变化

最后,我们来看看set代理

ts
function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    const oldValue = (target as any)[key]
    // 处理 ... ref
    const hadKey = hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    // 判断target === toRaw(receiver) ,不处理target原型链更新的情况
    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
  }
}

get中的track同理,trigger应该就是更新数据并通知依赖进行处理的逻辑了

ts
export function trigger(
  target: object,
  type: TriggerOpTypes, // 表示不同的变化,如ADD、SET等
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  // 找到target某个key的依赖
  const depsMap = targetMap.get(target)

  const effects = new Set<ReactiveEffect>()
  const computedRunners = new Set<ReactiveEffect>()
  // 将对应key的变化添加到effects或者computedRunners中
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || !shouldTrack) {
          if (effect.options.computed) {
            computedRunners.add(effect)
          } else {
            effects.add(effect)
          }
        }
      })
    }
  }

  // 根据type和key找到需要处理的depsMap,这里处理了各种特殊情况
  add(depsMap.get(key))
  // ...

  // 遍历effects和computedRunners
  const run = (effect: ReactiveEffect) => {
    // 如果effect自己配置了scheduler,则使用调度器运行effect
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect() // 可以看见effect实际上是一个函数
    }
  }

  // 计算属性先运行,这样可以保证其他属性在运行时能够获取到计算属性的值
  computedRunners.forEach(run)
  effects.forEach(run)
}

小结:实现极简版reactive

前面的代码忽略了shallow浅reactive、readonly等情况,我们可以实现一个50行代码的reactive

js
let activeEffect
let targetMap = new Map()

function reactive(obj){
    return new Proxy(obj, {
        get(target, key){
            track(target, key)
            return target[key]
        },
        set(target, key, value){
            target[key] = value
            trigger(target, key)
            return true
        }
    })
}

function track(target, key){
    let depMap = targetMap.get(target)
    if(!depMap) {
        targetMap.set(target, (depMap = new Map()))
    }
    let dep = depMap.get(key)
    if(!dep) {
        depMap.set(key, ( dep = new Set()))
    }
    if(!dep.has(activeEffect)){
        dep.add(activeEffect)
    }
}

function watchEffect(cb){
    activeEffect = cb
    cb()
}

function trigger(target, key){

    let depMap = targetMap.get(target)
    if(!depMap) return 
    let effects =  depMap.get(key)
    if(!effects) return 
    
    effects.forEach((effect)=>{
        effect()
    })
}

然后测试一下

js
let {reactive, watchEffect} = require('./reactive')


let state = reactive({
    x: 100
})

function render(){
  let msg = `render template with state.x = ${state.x}`
  console.log(msg)
}

watchEffect(render)

setTimeout(()=>{
    state.x = 200
}, 1000)

上面的代码有意忽略了属性嵌套、数组set多次执行等很多细节问题,主要是为了展示了reactive最基本的结构。

避免effect重复执行

我们还忽略了一个比较重要的特点,当依赖的状态连续变化时,如何避免中间不比较的trigger呢?比如在上面的demo代码中

js
setTimeout(() => {
  state.x = 100
  console.log(state.x) // 100
  state.x = 200
  console.log(state.x) // 200
}, 1000)
// 会连续打印两次 render template with state.x = 100 | 200

在某些时候,比如渲染视图,第一次set时就触发render方法是完全没有必要的且很浪费性能的事情。

在Vue2中,Watcher会被加入到一个队列中,并在加入时进行去重,在nextTick中统一运行队列。在上面的demo中,由于activeEffect都是render方法,我们可以通过debounce的方式实现只调用一次render

那么Vue3是如何实现的呢?我们来研究一下

在trgger的时候可以看见

ts
const run = (effect: ReactiveEffect) => {
    // 如果effect自己配置了scheduler,则使用调度器运行effect
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect() // 可以看见effect实际上是一个函数
    }
  }

doWatch中可以看见下面的代码

ts
scheduler = job => queuePostRenderEffect(job, instance && instance.suspense)
const runner = effect(getter, {
  //... 其他配置
  scheduler
})

可以看见在doWatch构造的effect中,会传入一个scheduler配置,当effect run的时候,会调用这个scheduler,此处就会执行queuePostRenderEffect

ts
// 使用了两个全局队列来维护
const queue: (Job | null)[] = []
const postFlushCbs: Function[] = []

export const queuePostRenderEffect = __FEATURE_SUSPENSE__
  ? queueEffectWithSuspense
  : queuePostFlushCb

export function queuePostFlushCb(cb: Function | Function[]) {
  // 将effect放入postFlushCbs队列
  if (!isArray(cb)) {
    postFlushCbs.push(cb)
  } else {
    postFlushCbs.push(...cb)
  }
  queueFlush()
}

export function queueJob(job: Job) {
  if (!queue.includes(job)) {
    queue.push(job)
    queueFlush()
  }
}

function queueFlush() {
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    nextTick(flushJobs) // nextTick直接使用的Promise.then,在nextTick中执行flushJobs
  }
}

function flushJobs(seen?: CountMap) {
  isFlushPending = false
  isFlushing = true
  let job

  // 组件由父组件向子组件更新,父组件的id始终比子组件小(先构造)
  queue.sort((a, b) => getId(a!) - getId(b!))

  while ((job = queue.shift()) !== undefined) {
    if (job === null) {
      continue
    }
    // 依次运行effect
    callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
  }

  flushPostFlushCbs(seen)
  isFlushing = false
  // 如果在运行过程中调用了queueJob或者queuePostRenderEffect,则继续执行flushJobs
  if (queue.length || postFlushCbs.length) {
    flushJobs(seen)
  }
}

// 依次运行postFlushCbs中的回调,在前面scheduler中添加的job就会通过queuePostRenderEffect放在postFlushCbs中
export function flushPostFlushCbs(seen?: CountMap) {
  if (postFlushCbs.length) {
    // 关键的异步,对postFlushCbs进行去重,这意味着即使postFlushCbs存在多个相同的effect,也只会被执行一次
    const cbs = [...new Set(postFlushCbs)]
    postFlushCbs.length = 0
    for (let i = 0; i < cbs.length; i++) {
      cbs[i]()
    }
  }
}

这里的代码比较简单的展示了我们的effect是如何执行的

  • 首先通过注册配置项scheduler,在trigger的时候调用scheduler(effect)
  • scheduler调用queuePostFlushCb将effect放入全局队列postFlushCbs中,同时将flushJobs注册到nextTick
  • 在flushJobs中会清空queue和postFlushCbs,在清空postFlushCbs之前,会通过Set对postFlushCbs进行去重
  • 这样相同的effect在同一次flushJobs中,只会被执行一次,而不论之前trigger的时候调用了多少次scheduler(effect)

Ref

前面的源码分析让我们了解了Vue3中是如何通过Proxy代理对象,并在get的时候track effect,在set的时候trigger effect。

在某些时候需要一个依赖于其他状态的数据,可以通过计算属性来获得,计算属性作为一个函数,可以返回各种类型的值,并且当计算属性依赖的状态发生变化时,会自动重新计算并通知依赖该计算实现的地方。

官网阐述了一种最简单的实现方案,通过闭包和watchEffect实现computed(强烈建议大家先去看看官网这篇文章,对于理解Vue3的设计有很大帮助)

js
function computed(getter) {
  let value
  watchEffect(() => {
    value = getter()
  })
  return value
}

那么问题来了,当计算属性返回的是基础类型的值,虽然能够重新运行watchEffect的回调并更新值,但无法通知之前那个oldValue的依赖, 这是因为JavaScript中普通类型是按值而非引用传递的。

解决这个问题的办法就是返回一个对象,然后代理响应值收集依赖,Vue3把这种新的类型叫做Ref

js
function computed(getter) {
  const ref = {
    value: null,
  }
  watchEffect(() => {
    ref.value = getter()
  })
  return ref
}

接下来就从computed开始,看看Ref的作用与实现

下面是基本的demo代码

js
const state = reactive({
  count: 1
})
const doubleCount = computed(() => {
  return state.count * 2
})

function render() {
  document.body.innerHTML = `count is ${doubleCount.value}`
}

watchEffect(render)

setTimeout(() => {
  state.count = 100
}, 1000)

computed

下面是computed的源码

ts
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set // 处理set computed
  }

  let dirty = true // 判断getter是否需要重新运算
  let value: T 
  let computed: ComputedRef<T>

  const runner = effect(getter, {
    lazy: true, // 只有当用到computed的时候才运行求职
    computed: true, // 将effect.computed标志为true,这样会在普通的effect之前运行
    // 自定义调度器,在trigger时调用
    scheduler: () => {
      // 通知使用了computed.value的effect,只有当计算属性的getter已经被运行过才进行通知
      if (!dirty) {
        dirty = true
        trigger(computed, TriggerOpTypes.SET, 'value')
      }
    }
  })
  computed = {
    __v_isRef: true,
    effect: runner,
    get value() {
      if (dirty) {
        value = runner() // 调用runner 完成activeEffect设置,这样当计算属性依赖的其他状态发生变化时,可以重新触发getter,
        dirty = false // 缓存已经计算过的值
      }
      // runner运行完毕后会重置activeEffect为上一个effect,然后将依赖该computed的activeEffect添加到依赖中
      track(computed, TrackOpTypes.GET, 'value')
      return value
    },
    set value(newValue: T) {
      setter(newValue)
    }
  } as any
  // 可以看见,计算属性返回的是一个特殊的对象
  return computed
}

OK,看起来就比较清楚了,结合上面的例子

js
const doubleCount = computed(() => {
  return state.count * 2
})
function render() {
  document.body.innerHTML = `count is ${doubleCount.value}`
}
watchEffect(render)

把整个流程简化为

上游数据 -> computed -> 下游数据

则具体过程

  • 调用computed(getter)时会初始化一个封装了getter的effect
  • 当触发计算属性get value时,会运行的effect
    • 此时触发getter中计算属性依赖的上游数据的get,同时通过调用track收集依赖于当前计算属性的下游数据
    • 当上游数据发生变化时,会重新触发effect,由于这里自定义了scheduler,因此会使用scheduler(effect)的方式运行effect,
    • scheduler(effect)中重置dirty,然后调用trigger通知下游数据

Ref

同理,我们可以看看普通的Ref的实现

ts
export function ref(value?: unknown) {
  return createRef(value)
}
function createRef(rawValue: unknown, shallow = false) {
  if (isRef(rawValue)) {
    return rawValue
  }
  let value = shallow ? rawValue : convert(rawValue)
  const r = {
    __v_isRef: true,
    get value() {
      track(r, TrackOpTypes.GET, 'value')
      return value
    },
    set value(newVal) {
      if (hasChanged(toRaw(newVal), rawValue)) {
        rawValue = newVal
        value = shallow ? newVal : convert(newVal)
        trigger(
          r,
          TriggerOpTypes.SET,
          'value'
        )
      }
    }
  }
  return r
}

有了computed的经验,这里这里看起来就比较容易了,在get value的时候track收集activeEffect,在set value的时候,如果值发生了变化,就通过trigger通知effect。

小结

本文主要整理了Vue3中新的响应式系统,

  • 通过effect封装变化,在Vue2中是通过Watcher负责的
  • 通过Proxy代理对象每个属性的set和get,在get时通过track收集activeEffect,在set时通过trigger通知对应属性的所有依赖更新
  • 通过Map保存每个reactive对象中每个属性的依赖
  • 通过effect队列在nextTick中统一运行effect,并通过Set进行effect去重
  • 为了解决普通类型按值传递的问题,Vue3实现了Ref类型对象,computed也可以当做是一种特殊的Ref。

至此,对于Vue3的响应式系统有了一定的了解,接下来去看组合式API应该就比较方便了。

你要请我喝一杯奶茶?

版权声明:自由转载-非商用-保持署名和原文链接。

本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。