Vue響應(yīng)式系統(tǒng)的基本原理

關(guān)于Vue.js

Vue.js是一款MVVM框架顶瞳,通過響應(yīng)式在修改數(shù)據(jù)的時候更新視圖昂利。Vue.js的響應(yīng)式原理依賴于Object.defineProperty诽里,尤大大在Vue.js文檔中就已經(jīng)提到過纫溃,這也是Vue.js不支持IE8 以及更低版本瀏覽器的原因父能。Vue通過設(shè)定對象屬性的 setter/getter 方法來監(jiān)聽數(shù)據(jù)的變化臣镣,通過getter進(jìn)行依賴收集盼理,而每個setter方法就是一個觀察者谈山,在數(shù)據(jù)變更的時候通知訂閱者更新視圖 ,下面來一探究竟宏怔。

4.1奏路、Object.defineProperty

Object.defineProperty方法會直接在一個對象上定義一個新的屬性畴椰,或者修改對象的現(xiàn)有屬性并返回這個對象,它的語法如下:

Object.defineProperty(obj, prop, descriptor)

  • obj是當(dāng)前要操作的對象
  • props是要定義或修改的屬性名稱
  • descriptor是要被定義或修改的屬性的描述符

?較核?的是 descriptor 鸽粉,它有很多可選鍵值斜脂,具體的可以去參閱它的?檔。這?我們最關(guān)?的是
get 和 set 潜叛, get 是?個給屬性提供的 getter ?法秽褒,當(dāng)我們訪問了該屬性的時候會觸發(fā) getter ?
法; set 是?個給屬性提供的 setter ?法威兜,當(dāng)我們對該屬性做修改的時候會觸發(fā) setter ?法销斟。
?旦對象擁有了 getter 和 setter,我們可以簡單地把這個對象稱為響應(yīng)式對象椒舵。那么 Vue.js 把哪些對象
變成了響應(yīng)式對象了呢蚂踊,接下來我們從源碼層?分析。

4.2笔宿、initState初始化數(shù)據(jù)

我們上面講過犁钟,在 Vue 的初始化階段, _init ?法執(zhí)?的時候泼橘,會執(zhí)? initState(vm) ?法涝动,這個?法主要是對 props 、 methods 炬灭、 data 醋粟、 computed 和 wathcer 等屬性做了初始化操作。這?我們重點分析 props 和 data 重归。

4.2.1米愿、 initProps

function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  //初始化vm_props對象,最終可以通過vn._props訪問props數(shù)據(jù)
  const props = vm._props = {}
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // root instance props should be converted
  if (!isRoot) {
    toggleObserving(false)
  }
  //遍歷props中所有數(shù)據(jù)
  for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        )
      }
      defineReactive(props, key, value, () => {
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
            `overwritten whenever the parent component re-renders. ` +
            `Instead, use a data or computed property based on the prop's ` +
            `value. Prop being mutated: "${key}"`,
            vm
          )
        }
      })
    } else {
      //調(diào)用defineReactive,把每個prop對應(yīng)的值變成響應(yīng)式
      defineReactive(props, key, value)
    }
    if (!(key in vm)) {
      //通過proxy將vm._props.xxx代理到vm.xxx上
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

總結(jié):props 的初始化主要過程鼻吮,就是遍歷定義的 props 配置育苟。遍歷的過程主要做兩件事情:?個是調(diào)? defineReactive ?法把每個 prop 對應(yīng)的值變成響應(yīng)式,可以通過 vm._props.xxx 訪問到定
義 props 中對應(yīng)的屬性椎木。對于 defineReactive ?法违柏,我們稍后會介紹;另?個是通過 proxy
把 vm._props.xxx 的訪問代理到 vm.xxx 上香椎。

4.2.2漱竖、initData

function initData (vm: Component) {
  //獲取實例上的數(shù)據(jù)
  let data = vm.$options.data 
  //先判斷data是否是一個函數(shù)
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  //將data的數(shù)據(jù)和props,methods上的數(shù)據(jù)進(jìn)行比較,不能出現(xiàn)重復(fù)的定義
  //因為他們最終都會掛載到vm上
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
    //如果methods上存在這個鍵值
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    //如果props中存在這個鍵值
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } //如果沒有重復(fù)定義
    else if (!isReserved(key)) {
    //給數(shù)據(jù)進(jìn)行代理
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  //對數(shù)據(jù)進(jìn)行響應(yīng)式的處理
  observe(data, true /* asRootData */)
}

總結(jié):data初始化做了三件事

  • 遍歷data士鸥,檢查data中的數(shù)據(jù)是否和methods,props中定義的數(shù)據(jù)重復(fù)
  • 遍歷data,將每一個值vm._data.xxx都代理到vm.xxx上
  • 調(diào)用observer方法觀測整個data的變化谆级,使data變成響應(yīng)式數(shù)據(jù)

我們看到烤礁,無論是props還是data初始化讼积,都是把他們變成響應(yīng)式對象,在這個過程中我們使用了幾個重要的函數(shù)函數(shù)脚仔,下面就是介紹這些函數(shù)勤众。

4.2.3、proxy

首先介紹下代理鲤脏,代理的作用就是吧props和data上的屬性代理到vm實例上们颜,這也是為什么我們定義了props可以直接通過this.xxx調(diào)用。

const sharedPropertyDefinition = {
  enumerable: true,//是否是可枚舉的 
  configurable: true,//是否是可配置的
  get: noop,//空函數(shù)
  set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
//初始化變量的get方法,把target[sourceKey][key]的讀取變成了對target[key]的讀取
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
//初始化變量的set方法猎醇,把target[sourceKey][key]的設(shè)置變成了對target[key]的設(shè)置
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

4.1.4窥突、observer

observe ?法的作?就是給? VNode 的對象類型數(shù)據(jù)添加?個 Observer ,如果已經(jīng)添加過則直接返回硫嘶,否則在滿??定條件下去實例化?個 Observer 對象實例阻问。接下來我們來看?下 Observer
類。

export class Observer {
  value: any;
    //observer和watcher的紐帶沦疾,當(dāng)數(shù)據(jù)發(fā)生變化時會被observer觀察到称近,然后由dep通知watcher去更新視圖
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data
  constructor (value: any) {
    this.value = value//被觀察到的數(shù)據(jù)對象
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)//增加一個標(biāo)志,表示已經(jīng)被observer觀察
    // 如果value是數(shù)組哮塞,就辨遍歷數(shù)組刨秆,對數(shù)組中每一項進(jìn)行observer觀察
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)//遍歷數(shù)組的函數(shù)
    } else {
    //如果是對象,就遍歷對象的每一個key忆畅,對每個key調(diào)用defineReactive獲取對key的set和get的控制權(quán)
      this.walk(value)
    }
  }

Observer 是?個類衡未,它的作?是給對象的屬性添加 getter 和 setter,?于依賴收集和派發(fā)更新邻眷。在 Observer 的構(gòu)造函數(shù)中眠屎,會對 value 做判斷,對于數(shù)組會調(diào)? observeArray ?法肆饶,否則對純對象調(diào)? walk ?法改衩。可以看到 observeArray 是遍歷數(shù)組再次調(diào)? observe ?法驯镊,?walk ?法是遍歷對象的 key 調(diào)? defineReactive ?法葫督。

  • observeArray:遍歷數(shù)組,對數(shù)組的每個元素調(diào)用observer
  • walk:遍歷對象的每個Key板惑,對對象上的每個key調(diào)用defineReactive
  • defineReactive:通過Object.defineProperty 設(shè)置對象的key屬性橄镜,使得我們能夠獲得該屬性的該屬性的get/set使用權(quán),一般是由Watcher的實例進(jìn)行g(shù)et操作冯乘,此時Watcher實例對象會被添加到Dep數(shù)組中洽胶,在外部操作觸發(fā)set時,通過Dep通知Watcher進(jìn)行更新裆馒。

4.1.5姊氓、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,
     //Dep.target全局變量指向當(dāng)前正在解析指令的Compile生成的Watcher
      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()//被更新了,通知所有watcher去更新
    }
  })
}

defineReactive 函數(shù)最開始初始化 Dep 對象的實例翔横,接著拿到 obj 的屬性描述符读跷,然后對?對
象遞歸調(diào)? observe ?法,這樣就保證了?論 obj 的結(jié)構(gòu)多復(fù)雜禾唁,它的所有?屬性也能變成響應(yīng)
式的對象效览,這樣我們訪問或修改 obj 中?個嵌套較深的屬性,也能觸發(fā) getter 和 setter荡短。最后利?
Object.defineProperty 去給 obj 的屬性 key 添加 getter 和 setter

4.3丐枉、依賴收集

經(jīng)過上面的分析我們知道,Vue會把普通對象變成響應(yīng)式對象肢预,響應(yīng)式對象的getter就是用來收集依賴的矛洞,再看看getter的實現(xiàn)。

get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val,
      //Dep.target全局變量指向當(dāng)前正在解析指令的Compile生成的Watcher
      if (Dep.target) {
        dep.depend()//被讀取了,將這個以來收集起來
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },

getter函數(shù)最重要的一步就是通過調(diào)用depend函數(shù)進(jìn)行依賴的收集,depend函數(shù)是dep定義的一個函數(shù)漠畜,稍后會詳細(xì)介紹。

4.3.1抽兆、Dep

Dep是observer和watcher的紐帶,當(dāng)數(shù)據(jù)發(fā)生變化時會被observer觀察到族淮,然后由dep通知watcher辫红。

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

  constructor () {
    this.id = uid++//每個dep都有唯一的id
    this.subs = []//用于存放依賴
  }

  //向subs數(shù)組添加依賴
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  //移除依賴
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  //設(shè)置Watcher的依賴,這里添加Deo.target目的是判斷是不是Watcher的構(gòu)造函數(shù)的調(diào)用
  //也就是說判斷他是Watcher的this.get調(diào)用的的而不是普通調(diào)用
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  //通知所有綁定的Watcher調(diào)用update()進(jìn)行更新
  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()
    }
  }
}

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
//這是全局唯一的祝辣,在任何時候只有一個watcher正在評估
Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  // 將當(dāng)前的watcher推入堆棧中贴妻,關(guān)于為什么要推入堆棧,主要是要處理模板或render函數(shù)中嵌套了多層組件蝙斜,需要遞歸處理
  targetStack.push(target)
  // 設(shè)置當(dāng)前watcher到全局的Dep.target名惩,通過在此處設(shè)置,key使得在進(jìn)行g(shù)et的時候?qū)Ξ?dāng)前的訂閱者進(jìn)行依賴收集
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}
  • add:接受參數(shù)為Watcher實例孕荠,并把Watcher實例記錄依賴的數(shù)組中
  • depend:Dep.target存放的是當(dāng)前需要操作的Watcher實例娩鹉,調(diào)用depend會調(diào)用該實例的addDep方法
  • notify:通知數(shù)組中所有Watcher進(jìn)行更新操作

4.3.2、Watcher

Watcher作為觀察者稚伍,用來訂閱數(shù)據(jù)變化并執(zhí)行相應(yīng)的操作弯予,比如視圖的更新。

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean//是否為渲染watcher
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    //當(dāng)前Watcher添加到vue實例上
    vm._watchers.push(this)
    // options
    //參數(shù)配置个曙,默認(rèn)是false
    if (options) {
      ....
    } 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 = []
    //內(nèi)容不可重復(fù)
    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') {
    //將watcher對象的getter設(shè)置成uptateComponent
      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()
  }
  //在get函數(shù)中锈嫩,主要是收集一些依賴,然后在初始化或者有更新時,調(diào)用this.getter(對應(yīng)著updateComponent函數(shù))
  get () {
    //將Dep的target添加到targetStack呼寸,同時Dep的target賦值為當(dāng)前watcher
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      //調(diào)用updateComponent方法那槽,之后在updateComponent中接著會調(diào)用_update方法更新dom
      //這時掛載到vue原型上的方法,而_render方法重新渲染了VNode
      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)
      }
      //update執(zhí)行完成之后等舔,又將dep.target從targetStack彈出
      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
    ......
  }
  //通知watcher更新
  update () {
   .......
  }
  run () {
   .......
  }
.............
}

  • addDep:接收參數(shù)dep,讓當(dāng)前Watcher訂閱dep
  • cleanupDeps:將新的newDepIds(這里保存的是dep的id)和舊的deps去對比糟趾,找存在于出舊的deps但不存在于新的newDeplds中的dep慌植,就從這些dep中移除當(dāng)前的依賴,這樣可以有效地避免沒必要的updata义郑,稍后在new Watcher()渲染過程分析中我會可以舉個簡單的例子說明蝶柿。
  • updata:立即執(zhí)行watcher或者將watcher加入隊列等待統(tǒng)一flush
  • run:運(yùn)行watcher,調(diào)用this.get()求值非驮,然后觸發(fā)回調(diào)

4.3.3交汤、new Watcher發(fā)生了什么

之前我們講過,在$mount函數(shù)中我們主要是通過調(diào)用mountComponent()函數(shù)去實現(xiàn)我們的數(shù)據(jù)掛載劫笙,而在mountComponent函數(shù)中就使用了new Watcher(),在這個過程中我們調(diào)用了每個數(shù)據(jù)的getter函數(shù)芙扎,這樣就實現(xiàn)了每個數(shù)據(jù)首次依賴的收集。

當(dāng)我們?nèi)嵗?個渲染 watcher 的時候填大,?先進(jìn)? watcher 的構(gòu)造函數(shù)邏輯戒洼,然后會執(zhí)?它的
this.get() ?法,進(jìn)? get 函數(shù)允华,?先會執(zhí)?

pushTarget(this)


//函數(shù)的實現(xiàn)
export function pushTarget (_target: Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}

執(zhí)行這個函數(shù)就是把 Dep.target 賦值為當(dāng)前的渲染 watcher 并壓棧(為了恢復(fù)?)圈浇。接著?執(zhí)?了

value = this.getter.call(vm, vm)
//this.getter  對應(yīng)就是  updateComponent  函數(shù),這實際上就是在執(zhí)?:
vm._update(vm._render(), hydrating)

它會先執(zhí)? vm._render() ?法靴寂,因為之前分析過這個?法會?成 渲染 VNode磷蜀,并且在這個過程中
會對 vm 上的數(shù)據(jù)訪問,這個時候就觸發(fā)了數(shù)據(jù)對象的 getter百炬。那么每個對象值的 getter 都持有?個 dep 褐隆,在觸發(fā) getter 的時候會調(diào)? dep.depend() ?法,也就會執(zhí)?

Dep.target.addDep(this)

剛才我們提到這個時候 Dep.target 已經(jīng)被賦值為當(dāng)前在操作的 watcher 收壕,那么就執(zhí)?到 addDep ?法:

addDep (dep: Dep) {
    const id = dep.id//獲取dep的id
    //如果新的DepIds數(shù)組中沒有當(dāng)前dep妓灌,將入組
    if (!this.newDepIds.has(id)) {
        this.newDepIds.add(id)
        this.newDeps.push(dep)
        //如果dep沒有當(dāng)前正在操作的Watcher
        if (!this.depIds.has(id)) {
            dep.addSub(this)
        }
    }
}

這時候會做?些邏輯判斷(保證同?數(shù)據(jù)不會被添加多次)后執(zhí)? dep.addSub(this) ,那么就會執(zhí)
? this.subs.push(sub) 蜜宪,也就是說把當(dāng)前的 watcher 訂閱到這個數(shù)據(jù)持有的 dep 的 subs中虫埂。

所以在 vm._render() 過程中,會觸發(fā)所有數(shù)據(jù)的 getter圃验,這樣實際上已經(jīng)完成了?個依賴收集的過程掉伏。那么到這?就結(jié)束了么,其實并沒有,再完成依賴收集后斧散,還有?個邏輯要執(zhí)?供常,?先是:

if (this.deep) {
    traverse(value)
}

這個是要遞歸去訪問 value ,觸發(fā)它所有?項的 getter 鸡捐,這個之后會詳細(xì)講栈暇。接下來執(zhí)?:

popTarget()
//函數(shù)實現(xiàn)
export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

實際上就是把 Dep.target 恢復(fù)成上?個狀態(tài),因為當(dāng)前 vm 的數(shù)據(jù)依賴收集已經(jīng)完成箍镜,那么對應(yīng)的渲染 Dep.target 也需要改變源祈。最后執(zhí)?this.cleanupDeps()這里著重講下this.cleanupDeps()這個函數(shù)。

考慮到 Vue 是數(shù)據(jù)驅(qū)動的色迂,所以每次數(shù)據(jù)變化都會重新render香缺,那么 vm._render() ?法?會再次執(zhí)?,并再次觸發(fā)數(shù)據(jù)的 getters歇僧,我們知道每次執(zhí)行一次render都是一次的性能消耗图张,那有什么辦法能幫助我們?nèi)プ柚箾]必要render,所以 Wathcer 在構(gòu)造函數(shù)中會初始化 2 個 Dep 實例數(shù)組诈悍, newDeps 表?新添加的 Dep 實例數(shù)組祸轮,? deps 表?上?次添加的 Dep 實例數(shù)組。在執(zhí)? cleanupDeps 函數(shù)的時候侥钳,會?先遍歷 deps 倔撞,移除對 dep 的訂閱,然后把 newDepIds和 depIds 交換慕趴, newDeps 和 deps 交換痪蝇,并把 newDepIds 和 newDeps 清空。那么為什么需要做 deps 訂閱的移除呢冕房,在添加 deps 的訂閱過程躏啰,已經(jīng)能通過 id 去重避免重復(fù)訂閱了。

考慮到?種場景耙册,我們的模板會根據(jù) v-if 去渲染不同?模板 a 和 b给僵,當(dāng)我們滿?某種條件的時候渲染 a 的時候,會訪問到 a 中的數(shù)據(jù)详拙,這時候我們對 a 使?的數(shù)據(jù)添加了 getter帝际,做了依賴收集,那么當(dāng)我們?nèi)バ薷?a 的數(shù)據(jù)的時候饶辙,理應(yīng)通知到這些訂閱者蹲诀。那么如果我們?旦改變了條件渲染了 b 模板,?會對 b 使?的數(shù)據(jù)添加了 getter弃揽,如果我們沒有依賴移除的過程脯爪,那么這時候我去修改 a 模板的數(shù)據(jù)则北,會通知 a 數(shù)據(jù)的訂閱的回調(diào),這顯然是有浪費(fèi)的痕慢。因此 Vue 設(shè)計了在每次添加完新的訂閱尚揣,會移除掉舊的訂閱,這樣就保證了在我們剛才的場景中掖举,如果渲染 b 模板的時候去修改 a 模板的數(shù)據(jù)快骗,a 數(shù)據(jù)訂閱回調(diào)已經(jīng)被移除了,所以不會有任何浪費(fèi)塔次,真的是?常贊嘆 Vue 對?些細(xì)節(jié)上的處理滨巴。

4.4、異步更新DOM策略及nextTick

Vue實現(xiàn)響應(yīng)式并不是數(shù)據(jù)變化后DOM立即變化俺叭,而是按照一定策略進(jìn)行DOM更新,在Vue文檔中指出Vue是異步執(zhí)行DOM更新泰偿。那Vue為什么要使用異步更新又是怎么實現(xiàn)的熄守?

我們先看看Watcher隊列 ,當(dāng)觸發(fā)某個數(shù)據(jù)的setter時耗跛,Dep就會通過調(diào)用notify去通知所有的依賴watcher.

  //通知watcher更新
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      /*同步則執(zhí)行run直接渲染視圖*/
      this.run()
    } else {
        /*異步推送到觀察者隊列中裕照,下一個tick時調(diào)用。*/
      queueWatcher(this)
    }
  }

從源碼中我們可以知道Vue.js默認(rèn)是使用異步更新调塌,當(dāng)執(zhí)行updata時晋南,會調(diào)用queueWatcher函數(shù)。

//將一個觀察者對象push進(jìn)觀察者隊列羔砾,在隊列中已經(jīng)存在相同的id則該觀察者對象將被跳過负间,除非是在隊列被刷新時推送。
export function queueWatcher (watcher: Watcher) {
    //獲取watcher的id
  const id = watcher.id
    //檢測id是否存在姜凄,已經(jīng)存在直接跳過政溃,不存在直接標(biāo)志哈希表has
  if (has[id] == null) {
    has[id] = true
        /*如果沒有flush掉,直接push到隊列中即可*/
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      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)
    }
  }
}

這?引?了?個隊列的概念态秧,這也是 Vue 在做派發(fā)更新的時候的?個優(yōu)化的點董虱,它并不會每次數(shù)據(jù)改
變都觸發(fā) watcher 的回調(diào),?是把這些 watcher 先添加到?個隊列?申鱼,然后在 nextTick 后執(zhí)
? flushSchedulerQueue 愤诱。這?有?個細(xì)節(jié)要注意?下,?先? has 對象保證同?個 Watcher 只添加?次捐友;接著對flushing 的判斷淫半,else 部分的邏輯稍后我會講;最后通過 wating 保證對
nextTick(flushSchedulerQueue) 的調(diào)?邏輯只有?次匣砖。那什么是nextTick?

/* @flow */
/* globals MutationObserver */

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

export let isUsingMicroTask = false

const callbacks = []
let pending = false

 /*下一個tick時的回調(diào)*/
function flushCallbacks () {
  pending = false
    //將每次保存的cb函數(shù)取出并執(zhí)行
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
let timerFunc

/* 
 一共有Promise撮慨、MutationObserver以及setTimeout三種嘗試得到timerFunc的方法
 優(yōu)先使用Promise竿痰,在Promise不存在的情況下使用MutationObserver,這兩個方法都會在microtask中執(zhí)行砌溺,會比setTimeout更早執(zhí)行影涉,所以優(yōu)先使用。如果上述兩種方法都不支持的環(huán)境則會使用setTimeout规伐,在task尾部推入這個函數(shù)蟹倾,等待調(diào)用執(zhí)行。
 */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    // In problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  // e.g. PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  /*新建一個textNode的DOM對象猖闪,用MutationObserver綁定該DOM并指定回調(diào)函數(shù)鲜棠,在DOM變化的時候則會觸發(fā)回調(diào),該回調(diào)會進(jìn)入主線程(比任務(wù)隊列優(yōu)先執(zhí)行),即textNode.data = String(counter)時便會觸發(fā)回調(diào)*/
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // Fallback to setImmediate.
  // Technically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  /*使用setTimeout將回調(diào)推入任務(wù)隊列尾部*/
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

export function nextTick (cb?: Function, ctx?: Object) {
  //cb是一個函數(shù)遍歷隊列中的watcher培慌,執(zhí)行watcher.run();
  let _resolve
  /*cb存到callbacks中*/
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

這里解釋一下豁陆,一共有Promise、MutationObserver以及setTimeout三種嘗試得到timerFunc的方法吵护。 優(yōu)先使用Promise盒音,在Promise不存在的情況下使用MutationObserver,這兩個方法的回調(diào)函數(shù)都會在microtask中執(zhí)行馅而,它們會比setTimeout更早執(zhí)行祥诽,所以優(yōu)先使用。 如果上述兩種方法都不支持的環(huán)境則會使用setTimeout瓮恭,在task尾部推入這個函數(shù)雄坪,等待調(diào)用執(zhí)行。 為什么在miscrotask執(zhí)行會更早呢屯蹦,JS 的 event loop 執(zhí)行時會區(qū)分 task 和 microtask维哈,引擎在每個 task 執(zhí)行完畢,從隊列中取下一個 task 來執(zhí)行之前登澜,會先執(zhí)行完所有 microtask 隊列中的 microtask笨农。setTimeout 回調(diào)會被分配到一個新的 task 中執(zhí)行,而 Promise 的 resolver帖渠、MutationObserver 的回調(diào)都會被安排到一個新的 microtask 中執(zhí)行谒亦,會比 setTimeout 產(chǎn)生的 task 先執(zhí)行。

綜上空郊,nextTick的目的就是產(chǎn)生一個回調(diào)函數(shù)加入task或者microtask中份招,當(dāng)前棧執(zhí)行完以后(可能中間還有別的排在前面的函數(shù))調(diào)用該回調(diào)函數(shù),起到了異步觸發(fā)(即下一個tick時觸發(fā))的目的狞甚。

為什么要異步更新視圖呢锁摔?

來看一下下面這一段代碼

<template>
  <div>
    <div>{{test}}</div>
  </div>
</template>
export default {
    data () {
        return {
            test: 0
        };
    },
    mounted () {
      for(let i = 0; i < 1000; i++) {
        this.test++;
      }
    }
}

現(xiàn)在有這樣的一種情況,mounted的時候test的值會被++循環(huán)執(zhí)行1000次哼审。 每次++時谐腰,都會根據(jù)響應(yīng)式觸發(fā)setter->Dep->Watcher->update->patch孕豹。 如果這時候沒有異步更新視圖,那么每次++都會直接操作DOM更新視圖十气,這是非常消耗性能的励背。 所以Vue.js實現(xiàn)了一個queue隊列,在下一個tick的時候會統(tǒng)一執(zhí)行queue中Watcher的run砸西。同時叶眉,擁有相同id的Watcher不會被重復(fù)加入到該queue中去,所以不會執(zhí)行1000次Watcher的run芹枷。最終更新視圖只會直接將test對應(yīng)的DOM的0變成1000衅疙。 保證更新視圖操作DOM的動作是在當(dāng)前棧執(zhí)行完以后下一個tick的時候調(diào)用,大大優(yōu)化了性能 鸳慈。

4.5饱溢、computed和watch

  • computed

computed實質(zhì)上是一個computed watcher,當(dāng)它所依賴的數(shù)據(jù)發(fā)生變化時會進(jìn)行重新計算走芋,然后對比新舊值绩郎,如果發(fā)生了變化就會觸發(fā)渲染watcher重新渲染,所以對于計算屬性Vue想確保不僅僅是計算屬性依賴的值發(fā)生變化绿聘,而是想當(dāng)計算屬性最終計算的值發(fā)生變化才會觸發(fā)渲染watcher重新渲染,這本質(zhì)上是一種優(yōu)化次舌。

  • watch

  • ?旦我們 watch 的數(shù)據(jù)發(fā)送變化熄攘,它最終會執(zhí)? watcher 的run ?法,執(zhí)?回調(diào)函數(shù) cb 彼念,并且如果我們設(shè)置了 immediate 為 true挪圾,則直接會執(zhí)?回調(diào)函數(shù)cb ,最后返回了?個 unwatchFn ?法,它會調(diào)? teardown ?法去移除這個 watcher 逐沙。

拓展:watcher 總共有 4 種類型哲思,這里介紹deep watcher

  • deep watcher

    通常,如果我們想對?下對象做深度觀測的時候吩案,需要設(shè)置這個屬性為 true棚赔,考慮到這種情況:

    var vm = new Vue({
        data() {
            a: {
                b: 1
            }
        },
        watch: {
            a: {
                handler(newVal) {
                    console.log(newVal)
                }
            }
        }
    })
    vm.a.b = 2
    
    

    這個時候是不會 log 任何數(shù)據(jù)的,因為我們是 watch 了 a 對象徘郭,只觸發(fā)了 a 的 getter靠益,并沒有觸發(fā)a.b 的 getter,所以并沒有訂閱它的變化残揉,導(dǎo)致我們對 vm.a.b = 2 賦值的時候胧后,雖然觸發(fā)了setter,但沒有可通知的對象抱环,所以也并不會觸發(fā) watch 的回調(diào)函數(shù)了壳快。?我們只需要對代碼做稍稍修改纸巷,就可以觀測到這個變化了

    watch: {
        a: {
            deep: true,
            handler(newVal) {
                console.log(newVal)
            }
        }
    }
    

    這樣就創(chuàng)建了?個 deep watcher 了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末眶痰,一起剝皮案震驚了整個濱河市瘤旨,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌凛驮,老刑警劉巖裆站,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異黔夭,居然都是意外死亡宏胯,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進(jìn)店門本姥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肩袍,“玉大人,你說我怎么就攤上這事婚惫》沾停” “怎么了?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵先舷,是天一觀的道長艰管。 經(jīng)常有香客問我,道長蒋川,這世上最難降的妖魔是什么牲芋? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮捺球,結(jié)果婚禮上缸浦,老公的妹妹穿的比我還像新娘。我一直安慰自己氮兵,他們只是感情好裂逐,可當(dāng)我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著泣栈,像睡著了一般卜高。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上南片,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天篙悯,我揣著相機(jī)與錄音,去河邊找鬼铃绒。 笑死鸽照,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的颠悬。 我是一名探鬼主播矮燎,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼定血,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了诞外?” 一聲冷哼從身側(cè)響起澜沟,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎峡谊,沒想到半個月后茫虽,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡既们,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年濒析,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片啥纸。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡号杏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出斯棒,到底是詐尸還是另有隱情盾致,我是刑警寧澤,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布荣暮,位于F島的核電站庭惜,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏穗酥。R本人自食惡果不足惜护赊,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望迷扇。 院中可真熱鬧百揭,春花似錦爽哎、人聲如沸蜓席。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽厨内。三九已至,卻和暖如春渺贤,著一層夾襖步出監(jiān)牢的瞬間雏胃,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工志鞍, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留瞭亮,地道東北人。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓固棚,卻偏偏與公主長得像统翩,于是被迫代替她去往敵國和親仙蚜。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,446評論 2 348