從原始碼解析vue的響應式原理-依賴收集、依賴觸發

叫我女王大人發表於2018-08-05

前言

vue官方對響應式原理的解釋:深入響應式原理

從原始碼解析vue的響應式原理-依賴收集、依賴觸發

總結下官方的描述,大概分為一下幾點:

  • 元件例項有自己的watcher物件,用於記錄資料依賴
  • 元件中的data的每個屬性都有自己的getter、setter方法,用於收集依賴和觸發依賴
  • 元件渲染過程中,呼叫data中的屬性的getter方法,將依賴收集至watcher物件
  • data中的屬性變化,會呼叫setter中的方法,告訴watcher有依賴發生了變化
  • watcher收到依賴變化的訊息,重新渲染虛擬dom,實現頁面響應

​ 然鵝,官方的介紹只是一個大致的流程,我們還是不知道vue到底是怎樣給data的每個屬性設定getter、setter方法?物件屬性和陣列屬性的實現又有什麼不同?怎樣實現依賴的收集和依賴的觸發? 想要搞清楚這些,不得不看一波原始碼了。下面,請跟我從vue原始碼分析vue的響應式原理

--- 下面我要開始我的表演了---

例項初始化階段

vue原始碼的 instance/init.js 中是初始化的入口,其中初始化分為下面幾個步驟:

//初始化生命週期
initLifecycle(vm)
//初始化事件
initEvents(vm)
//初始化render
initRender(vm)
//觸發beforeCreate事件
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
//初始化狀態,!!!此處劃重點!!!
initState(vm)
initProvide(vm) // resolve provide after data/props
//觸發created事件
callHook(vm, 'created')
複製程式碼

其中劃重點的 initState() 方法中進行了 props、methods、data、computed以及watcher的初始化。在instance/state.js中可以看到如下程式碼。

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  //初始化props
  if (opts.props) initProps(vm, opts.props)
  //初始化methods
  if (opts.methods) initMethods(vm, opts.methods)
  //初始化data!!!再次劃重點!!!
  if (opts.data) {
    initData(vm)
  } else {
  	//即使沒有data,也要呼叫observe觀測_data物件
    observe(vm._data = {}, true /* asRootData */)
  }
  //初始化computed
  if (opts.computed) initComputed(vm, opts.computed)
  //初始化watcher
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
複製程式碼

劃重點的initData()方法中進行了data的初始化。程式碼依舊在instance/state.js中可以看到。initData()方法程式碼如下(刪節版)。

/* 初始化data */
function initData (vm: Component) {
  //判斷data是否是一個物件
  if (!isPlainObject(data)) {
    ...
  }
  //判斷data中的屬性是否和method重名
  if (methods && hasOwn(methods, key)) {
    ...
  }
  //判斷data中的屬性是否和props重名
  if (props && hasOwn(props, key)) {
    ...
  }
  //將vm中的屬性轉至vm._data中
  proxy(vm, `_data`, key)
  //呼叫observe觀測data物件
  observe(data, true /* asRootData */)
}
複製程式碼

initData()函式中除了前面一系列對data的判斷之外就是資料的代理和observe方法的呼叫。其中資料代proxy(vm, `_data`, key)作用是將vm的屬性代理至vm._data上,例如:

//程式碼如下
const per = new VUE({
        data:{
            name: 'summer',
            age: 18,
        }
    })
複製程式碼

當我們訪問per.name時,實際上訪問的是per._data.name 而下面一句observe(data, true /* asRootData */)才是響應式的開始。

小結

總結一下初始化過程大概如下圖

從原始碼解析vue的響應式原理-依賴收集、依賴觸發

響應式階段

observe函式的程式碼在observe/index.js,observe是一個工廠函式,用於為物件生成一個Observe例項。而真正將物件轉化為響應式物件的是observe工廠函式返回的Observe例項。

Observe建構函式

Observe建構函式程式碼如下(刪減版)。

export class Observer {
  constructor (value: any) {
  	//物件本身
    this.value = value
    //依賴收集器
    this.dep = new Dep()
    this.vmCount = 0
    //為物件新增__ob__屬性
    def(value, '__ob__', this)
    //若物件是array型別
    if (Array.isArray(value)) {
     	...
    } else {
      //若物件是object型別
      ...
    }
  }
複製程式碼

從程式碼分析,Observe建構函式做了三件事:

  • 為物件新增__ob__屬性,__ob__中包含value資料物件本身、dep依賴收集器、vmCount。資料經過這個步驟以後的變化如下:
	//原資料
	const data = {
        name: 'summer'
	}
	//變化後資料
	const data = {
        name: 'summer',
        __ob__: {
            value: data, //data資料本身
            dep: new Dep(), //dep依賴收集器
            vmCount: 0
        }
	}
複製程式碼
  • 若物件是array型別,則進行array型別操作
  • 若物件是object型別,則進行object型別操作

資料是object型別

當資料是object型別時,呼叫了一個walk方法,在walk方法中遍歷資料的所有屬性,並呼叫defineReactive方法。defineReactive方法的程式碼仍然在observe/index.js中,刪減版如下:

export function defineReactive (...) {
  //dep儲存依賴的變數,每個屬性欄位都有一個屬於自己的dep,用於收集屬於該欄位的依賴
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }
  //快取原有的get、set方法
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  // 為每個屬性建立childOb,並且對每個屬性進行observe遞迴
  let childOb = !shallow && observe(val)
  //為屬性加入getter/setter方法
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      ...
    },
    set: function reactiveSetter (newVal) {
      ...
  })
}
複製程式碼

defineReactive方法主要做了以下幾件事:

  • 為每個屬性例項化一個dep依賴收集器,用於收集該屬性的相關依賴。【通過getter、setter引用】
  • 快取屬性原有的get和set方法,保證後面重寫get、set方法時行為正常。
  • 為每個屬性建立childOb。(其實是一個對屬性進行進行observe遞迴的過程,並將結果儲存在childOb中。物件或陣列屬性的childOb為__ob__,其他屬性的childOb為undefined)。【通過getter、setter引用】
  • 將物件中的每一個屬性都加上getter、setter方法。

經過defineReactive處理的資料變化如下, 每個屬性都有自己的dep、childOb、getter、setter,並且每個object型別的屬性都有__ob__

//原資料
const data = {
    user: {
        name: 'summer'
    },
    other: '123'
}
//處理後資料
const data = {
    user: {
        name: 'summer',
        [name dep,]
        [name childOb: undefined]
        name getter,//引用name dep和name childOb
        name setter,//引用name dep和name childOb
        
        __ob__:{data, user, vmCount}
    },
    [user dep,]
    [user childOb: user.__ob__,]
    user getter,//引用user dep和user childOb
    user setter,//引用user dep和user childOb
    
    other: '123',
    [other dep,]
    [other childOb: undefined,]
    other getter,//引用other dep和other childOb
    other setter,//引用other dep和other childOb
    
    __ob__:{data, dep, vmCount}
}
複製程式碼

剛剛講到defineReactive函式的最後一步是每一個屬性都加上getter、setter方法。那麼getter和setter函式到底做了什麼呢?

getter方法中:

getter函式內部程式碼如下:

get: function reactiveGetter () {
	//呼叫原屬性的get方法返回值
	const value = getter ? getter.call(obj) : val
    //如果存在需要被收集的依賴
    if (Dep.target) {
        /* 將依賴收集到該屬性的dep中 */
        dep.depend()
        if (childOb) {
          //每個物件的obj.__ob__.dep中也收集該依賴
          childOb.dep.depend()
          //如果屬性是array型別,進行dependArray操作
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
    }
	return value
},
複製程式碼

getter方法主要做了兩件事:

  • 呼叫原屬性的get方法返回值
  • 收集依賴
    1. Dep.target表示一個依賴,即觀察者,大部分情況下是一個依賴函式。
    2. 如果存在依賴,則收集依賴到該屬性的dep依賴收集器中
    3. 如果存在childOb(即屬性是物件或者陣列),則將該依賴收集到childOb也就是__ob__的依賴收集器__ob__.dep中,這個依賴收集器在使用$set 或 Vue.set 給屬性物件新增新屬性時觸,也就是說Vue.set 或 Vue.delete 會觸發__ob__.dep中的依賴。
    4. 如果屬性的值是陣列,則呼叫dependArray函式,將依賴收集到陣列中的每一個物件元素的__ob__.dep中。確保在使用$set 或 Vue.set時,陣列中巢狀的物件能正常響應。程式碼如下:
//資料
const data = {
    user: [
        {
            name: 'summer'
        }
    ]
}
// 頁面顯示
{{user}}
<Button @click="addAge()">addAge</Button>
//addAge方法,為陣列中的巢狀物件新增age屬性
change2: function(){
	this.$set(this.user[0], 'age', 18)
}
複製程式碼
//dependArray函式
function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    //將依賴收集到每一個子物件/陣列中
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}
複製程式碼
//轉化後資料
const data = {
    user: [
       {
            name: 'summer',
            __ob__: {user[0], dep, vmCount}
        }
        __ob__: {user, dep, vmCount}
    ]
}
複製程式碼

dependArray的作用就是將user的依賴收集到它內部的user[0]物件的__ob__.dep中,使得進行addAge操作時,頁面可以正常的響應變化。

setter方法中:

setter函式內部程式碼如下:

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()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      //由於屬性的值發生了變化,則為屬性建立新的childOb,重新observe
      childOb = !shallow && observe(newVal)
      //在set方法中執行依賴器中的所有依賴
      dep.notify()
      }
})
複製程式碼

setter方法主要做了三件事:

  • 為屬性設定正確的值
  • 由於屬性的值發生了變化,則為屬性建立新的childOb,重新observe
  • 執行依賴器中的所有依賴

資料是純物件型別的處理講完了,下面看下資料是array型別的操作。

資料是array型別

observer/index.js中對array處理的部分:

if (Array.isArray(value)) {
	const augment = hasProto
		? protoAugment
		: copyAugment
	//攔截修改陣列方法
	augment(value, arrayMethods, arrayKeys)
	//遞迴觀測陣列中的每一個值
	this.observeArray(value)
}
複製程式碼

當資料型別是array型別時

  1. 使用protoAugment方法為資料指定建構函式__proto為arrayMethods,出於相容性考慮如果瀏覽器不支援__proto__ ,則使用arrayMethods重寫陣列資料中的所有相關方法。
  2. 遞迴觀測陣列中的每一個值
arrayMethods攔截修改陣列方法

arrayMethods中的定義在observe/array.js中,程式碼如下:

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

//修改陣列的方法
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  //攔截修改陣列的方法,當修改陣列方法被呼叫時觸發陣列中的__ob__.dep中的所有依賴
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    //對新增元素使用observeArray進行觀測
    if (inserted) ob.observeArray(inserted)
    //觸發__ob__.dep中的所有依賴
    ob.dep.notify()
    return result
  })
})

複製程式碼

在arrayMethods中做了如下幾件事:

  • 需要攔截的修改陣列的方法有:push、pop、shift、unshift、splice、sort、reverse
  • 當陣列有新增元素時,使用observeArray對新增的元素進行觀測
  • 攔截了修改陣列的方法,當修改陣列方法被呼叫時觸發陣列中的__ob__.dep的所有依賴
observeArray遞迴觀測陣列中的每一項

observeArray程式碼如下:

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

在observeArray方法,對陣列中的所有屬性進行observe遞迴。然而這裡有一個問題就是無法觀測陣列中的所有非Object的基本型別。observe方法的第一句就是

if (!isObject(value) || value instanceof VNode) {
	return
}
複製程式碼

也就是說陣列中的非Object型別的值是不會被觀測到的,如果有資料:

const data = {
    arr: [{
    	test: 0
   	}, 1, 2],
}
複製程式碼

此時如果改變arr[0].test=3可以被觸發響應,而改變arr[1]=4不能觸發響應,因為observeArray觀測資料中的每一項時,observe(arr[0])是一個觀測一個物件可以被觀測。observe(arr[1])時觀測一個基本型別資料,不可以被觀測。

小結

響應式階段流程圖

從原始碼解析vue的響應式原理-依賴收集、依賴觸發

參考文章:揭開資料響應系統的面紗

相關文章