深入解析vue响应式原理

本文主要通过结合vue官方文档及源码,对vue响应式原理进行深入分析。

一、定义

作为vue最独特的特性,响应式可以说是vue的灵魂了,表面上看就是数据发生变化后,对应的界面会重新渲染,那么响应式系统的底层细节到底是怎么一回事呢?

Tips:vue的响应式系统在vue2.0和vue3.0版本中的底层实现有所不同,简单了来说就是处理属性的getter/setter部分从Object.defineProperty替换成了Proxy(不过vue3也保留了Object.defineProperty方式用于支持IE浏览器)

vue2.0实现原理

当一个普通的javascript对象传入vue实例作为data选项时,vue将遍历data的所有属性,并使用Object.defineProperty重写这些属性的getter/setter方法,这些属性的getter/setter对于用户不可见,但是vue可以利用他们来追踪依赖,在属性值被访问和修改时通知变更。每个组件实例都对应一个watcher实例,它会在组件渲染的过程中访问过的属性设置为依赖。之后当属性的setter触发时,会通知watcher对关联的组件进行重新渲染。

vue3.0实现原理

当一个普通的javascript对象传入vue实例作为data选项时,vue会将其转化为Proxy。首次渲染后,组件将跟踪在渲染过程中被访问的属性,组件就成了这些属性的订阅者。当proxy拦截到set操作时,将通知所有订阅了该属性的组件进行重新渲染。

二、vue2.0源码实现

Tips:本节采用的源码是v2.6.11

在vue2.0中,vue的响应式系统是基于数据拦截+发布订阅的模式,主要包含三个部分:

  1. Observer:通过Object.defineProperty拦截data属性的setter/getter方法,从而使每个属性都拥有一个Dep,当触发getter时收集依赖(使用该属性的watcher),当触发setter时通知更新;
  2. Dep:依赖收集器,用于维护依赖data属性的所有Watcher,并分发更新;
  3. Watcher:将视图依赖的属性绑定到Dep中,当数据修改时触发setter,调用Dep的notify方法,通知所有依赖该属性的Watcher进行update更新视图,使属性值与视图绑定起来;
  4. Compile:模板指令解析器,对模板每个元素节点的指令进行扫描解析,根据指令模板替换属性数据,同时注入Watcher更新数据的回调方法。(本文不涉及Compile的源码部分)


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
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        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) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

在数据处理方法中,vue实例的data会作为value参数传递给Observer构造函数,Observer对value参数主要做了如下处理:

  1. 当value为数组时,首先会将数组进行变异,在copyAugment方法中会将数组的'push','pop','shift','unshift','splice','sort','reverse'七个方法触发dep.notify更新(即通知关联Watcher重新渲染界面,对这块感兴趣的朋友可以看一下array.js的源码)。然后再遍历每个值执行observe方法;
  2. 当value为对象时,遍历每个属性执行defineReactive方法。

针对数组进行处理的boserve方法:

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  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
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

简单来说,observe方法主要用于判断数组元素类型,如果是数组或对象将再次传入Observer构造函数。

针对对象进行处理的defineReactive方法:

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  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
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

该方法主要通过Object.defineProperty定义了value每个属性的getter/setter方法,在getter中收集依赖,将属性与Watcher实例相关联;在setter中获取数据变更,通知属性关联得Watcher重新渲染视图。同时也会将属性继续传入Observe方法,一层一层向下处理。

总结:Observer主要通过遍历data对象,并对data对象进行递归处理(这点体现在data对象中嵌套对象也是支持响应式的),遍历递归出来的属性,只针对数组和对象进行下一步处理。其中数组比较特殊,通过数组变异方法监听了数组的7个操作函数,用于通知Watcher重新渲染界面。如果是对象,将遍历该对象的属性,为每个属性定义getter/setter方法,用于绑定依赖关系和通知Wathcer更新界面。

Dep

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

总结:从Observer中我们可以知道一个数据会对应一个Dep,Dep中的subs数组就是用于收集与该数据关联的Watcher,使两者绑定起来。里面还有个Dep.target是全局共享的,他表示目前正在收集依赖的那个Watcher,且同一时间有且只有一个。

Watcher

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;
  newDeps: Array;
  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
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    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 // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for 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
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      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
  }

  /**
   * Add a dependency to this directive.
   */
  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)
      }
    }
  }

  /**
   * Clean up for dependency collection.
   */
  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
  }

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  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)
        }
      }
    }
  }
}

总结:Watcher主要做了如下工作:

  • 初始化属性值,主要是deps、newDeps、depIds、newDepIds,分别表示现有依赖和新一轮要收集的依赖,这里的依赖指的就是上面的Dep;
  • 通过传入构造函数的expOrFn参数设置getter属性,如果expOrFn是函数,直接设置为getter,如果expOrFn为表达式,则将表达式传入parsePath设置为getter;
  • 执行get方法,这里主要用于收集依赖,并获取属性的值,方法最后要调用cleanupDeps来清除依赖。这是因为数据更新之后依赖有可能发生改变,所以要清除后重新收集依赖;
  • update方法是触发更新之后调用,这里分为三种情况(1.lazy延迟加载,暂不调用数据更新;2.sync立即调用更新;3.放入队列中,等待下一个事件循环后执行。),数据更新的时候都会调用run方法,run方法首先会重新调用get收集依赖,然后使用this.cb.call更新模板或表达式的值。

三、vue3.0源码实现

Tips:本节采用的源码是vue3.0.5

vue3.0中响应式系统大致分为以下几个阶段

  1. 初始化阶段:通过组件初始化方法,将data对象转化为proxy对象,并创建一个负责渲染的effect实例;
  2. get依赖收集阶段:通过解析组件模板,替换对应的data属性数据,这个过程会触发属性的getter,通过stack方法将proxy对象、key和effect存入dep;
  3. set派发更新阶段:当data属性数据值变化时,触发属性的getter,通过trigger方法,根据proxy和key找到对应的dep,再调用effect执行界面渲染更新。

Effect

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    effect()
  }
  return effect
}

export function stop(effect: ReactiveEffect) {
  if (effect.active) {
    cleanup(effect)
    if (effect.options.onStop) {
      effect.options.onStop()
    }
    effect.active = false
  }
}

let uid = 0

function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(): unknown {
    if (!effect.active) {
      return options.scheduler ? undefined : fn()
    }
    if (!effectStack.includes(effect)) {
      cleanup(effect)
      try {
        enableTracking()
        effectStack.push(effect)
        activeEffect = effect
        return fn()
      } finally {
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  effect.id = uid++
  effect.allowRecurse = !!options.allowRecurse
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}

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

Track

export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
    if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
}

Trigger

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
  }

  const effects = new Set<ReactiveEffect>()
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || effect.allowRecurse) {
          effects.add(effect)
        }
      })
    }
  }

  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) {
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      add(depsMap.get(key))
    }

    // also run for iteration key on ADD | DELETE | Map.SET
    switch (type) {
      case TriggerOpTypes.ADD:
        if (!isArray(target)) {
          add(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // new index added to array -> length changes
          add(depsMap.get('length'))
        }
        break
      case TriggerOpTypes.DELETE:
        if (!isArray(target)) {
          add(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          add(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }

  const run = (effect: ReactiveEffect) => {
    if (__DEV__ && effect.options.onTrigger) {
      effect.options.onTrigger({
        effect,
        target,
        key,
        type,
        newValue,
        oldValue,
        oldTarget
      })
    }
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }

  effects.forEach(run)
}

未完待续...

posted @ 2021-01-20 19:53  0nTheRoad  阅读(899)  评论(0编辑  收藏  举报