Vue2.x 源码 - 响应式原理

上一篇:Vue2.x 源码 - VNode渲染过程(update、patch)

什么是响应式

在改变数据的时候,视图会跟着更新;React 是通过 this.setState 去改变数据,然后根据新的数据重新渲染出虚拟DOM,最后通过对比虚拟DOM找到需要更新的节点进行更新;而 Vue 则是利用了 Object.defineProperty 的方法里面的 settergetter 方法的观察者模式来实现。

Object.defineProperty

Vue2.x 实现响应式的核心是利用的 ES5 的 Object.defineProperty ,这也是 Vue 不兼容 IE8 及其以下浏览器的原因;Object.defineProperty 的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性,并返回该对象;

Object.defineProperty(obj, prop, descriptor)

obj :需要定义属性的当前对象
prop :当前需要定义的属性名
desc: 要定义或修改的属性描述符

let obj = {};
Object.defineProperty( obj, 'name', {
	//value:'1',
	//writable:true,
	enumerable: true,
    configurable: true,
	get: function () {
    	return temp
    },
    set: function (val) {
        temp = val
    }
})

描述符分为:数据描述符、存取描述符;

数据描述符
1、value:属性的默认值,可以是任何有效的 JavaScript 值(数值,对象,函数等),默认为 undefined;
2、writable:只有当值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符改变,默认为 false;
3、configurable:只有当值为 true 时,属性才能被重新定义(注意这里不是重新赋值),同时该属性也能从对应的对象上被删除,默认为 false;
4、enumerable:只有当值为 true 时,属性才会出现在对象的枚举属性(for in 或者 Object.keys())中,默认为 false;

存取描述符
1、get:属性的 getter 函数,如果没有 getter,则为 undefined;当访问该属性时会调用此函数,执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象);该函数的返回值会被用作属性的值,默认为 undefined;
2、set:属性的 setter 函数,如果没有 setter,则为 undefined;当属性值被修改时会调用此函数;该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象,默认为 undefined;

注意:

1、get 返回的值才是最终属性的值,修改属性值的时候会先触发 set 去设置这个修改的值,然后才会触发 get 去获取最新的值并返回;
2、如果一个描述符同时拥有 value || writable 和 get || set 键,则会产生一个异常,在使用的时候需要注意;

响应式的实现

先看一下 Vue 官网提供的流程图:
在这里插入图片描述
过程梳理:

1、init:在初始化(init)的时候会调用 observe方法为 data 绑定 getter 、setter 方法,当数据被读取时会触发 getter 方法,被赋值时会触发 setter 方法;每一个 data 的属性都有一个 dep 对象,调用 getter 的时候会去 dep 中注册函数,调用 setter 的时候会去通知 dep 中对应的函数执行;这个绑定的过程也叫数据劫持
2、mount:在 mount 阶段(mountComponent)会创建一个 Watcher 对象,Watcher 实际上是连接 Vue 组件与 Dep 的桥梁;创建 Watcher 的时候 Watcher 构造函数中的this.getter.call(vm, vm)函数会被执行;Watcher 构造函数里面的getter就是updateComponent此时 Watcher 会立即调用组件的 render 函数去生成虚拟 DOM;在调用 render 的时候,就会需要用到 data 的属性值,此时会触发上一步的 getter 函数,将当前的 Watcher 函数注册进 Dep里;这个过程也叫依赖收集
3、update:当 data 属性发生改变之后,由 setter 触发调用 Dep 的 notify 函数去通知 Dep 里对应的 Watcher 对象,通知它们去重新渲染组件;

下面详细说一下这个过程:

数据劫持(observe)

在初始化环节,会为 data / props 通过 observe 方法绑定 getter / setter 方法;在 src/core/observer/index.js 文件里:

export function observe (value: any, asRootData: ?boolean): Observer | void {
   // 不为对象或者是VNode实例时不进行任何操作
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  //判断是否有__ob__属性且__ob__为Observer实例
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
  //是数组或者普通对象,非服务端渲染,是可拓展对象,非 vue 实例对象
    ob = new Observer(value)
  }
  // 根 data,并且是响应式
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

1、该方法首先判断传递的 value 不是对象或者是 VNode实例时则直接返回不做处理;
2、然后判断是不是有 _ob_ 属性且_ob__Observer实例,是则直接返回避免重复定义响应式;否则在满足条件的情况下重新创建 Observer 实例;
下面看看 Observer

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data
  constructor (value: any) {
    this.value = value
    //实例化dep
    this.dep = new Dep()
    this.vmCount = 0
    // 把自身实例添加到数据对象 value 的 __ob__ 属性上
    def(value, '__ob__', this)
    // value 是否为数组的不同调用
    if (Array.isArray(value)) {
      if (hasProto) {
       //通过使用__proto__截取原型链来增加目标对象或数组
        protoAugment(value, arrayMethods)
      } else {
       //通过定义隐藏属性来增加目标对象或数组
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  // 取出所有属性遍历
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  //观察数组项的列表
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

1、⾸先实例化 Dep 对象;
2、接着通过执⾏ def 函数把⾃⾝实例添加到数据对象 value 的 __ob__ 属性上,def 函数是⼀个⾮常简单的 Object.defineProperty 的封装,使用def定义__ob__的目的是让__ob__在对象属性遍历的时候不可被枚举出来;
3、接下来会对 value 做判断,对于数组会调⽤ observeArray ⽅法,这里会将自定义的一些数组处理方法arrayMethods集合绑定到原型上; 否则对纯对象调⽤ walk ⽅法;
4、 observeArray 方法是遍历数组再次调⽤ observe ⽅法,⽽ walk ⽅法是遍历对象的 key 调⽤ defineReactive ⽅法;

插入一下 def 的作用:

export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

def 函数接收四个参数,分别是 源对象,要在对象上定义的键名,对应的值,以及是否可枚举,如果不传递 enumerable 参数则代表定义的属性是不可枚举的。

下面看看defineReactive ⽅法:
defineReactive 的功能就是定义⼀个响应式对象,给对象动态添加 getter 和 setter;

// 定义对象上的响应性属性
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  //初始化 Dep
  const dep = new Dep()
  //获取了当前obj.key的属性描述
  const property = Object.getOwnPropertyDescriptor(obj, key)
  //如果当前key不可被重新定义则返回
  if (property && property.configurable === false) {
    return
  }
  // 没有getter有setter,而且只传了2个参数,那就给val赋值一下
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  //深度检测shallow 为true不会深度检测
  let childOb = !shallow && observe(val)
  //响应式绑定
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        // 进行依赖收集
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 对新的值进行监听
      childOb = !shallow && observe(newVal)
      // 通知所有订阅者,内部调用 watcher 的 update 方法 
      dep.notify()
    }
  })
}

1、初始化 Dep 实例,然后属性的 configurable 为 false 时直接返回,没有 getter 则给 val 赋值;
2、这里对 val 调用observe ,有个 shallow 是用来控制不深度监测的对象,默认深度监测,$attrs 、$ listeners 不会深度监测;
3、调用 Object.defineProperty 的设置 get 和 set ,同时对 Dep 进行操作;

reactiveGetter
1、先判断此属性是否有getter,若存在就直接执行获取值,否则就返回之前的 val 赋值给 value;
2、target 存在,调用 dep 进行依赖收集;
3、childOb 存在说明是深度监测,则收集 childOb 的依赖;
4、value 如果是数组,调用 dependArray 将每一个是对象的子项的依赖收集起来;
5、 dependArray 循环数组,判断是子项是否有__ob__ ,有说明是对象则将子项的 __ob__加入子项的 dep ,如果子项是数组则递归调用 dependArray 方法;
reactiveSetter
1、先判断此属性是否有getter,若存在就直接执行获取值,否则就返回之前的 val 赋值给 value;
2、如果值没有改变或者 新值和旧值都为 NaN 的情况就直接 return;
3、开发环境,如果 customSetter参数存在,就调用此函数;
4、有 getter 没有 setter 直接返回;
5、如果有setter就调用setter处理新的值,否则直接复制给 val;
6、深度监测情况下(默认),对新值调用observe进行监听,因为旧值被覆盖,旧值的;
7、最后调用dep.notify() 通知更新;

每一个 observe 都有一个对应的 Dep,它内部维护一个数组,保存与该observe 相关的Watcher;所以在依赖收集的时候才会出现这种 dep.depend()childOb.dep.depend()不同层级的依赖收集;

依赖收集(Dep)

依赖的收集主要发生在 defineReactive 触发 getter 的时候,通过 dep.depend 来对当前 Observer 上的依赖进行收集,在上面已经有很详细的介绍,但是依赖收集的主要位置 Dep 我们还不知道它是什么。这里主要介绍一下 Dep。
src/core/observer/dep.js 文件里:

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
  constructor () {
    this.id = uid++
    this.subs = []
  }
  //新增sub
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  //删除sub
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  //依赖收集
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  //触发更新
  notify () {
    //获取一个一样的新数组
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // 如果没有运行异步,那么在调度程序中sub是不会排序的,我们现在需要对它们排序,以确保它们以正确的顺序触发
      //这里通过watcher 的id来排序
      subs.sort((a, b) => a.id - b.id)
    }
    //村换当前observer下的watcher,触发更新
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

1、Dep 类首先定义了一个静态属性 target,它就是各种Watcher的实例;然后又定义了两个实例属性,id 是 Dep 的主键,会在实例化的时候自增,subs 是一个存储各种 Watcher 的数组。例如 render watcher、user watcher 和 computed watcher 等;
2、addSubremoveSub对应的就是往 subs 数组中添加和移除各种Watcher
3、notify 当这个响应式数据发生变化的时候,通知 subs 里面的各种watcher,然后执行其watcherupdate()方法。这属于派发更新的过程,后面会将;

可以看到 Dep 主要就是提供一个数组来存储 各种 Watcher,提供对 sub 数组新增和删除的方法,以及触发对应 watcher 更新的方法;Dep 完全可以看成是对 Watcher 的管理者;

订阅者 (Watcher)

src/core/observer/watcher.js 文件里:

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    //render watch 将当前watcher赋值给 vm上的_watcher 
    if (isRenderWatcher) {
      vm._watcher = this
    }
    //将watcher放到vm上的_watchers数组里面
    vm._watchers.push(this)
    // 参数
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // f缓存
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // 获取getter函数
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    //lazy为true则延迟get方法执行
    this.value = this.lazy
      ? undefined
      : this.get()
  }
  //getter
  get () {
  //把 Dep.target 赋值为当前的渲染 watcher
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      //获取value值
      value = this.getter.call(vm, vm)
    } catch (e) {
     //watch的watcher
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
  //新增
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }
  //清除
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }
  //更新
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  /**
   * Remove self from all dependencies' subscriber list.
   */
  teardown () {
    if (this.active) {
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }
}

1、Watcher 是⼀个 Class,在它的构造函数中,定义了⼀些和 Dep 相关的属性和⼀些原型的⽅法;
2、当我们去实例化一个 watcher 的时候会执行内部的this.get方法:

1、get 方法首先会把当前Watcher实例压栈到target栈数组中,然后把Dep.target设置为当前的Watcher实例;
2、执行this.getter 获取 value,this.getter 实际上就是 updateComponent 函数,实际上就是在执⾏ vm._update(vm._render(), hydrating),在执行 vm.render 的时候会去 vm 上访问数据,这个时候就会触发数据对象的 getter;
3、如果 deep 为true 则把每一个属性都跟踪作为深度监视的依赖,一般是 watch 使用;
4、然后把当前target栈数组的最后一个移除,然后把Dep.target设置为倒数第二个;
5、清空依赖;

3、在依赖收集的时候执行 dep.depend 会触发 addDep 方法;

1、当前dep是否已经在新dep id集合中,不在则更新新dep id集合以及新dep数组;
2、当前dep是否在旧dep id集合中,不在则调用dep.addSub(this)方法,把当前Watcher实例添加到dep实例的subs数组中

4、清空依赖 cleanupDeps:

1、首先遍历旧依赖列表deps,如果发现其中某个dep不在新依赖id集合newDepIds中,则调用dep.removeSub(this)移除依赖;
2、在遍历完deps数组后,会把deps和newDeps、depIds和newDepIds的值进行交换,然后清空newDeps和newDepIds(老的删掉,新的赋值给老的);

5、派发更新 update:

1、lazy 是computed 的标志,这里会设置缓存;
2、是同步更新则走 run 否则走 queueWatcher;

6、同步run:官网没有对于 options 的 sync 配置,所以这里只说一下他的作用—当dep通知某个watcher实例需要更新的时候,这个watcher实例直接调用callback方法进行更新;

7、异步queueWatcher:

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  //这个if表示,如果这个watcher尚未被flush则return
  if (has[id] == null) {
  //再次把watcher置为true
    has[id] = true
    if (!flushing) {
    //如果当前不是正在更新watcher数组的话,那watcher会被直接添加到队列末尾
      queue.push(watcher)
    } else {
      //在watcher队列更新过程中,用户再次更新了队列中的某个watcher 
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}

queue:各种 Watcher 执行队列;
has:防止重复添加Watcher的标志对象:

1、先通过获取当前Watcher的自增id,判断在标志对象has中是否已经存在,如果不存在,则对这个id进行标记,赋值为true;
2、判断是否为flushing状态,如果不是,则代表我们可以正常的把当前Watcher推入到queue队列数组中
3、是否为waiting状态,如果不是,则代表可以执行queue队列数组,然后设置waiting为true,最后调用nextTick(flushSchedulerQueue);

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id
  queue.sort((a, b) => a.id - b.id)
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()
  resetSchedulerState()
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}

它主要做几件事情:还原flushing状态、排序queue队列、遍历queue、还原状态、触发组件钩子函数
1、首先对 flushing 进行了还原,这样做的目的是为了不影响在执行 queue 队列的时候,有 Watcher 推入到 queue 队列中;
2、使用数组的 sort 方法,把 queue 队列中的 Watcher 按照自增 id 的值从小到大进行了排序,这样做是为了保证以下三种场景:

1、组件从父组件更新到子组件更新,因为父类是在子类之前创建;
2、组用户自定义 Watcher 在组件渲染之前创建,因为用户自定义 Watcher 是在组件渲染之前创建的;
3、如果组件在父组件的监视程序运行期间被销毁,则可以跳过其监视程序;

3、遍历 queue,释放当前 Watcher 在 has 标志对象中的状态,然后调用watcher.run()方法;
4、当 queue 队列都执行完毕时,把所有相关状态还原为初始状态,这其中包括 queue、has 和 index 等;
5、调用 callActivatedHookscallUpdatedHooks 分别是为了触发组件activatedupdated钩子函数,其中activated是与 keep-alive 相关的钩子函数;

派发更新(update)

当响应式数据发生变动的时候,通知所有订阅了这个数据变化的Watcher(既 Dep 依赖)执行update
1、对于render watcher 渲染Watcher而言,update就是触发组件重新进行渲染;
2、对于computed watcher 计算属性Watcher而言,update就是对计算属性重新求值;
3、对于user watcher用户自定义Watcher而言,update就是调用用户提供的回调函数;

以上就是响应式的整个流程!

注意

1、Vue2.x 不能监听数组下标和 length 长度的变化,无法监听到对象属性的动态添加和删除;这个是硬伤,不过 2.x 的 API 中专门提供了 $get $set $delete 来解决这个问题;

this.$set(this.obj, key, value)

Vue3.x 直接正面解决着个问题,使用 ES6 提供的 Proxy 来替代 Object.defineProperty;解决了 Vue2.x 不能监听数组改变的缺点,并且还支持劫持整个对象,并返回一个新对象;

需要说明的是:对于数组下标(push)和长度变化 Object.defineProperty 是可以监测的到的,Vue2.x 无法监听完全是 Vue2.x 自己做的限制;在 Observer 里面,当数据是数组时调用 observeArray 遍历数组的每一项重新调用 observe,这里只会对对象属性进行监听,并没有对数组的属性进行监听;

 observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }

按照尤大大的说法就是:“性能代价和获得的用户体验收益不成正比”;换句话说就是:性能消耗大,不划算;

总结

1、Vue 的数据更新是异步的;
2、响应式主要是使用 Object.definedProperty 的 get 和 set 方法来收集依赖和触发更新;
在这里插入图片描述

下一篇:Vue2.x 源码 - computed 和 watch 的依赖收集和更新

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值