Vue源碼之數據的observer

所以我們打開 core/instance/state.js 文件 找到 initState 函數

export function initState (vm: Component) {
  vm._watchers = [] //儲存watcher對象
  const opts = vm.$options  //options引用
  if (opts.props) initProps(vm, opts.props)  //初始化props
  if (opts.methods) initMethods(vm, opts.methods) //初始化方法
  if (opts.data) {
    initData(vm)     //初始化data
  } else {
    observe(vm._data = {}, true /* asRootData */) //這裏是data爲空時observe 函數觀測一個空對象:{}
  }
  if (opts.computed) initComputed(vm, opts.computed) //初始化computed
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }  //初始化watch
}

現在要看的是這一段

if (opts.data) {
    initData(vm)     //初始化data
  } else {
    observe(vm._data = {}, true /* asRootData */) //這裏是data爲空時observe 函數觀測一個空對象:{}
  }

初始化data的過程 首先判斷 opts.data 是否存在,即 data 選項是否存在,如果存在則調用 initData(vm) 函數初始化 data 選項,否則通過 observe 函數觀測一個空的對象,並且 vm._data 引用了該空對象。其中 observe 函數是將 data 轉換成響應式數據的核心入口,

在core/instance/state.js 文件,initData 函數的一開始是這樣一段代碼

let data = vm.$options.data  //定義data對象
data = vm._data = typeof data === 'function'
  ? getData(data, vm)  執行這個函數
  : data || {}    
  //這裏是初始化data第一步就是執行data的構造函數 因爲之前都是包裝成函數的 但是爲什麼還要判斷呢
  因爲 beforeCreate 生命週期鉤子函數是在 mergeOptions 函數之後 initData 之前被調用的,如果在 beforeCreate 生命週期鉤子函數中修改了 vm.$options.data 的值,那麼在 initData 函數中對於 vm.$options.data 類型的判斷就是必要的了
export function getData (data: Function, vm: Component): any {
  // #7573 disable dep collection when invoking data getters
  pushTarget()
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, `data()`)
    return {}
  } finally {
    popTarget()
  }
}  這裏的pushTarget(),popTarget()其實是在props數據和data初始化時的收集冗餘依賴的 等到後面再說

總直到目前位置正常情況就是通過getData函數獲取data選項的數據對象 然後回到initData中

data = vm._data = getData(data, vm) 這裏重寫了data和vm實例上的_data

接着是個if

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

判斷data對象是否是個純對象不是就在生產環境打出警告

再往下是這樣的一串代碼

const keys = Object.keys(data) //獲取data所有的鍵
const props = vm.$options.props  //獲取props引用
const methods = vm.$options.methods  //獲取methods引用
let i = keys.length   //遍歷key
while (i--) {
  const key = keys[i]
  if (process.env.NODE_ENV !== 'production') {
    if (methods && hasOwn(methods, key)) {  //methods不能和data重名
      warn(
        `Method "${key}" has already been defined as a data property.`,
        vm
      )
    }
  }
  if (props && hasOwn(props, key)) {
  //props不能和data重名
    process.env.NODE_ENV !== 'production' && warn(
      `The data property "${key}" is already declared as a prop. ` +
      `Use prop default value instead.`,
      vm
    )
  } else if (!isReserved(key)) {
    proxy(vm, `_data`, key)  //isReserved判斷key中是否帶有$,_這些保留字符防止Vue自生屬性方法衝突
    //proxy就是對data的代理訪問
  }
}

其中關鍵點在於 proxy 函數,該函數同樣定義在 core/instance/state.js 文件中,其內容如下:

export function proxy (target: Object, sourceKey: string, key: string) {  
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }  
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}  通過 Object.defineProperty在_vm上定義和data數據字段同名的訪問器屬性,其實就是代理到vm._data上的
比如當我們通過 vm.a訪問其實就是vm._data.a

經過一系列處理 終於來到關鍵

observe(data, true /* asRootData */)

給數據添加依賴,即觀察者

到這裏initData的作用就出來了

  1. 根據 vm.$options.data 選項獲取真正想要的數據(注意:此時vm.$options.dat是函數)
  2. 校驗得到的數據是否是一個純對象
  3. 檢查數據對象 data 上的鍵是否與 props 對象上的鍵衝突
  4. 檢查 methods 對象上的鍵是否與 data 對象上的鍵衝突
  5. 在 Vue 實例對象上添加代理訪問數據對象的同名屬性
  6. 最後調用 observe

這裏先理解一下Vue的數據響應思想 在Vue中我們一啊不能可以用$watch來監聽一個數據

const obj = new Vue({
  data: {
    a: 1
  }
})

obj.$watch('a', () => {
  console.log('修改了 a')
})
obj.a = 2 //console.log('修改了 a')

所以這裏的watch就是一個監聽實現函數

function $watch('要監聽的數據','依賴即數據變化後的回掉'){
}

其實原理都知道 就是Object.defineProperty去修改訪問器屬性

Object.defineProperty(data, 'a', {
  set () {
    console.log('設置了屬性 a')
  },
  get () {
    console.log('讀取了屬性 a')
  }
})

即屬性攔截,所以大致的思想就有了 收集依賴 添加攔截器

const dep = [] //存儲依賴
Object.defineProperty(data, 'a', {
  set () {
    // 當屬性被設置的時候,將隊列裏的依賴都執行一次
    dep.forEach(fn => fn())
  },
  get () {
    // 當屬性被獲取的時候,把依賴放到隊列裏
    dep.push(fn)
  }
})

但是新問題出現了 怎麼在訪問a的時候獲取fn呢 重點是當我們watch時是能夠知道 監聽的是誰

const data = {
  a: 1
}

const dep = []
Object.defineProperty(data, 'a', {
  set () {
    dep.forEach(fn => fn())
  },
  get () {
    // 此時 Target 變量中保存的就是依賴函數
    dep.push(Target)
  }
})

// Target 是全局變量
let Target = null
function $watch (exp, fn) {
  // 將 Target 的值設置爲 fn
  Target = fn  
  // 讀取字段值,觸發 get 函數
  data[exp]
}

這樣就能簡單實現一個watch,但是我們怎麼能實現多個屬性呢 即遍歷data

for (const key in data) {
  const dep = []
   let val = data[key] // 緩存字段原有的值
  Object.defineProperty(data, key, {
    set () {
     // 如果值沒有變什麼都不做
      if (newVal === val) return
      // 使用新值替換舊值
      val = newVal
      dep.forEach(fn => fn())
    },
    get () {
      dep.push(Target)
      return val  // 將該值返回
    }
  })
}

這樣就差不多可以了,可是還是有點問題,比對象嵌套呢

a: {
    b: 1
  }
}

故我們要遞歸定義

function walk (data) {
  for (let key in data) {
    const dep = []
    let val = data[key]
    // 如果 val 是對象,遞歸調用 walk 函數將其轉爲訪問器屬性
    const nativeString = Object.prototype.toString.call(val)
    if (nativeString === '[object Object]') {
      walk(val)
    }
    Object.defineProperty(data, key, {
      set (newVal) {
        if (newVal === val) return
        val = newVal
        dep.forEach(fn => fn())
      },
      get () {
        dep.push(Target)
        return val
      }
    })
  }
}

walk(data)

但是這裏的watch就不能實現了 如果按照watch('a.b',fn),這樣 data['a.b']是訪問不到屬性的 故我們需要小小的改造一下下

function $watch (exp, fn) {
  Target = fn
  let pathArr,
      obj = data
  // 檢查 exp 中是否包含 .
  if (/\./.test(exp)) {
    // 將字符串轉爲數組,例:'a.b' => ['a', 'b']
    pathArr = exp.split('.')
    // 使用循環讀取到 data.a.b
    pathArr.forEach(p => {
      obj = obj[p]
    })
    return
  }
  data[exp]  
}

其實watch就是一隻訪問到你定義的字段,然後觸發get添加依賴 那要是 exp這裏是個函數 函數裏有data某個屬性呢比如

funciotn render(){
   document.write(`姓名:${data.name}; 年齡:${data.age}`)
}
watch(render,render)

所以還要加上函數判斷

if (typeof exp === 'function') {
    exp()
    return
  }

第二個參數依然是 render 函數其實就是將數據相應依賴到dom上去 大致如此

接下來看看observe工廠函數的實現

observe 工廠函數

回到initData函數最後

observe(data, true /* asRootData */)

打開core/observer/index.js 找到observe函數

export function observe (value: any, asRootData: ?boolean): Observer | void {
   //接受兩個參數vlue數據 asRootData 數據是否是根級數據  返回空或者檢查者對象
  if (!isObject(value) || value instanceof VNode) {
    return    //觀測的數據不是一個對象或者是 VNode實例
  }
  let ob: Observer | void  //定義observer實例 並最後返回
  //hasOwn 函數檢測數據對象 value 自身是否含有 __ob__ 屬性,
  並且 __ob__ 屬性應該是 Observer 的實例。如果爲真則直接將數據對象自身的 __ob__ 屬性的值作爲 ob 的值:ob = value.__ob__
  if (hasOwn(value, '__ob__') && value.__ob__    instanceof Observer) {
    ob = value.__ob__  //即value已經被監聽了
  } else if ( 
    shouldObserve &&  //shouldObserve core/observer/index.js裏 定義變量其實是一個開關 
    !isServerRendering() &&  //用來判斷是否是服務端渲染
    (Array.isArray(value) || isPlainObject(value)) && //只有當數據對象是數組或純對象的時候
    Object.isExtensible(value) &&  //對象可擴展 即不是凍結對象等等
    !value._isVue //不是Vue實例 
  ) {
    ob = new Observer(value)  //創建一個 Observer 實例
  }
  if (asRootData && ob) {
    ob.vmCount++  
  }
  return ob
}

Observer 構造函數

core/observer/index.js下 簡化後代碼

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  constructor (value: any) {
    // ...
  }

  walk (obj: Object) {
    // ...
  }
  
  observeArray (items: Array<any>) {
    // ...
  }
}

我們應該知道一個Observer有三個屬性 value,dep,vmCount 兩個方法 walk,observeArray 首先看看構造函數

constructor (value: any) {
  this.value = value  //引用數據對象
  this.dep = new Dep()  //創建一個dep對象,這個dep是之前想到的收集依賴的嘛 其實不是
  this.vmCount = 0 //實例對象的vmCount初始化爲0
  def(value, '__ob__', this) //__ob__給數據定義了__ob__ 其實就是當前的Observe實例
  //const data = {
  //a: 1,
  // __ob__ 是不可枚舉的屬性
  //__ob__: {
    //value: data, // value 屬性指向 data 數據對象本身,這是一個循環引用
    //dep: dep實例對象, // new Dep()
    //vmCount: 0
  //}
//}
  if (Array.isArray(value)) {  
    const augment = hasProto  //判斷是數組還是一個純對象
      ? protoAugment
      : copyAugment        //數組處理之後講
    augment(value, arrayMethods, arrayKeys)
    this.observeArray(value)
  } else {   純對象  
    this.walk(value)       //調用walk方法
  }
}
walk (obj: Object) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i])
  }
} //遍歷可枚舉屬性然後爲每個屬性 defineReactive

這個defineReactive在 core/observer/index.js 其核心心是將數據對象的數據屬性轉換爲訪問器屬性數據對象的屬性設置一對 getter/setter

export function defineReactive (//5個參數
  obj: Object,  //一個對象
  key: string, //對象的key  //在walk裏只用了這兩個
  val: any, 
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()  //創建dep實例 在之前創建Observer時其也有一個 __ob__.dep 這兩個作用是不同的
  //這裏dep纔是我們之前想到的dep
  //首先通過 Object.getOwnPropertyDescriptor 函數獲取該字段可能已有的屬性描述對象講該對象放入property 
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return //若是存在且不能修改 就返回
  }
 //取得之前的get 和set引用
  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  //當傳遞兩個參數時 並且沒有getter或者有setter 這裏就是一個特殊情況 
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  //childOb !shallow 情況下 observe(val)遞歸調用深度監聽
  
  //非深度觀測的場景,即 initRender 函數中在 Vue 實例對象上定義 $attrs 屬性和 $listeners 屬性時就是非深度觀測
  //defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true) // 最後一個參數 shallow 爲 true
//defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)

//observe(val) 深度觀測數據對象時,這裏的 val 未必有值所以必須在滿足 沒有get但是有set 且參數爲2時才能夠取到val值
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true, //可枚舉
    configurable: true, //可修改
    get: function reactiveGetter () {
      // 省略...
    },
    set: function reactiveSetter (newVal) {
      // 省略...
    }
  })
}

這裏之後數據data對象變成了什麼呢

const data = {
  // 屬性 a 通過 setter/getter 通過閉包引用着 dep 和 childOb
  a: {
    // 屬性 b 通過 setter/getter 通過閉包引用着 dep 和 childOb
    b: 1
    __ob__: {a, dep, vmCount}
  }
  __ob__: {data, dep, vmCount}
}  //a裏的 childOb ===data.a.__ob__ b的childOb就是undefined

這裏Observer 已經執行完了,因爲set,get是之後才能觸發的 先來看看get

get: function reactiveGetter () {
  const value = getter ? getter.call(obj) : val //正確取到原值 從緩存的getter或者是直接獲取到的val 保證getter正常運作
  if (Dep.target) {    //if裏收集依賴  Dep.target其實就是 保存的觀察者
    dep.depend() //將 依賴收集到本dep中
    if (childOb) {   //子實例存在
      childOb.dep.depend()   
      if (Array.isArray(value)) {  //若讀取值是一個數組 就dependArray依次讀取每個元素依賴   對象和數組是不同處理的
        dependArray(value)
      }
    }
  }
  return value
}

這裏有個重點就是依賴即在本身的dep中也在childOb.dep中

其實就是 data.a.dep和data.a.ob.dep裏 第一個”dep“裏收集的依賴的觸發時機是當屬性值被修改時觸發,即在 set 函數中觸發:dep.notify() 而第二個”dep“裏收集的依賴的觸發時機是在使用 $set 或 Vue.set 給數據對象添加新屬性時觸發, 所以就 ob.dep和dep存的是相同的依賴

Vue.set = function (obj, key, val) {
  defineReactive(obj, key, val)
  obj.__ob__.dep.notify()
} //假設set

如上代碼所示,當我們使用上面的代碼給 data.a 對象添加新的屬性: Vue.set(data.a, 'c', 1) 這裏就能夠觸發收集在data.a.ob.dep裏的依賴了 也就是data.a的依賴 ob_ 屬性以及 ob.dep 的主要作用是爲了添加、刪除,屬性時有能力觸發依賴,而這就是 Vue.set 或 Vue.delete 的原理

set如何觸發依賴

set: function reactiveSetter (newVal) {
  const value = getter ? getter.call(obj) : val //取的原屬性值
  /* eslint-disable no-self-compare */
  if (newVal === value || (newVal !== newVal && value !== value)) {
    return
  }    //新舊值全等或者 新值 舊值自身不等   NaN === NaN // false

  /* eslint-enable no-self-compare */
  if (process.env.NODE_ENV !== 'production' && customSetter) {
    customSetter()   //非生產環境   customSetter就是上面講的定義$attr裏的第四個參數 其實就是打印一下輔助函數說明只讀 
  }
  if (setter) {
    setter.call(obj, newVal)   //如果有緩存setter就觸發setter
  } else {
    val = newVal   //設置新值
  }
  childOb = !shallow && observe(newVal) //爲新值創建observe是實例
  dep.notify() //依次執行依賴
}

這裏再提一下前面的代碼

if ((!getter || setter) && arguments.length === 2) {
  val = obj[key]
}

該if有兩個條件其一是(!getter || setter)其二arguments.length 滿足這些條件纔會到obj[key]上取值 否則不會觸發取值 所以也不會深度觀察 對於第二個條件,很好理解,當傳遞參數的數量爲 2 時,說明沒有傳遞第三個參數 val,那麼當然需要通過執行 val = obj[key] 去獲取屬性值。比較難理解的是第一個條件,即 (!getter || setter)要理解這個問題你需要知道 Vue 代碼的變更,以及爲什麼變更。其實在最初並沒有上面這段 if 語句塊,在 walk 函數中是這樣調用 defineReactive 函數的:

walk (obj: Object) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    // 這裏傳遞了第三個參數
    defineReactive(obj, keys[i], obj[keys[i]])
  }
}

//現在
walk (obj: Object) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    // 在 walk 函數中調用 defineReactive 函數時暫時不獲取屬性值
    defineReactive(obj, keys[i])
  }
}

即obj[keys]z在walk內部獲取只有在屬性沒有 get 函數 其實就是當屬性值本身有get函數時不觸發而等到真正調用時再去觸發 而且時如果一屬性本身有自己的getter函數纔會去深度觀測 有兩方面的原因,第一:由於當屬性存在原本的 getter 時在深度觀測之前不會取值,所以在在深度觀測語句執行之前取不到屬性值從而無法深度觀測。第二:之所以在深度觀測之前不取值是因爲屬性原本的 getter 由用戶定義,用戶可能在 getter 中做任何意想不到的事情,這麼做是出於避免引發不可預見行爲的考慮

哪爲什麼還必須要存在set呢 數據對象的某一個屬性只擁有 get 攔截器函數而沒有 set 攔截器函數時,此時該屬性不會被深度觀測 是經過 defineReactive 函數的處理之後,該屬性將被重新定義 getter 和 setter,此時該屬性變成了既擁有 get 函數又擁有 set 函數。並且當我們嘗試給該屬性重新賦值時,那麼新的值將會被觀測。這時候矛盾就產生了。

定義響應式數據時行爲的不一致:原本該屬性不會被深度觀測,但是重新賦值之後,新的值卻被觀測了

爲了解決這個問題,採用的辦法是當屬性擁有原本的 setter 時,即使擁有 getter 也要獲取屬性值並觀測之

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章