关于Vue3中的响应式原理

目录
  • 一、简介
  • 二、响应核心
    • 1.核心源码
    • 2.逐步分析上述示例代码
    • 3.收集依赖和触发依赖更新
  • 三、V3.2的响应式优化
  • 四、后话

一、简介

本章内容主要通过具体的简单示例来分析Vue3是如何实现响应式的。理解本章需要了解Vue3的响应式对象。只注重原理设计层面,细节不做太多讲解。

二、响应核心

1.核心源码

export class ReactiveEffect<T = any> {
  //是否激活
  active = true
  //依赖列表
  deps: Dep[] = []
  // can be attached after creation
  computed?: boolean
  //是否允许递归
  allowRecurse?: 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 | null
  ) {
  	//将自身添加到一个全局的EffectScope容器中
    recordEffectScope(this, scope)
  }
  run() {
    if (!this.active) {
      //没有激活,直接调用回调方法
      return this.fn()
    }
    //栈中不存在当前对象
    if (!effectStack.includes(this)) {
      try {
        //推入栈顶,并且置为全局激活对象
        effectStack.push((activeEffect = this))
        //开启依赖收集开关
        enableTracking()
        //位操作符:用于优化
        trackOpBit = 1 << ++effectTrackDepth
        //源码中maxMarkerBits取30 猜测是因为整数位运算时是按照32位计算 当1<<31时为负值了,后续负值的位运算得不到预期结果 所以取的最大30
        if (effectTrackDepth <= maxMarkerBits) {
          //将当前依赖列表的所有依赖置为"已经收集"
          initDepMarkers(this)
        } else {
          //不采用优化模式,使用老流程,直接移除依赖的全部状态
          cleanupEffect(this)
        }
        //调用回调
        return this.fn()
      } finally {
        if (effectTrackDepth <= maxMarkerBits) {
          //断掉依赖关联
          finalizeDepMarkers(this)
        }
        //重置位操作状态
        trackOpBit = 1 << --effectTrackDepth
        //重置依赖收集状态
        resetTracking()
        //栈顶出栈
        effectStack.pop()
        const n = effectStack.length
        activeEffect = n > 0 ? effectStack[n - 1] : undefined
      }
    }
  }
  stop() {
    if (this.active) {
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
      this.active = false
    }
  }
}

上述ReactiveEffect对象,其实需要关注的就是一个run方法,这个方法设计得十分巧妙,所有动态响应的本质其实都是通过调用run方法实现的

比如如下代码:

    let dummy
    const counter = reactive({ num: 0 })
    let innerfunc = () => dummy = counter.num;
    effect(innerfunc)
    //下面的赋值,最终会执行innerfunc,所以dummy会变成7
    counter.num = 7

可能会有疑惑,上述代码并没有出现ReactiveEffect类型的对象,它其实是在effect方法中创建的,我们接下来分析下effect方法。

export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  if ((fn as ReactiveEffectRunner).effect) {
    fn = (fn as ReactiveEffectRunner).effect.fn
  }
  //创建对象并传参
  const _effect = new ReactiveEffect(fn)
  if (options) {
    extend(_effect, options)
    if (options.scope) recordEffectScope(_effect, options.scope)
  }
  if (!options || !options.lazy) {
    //执行
    _effect.run()
  }
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  runner.effect = _effect
  return runner
}

这个方法的简单用法很简单,就是创建一个ReactiveEffect类型对象,然后执行run方法。

可能对于recordEffectScope方法有疑惑,其实这个方法和响应式无关,它的主要作用是将一个ReactiveEffect对象放入一个effectScope容器对象内,这个容器对象可以方便快捷的对容器内所有的ReactiveEffect对象和其子effectScope调用stop方法。只关注响应式的话可以不作考虑。

2.逐步分析上述示例代码

    let dummy
    //步骤1:创建一个响应式对象
    const counter = reactive({ num: 0 })
    let innerfunc = () => dummy = counter.num;
    //步骤2:调用effect方法
    effect(innerfunc)
    //步骤3:修改响应式对象数据
    counter.num = 7

上述的测试代码看似就3个步骤,其实内部做的东西非常多,我们来跟踪下运行流程。

步骤1:这一步很简单,就是单纯的创建一个Proxy对象,此时counter对象变成响应式的。

步骤2:effect方法里面最终调用的是run方法,而run方法主要是将自身挂载到全局激活并入栈,此时调用回调方法。回调方法此时为上面innerfunc方法,调用这个方法会读取counter.num属性,读取响应式对象的属性会调用代理拦截处理的get方法,在get方法里面,会收集依赖。此时将依赖存于栈顶的那个ReactiveEffect对象的deps属性中。

步骤3:当响应式对象的属性修改后,会触发依赖更新,由于触发更新的依赖列表里面存在effect方法里面创建的ReactiveEffect对象,所以会重新调用其run方法,在这儿也就会调用innerfunc方法。所以dummy属性就会跟随counter.num属性的变化而变化

备注:上述三步骤中,提及了收集依赖和触发依赖更新。接下来我们便看一下是如何收集依赖和触发依赖更新的。

3.收集依赖和触发依赖更新

(1).收集依赖

export function track(target: object, type: TrackOpTypes, key: unknown) {
  //是否允许收集
  if (!isTracking()) {
    return
  }
  //对象map
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  //依赖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)
}
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!)
  }
  if (shouldTrack) {
    //收集依赖
    dep.add(activeEffect!)
    activeEffect!.deps.push(dep)
    if (__DEV__ && activeEffect!.onTrack) {
      activeEffect!.onTrack(
        Object.assign(
          {
            effect: activeEffect!
          },
          debuggerEventExtraInfo
        )
      )
    }
  }
}

上述代码是收集依赖的核心代码。看过我响应式对象文章的话,应该会注意到,在涉及“读”相关操作时,就会调用track方法来收集依赖。此时就是调用的上述track方法。track方法很简单,主要是找到对应的依赖列表,如果没有就创建一个依赖列表。

trackEffects里面先只需要关注收集依赖的逻辑,可以很明显的看到,里面就是一个依赖的双向添加。至于上面的那些逻辑,最主要的目的是防止重复添加依赖,我会在后面的优化环节详细讲。

依赖模型存储模型大致如下:

(2).触发依赖更新

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  //获取依赖map
  const depsMap = targetMap.get(target)
  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)) {
    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)) {
          // 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))
    }
  }
}
export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  for (const effect of isArray(dep) ? dep : [...dep]) {
    if (effect !== activeEffect || effect.allowRecurse) {
      if (__DEV__ && effect.onTrigger) {
        effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
      }
      if (effect.scheduler) {
        effect.scheduler()
      } else {
        //只需要关注这儿
        effect.run()
      }
    }
  }
}

trigger方法在响应式对象的"写"操作中调用,这个方法整体上只是根据不同的依赖更新类型,将依赖添加进一个依赖数组里面,最终通过triggerEffects方法更新这个依赖数组里面的依赖。

在triggerEffects方法里面,暂时只需要关注effect.run即可,此时调用的是ReactiveEffect关联的那个回调方法,这时候也就正确的响应式变化了。

至于effect.scheduler,我会在后续的计算属性篇章中讲到,这个方法是给计算属性用的。

三、V3.2的响应式优化

上述篇幅只讲述了一个整体的响应式变化原理,接下来介绍一下V3.2带来的响应式性能优化。我们先看一下Dep类型的定义

export type Dep = Set<ReactiveEffect> & TrackedMarkers
/**
 * wasTracked and newTracked maintain the status for several levels of effect
 * tracking recursion. One bit per level is used to define whether the dependency
 * was/is tracked.
 */
type TrackedMarkers = {
  /**
   * wasTracked
   */
  w: number
  /**
   * newTracked
   */
  n: number
}

可以看到,依赖列表不是一个简简单单的Set集合,它还存在2个用于辅助的属性。我们创建依赖也是通过createDep方法,实现如下:

export const createDep = (effects?: ReactiveEffect[]): Dep => {
  const dep = new Set<ReactiveEffect>(effects) as Dep
  dep.w = 0
  dep.n = 0
  return dep
}

我在这儿先说明一下这2个属性的作用。w属性用于判断依赖是否已经被收集,n属性用于判断依赖在本次调用中是否用到。可能现在还对此有疑惑,我用以下一个简单示例来解释。

let status = true;
let dummy
const depA = reactive({ num: 0 })
const depB = reactive({ num: 10 })
let innerfunc = () => {
	dummy = depA.num
	if(status){
		dummy += depB.num
		status = false
	}
	console.log(dummy);
}
effect(innerfunc)
depA.num = 7
depB.num = 20
//输出为 10 7

我们来分析以下上述代码的流程,首先调用effect方法,会执行一次关联的innerfunc,此时读取了depA和depB的num属性,所以此时ReactiveEffect对象里面的deps属性存在2个依赖,并且输出10。当修改depA.num属性时,会触发run方法,此时关注以下代码:

        if (effectTrackDepth <= maxMarkerBits) {
          //将当前依赖列表的所有依赖置为"已经收集"
          initDepMarkers(this)
        } else {
          //不采用优化模式,使用老流程,直接移除依赖的全部状态
          cleanupEffect(this)
        }

因为调用effect方法时,收集过一次依赖,所以initDepMarkers方法将所有的依赖都标记为已经收集。在run方法最后,会调用innerfunc方法。在innerfunc方法中,这一次调用中又会去收集依赖,此时关注trackEffects中的以下代码:

  if (effectTrackDepth <= maxMarkerBits) {
    if (!newTracked(dep)) {
      //本一轮调用新收集的依赖
      dep.n |= trackOpBit // set newly tracked
      //是否应该被收集
      shouldTrack = !wasTracked(dep)
    }
  } else {
    // Full cleanup mode.
    shouldTrack = !dep.has(activeEffect!)
  }

在run方法中,有一个同样的判断effectTrackDepth <= maxMarkerBits,这个判断是用于控制是否优化的,后面会讲为什么会存在这个判断以及为什么maxMarkerBits的取值为30

在这个收集逻辑中,会将本次回调中第一次使用到的依赖置为"新增依赖",我们在看innerfunc,此时只会使用到depA,不会使用到depB,因此之前存在的关于depB对象的依赖在本次调用中没有用到。

shouldTrack属性表示依赖是否应该被收集,如果没有收集,则被收集。此时innerfunc里面输出的dummy为7。

接下来关注run里面的以下代码:

  if (effectTrackDepth <= maxMarkerBits) {
    //断掉依赖关联
    finalizeDepMarkers(this)
  }
  //重置位操作状态
  trackOpBit = 1 << --effectTrackDepth
  //重置依赖收集状态
  resetTracking()
  //栈顶出栈
  effectStack.pop()
  const n = effectStack.length
  activeEffect = n > 0 ? effectStack[n - 1] : undefined

上述代码存在于run方法里面的finally关键字内,当innerfunc执行完后,里面就会执行这里。首先便会根据判断通过finalizeDepMarkers方法去断掉依赖关联。

我们看以下方法的实现:

export const initDepMarkers = ({ deps }: ReactiveEffect) => {
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].w |= trackOpBit // set was tracked
    }
  }
}
export 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
      }
      // clear bits
      dep.w &= ~trackOpBit
      dep.n &= ~trackOpBit
    }
    deps.length = ptr
  }
}

这2个方法巧妙的通过位运算将调用分层。一开始将存在的依赖打上收集标签,如果在本层中没有使用到,则断掉依赖关联。当设置depB.num = 20时,首先会找到依赖列表,由于依赖列表中已经不存在ReactiveEffect对象了,所以不会触发依赖更新,此时不会有新的输出。

这儿是一个优化,断掉不必要的关联依赖,减少方法的调用。但我们在写类似代码时必须非常小心,由于断掉了依赖关联,有可能会因为写法不规范导致响应失效的情况。

接下来解释为什么要使用位运算,以及保留不走位运算的逻辑。

关注以下代码:

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

当每次触发依赖更新时,如果都调用以上方法,会涉及大量的集合删减操作。

我没看过Set集合的实现,但无非就是数组或者链表。如果使用数组,增删操作会导致数组扩容或者移位,频繁操作会耗费大量性能,如果是链表,也要经过一次查找,大量的调用是会消耗性能的。

那么为什么又要保留这个方法呢,这是因为js引擎在进行整数位运算时几乎都是按32位运算的,1 << 31后为负值,得不到预期结果,因此保留原逻辑。但其实这个逻辑几乎不可能调到,如果真调用到这个原始逻辑,我只能说得检查一下代码是否规范了。

四、后话

关于响应式就介绍到这儿,个人理解,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • vue如何使用媒体查询实现响应式

    目录 使用媒体查询实现响应式的两种方式 前提 1.在每个组件中为其使用媒体查询 2.写n套页面,在使用这些页面的组件中进行一次媒体查询 vue中的媒体查询 语法 使用媒体查询实现响应式的两种方式 前提 依赖: sass,sass-loader 1.在每个组件中为其使用媒体查询 这种方法的有点是减少了重写不同终端同一组件的工作量,缺点是每个组件都要使用媒体查询,当一套页面组件不同时,需要进行组件的显示与隐藏(display:none!important),在不同终端区别不大的情况下建议使用这种方法

  • vue中响应式布局如何将字体大小改成自适应

    目录 响应式布局将字体大小改成自适应 vue文字大小自适应问题 响应式布局将字体大小改成自适应 1.在app.vue的生命周期函数中添加一段代码来设置页面的rem mounted: function() {     // 页面开始加载时修改font-size     var html = document.getElementsByTagName("html")[0];     var oWidth = document.body.clientWidth || document.doc

  • 关于vue2响应式缺陷的问题

    目录 vue2响应式缺陷 1.对象新增的属性没有响应式 2.数组的部分操作没有响应式 vue2与vue3的响应式原理 vue2响应式 vue3响应式雏形 vue3的响应式相较于vue2的优势 vue2响应式缺陷 响应式 : 数据改变 ==> 视图跟着改变 vue2响应式缺陷 1.对象新增的属性没有响应式 对象,新增属性b,修改b的值,值改变但视图并未更新 解决方案 : 使用vue提供的 api $set(对象,属性名,值) 效果如属性c 2.数组的部分操作没有响应式 数组中有7种操作有响应式 a

  • Vue响应式原理模拟实现原理探究

    目录 前置知识 数据驱动 数据响应式的核心原理 Vue 2.x Vue 3.x 发布订阅和观察者模式 发布/订阅模式 观察者模式 Vue响应式原理模拟实现 Vue Observer对data中的属性进行监听 Compiler Watcher Dep 测试代码 前置知识 数据驱动 数据响应式——Vue 最标志性的功能就是其低侵入性的响应式系统.组件状态都是由响应式的 JavaScript 对象组成的.当更改它们时,视图会随即自动更新. 双向绑定——数据改变,视图改变:视图改变,数据也随之改变 数据

  • vue视图响应式更新详细介绍

    目录 概述 思路 第一步统一封装更新函数 第二步监听并触发视图更新 引入Dep管家 实现下语法糖v-model 概述 前面两篇文章已经实现了对数据的变化的监听以及模板语法编译初始化,但是当数据变化时,视图还不能够跟随数据实时更新.本文就在之前的基础上介绍下视图响应式更新部分. 思路 统一封装更新函数 待数据发生改变时调用对应的更新函数 这里思考个问题: 在何时注册这个更新函数? 如何找到对应的更新函数? 第一步统一封装更新函数 基于上篇文章compile的部分,将数据初始化的部分统一封装起来.

  • 关于Vue3中的响应式原理

    目录 一.简介 二.响应核心 1.核心源码 2.逐步分析上述示例代码 3.收集依赖和触发依赖更新 三.V3.2的响应式优化 四.后话 一.简介 本章内容主要通过具体的简单示例来分析Vue3是如何实现响应式的.理解本章需要了解Vue3的响应式对象.只注重原理设计层面,细节不做太多讲解. 二.响应核心 1.核心源码 export class ReactiveEffect<T = any> { //是否激活 active = true //依赖列表 deps: Dep[] = [] // can b

  • vue3中的响应式原理-effect

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

  • Vue3.0数据响应式原理详解

    基于Vue3.0发布在GitHub上的第一版源码(2019.10.05)整理 预备知识 ES6 Proxy,整个响应式系统的基础. 新的composition-API的基本使用,目前还没有中文文档,可以先通过这个仓库(composition-api-rfc)了解,里面也有对应的在线文档. 先把Vue3.0跑起来 先把vue-next仓库的代码clone下来,安装依赖然后构建一下,vue的package下的dist目录下找到构建的脚本,引入脚本即可. 下面一个简单计数器的DEMO: <!DOCTY

  • 一文详解Vue3响应式原理

    目录 回顾 vue2.x 的响应式 vue3的响应式 Reflect 回顾 vue2.x 的响应式 实现原理: 对象类型:通过object.defineProperty()对属性的读取.修改进行拦截(数据劫持) 数组类型:通过重写更新数组的一系列方法来实现拦截(对数组的变更方法进行了包裹) Object.defineProperty(data,'count ",{ get(){}, set(){} }) 存在问题: 新增属性.删除属性,界面不会更新 直接通过下标修改数组,界面不会自动更新 但是

  • Vue3 Reactive响应式原理逻辑详解

    目录 前言 一.怎么实现变量变化 二.怎么实现变量变化 三.将多个dep存储在Map中 四.将多个object的depsMap继续存储起来 五.核心 六.源码解析(TypeScript) 前言 本篇文章主要讲解vue响应式原理的逻辑,也就是vue怎么从最开始一步步推导出响应式的结构框架. 先从头构建一个简单函数推导出Vue3的Reactive原理,最后再进行源码的验证. 一.怎么实现变量变化 怎么实现变量变化,相关依赖的结果也跟着变化 当原本price=5变为price=20后total应该变为

  • vue数据响应式原理知识点总结

    vue2.0数据响应式原理 对象 Obect.defineproperty 定义对象的属性mjm defineproperty 其实不是核心的为一个对象做数据双向绑定,而是去给对象做属性标签,设置一系列操作权限,只不过属性里的get和set实现了响应式 var ob = { a: 1, b: 2 } //1-对象 2-属性 3-对于属性的一系列配置 Object.defineProperty(ob, 'a' , { //a对象则是ob的绝对私有属性,,默认都是true writable: fal

  • vue2.0/3.0的响应式原理及区别浅析

    前言 自从vue3.0正式上线以来,好多小伙伴都转战vue3.0了,这里把我自己总结的3.0和2.0的原理以及他俩的区别写出来,方便自己学习. step 一,vue2.0的响应式原理 先看看官网的解释: 当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty把这些属性全部转为 getter/setter.Object.defineProperty 是 ES5 中一个无法 shim 的特性

  • 详解Vue3的响应式原理解析

    目录 Vue2响应式原理回顾 Vue3响应式原理剖析 嵌套对象响应式 避免重复代理 总结 Vue2响应式原理回顾 // 1.对象响应化:遍历每个key,定义getter.setter // 2.数组响应化:覆盖数组原型方法,额外增加通知逻辑 const originalProto = Array.prototype const arrayProto = Object.create(originalProto) ;['push', 'pop', 'shift', 'unshift', 'splic

  • 浅析一下Vue3的响应式原理

    目录 Proxy Reflect 举个例子 reactive effect track trigger Proxy Vue3 的响应式原理依赖了 Proxy 这个核心 API,通过 Proxy 可以劫持对象的某些操作. const obj = { a: 1 }; const p = new Proxy(obj, {   get(target, property, receiver) {     console.log("get");     return Reflect.get(tar

  • 详细分析vue响应式原理

    前言 响应式原理作为 Vue 的核心,使用数据劫持实现数据驱动视图.在面试中是经常考查的知识点,也是面试加分项. 本文将会循序渐进的解析响应式原理的工作流程,主要以下面结构进行: 分析主要成员,了解它们有助于理解流程 将流程拆分,理解其中的作用 结合以上的点,理解整体流程 文章稍长,但大部分是代码实现,还请耐心观看.为了方便理解原理,文中的代码会进行简化,如果可以请对照源码学习. 主要成员 响应式原理中,Observe.Watcher.Dep这三个类是构成完整原理的主要成员. Observe,响

随机推荐