Vue原理解析(六):全面深入理解響應(yīng)式原理(上)-對(duì)象基礎(chǔ)篇

上一篇:Vue原理解析(五):徹底搞懂虛擬Dom到真實(shí)Dom的生成過(guò)程

vue之所以能數(shù)據(jù)驅(qū)動(dòng)視圖發(fā)生變更的關(guān)鍵,就是依賴(lài)它的響應(yīng)式系統(tǒng)了。響應(yīng)式系統(tǒng)如果根據(jù)數(shù)據(jù)類(lèi)型區(qū)分,對(duì)象和數(shù)組它們的實(shí)現(xiàn)會(huì)有所不同;解釋響應(yīng)式原理,如果只是為了說(shuō)明響應(yīng)式原理而說(shuō),但不是從整體流程出發(fā),不在vue組件化的整體流程中找到響應(yīng)式原理的位置,對(duì)深刻理解響應(yīng)式原理并不太好。接下來(lái)筆者會(huì)從整體流程出發(fā),試著站在巨人的肩膀上分別說(shuō)明對(duì)象和數(shù)組的實(shí)現(xiàn)原理。

對(duì)象的響應(yīng)式原理

對(duì)象響應(yīng)式數(shù)據(jù)的創(chuàng)建

  • 在組件的初始化階段,將對(duì)傳入的狀態(tài)進(jìn)行初始化,以下以data為例,會(huì)將傳入的數(shù)據(jù)包裝為響應(yīng)式的數(shù)據(jù)。
對(duì)象示例:

main.js
new Vue({  // 根組件
  render: h => h(App)
})

--------------------------------------------------------------------------------------

app.vue
<template>
  <div>{{info.name}}</div>  // 只用了info.name屬性
</template>
export default {  // app組件
  data() {
    return {
      info: {
        name: 'cc',
        sex: 'man'  // 即使是響應(yīng)式數(shù)據(jù),沒(méi)被使用就不會(huì)進(jìn)行依賴(lài)收集
      }
    }
  }
}

接下來(lái)的分析將以上面代碼為示例,這種結(jié)構(gòu)其實(shí)是一個(gè)嵌套組件,只不過(guò)根組件一般定義的參數(shù)比較少而已,理解這個(gè)還是很重要的。

在組件new Vue()后的執(zhí)行vm._init()初始化過(guò)程中,當(dāng)執(zhí)行到initState(vm)時(shí)就會(huì)對(duì)內(nèi)部使用到的一些狀態(tài),如props、datacomputed、watchmethods分別進(jìn)行初始化,再對(duì)data進(jìn)行初始化的最后有這么一句:

function initData(vm) {  //初始化data
  ...
  observe(data) //  info:{name:'cc',sex:'man'}
}

這個(gè)observe就是將用戶(hù)定義的data變成響應(yīng)式的數(shù)據(jù),接下來(lái)看下它的創(chuàng)建過(guò)程:

export function observe(value) {
  if(!isObject(value)) {  // 不是數(shù)組或?qū)ο螅僖?jiàn)
    return
  }
  return new Observer(value)
}

簡(jiǎn)單理解這個(gè)observe方法就是Observer這個(gè)類(lèi)的工廠方法,所以還是要看下Observer這個(gè)類(lèi)的定義:

export class Observer {
  constructor(value) {
    this.value = value
    this.walk(value)  // 遍歷value
  }
  
  walk(obj) {
    const keys = Object.keys(obj)
    for(let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])  // 只傳入了兩個(gè)參數(shù)
    }
  }
}

當(dāng)執(zhí)行new Observer時(shí),首先將傳入的對(duì)象掛載到當(dāng)前this下,然后遍歷當(dāng)前對(duì)象的每一項(xiàng),執(zhí)行defineReactive這個(gè)方法,看下它的定義:

export function defineReactive(obj, key, val) {

  const dep = new Dep()  // 依賴(lài)管理器
  
  val = obj[key]  // 計(jì)算出對(duì)應(yīng)key的值
  observe(val)  // 遞歸包裝對(duì)象的嵌套屬性
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      ... 收集依賴(lài)
    },
    set(newVal) {
      ... 派發(fā)更新
    }
  })
}

這個(gè)方法的作用就是使用Object.defineProperty創(chuàng)建響應(yīng)式數(shù)據(jù)。首先根據(jù)傳入的objkey計(jì)算出val具體的值;如果val還是對(duì)象,那就使用observe方法進(jìn)行遞歸創(chuàng)建,在遞歸的過(guò)程中使用Object.defineProperty將對(duì)象的每一個(gè)屬性都變成響應(yīng)式數(shù)據(jù):

...
data() {
  return {
    info: {
      name: 'cc',
      sex: 'man'
    } 
  }
}
這段代碼就會(huì)有三個(gè)響應(yīng)式數(shù)據(jù):
  info, info.name, info.sex

知識(shí)點(diǎn):Object.defineProperty內(nèi)的get方法,它的作用就是誰(shuí)訪問(wèn)到當(dāng)前key的值就用defineReactive內(nèi)的dep將它收集起來(lái),也就是依賴(lài)收集的意思。set方法的作用就是當(dāng)前key的值被賦值了,就通知dep內(nèi)收集到的依賴(lài)項(xiàng),key的值發(fā)生了變更,視圖請(qǐng)變更吧~

這個(gè)時(shí)候getset只是定義了,并不會(huì)觸發(fā)。什么是依賴(lài)我們接下來(lái)說(shuō)明,首先還是用一張圖幫大家理清響應(yīng)式數(shù)據(jù)的創(chuàng)建過(guò)程:

image

依賴(lài)收集

什么是依賴(lài)了?我們看下之前mountComponent的定義:

function mountComponent(vm, el) {
  ...
  const updateComponent = function() {
    vm._update(vm._render())
  }
  
  new Watcher(vm, updateComponent, noop, {  // 渲染watcher
    ...
  }, true)  // true為標(biāo)志,表示是否是渲染watcher
  ...
}

我們首先說(shuō)明下這個(gè)Watcher類(lèi),它類(lèi)似與之前的VNode類(lèi),根據(jù)傳入的參數(shù)不同,可以分別實(shí)例化出三種不同的Watcher實(shí)例,它們分別是用戶(hù)watcher,計(jì)算watcher以及渲染watcher

用戶(hù)(user) watcher

  • 也就是用戶(hù)自己定義的,如:
new Vue({
  data {
    msg: 'hello Vue!'
  }
  created() {
    this.$watch('msg', cb())  // 定義用戶(hù)watcher
  },
  watch: {
    msg() {...}  // 定義用戶(hù)watcher
  }
})

這里的兩種方式內(nèi)部都是使用Watcher這個(gè)類(lèi)實(shí)例化的,只是參數(shù)不同,具體實(shí)現(xiàn)我們之后章節(jié)說(shuō)明,這里大家只用知道這個(gè)是用戶(hù)watcher即可。

計(jì)算(computed) watcher

  • 顧名思義,這個(gè)是當(dāng)定義計(jì)算屬性實(shí)例化出來(lái)的一種:
new Vue({
  data: {
    msg: 'hello'  
  },
  computed() {
    sayHi() {  // 計(jì)算watcher
      return this.msg + 'vue!'
    }
  }
})

渲染(render) watcher

  • 只是用做視圖渲染而定義的Watcher實(shí)例,再組件執(zhí)行vm.$mount的最后會(huì)實(shí)例化Watcher類(lèi),這個(gè)時(shí)候就是以渲染watcher的格式定義的,收集的就是當(dāng)前渲染watcher的實(shí)例,我們來(lái)看下它內(nèi)部是如何定義的:
class Watcher {
  constructor(vm, expOrFn, cb, options, isRenderWatcher) {
    this.vm = vm
    if(isRenderWatcher) {  // 是否是渲染watcher
      vm._watcher = this  // 當(dāng)前組件下掛載vm._watcher屬性
    }
    vm._watchers.push(this)  //vm._watchers是之前初始化initState時(shí)定義的[]
    this.before = options.before  // 渲染watcher特有屬性
    this.getter = expOrFn  // 第二個(gè)參數(shù)
    this.get()  // 實(shí)例化就會(huì)執(zhí)行this.get()方法
  }
  
  get() {
    pushTarget(this)  // 添加
    ...
    this.getter.call(this.vm, this.vm)  // 執(zhí)行vm._update(vm._render())
    ...
    popTarget()  // 移除
  }
  
  addDep(dep) {
    ...
    dep.addSub(this)  // 將當(dāng)前watcher收集到dep實(shí)例中
  }
}

當(dāng)執(zhí)行new Watcher的時(shí)候內(nèi)部會(huì)掛載一些屬性,然后執(zhí)行this.get()這個(gè)方法,首先會(huì)執(zhí)行一個(gè)全局的方法pushTarget(this),傳入當(dāng)前watcher的實(shí)例,我們看下這個(gè)方法定義的地方:

Dep.target = null
const targetStack = []  // 組件從父到子對(duì)應(yīng)的watcher實(shí)例集合

export function pushTarget (_target) {  // 添加
  if (Dep.target) {
    targetStack.push(Dep.target)  // 添加到集合內(nèi)
  }
  Dep.target = _target  // 當(dāng)前的watcher實(shí)例
}

export function popTarget() {  // 移除
  targetStack.pop()  // 移除數(shù)組最后一項(xiàng)
  Dep.target = targetStack[targetStack.length - 1]  // 賦值為數(shù)組最后一項(xiàng)
}

首先會(huì)定義一個(gè)Dep類(lèi)的靜態(tài)屬性Dep.targetnull,這是一個(gè)全局會(huì)用到的屬性,保存的是當(dāng)前組件對(duì)應(yīng)渲染watcher的實(shí)例;targetStack內(nèi)存儲(chǔ)的是再執(zhí)行組件化的過(guò)程中每個(gè)組件對(duì)應(yīng)的渲染watcher實(shí)例集合,使用的是一個(gè)先進(jìn)后出的形式來(lái)管理數(shù)組的數(shù)據(jù),這里可能有點(diǎn)不太好懂,稍等再看到最后的流程圖后自然就明白了;然后將傳入的watcher實(shí)例賦值給全局屬性Dep.target,再之后的依賴(lài)收集過(guò)程中就是收集的它。

watcherget這個(gè)方法然后會(huì)執(zhí)行getter這個(gè)方法,它是new Watcher時(shí)傳入的第二個(gè)參數(shù),這個(gè)參數(shù)就是之前的updateComponent變量:

function mountComponent(vm, el) {
  ...
  const updateComponent = function() {  //第二個(gè)參數(shù)
    vm._update(vm._render())
  }
  ...
}

只要一執(zhí)行就會(huì)執(zhí)行當(dāng)前組件實(shí)例上的vm._update(vm._render())render函數(shù)轉(zhuǎn)為VNode,這個(gè)時(shí)候如果render函數(shù)內(nèi)有使用到data中已經(jīng)轉(zhuǎn)為了響應(yīng)式的數(shù)據(jù),就會(huì)觸發(fā)get方法進(jìn)行依賴(lài)的收集,補(bǔ)全之前依賴(lài)收集的邏輯:

export function defineReactive(obj, key, val) {
  const dep = new Dep()  // 依賴(lài)管理器
  
  val = obj[key]  // 計(jì)算出對(duì)應(yīng)key的值
  observe(val)  // 遞歸的轉(zhuǎn)化對(duì)象的嵌套屬性
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {  // 觸發(fā)依賴(lài)收集
      if(Dep.target) {  // 之前賦值的當(dāng)前watcher實(shí)例
        dep.depend()  // 收集起來(lái),放入到上面的dep依賴(lài)管理器內(nèi)
        ...
      }
      return val
    },
    set(newVal) {
      ... 派發(fā)更新
    }
  })
}

這個(gè)時(shí)候我們知道watcher是個(gè)什么東西了,簡(jiǎn)單理解就是數(shù)據(jù)和組件之間一個(gè)通信工具的封裝,當(dāng)某個(gè)數(shù)據(jù)被組件讀取時(shí),就將依賴(lài)數(shù)據(jù)的組件使用Dep這個(gè)類(lèi)給收集起來(lái)。

當(dāng)前例子data內(nèi)的屬性是只有一個(gè)渲染watcher的,因?yàn)闆](méi)有被其他組件所使用。但如果該屬性被其他組件使用到,也會(huì)將使用它的組件收集起來(lái),例如作為了props傳遞給了子組件,再dep的數(shù)組內(nèi)就會(huì)存在多個(gè)渲染watcher。我們來(lái)看下Dep類(lèi)這個(gè)依賴(lài)管理器的定義:

let uid = 0
export default class Dep {
  constructor() {
    this.id = uid++
    this.subs = []  // 對(duì)象某個(gè)key的依賴(lài)集合
  }
  
  addSub(sub) {  // 添加watcher實(shí)例到數(shù)組內(nèi)
    this.subs.push(sub)
  }
  
  depend() {
    if(Dep.target) {  // 已經(jīng)被賦值為了watcher的實(shí)例
      Dep.target.addDep(this)  // 執(zhí)行watcher的addDep方法
    }
  }
}

----------------------------------------------------------
class Watcher{
  ...
  addDep(dep) {  // 將當(dāng)前watcher實(shí)例添加到dep內(nèi)
    ...
    dep.addSub(this)  // 執(zhí)行dep的addSub方法
  }
}

這個(gè)Dep類(lèi)的作用就是管理屬性對(duì)應(yīng)的watcher,如添加/刪除/通知。至此,依賴(lài)收集的過(guò)程算是完成了,還是以一張圖片加深對(duì)過(guò)程的理解:

image

派發(fā)更新

如果只是收集依賴(lài),那其實(shí)是沒(méi)任何意義的,將收集到的依賴(lài)在數(shù)據(jù)發(fā)生變化時(shí)通知到并引起視圖變化,這樣才有意義。如現(xiàn)在我們對(duì)數(shù)據(jù)重新賦值:

app.vue
export default {  // app組件
  ...
  methods: {
    changeInfo() {
      this.info.name = 'ww';
    }
  }
}

這個(gè)時(shí)候就會(huì)觸發(fā)創(chuàng)建響應(yīng)式數(shù)據(jù)時(shí)的set方法了,我們?cè)傺a(bǔ)全那里的邏輯:

export function defineReactive(obj, key, val) {
  const dep = new Dep()  // 依賴(lài)管理器
  
  val = obj[key]  // 計(jì)算出對(duì)應(yīng)key的值
  observe(val)  // 遞歸轉(zhuǎn)化對(duì)象的嵌套屬性
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      ... 依賴(lài)收集
    },
    set(newVal) {  // 派發(fā)更新
      if(newVal === val) {  // 相同
        return
      }
      val = newVal  // 賦值
      observer(newVal)  // 如果新值是對(duì)象也遞歸包裝
      dep.notify()  // 通知更新
    }
  })
}

當(dāng)賦值觸發(fā)set時(shí),首先會(huì)檢測(cè)新值和舊值,不能相同;然后將新值賦值給舊值;如果新值是對(duì)象則將它變成響應(yīng)式的;最后讓對(duì)應(yīng)屬性的依賴(lài)管理器使用dep.notify發(fā)出更新視圖的通知。我們看下它的實(shí)現(xiàn):

let uid = 0
class Dep{
  constructor() {
    this.id = uid++
    this.subs = []
  }
  
  notify() {  // 通知
    const subs = this.subs.slice()
    for(let i = 0, i < subs.length; i++) {
      subs[i].update()  // 挨個(gè)觸發(fā)watcher的update方法
    }
  }
}

這里做的事情只有一件,將收集起來(lái)的watcher挨個(gè)遍歷觸發(fā)update方法:

class Watcher{
  ...
  update() {
    queueWatcher(this)
  }
}

---------------------------------------------------------
const queue = []
let has = {}

function queueWatcher(watcher) {
  const id = watcher.id
  if(has[id] == null) {  // 如果某個(gè)watcher沒(méi)有被推入隊(duì)列
    ...
    has[id] = true  // 已經(jīng)推入
    queue.push(watcher)  // 推入到隊(duì)列
  }
  ...
  nextTick(flushSchedulerQueue)  // 下一個(gè)tick更新
}

執(zhí)行update方法時(shí)將當(dāng)前watcher實(shí)例傳入到定義的queueWatcher方法內(nèi),這個(gè)方法的作用是把將要執(zhí)行更新的watcher收集到一個(gè)隊(duì)列queue之內(nèi),保證如果同一個(gè)watcher內(nèi)觸發(fā)了多次更新,只會(huì)更新一次對(duì)應(yīng)的watcher,我們舉兩個(gè)小示例:

export default {
  data() {
    return {  // 都被模板引用了
      num: 0,
      name: 'cc',
      sex: 'man'
    }
  },
  methods: {
    changeNum() {  // 賦值100次
      for(let i = 0; i < 100; i++) {
        this.num++
      }
    },
    changeInfo() {  // 一次賦值多個(gè)屬性的值
      this.name = 'ww'
      this.sex = 'woman'
    }
  }
}

這里的三個(gè)響應(yīng)式屬性它們收集都是同一個(gè)渲染watcher。所以當(dāng)賦值100次的情況出現(xiàn)時(shí),再將當(dāng)前的渲染watcher推入到的隊(duì)列之后,之后賦值觸發(fā)的set隊(duì)列內(nèi)并不會(huì)添加任何渲染watcher;當(dāng)同時(shí)賦值多個(gè)屬性時(shí)也是,因?yàn)樗鼈兪占亩际峭粋€(gè)渲染watcher,所以推入到隊(duì)列一次之后就不會(huì)添加了。

知識(shí)點(diǎn):vue還是挺聰明的,通過(guò)這兩個(gè)實(shí)例大家也看出來(lái)了,派發(fā)更新通知的粒度是組件級(jí)別,至于組件內(nèi)是哪個(gè)屬性賦值了,派發(fā)更新并不關(guān)心,而且怎么高效更新這個(gè)視圖,那是之后diff比對(duì)做的事情。

隊(duì)列有了,執(zhí)行nextTick(flushSchedulerQueue)再下一次tick時(shí)更新它,這里的nextTick就是我們經(jīng)常使用的this.$nextTick方法的原始方法,它們作用一致,實(shí)現(xiàn)原理之后章節(jié)說(shuō)明??聪聟?shù)flushSchedulerQueue是個(gè)啥?

let index = 0

function flushSchedulerQueue() {
  let watcher, id
  queue.sort((a, b) => a.id - b.id)  // watcher 排序
  
  for(index = 0; index < queue.length; index++) {  // 遍歷隊(duì)列
    watcher = queue[index]  
    if(watcher.before) {  // 渲染watcher獨(dú)有屬性
      watcher.before()  // 觸發(fā) beforeUpdate 鉤子
    }
    id = watcher.id
    has[id] = null
    watcher.run()  // 真正的更新方法
    ...
  }
}

原來(lái)是個(gè)函數(shù),再nextTick方法的內(nèi)部會(huì)執(zhí)行第一個(gè)參數(shù)。首先會(huì)將queue這個(gè)隊(duì)列進(jìn)行一次排序,依據(jù)是每次new Watcher生成的id,以從小到大的順序。當(dāng)前示例只是做渲染,而且隊(duì)列內(nèi)只存在了一個(gè)渲染watcher,所以是不存在順序的。但是如果有定義user watchercomputed watcher加上render watcher后,它們之間就會(huì)存在一個(gè)執(zhí)行順序的問(wèn)題了。

知識(shí)點(diǎn):watcher的執(zhí)行順序是先父后子,然后是從computed watcheruser watcher最后render watcher,這從它們的初始化順序就能看出。

然后就是遍歷這個(gè)隊(duì)列,因?yàn)槭卿秩?code>watcher,所有是有before屬性的,執(zhí)行傳入的before方法觸發(fā)beforeUpdate鉤子。最后執(zhí)行watcher.run()方法,執(zhí)行真正的派發(fā)更新方法。我們?nèi)タ聪?code>run干了啥:

class Watcher {
  ...
  run () {  
    if (this.active) {
      this.getAndInvoke(this.cb) // 有一種要抓狂的感覺(jué)
    }
  }
  
  getAndInvoke(cb) {  // 渲染watcher的cb為noop空函數(shù)
    const value = this.get()
    
    ... 后面是用戶(hù)watcher邏輯
  }
}

執(zhí)行run就是執(zhí)行getAndInvoke方法,因?yàn)槭卿秩?code>watcher,參數(shù)cbnoop空函數(shù)??戳诉@么多,其實(shí)...就是重新執(zhí)行一次this.get()方法,讓vm._update(vm._render())再走一遍而已。然后生成新舊VNode,最后進(jìn)行diff比對(duì)以更新視圖。

最后我們來(lái)說(shuō)下vue基于Object.defineProperty響應(yīng)式系統(tǒng)的一些不足。如只能監(jiān)聽(tīng)到數(shù)據(jù)的變化,所以有時(shí)data中要定義一堆的初始值,因?yàn)榧尤肓隧憫?yīng)式系統(tǒng)后才能被感知到;還有就是常規(guī)JavaScript操作對(duì)象的方式,并不能監(jiān)聽(tīng)到增加以及刪除,例如:

export default {
  data() {
    return {
      info: {
        name: 'cc'
      }
    }
  },
  methods: {
    addInfo() {  // 增加屬性
      this.info.sex = 'man'
    },
    delInfo() {  // 刪除屬性
      delete info.name
    }
  }
}

數(shù)據(jù)是被賦值了,但是視圖并不會(huì)發(fā)生變更。vue為了解決這個(gè)問(wèn)題,提供了兩個(gè)API$set$delete,它們又是怎么辦到的了?原理之后章節(jié)分析。

最后慣例的面試問(wèn)答就扯扯最近工作中遇到趣事吧。對(duì)于一個(gè)數(shù)據(jù)不會(huì)變更的列表,筆者把它定義再了created鉤子內(nèi),很少結(jié)對(duì)編程,這次例外。

created() {
  this.list = [...]
}

旁邊的妹子接過(guò)后:

妹子: 這個(gè)列表怎么data里沒(méi)有阿?在哪定義的?
我:我定義在created鉤子里了。
妹子:你怎么定義在這了?
我:因?yàn)樗遣粫?huì)被變更的,所以不需要... 算了,那你移到data里吧。
妹子:嗯?。?好。 小聲說(shuō)道:我還是第一次看見(jiàn)這么寫(xiě)的。
我:...有種被嫌棄了的感覺(jué)

面試官微笑而又不失禮貌的問(wèn)道:

  • 當(dāng)前組件模板中用到的變量一定要定義在data里么?

懟回去:

  • data中的變量都會(huì)被代理到當(dāng)前this下,所以我們也可以在this下掛載屬性,只要不重名即可。而且定義在data中的變量在vue的內(nèi)部會(huì)將它包裝成響應(yīng)式的數(shù)據(jù),讓它擁有變更即可驅(qū)動(dòng)視圖變化的能力。但是如果這個(gè)數(shù)據(jù)不需要驅(qū)動(dòng)視圖,定義在createdmounted鉤子內(nèi)也是可以的,因?yàn)椴粫?huì)執(zhí)行響應(yīng)式的包裝方法,對(duì)性能也是一種提升。

下一篇:Vue原理解析(七):全面深入理解響應(yīng)式原理(下)-數(shù)組進(jìn)階篇

順手點(diǎn)個(gè)贊或關(guān)注唄,找起來(lái)也方便~

分享一個(gè)筆者自己寫(xiě)的組件庫(kù),哪天可能會(huì)用的上了 ~ ↓

你可能會(huì)用的上的一個(gè)vue功能組件庫(kù),持續(xù)完善中...

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容