深刻解析vue響應式原理

本文主要經過結合vue官方文檔及源碼,對vue響應式原理進行深刻分析。

1、定義

做爲vue最獨特的特性,響應式能夠說是vue的靈魂了,表面上看就是數據發生變化後,對應的界面會從新渲染,那麼響應式系統的底層細節究竟是怎麼一回事呢?javascript

Tips:vue的響應式系統在vue2.0和vue3.0版本中的底層實現有所不一樣,簡單了來講就是處理屬性的getter/setter部分從Object.defineProperty替換成了Proxy(不過vue3也保留了Object.defineProperty方式用於支持IE瀏覽器)vue

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操做時,將通知全部訂閱了該屬性的組件進行從新渲染。

2、vue2.0源碼實現

Tips:本節採用的源碼是v2.6.11java

在vue2.0中,vue的響應式系統是基於數據攔截+發佈訂閱的模式,主要包含三個部分:react

  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參數主要作了以下處理:typescript

  1. 當value爲數組時,首先會將數組進行變異,在copyAugment方法中會將數組的'push','pop','shift','unshift','splice','sort','reverse'七個方法觸發dep.notify更新(即通知關聯Watcher從新渲染界面,對這塊感興趣的朋友能夠看一下array.js的源碼)。而後再遍歷每一個值執行observe方法;
  2. 當value爲對象時,遍歷每一個屬性執行defineReactive方法。

針對數組進行處理的boserve方法:express

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方法,一層一層向下處理。async

總結: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更新模板或表達式的值。

3、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)
}

未完待續...

相關文章
相關標籤/搜索