源码分析Vue3响应式核心之effect

目录
  • 一、effect用法
    • 1、基本用法
    • 2、lazy属性为true
    • 3、options中包含onTrack
  • 二、源码分析
    • 1、effect方法的实现
    • 2、ReactiveEffect函数源码
  • 三、依赖收集相关
    • 1、如何触发依赖收集
    • 2、track源码
    • 3、trackEffects(dep, eventInfo)源码解读
  • 四、触发依赖
    • 1、trigger依赖更新
    • 2、triggerEffects(deps[0], eventInfo)
    • 3、triggerEffect(effect, debuggerEventExtraInfo)

vue3响应式核心文章汇总:

vue3响应式核心之reactive源码详解

vue3响应式核心之effect源码详解

上篇vue3响应式核心之reactive源码详解详细讲解了reactive的原理和实现源码,本篇文章将讲解effect源码。

通常情况下我们是不会直接使用effect的,因为effect是一个底层的API,在我们使用Vue3的时候Vue默认会帮我们调用effect。 effect翻译为作用,意思是使其发生作用,这个使其的其就是我们传入的函数,所以effect的作用就是让我们传入的函数发生作用,也就是执行这个函数。 执行过程简图如下:

接下来先通过例子了解effect的基本用法,然后再去了解原理。

一、effect用法

1、基本用法

const obj = reactive({count: 1})

const runner = effect(() => {
  console.log(obj.count)
})

obj.count++

结果会先打印1, 然后在obj.count++之后打印出2。

流程简图如下:

运行effect(fun)

// 先执行
fun()  // 打印出1

const runner = new ReactiveEffect(fn)

return runner

runner: {
  run() {
    this.fun() //执行fun
  },
  stop() {

  }
}

console.log(obj.count)track依赖收集 结构如下:

obj.count++触发依赖,执行runner.run(), 实际运行的是

() => {
  console.log(obj.count)
}

所以又打印出2

2、lazy属性为true

此值为 true 时,只有在第一次手动调用 runner 后,依赖数据变更时,才会自动执行 effect 的回调,可以理解为 effect 的是在手动调用 runner 后才首次执行

const obj = reactive({count: 1})

const runner = effect(() => {
  console.log(obj.count)
}, {
  lazy: true
})
runner()
obj.count++

只会打印出2

原因是effect源码中有如下逻辑:

3、options中包含onTrack

let events = []
const onTrack = (e) => {
  events.push(e)
}
const obj = reactive({ foo: 1, bar: 2 })
const runner = effect(
  () => {
    console.log(obj.foo)
  },
  { onTrack }
)
console.log('runner', runner)
obj.foo++
console.log("events", events)

看下events的打印结果:

[
  {
    effect: runner,  // effect 函数的返回值
    target: toRaw(obj),  // 表示的是哪个响应式数据发生了变化
    type: TrackOpTypes.GET,  // 表示此次记录操作的类型。 get 表示获取值
    key: 'foo'
 }
]

二、源码分析

1、effect方法的实现

// packages/reactivity/src/effect.ts
export interface ReactiveEffectOptions extends DebuggerOptions {
  lazy?: boolean
  scheduler?: EffectScheduler
  scope?: EffectScope
  allowRecurse?: boolean
  onStop?: () => void
}

export function effect<T = any>(
  fn: () => T, // 副作用函数
  options?: ReactiveEffectOptions // 结构如上
): ReactiveEffectRunner {
  // 如果 fn 对象上有 effect 属性
  if ((fn as ReactiveEffectRunner).effect) {
    // 那么就将 fn 替换为 fn.effect.fn
    fn = (fn as ReactiveEffectRunner).effect.fn
  }
  // 创建一个响应式副作用函数
  const _effect = new ReactiveEffect(fn)
  if (options) {
    // 将配置项合并到响应式副作用函数上
    extend(_effect, options)
    // 如果配置项中有 scope 属性(该属性的作用是指定副作用函数的作用域)
    if (options.scope) recordEffectScope(_effect, options.scope)
  }
  if (!options || !options.lazy) { // options.lazy 不为true
    _effect.run() // 执行响应式副作用函数 首次执行fn()
  }
  // _effect.run作用域绑定到_effect
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  // 将响应式副作用函数赋值给 runner.effect
  runner.effect = _effect
  return runner
}

核心代码:

创建一个响应式副作用函数const _effect = new ReactiveEffect(fn),其运行结果如下:

非lazy状态执行响应式副作用函数_effect.run()

if (!options || !options.lazy) { // options.lazy 不为true
  _effect.run() // 执行响应式副作用函数 首次执行fn()
}

_effect.run作用域绑定到_effect

// _effect.run作用域绑定到_effect
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner

返回副作用函数runner

2、ReactiveEffect函数源码

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
  ) {
    // 记录当前 ReactiveEffect 对象的作用域
    recordEffectScope(this, scope)
  }

  run() {
    // 如果当前 ReactiveEffect 对象不处于活动状态,直接返回 fn 的执行结果
    if (!this.active) {
      return this.fn()
    }
    // 寻找当前 ReactiveEffect 对象的最顶层的父级作用域
    let parent: ReactiveEffect | undefined = activeEffect
    let lastShouldTrack = shouldTrack // 是否要跟踪
    while (parent) {
      if (parent === this) {
        return
      }
      parent = parent.parent
    }
    try {
      // 记录父级作用域为当前活动的 ReactiveEffect 对象
      this.parent = activeEffect
      activeEffect = this  // 将当前活动的 ReactiveEffect 对象设置为 “自己”
      shouldTrack = true // 将 shouldTrack 设置为 true (表示是否需要收集依赖)
      // effectTrackDepth 用于标识当前的 effect 调用栈的深度,执行一次 effect 就会将 effectTrackDepth 加 1
      trackOpBit = 1 << ++effectTrackDepth

      if (effectTrackDepth <= maxMarkerBits) {
        // 初始依赖追踪标记
        initDepMarkers(this)
      } else {
        // 清除依赖追踪标记
        cleanupEffect(this)
      }
      // 返回副作用函数执行结果
      return this.fn()
    } finally {
      // 如果 effect调用栈的深度 没有超过阈值
      if (effectTrackDepth <= maxMarkerBits) {
        // 确定最终的依赖追踪标记
        finalizeDepMarkers(this)
      }
      // 执行完毕会将 effectTrackDepth 减 1
      trackOpBit = 1 << --effectTrackDepth
      // 执行完毕,将当前活动的 ReactiveEffect 对象设置为 “父级作用域”
      activeEffect = this.parent
      // 将 shouldTrack 设置为上一个值
      shouldTrack = lastShouldTrack
      // 将父级作用域设置为 undefined
      this.parent = undefined
      // 延时停止,这个标志是在 stop 方法中设置的
      if (this.deferStop) {
        this.stop()
      }
    }
  }

  stop() {
    // stopped while running itself - defer the cleanup
    // 如果当前 活动的 ReactiveEffect 对象是 “自己”
    // 延迟停止,需要执行完当前的副作用函数之后再停止
    if (activeEffect === this) {
      // 在 run 方法中会判断 deferStop 的值,如果为 true,就会执行 stop 方法
      this.deferStop = true
    } else if (this.active) {// 如果当前 ReactiveEffect 对象处于活动状态
      cleanupEffect(this) // 清除所有的依赖追踪标记
      if (this.onStop) {
        this.onStop()
      }
      this.active = false // 将 active 设置为 false
    }
  }
}
  • run方法的作用就是执行副作用函数,并且在执行副作用函数的过程中,会收集依赖;
  • stop方法的作用就是停止当前的ReactiveEffect对象,停止之后,就不会再收集依赖了;
  • activeEffect和this并不是每次都相等的,因为activeEffect会跟着调用栈的深度而变化,而this则是固定的;

三、依赖收集相关

1、如何触发依赖收集

在副作用函数中, obj.count就会触发依赖收集

const runner = effect(() => {
  console.log(obj.count)
})

触发的入口在get拦截器里面

function createGetter(isReadonly = false, shallow = false) {
  // 闭包返回 get 拦截器方法
  return function get(target: Target, key: string | symbol, receiver: object) {
    // ...
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }
    // ...
  }

2、track源码

const targetMap = new WeakMap();
/**
 * 收集依赖
 * @param target target 触发依赖的对象,例子中的obj
 * @param type 操作类型 比如obj.count就是get
 * @param key 指向对象的key, 比如obj.count就是count
 */
export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (shouldTrack && activeEffect) { // 是否应该依赖收集 & 当前的new ReactiveEffect()即指向的就是当前正在执行的副作用函数

    // 如果 targetMap 中没有 target,就会创建一个 Map
    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())) // createDep 生成dep = { w:0, n: 0}
    }

    const eventInfo = __DEV__
      ? { effect: activeEffect, target, type, key }
      : undefined

    trackEffects(dep, eventInfo)
  }
}

shouldTrack在上面也讲过,它的作用就是控制是否收集依赖;

activeEffect就是我们刚刚讲的ReactiveEffect对象,它指向的就是当前正在执行的副作用函数;

track方法的作用就是收集依赖,它的实现非常简单,就是在targetMap中记录下target和key;

targetMap是一个WeakMap,它的键是target,值是一个Map,这个Map的键是key,值是一个Set;

targetMap的结构伪代码如下:

targetMap = {
  target: {
    key: dep
  },
  // 比如:
  obj: {
    count: {
       w: 0,
       n: 0
    }
  }
}

以上是最原始的depMap

dev环境为增加响应式调试会增加eventInfo

const eventInfo = __DEV__
  ? { effect: activeEffect, target, type, key }
  : undefined

eventInfo结构如下:

trackEffects(dep, eventInfo)

如果 dep 中没有当前的 ReactiveEffect 对象,就会添加进去, 作用就把对象的属性操作与副作用函数建立关联,接下来看trackEffects

3、trackEffects(dep, eventInfo)源码解读

export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  let shouldTrack = false
  if (effectTrackDepth <= maxMarkerBits) {
    if (!newTracked(dep)) {
      // 执行之前 dep = Set(0) {w: 0, n: 0}

      // 执行之后 dep = Set(0) {w: 0, n: 2}
      dep.n |= trackOpBit // set newly tracked

      shouldTrack = !wasTracked(dep)
    }
  } else {
    // Full cleanup mode.
    shouldTrack = !dep.has(activeEffect!)
  }

  if (shouldTrack) {
    // 将activeEffect添加到dep
    dep.add(activeEffect!)
    activeEffect!.deps.push(dep)
    if (__DEV__ && activeEffect!.onTrack) { // onTrack逻辑
      activeEffect!.onTrack(
        extend(
          {
            effect: activeEffect!
          },
          debuggerEventExtraInfo!
        )
      )
    }
  }
}

dep.add(activeEffect!) 如果 dep 中没有当前的 ReactiveEffect 对象,就会添加进去

最终生成的depTarget结构如下:

四、触发依赖

比如例子中代码obj.count++就会触发set拦截,触发依赖更新

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    //...
    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) // 触发ADD依赖更新
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue) //触发SET依赖更新
      }
    }
    //...
  }

1、trigger依赖更新

// 路径:packages/reactivity/src/effect.ts
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target) // 获取depsMap, targetMap是在track中创建的依赖
  if (!depsMap) {
    // never been tracked
    return
  }

  let deps: (Dep | undefined)[] = []
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    deps = [...depsMap.values()]
  } else if (key === 'length' && isArray(target)) {
    const newLength = Number(newValue)
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= newLength) {
        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)) {
          // 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)
      }
    }
    if (__DEV__) {
      triggerEffects(createDep(effects), eventInfo)
    } else {
      triggerEffects(createDep(effects))
    }
  }
}

const depsMap = targetMap.get(target) 获取 targetMap 中的 depsMap targetMap结构如下:

执行以上语句之后的depsMap结构如下:

将 depsMap 中 key 对应的 ReactiveEffect 对象添加到 deps 中deps.push(depsMap.get(key))之后的deps结构如下:

triggerEffects(deps[0], eventInfo)

  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])
      }
    }
  }

trigger函数的作用就是触发依赖,当我们修改数据的时候,就会触发依赖,然后执行依赖中的副作用函数。

在这里的实现其实并没有执行,主要是收集一些需要执行的副作用函数,然后在丢给triggerEffects函数去执行,接下来看看triggerEffects函数。

2、triggerEffects(deps[0], eventInfo)

export 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) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
  for (const effect of effects) {
    if (!effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
}

主要步骤

const effects = isArray(dep) ? dep : [...dep]获取effects

triggerEffect(effect, debuggerEventExtraInfo)执行effect,接下来看看源码

3、triggerEffect(effect, debuggerEventExtraInfo)

function triggerEffect(
  effect: ReactiveEffect,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  if (effect !== activeEffect || effect.allowRecurse) {
     // 如果 effect.onTrigger 存在,就会执行,只有开发模式下才会执行
    if (__DEV__ && effect.onTrigger) {
      effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
    }
    // 如果 effect 是一个调度器,就会执行 scheduler
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      // 其它情况执行 effect.run()
      effect.run()
    }
  }
}

effect.run()就是执行副作用函数

以上就是源码分析Vue3响应式核心之effect的详细内容,更多关于Vue3 effect的资料请关注我们其它相关文章!

(0)

相关推荐

  • vue3中的响应式原理-effect

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

  • 源码分析Vue3响应式核心之reactive

    目录 一.Reactive源码 1.reactive 2.接着看工厂方法createReactiveObject 二.baseHandlers 1.baseHandlers vue3响应式核心文章汇总: vue3响应式核心之reactive源码详解 vue3响应式核心之effect源码详解 vue3响应式核心分两篇文章讲解,本篇讲解reactive源码和实现原理,下一篇vue3响应式核心之effect源码详解讲解effect依赖收集与触发. 一.Reactive源码 1.reactive 源码路

  • 一文带你深入理解Vue3响应式原理

    目录 响应式原理 2.0的不足 reactive和effect的实现 effect track trigger 测试代码 递归实现reactive 总结 响应式原理 Vue2 使用的是 Object.defineProperty  Vue3 使用的是 Proxy 2.0的不足 对象只能劫持 设置好的数据,新增的数据需要Vue.Set(xxx)  数组只能操作七种方法,修改某一项值无法劫持. reactive和effect的实现 export const reactive = <T extends

  • 浅谈vue3中effect与computed的亲密关系

    在我刚看完vue3响应式的时候,心中就有一个不可磨灭的谜团,让我茶不思饭不想,总想生病.那么这个谜团是什么呢?就是在响应式中一直穿行在tranger跟track之间的effect.如果单纯的响应式原理根本就用不上effect,那么effect到底是干什么的呢? 船到桥头自然直,柳岸花明又一村.苦心人天不负,偶然间我看到了effect测试代码用例! it('should observe basic properties', () => { let dummy const counter = rea

  • 源码分析Vue3响应式核心之effect

    目录 一.effect用法 1.基本用法 2.lazy属性为true 3.options中包含onTrack 二.源码分析 1.effect方法的实现 2.ReactiveEffect函数源码 三.依赖收集相关 1.如何触发依赖收集 2.track源码 3.trackEffects(dep, eventInfo)源码解读 四.触发依赖 1.trigger依赖更新 2.triggerEffects(deps[0], eventInfo) 3.triggerEffect(effect, debugg

  • Vue源码学习之响应式是如何实现的

    目录 前言 一.一个响应式系统的关键要素 1.如何监听数据变化 2.如何进行依赖收集--实现 Dep 类 3.数据变化时如何更新--实现 Watcher 类 二.虚拟 DOM 和 diff 1.虚拟 DOM 是什么? 2.diff 算法--新旧节点对比 三.nextTick 四.总结 前言 作为前端开发,我们的日常工作就是将数据渲染到页面➕处理用户交互.在 Vue 中,数据变化时页面会重新渲染,比如我们在页面上显示一个数字,旁边有一个点击按钮,每次点击一下按钮,页面上所显示的数字会加一,这要怎么

  • 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

  • Vue3源码分析侦听器watch的实现原理

    目录 watch 的本质 watch 的函数签名 侦听多个源 侦听单一源 watch 的实现 watch 函数 source 参数 cb 参数 options 参数 doWatch 函数 doWatch 函数签名 初始化变量 递归读取响应式数据 定义清除副作用函数 封装 scheduler 调度函数 设置 job 的 allowRecurse 属性 flush 选项指定回调函数的执行时机 创建副作用函数 执行副作用函数 返回匿名函数,停止侦听 总结 watch 的本质 所谓的watch,其本质就

  • Vue3源码分析组件挂载初始化props与slots

    目录 前情提要 初始化组件 (1).setupComponent (2).initProps (3).initSlots 额外内容 总结 前情提要 上文我们分析了挂载组件主要调用了三个函数: createComponentInstance(创建组件实例).setupComponent(初始化组件).setupRenderEffect(更新副作用).并且上一节中我们已经详细讲解了组件实例上的所有属性,还包括emit.provide等的实现.本文我们将继续介绍组件挂载流程中的初始化组件. 本文主要内

  • Vue3 源码分析reactive readonly实例

    目录 引言 一.reactive 和 readonly 1. reactive相关类型 2. 相关全局变量与方法 3. reactive函数 4. 造物主createReactiveObject 5. shallowReactive.readonly和shallowReadonly 二.对应的 Handlers 1. baseHandlers 1.1 reactive 1.2 readonly 1.3 shallowReactive 1.4 shallowReadonly 2. cellecti

  • jQuery 1.9.1源码分析系列(十五)动画处理之缓动动画核心Tween

    在jQuery内部函数Animation中调用到了createTweens()来创建缓动动画组,创建完成后的结果为: 可以看到上面的缓动动画组有四个原子动画组成.每一个原子动画的信息都包含在里面了. 仔细查看createTweens函数,实际上就是遍历调用了tweeners ["*"]的数组中的函数(实际上就只有一个元素). function createTweens( animation, props ) { jQuery.each( props, function( prop, v

  • Android透明化和沉浸式状态栏实践及源码分析

    本文所提到的透明状态栏其实指的是将顶部的导航栏延伸到状态栏,使之浑然一体(Google官方建议状态栏颜色比导航栏的颜色略深一点),并不代表一定不设置背景色,比如导航栏是白色,则可设置状态栏为白色,视情况而定. 相比于iOS系统,Android系统对于状态栏的设置就显得稍微复杂了一点.Android系统提供了API 19以上对状态栏的设置接口,而直到API 23以上才提供对于icon颜色的设置,还有就是各家厂商(如魅族,小米等)对于状态栏的有自己的定制,对于需要使用浅色背景状态栏的应用,没处理好的

  • java编程Reference核心原理示例源码分析

    带着问题,看源码针对性会更强一点.印象会更深刻.并且效果也会更好.所以我先卖个关子,提两个问题(没准下次跳槽时就被问到). 我们可以用ByteBuffer的allocateDirect方法,申请一块堆外内存创建一个DirectByteBuffer对象,然后利用它去操作堆外内存.这些申请完的堆外内存,我们可以回收吗?可以的话是通过什么样的机制回收的? 大家应该都知道WeakHashMap可以用来实现内存相对敏感的本地缓存,为什么WeakHashMap合适这种业务场景,其内部实现会做什么特殊处理呢?

  • 手写 Vue3 响应式系统(核心就一个数据结构)

    目录 前言 响应式 总结 前言 响应式是 Vue 的特色,如果你简历里写了 Vue 项目,那基本都会问响应式实现原理.而且不只是 Vue,状态管理库 Mobx 也是基于响应式实现的.那响应式是具体怎么实现的呢?与其空谈原理,不如让我们来手写一个简易版吧. 响应式 首先,什么是响应式呢? 响应式就是被观察的数据变化的时候做一系列联动处理.就像一个社会热点事件,当它有消息更新的时候,各方媒体都会跟进做相关报道.这里社会热点事件就是被观察的目标.那在前端框架里,这个被观察的目标是什么呢?很明显,是状态

随机推荐