Vue3系列之effect和ReactiveEffect track trigger源码解析

目录
  • 引言
  • 一、ReactiveEffect
    • 1. 相关的全局变量
    • 2. class 声明
    • 3. cleanupEffect
  • 二、effect 函数
    • 1. 相关ts类型
    • 2. 函数声明
    • 3. stop函数
  • 三、track 依赖收集
    • 1. track
    • 2. createDep
    • 3. trackEffects
    • 4. 小结
  • 四、trigger
    • 1. triggerEffect
    • 2. triggerEffects
    • 3. trigger
  • 五、小结
    • 1. 依赖收集
    • 2. 触发更新

引言

介绍几个API的时候,我们发现里面常出现effecttracktrigger,虽然简单说了下track用于依赖收集,trigger来触发更新。但是毕竟没看到具体实现,心里没底。如今便可以一探究竟。

一、ReactiveEffect

1. 相关的全局变量

之前提到的effect,便是ReactiveEffect的实例。用到了一些重要的全局变量。

  • targetMap:弱映射,以目标对象targetkey,其收集到的依赖集depsMap为值,因此通过目标对象target可以获取到对应的所有依赖;
  • activeEffect:当前活跃的effect,随后会被收集起来;
  • shouldTrack:用作暂停和恢复依赖收集的标志;
  • trackStack:历史shouldTrack的记录栈。

targetMap对比reactive篇章中提到的proxyMap

  • 两者都是弱映射;
  • 都以目标对象targetkey
  • targetMap全局只有一个;而proxyMap有四种,分别对应reactiveshallowReactivereadonlyshallowReadonly
  • 一个target在一种proxyMap中最多只有一个对应的代理proxy,因此proxyMap的值为单个的proxy对象;
  • 一个target可以由很多的依赖dep,因此targetMap的值为数据集Map
const targetMap = new WeakMap<any, KeyToDepMap>()
export let activeEffect: ReactiveEffect | undefined
export let shouldTrack = true
const trackStack: boolean[] = []

以及控制暂停、恢复依赖收集的函数:

// 暂停收集
export function pauseTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = false
}
// 恢复收集
export function enableTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = true
}
// 重置为上一次的状态
export function resetTracking() {
  const last = trackStack.pop()
  shouldTrack = last === undefined ? true : last
}

2. class 声明

在构造器中初始化fn ( 执行run()的过程中调用 ) 、调度器scheduler,并通过recordEffectScope来记录实例的作用域;声明一些实例属性,以及runstop两个方法:

  • activeboolean类型,表示当前的effect是否起作用;
  • deps:当前effect的依赖;
  • parent:指向上一个活跃的effect,形成链表;
  • computed:可选,在computed函数得到的ComputedRefImpl里的effect具有这个属性;
  • allowRecurse,可选,表示是否允许自调用;
  • deferStop:私有,可选,表示stop()是否延迟执行;
  • onStop:可选,函数,在执行stop()时会调用onStop
  • onTrack
  • onTrigger:这两个listener为调试用,分别在依赖收集和响应式更新时触发;
  • runeffect最核心的方法。
  • stop:调用cleanupEffecteffect停止起作用,如果是stop当前活跃的effect,也就是自己停止自己,则会将deferStop调为true,从而延迟停止的时机;触发onStop;将active调为false
export class ReactiveEffect<T = any> {
  active = true
  deps: Dep[] = []
  parent: ReactiveEffect | undefined = undefined
  /**
   * Can be attached after creation
   * @internal
   */
  computed?: ComputedRefImpl<T>
  /**
   * @internal
   */
  allowRecurse?: boolean
  /**
   * @internal
   */
  private deferStop?: boolean
  onStop?: () => void
  // dev only
  onTrack?: (event: DebuggerEvent) => void
  // dev only
  onTrigger?: (event: DebuggerEvent) => void
  constructor(
    public fn: () => T,
    public scheduler: EffectScheduler | null = null,
    scope?: EffectScope
  ) {
    recordEffectScope(this, scope)
  }
  run() {
    if (!this.active) {
      return this.fn()
    }
    // 当前活跃的effect
    let parent: ReactiveEffect | undefined = activeEffect
    let lastShouldTrack = shouldTrack
    // 如果当前活跃的effect就是这个effect本身,则直接返回
    while (parent) {
      if (parent === this) {
        return
      }
      parent = parent.parent
    }
    // 依次活跃的effect形成链表,由parent属性连接
    try {
      this.parent = activeEffect
      activeEffect = this
      shouldTrack = true
      trackOpBit = 1 << ++effectTrackDepth
      if (effectTrackDepth <= maxMarkerBits) {
        // 遍历 this.deps 将其中的effect设置为已捕获 tracked
        initDepMarkers(this)
      } else {
        // 层级溢出则清除当前副作用
        cleanupEffect(this)
      }
      // 尾调用传入的fn
      return this.fn()
    } finally {
      // 因为前面有return,因此当 try 的代码块发生异常时执行
      if (effectTrackDepth <= maxMarkerBits) {
        // 该方法遍历 this.deps,将其中过气的effect删除,未捕获的effect加入
        // effect 就是其中的 dep
        finalizeDepMarkers(this)
      }
      trackOpBit = 1 << --effectTrackDepth
      // 复原一些状态
      activeEffect = this.parent
      shouldTrack = lastShouldTrack
      this.parent = undefined
      // 若设置了延迟停止,则执行stop,进行延迟清理
      if (this.deferStop) {
        this.stop()
      }
    }
  }
  // 清除副作用
  stop() {
    // stopped while running itself - defer the cleanup
    if (activeEffect === this) {
      this.deferStop = true
    } else if (this.active) {
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
      this.active = false
    }
  }
}

3. cleanupEffect

cleanupEffect用于清除副作用。接收一个effect,遍历effect.deps,并逐个删除副作用effect。随后清空effect.deps

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

二、effect 函数

1. 相关ts类型

effect函数有几个相关的类型:

  • ReactiveEffectOptionseffect函数的入参类型之一;
  • ReactiveEffectRunner:是一个函数,且具有effect属性的类型;
export interface DebuggerOptions {
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
}
export interface ReactiveEffectOptions extends DebuggerOptions {
  lazy?: boolean
  scheduler?: EffectScheduler
  scope?: EffectScope
  allowRecurse?: boolean
  onStop?: () => void
}
export interface ReactiveEffectRunner<T = any> {
  (): T
  effect: ReactiveEffect
}

2. 函数声明

effect函数有两个入参:

  • fn:是一个函数,经处理后用于创建 ReactiveEffect实例_effect
  • options:可选,用于覆盖_effect上的属性。
export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  // 处理fn
  if ((fn as ReactiveEffectRunner).effect) {
    fn = (fn as ReactiveEffectRunner).effect.fn
  }
  // 根据 fn 创建一个 _effect
  const _effect = new ReactiveEffect(fn)
  if (options) {
    // 用 options 覆盖 _effect 上的属性
    extend(_effect, options)
    if (options.scope) recordEffectScope(_effect, options.scope)
  }
  // 没有 lazy , 则 _effect 立即执行一次 run()
  if (!options || !options.lazy) {
    _effect.run()
  }
  // runner:拿到 _effect.run 并挂上 effect 属性,包装成 ReactiveEffectRunner 类型
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  // effect属性指回 _effect 自身,方便使用 runner 调用 run 和 stop
  runner.effect = _effect
  // 返回 runner
  return runner
}

3. stop函数

stop用于清除effect。入参为ReactiveEffectRunner

export function stop(runner: ReactiveEffectRunner) {
  runner.effect.stop()
}

三、track 依赖收集

1. track

一直在说track进行依赖收集,这里看下它到底怎么做的。

  • 以目标对象targetkeydepsMaptargetMap的值;以targetkeykey,使用createDep()创建依赖dep为值,存放在target对应的depsMap中。
  • 通过trackEffects(dep, eventInfo)来收集副作用。
// 全局变量 targetMap
const targetMap = new WeakMap<any, KeyToDepMap>()
export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (shouldTrack && activeEffect) {
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = createDep()))
    }
    const eventInfo = __DEV__
      ? { effect: activeEffect, target, type, key }
      : undefined
    trackEffects(dep, eventInfo)
  }
}

2. createDep

使用createDep创建一个新的dep。可以看到,dep是个Set实例,且添加了两个属性:

  • wwasTracked的首字母,表示当前依赖是否被收集;
  • nnewlyTracked的首字母,表示当前依赖是否是新收集的。
export const createDep = (effects?: ReactiveEffect[]): Dep => {
  const dep = new Set<ReactiveEffect>(effects) as Dep
  dep.w = 0
  dep.n = 0
  return dep
}

3. trackEffects

trackEffects用于收集副作用。主要把当前活跃的activeEffect加入dep,以及在activeEffect.deps中加入该副作用影响到的所有依赖。

export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: 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!)
  }
  // 当前依赖 dep 还未被捕获 / 当前依赖 dep 中,还没有当前活跃的副作用时,
  // 将当前活跃的副作用 effect 添加进 dep 里,同时在把 dep 加入受副作用影响的依赖集合 activeEffect.deps 中
  if (shouldTrack) {
    dep.add(activeEffect!)
    activeEffect!.deps.push(dep)
    if (__DEV__ && activeEffect!.onTrack) {
      activeEffect!.onTrack({
        effect: activeEffect!,
        ...debuggerEventExtraInfo!
      })
    }
  }
}

4. 小结

用一句比较拗口的话来说,依赖收集就是把当前活跃的副作用activeEffect存入全局变量targetMap中的 ( target 对应的 depsMap) 中 (targetkey)对应的 dep ( 类型为Set) 中,并把这个dep加入到受activeEffect副作用影响的所有依赖activeEffect.deps列表中。

四、trigger

触发更新实际上就是触发副作用,因此这一小节决定以与track相反的顺序来介绍。

1. triggerEffect

triggerEffect触发副作用从而更新。当触发更新的副作用effect允许自调用,且不是当前活跃的副作用时,通过调度器scheduler执行副作用或者直接执行run,是实际上触发更新的地方。

function triggerEffect(
  effect: ReactiveEffect,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  if (effect !== activeEffect || effect.allowRecurse) {
    if (__DEV__ && effect.onTrigger) {
      effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
    }
    // 实际触发更新的地方
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
  }
}

2. triggerEffects

接收一个dep和用于调试的额外信息。遍历dep中的effect,逐一使用triggerEffect来执行副作用。源码在这里有点蜜汁操作。

export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  const effects = isArray(dep) ? dep : [...dep]
  // 两者互斥,但是执行的操作相同?而且为什么不写在一个 for...of... 里 ?
  for (const effect of effects) {
    if (effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
  for (const effect of effects) {
    if (!effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
}

3. trigger

之前一直说trigger触发更新,其实是现在已经知道了,实际是triggerEffect来执行副作用从而实现更新。

这里是创建一个deps数组,根据targetkey和触发更新的操作类型type等参数,来获取所有的相关dep,放入deps。再取出deps中所有的dep里的所有effect,放入effects列表中,通过triggerEffects(effects)来触发所有的相关副作用,最终实现更新。

需要注意的是对于数组:

  • 修改length属性会导致该数组所有依赖的更新;
  • 修数组新增成员会引起length属性相关的依赖的更新,因为length的值发生了变化。
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) {
    // never been tracked
    return
  }
  // 用于聚集所有相关依赖
  let deps: (Dep | undefined)[] = []
  if (type === TriggerOpTypes.CLEAR) {
    // 调用了Set、Map实例的clear方法,将触发全部相关的副作用
    // collection being cleared
    // trigger all effects for target
    deps = [...depsMap.values()]
  } else if (key === 'length' && isArray(target)) {
    // 目标对象是数组,且修改了length属性时,会触发全部相关的副作用
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        deps.push(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      deps.push(depsMap.get(key))
    }
    // also run for iteration key on ADD | DELETE | Map.SET
    switch (type) {
      case TriggerOpTypes.ADD:
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // 数组下标成员的更改 会引起 length 属性相关的更新
          // new index added to array -> length changes
          deps.push(depsMap.get('length'))
        }
        break
      case TriggerOpTypes.DELETE:
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          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)
      }
    }
    // 这里triggerEffects接受的参数类型为Set,之前的是数组
    if (__DEV__) {
      triggerEffects(createDep(effects), eventInfo)
    } else {
      triggerEffects(createDep(effects))
    }
  }
}

五、小结

1. 依赖收集

targetMap中有depsMap(以targetkey);depsMap中有许多dep(以targetMapkeykey);简单理解为:在编译时根据targetkey,创建副作用,将activeEffect指向新建的副作用,并存放到相关的依赖dep里的过程就是依赖收集。

2. 触发更新

反过来,触发targetkey相关的dep中所有相关的副作用,通过各个副作用上的effect.scheduler()或者effect.run()来实现更新。

以上就是Vue3系列之effect和ReactiveEffect track trigger源码解析的详细内容,更多关于Vue3 effect和ReactiveEffect track trigger的资料请关注我们其它相关文章!

(0)

相关推荐

  • 深入理解Vue3里的EffectScope

    Vue 3.2 版本引入了新的 Effect scope API,使用 effectScope 创建一个 effect 作用域,可以捕获其中所创建的响应式副作用 (即计算属性和侦听器),这样捕获到的副作用可以一起处理.使用 getCurrentScope 返回当前活跃的 effect 作用域.使用 onScopeDispose 在当前活跃的 effect 作用域上注册一个处理回调函数.当相关的 effect 作用域停止时会调用这个回调函数. const scope = effectScope()

  • Vue3 中 watch 与 watchEffect 区别及用法小结

    目录 响应式依赖收集 Watch WatchEffect 什么时候用什么? 大部分时候用 watch 显式的指定依赖以避免不必要的重复触发,也避免在后续代码修改或重构时不小心引入新的依赖.watchEffect 适用于一些逻辑相对简单,依赖源和逻辑强相关的场景. 你可以认为他们是同一个功能的两种不同形态,底层的实现是一样的. watch- 显式指定依赖源,依赖源更新时执行回调函数 watchEffect - 自动收集依赖源,依赖源更新时重新执行自身 响应式依赖收集 首先先需要了解一下 vue 3

  • vue中关于trigger的用法

    目录 关于trigger的用法 vue的一些易混淆事件整理 - @change | trigger: 'blur' | onchange onpropertychange 和 oninput 事件的区别 @change trigger: ‘blur’ 关于trigger的用法 trigger: "blur": 失去焦点时触发 newPassword: [ { required: true, message: "新密码不能为空", trigger: "blu

  • vue3中的watch和watchEffect实例详解

    目录 首先总结一下两者的区别: 下面是根据上面的第三点做的一些小实验: 总结 闲来无事,比较了一下 vue3 中的 watch 和 watchEffect,总觉得官方文档没咋说清楚,今天就小小实践了一下. 首先总结一下两者的区别: 1.watch 是惰性执行,而 watchEffect 不是,不考虑 watch 的第三个参数配置的情况,watch 在组件第一次执行的时候是不会执行的,只有在之后依赖项变化的时候再执行,而 watchEffect 是在程序执行到此处的时候就立即执行,而后再响应其依赖

  • vue3中的响应式原理-effect

    目录 effect的基本实现 依赖收集 触发更新 分支切换与cleanup 停止effect 调度执行 深度代理 总结 effect的基本实现 export let activeEffect = undefined;// 当前正在执行的effect class ReactiveEffect {     active = true;     deps = []; // 收集effect中使用到的属性     parent = undefined;     constructor(public fn

  • 一文搞懂Vue3中watchEffect侦听器的使用

    目录 watchEffect 侦听器 watchEffect 侦听器使用 watchEffect 监听基本数据 watchEffect 监听复杂数据 watchEffect 啥时候执行 关闭 watchEffect 监听 上一节我们学习了 watch 侦听器的基础用法,用来监听页面数据的变化,那么今天呢,我们来学习一下 watch 侦听器的好兄弟 watchEffect 侦听器.这个相对来说比较简单,用的不是很多,当然了,根据自己的项目情况自行决定使用.这个就不详细说了,简单过一下子. watc

  • Vue3系列之effect和ReactiveEffect track trigger源码解析

    目录 引言 一.ReactiveEffect 1. 相关的全局变量 2. class 声明 3. cleanupEffect 二.effect 函数 1. 相关ts类型 2. 函数声明 3. stop函数 三.track 依赖收集 1. track 2. createDep 3. trackEffects 4. 小结 四.trigger 1. triggerEffect 2. triggerEffects 3. trigger 五.小结 1. 依赖收集 2. 触发更新 引言 介绍几个API的时候

  • Vue3源码解析watch函数实例

    目录 引言 一.watch参数类型 1. 选项options 2. 回调cb 3. 数据源source 二.watch函数 三.watch的核心:doWatch 函数 引言 想起上次面试,问了个古老的问题:watch和computed的区别.多少有点感慨,现在已经很少见这种耳熟能详的问题了,网络上八股文不少.今天,我更想分享一下从源码的层面来区别这八竿子打不着的两者.本篇针对watch做分析,下一篇分析computed. 一.watch参数类型 我们知道,vue3里的watch接收三个参数:侦听

  • Vue3 编译流程-源码解析

    前言: Vue3 发布已经很长一段时间了,最近也有机会在公司项目中用上了 Vue3 + TypeScript + Vite 的技术栈,所以闲暇之余抽空也在抽空阅读 Vue3 的源码.本着好记性不如烂笔头的想法,在阅读源码时顺便记录了一些笔记,也希望能争取写一些源码阅读笔记,帮助每个想看源码但可能存在困难的同学减少理解成本. Vue2.x 的源码我也有过一些简单的阅读,自 Vue3 重构后,Vue 项目的目录结构也发生了很大的变化,各个功能模块被分别放入了 packages 目录下,职责更加清晰,

  • Vue3 AST解析器-源码解析

    目录 1.生成 AST 抽象语法树 2.创建 AST 的根节点 3.解析子节点 4.解析模板元素 Element 5.示例:模板元素解析 上一篇文章Vue3 编译流程-源码解析中,我们从 packges/vue/src/index.ts 的入口开始,了解了一个 Vue 对象的编译流程,在文中我们提到 baseCompile 函数在执行过程中会生成 AST 抽象语法树,毫无疑问这是很关键的一步,因为只有拿到生成的 AST 我们才能遍历 AST 的节点进行 transform 转换操作,比如解析 v

  • vue3 keepalive源码解析解决线上问题

    目录 引言 1.keepalive功能 2.keepalive使用场景 3.在项目中的使用过程 4.vue3 keepalive源码调试 5.vue3 keealive源码粗浅分析 6.总结 引言 1.通过本文可以了解到vue3 keepalive功能 2.通过本文可以了解到vue3 keepalive使用场景 3.通过本文可以学习到vue3 keepalive真实的使用过程 4.通过本文可以学习vue3 keepalive源码调试 5.通过本文可以学习到vue3 keepalive源码的精简分

  • vue3模块创建runtime-dom源码解析

    目录 前言 创建模块 nodeOptions patchProps patchProp patchClass patchStyle patchEvent patchAttr 总结 前言 runtime-dom 是针对浏览器的运行时,包括 DOM 操作.props(例如class.事件.样式以及其它attributes)的更新等内容:本小节我们开启 runtime-dom 的篇章. 创建模块 在 packages/runtime-dom/ 目录下创建目录文件: │ │ └─ src │ │ ├─

  • Vue3源码分析reactivity实现方法示例

    目录 深入分析对于map.set.weakMap.weakSet的响应式拦截 (1).mutableInstrumentations (2).shallowInstrumentations (3).readonlyInstrumentations (4).shallowReadonlyInstrumentations ref.computed等方法的实现 (1).ref与shallowRef源码解析 (2).toRefs (4).computed (5)其他api源码 最后总结: 深入分析对于m

  • Java集合系列之LinkedHashMap源码分析

    这篇文章我们开始分析LinkedHashMap的源码,LinkedHashMap继承了HashMap,也就是说LinkedHashMap是在HashMap的基础上扩展而来的,因此在看LinkedHashMap源码之前,读者有必要先去了解HashMap的源码,可以查看我上一篇文章的介绍<Java集合系列[3]----HashMap源码分析>.只要深入理解了HashMap的实现原理,回过头来再去看LinkedHashMap,HashSet和LinkedHashSet的源码那都是非常简单的.因此,读

  • vue3 源码解读之 time slicing的使用方法

    今天给大家带来一篇源码解析的文章,emm 是关于 vue3 的,vue3 源码放出后,已经有很多文章来分析它的源码,我觉得很快又要烂大街了,哈哈 不过今天我要解析的部分是已经被废除的 time slicing 部分,这部分源码曾经出现在 vue conf 2018 的视频中,但是源码已经被移除掉了,之后可能也不会有人关注,所以应该不会烂大街 打包 阅读源码之前,需要先进行打包,打包出一份干净可调试的文件很重要 vue3 使用的 rollup 进行打包,我们需要先对它进行改造 import cle

  • vue3调度器effect的scheduler功能实现详解

    目录 一.调度执行 二.单元测试 三.代码实现 四.回归实现 五.结语 一.调度执行 说到scheduler,也就是vue3的调度器,可能大家还不是特别明白调度器的是什么,先大概介绍一下. 可调度性是响应式系统非常重要的特性.首先我们要明确什么是可调度性.所谓可调度性,指的是当trigger 动作触发副作用函数重新执行时,有能力决定副作用函数执行的时机.次数以及方式. 有了调度函数,我们在trigger函数中触发副作用函数重新执行时,就可以直接调用用户传递的调度器函数,从而把控制权交给用户. 举

随机推荐