1. 程式人生 > >vue原始碼(八)揭開資料響應系統的面紗

vue原始碼(八)揭開資料響應系統的面紗

本文是學習vue原始碼,之所以轉載過來是方便自己隨時檢視,在這裡要感謝HcySunYang大神,提供的開源vue原始碼解析,寫的非常非常好,簡單易懂,比自己看要容易多了,他的文章連結地址是http://hcysun.me/vue-design/art/

相信很多同學都對 Vue 的資料響應系統有或多或少的瞭解,本章將完整的覆蓋 Vue 響應系統的邊邊角角,讓你對其擁有一個完善的認識。接下來我們還是接著上一章的話題,從 initState 函式開始。我們知道 initState 函式是很多選項初始化的彙總,在 initState

 函式內部使用 initProps 函式初始化 props 屬性;使用 initMethods 函式初始化 methods 屬性;使用 initData 函式初始化 data 選項;使用 initComputed 函式和 initWatch 函式初始化 computed 和 watch 選項。那麼我們從哪裡開始講起呢?這裡我們決定以 initData 為切入點為大家講解 Vue
 的響應系統,因為 initData 幾乎涉及了全部的資料響應相關的內容,這樣將會讓大家在理解 propscomputedwatch 等選項時不費吹灰之力,且會有一種水到渠成的感覺。

話不多說,如下是 initState 函式中用於初始化 data 選項的程式碼:

if (opts.data) {
  initData(vm)
} else {
  observe(vm._data = {}, true /* asRootData */)
}

首先判斷 opts.data 是否存在,即 data

 選項是否存在,如果存在則呼叫 initData(vm) 函式初始化 data 選項,否則通過 observe 函式觀測一個空的物件,並且 vm._data 引用了該空物件。其中 observe 函式是將 data 轉換成響應式資料的核心入口,另外例項物件上的 _data 屬性我們在前面的章節中講解 $data 屬性的時候講到過,$data 屬性是一個訪問器屬性,其代理的值就是 _data

下面我們就從 initData(vm) 開始開啟資料響應系統的探索之旅。

#例項物件代理訪問資料 data

我們找到 initData 函式,該函式與 initState 函式定義在同一個檔案中,即 core/instance/state.js 檔案,initData 函式的一開始是這樣一段程式碼:

let data = vm.$options.data
data = vm._data = typeof data === 'function'
  ? getData(data, vm)
  : data || {}

首先定義 data 變數,它是 vm.$options.data 的引用。在 Vue選項的合併 一節中我們知道 vm.$options.data 其實最終被處理成了一個函式,且該函式的執行結果才是真正的資料。在上面的程式碼中我們發現其中依然存在一個使用 typeof 語句判斷 data 資料型別的操作,我們知道經過 mergeOptions 函式處理後 data 選項必然是一個函式,那麼這裡的判斷還有必要嗎?答案是有,這是因為 beforeCreate 生命週期鉤子函式是在 mergeOptions 函式之後 initData 之前被呼叫的,如果在 beforeCreate 生命週期鉤子函式中修改了 vm.$options.data 的值,那麼在 initData 函式中對於 vm.$options.data 型別的判斷就是必要的了。

回到上面那段程式碼,如果 vm.$options.data 的型別為函式,則呼叫 getData 函式獲取真正的資料,getData 函式就定義在 initData 函式的下面,我們看看其作用是什麼:

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

getData 函式接收兩個引數:第一個引數是 data 選項,我們知道 data 選項是一個函式,第二個引數是 Vue 例項物件。getData 函式的作用其實就是通過呼叫 data 函式獲取真正的資料物件並返回,即:data.call(vm, vm),而且我們注意到 data.call(vm, vm) 被包裹在 try...catch 語句塊中,這是為了捕獲 data 函式中可能出現的錯誤。同時如果有錯誤發生那麼則返回一個空物件作為資料物件:return {}

另外我們注意到在 getData 函式的開頭呼叫了 pushTarget() 函式,並且在 finally 語句塊中呼叫了 popTarget(),這麼做的目的是什麼呢?這麼做是為了防止使用 props 資料初始化 data 資料時收集冗餘依賴的,等到我們分析 Vue 是如何收集依賴的時候會回頭來說明。總之 getData 函式的作用就是:“通過呼叫 data 選項從而獲取資料物件”

我們再回到 initData 函式中:

data = vm._data = getData(data, vm)

當通過 getData 拿到最終的資料物件後,將該物件賦值給 vm._data 屬性,同時重寫了 data 變數,此時 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
  )
}

上面的程式碼中使用 isPlainObject 函式判斷變數 data 是不是一個純物件,如果不是純物件那麼在非生產環境會列印警告資訊。我們知道,如果一切都按照預期進行,那麼此時 data 已經是一個最終的資料物件了,但這僅僅是我們的期望而已,畢竟 data 選項是開發者編寫的,如下:

new Vue({
  data () {
    return '我就是不返回物件'
  }
})

上面的程式碼中 data 函式返回了一個字串而不是物件,所以我們需要判斷一下 data 函式返回值的型別。

再往下是這樣一段程式碼:

// proxy data on instance
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') {
    if (methods && hasOwn(methods, key)) {
      warn(
        `Method "${key}" has already been defined as a data property.`,
        vm
      )
    }
  }
  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
    )
  } else if (!isReserved(key)) {
    proxy(vm, `_data`, key)
  }
}

上面的程式碼中首先使用 Object.keys 函式獲取 data 物件的所有鍵,並將由 data 物件的鍵所組成的陣列賦值給 keys 常量。接著分別用 props 常量和 methods 常量引用 vm.$options.props 和 vm.$options.methods。然後開啟一個 while 迴圈,該迴圈用來遍歷 keys 陣列,那麼遍歷 keys 陣列的目的是什麼呢?我們來看迴圈體內的第一段 if 語句:

const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
  if (methods && hasOwn(methods, key)) {
    warn(
      `Method "${key}" has already been defined as a data property.`,
      vm
    )
  }
}

上面這段程式碼的意思是在非生產環境下如果發現在 methods 物件上定義了同樣的 key,也就是說 data 資料的 key 與 methods 物件中定義的函式名稱相同,那麼會列印一個警告,提示開發者:你定義在 methods 物件中的函式名稱已經被作為 data 物件中某個資料欄位的 key 了,你應該換一個函式名字。為什麼要這麼做呢?如下:

const ins = new Vue({
  data: {
    a: 1
  },
  methods: {
    b () {}
  }
})

ins.a // 1
ins.b // function

在這個例子中無論是定義在 data 資料物件,還是定義在 methods 物件中的函式,都可以通過例項物件代理訪問。所以當 data 資料物件中的 key 與 methods 物件中的 key 衝突時,豈不就會產生覆蓋掉的現象,所以為了避免覆蓋 Vue 是不允許在 methods 中定義與 data 欄位的 key 重名的函式的。而這個工作就是在 while 迴圈中第一個語句塊中的程式碼去完成的。

接著我們看 while 迴圈中的第二個 if 語句塊:

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
  )
} else if (!isReserved(key)) {
  proxy(vm, `_data`, key)
}

同樣的 Vue 例項物件除了代理訪問 data 資料和 methods 中的方法之外,還代理訪問了 props 中的資料,所以上面這段程式碼的作用是如果發現 data 資料欄位的 key 已經在 props 中有定義了,那麼就會列印警告。另外這裡有一個優先順序的關係:props優先順序 > data優先順序 > methods優先順序。即如果一個 key 在 props 中有定義了那麼就不能在 data 中出現;如果一個 key 在 data 中出現了那麼就不能在 methods 中出現了。

另外上面的程式碼中當 if 語句的條件不成立,則會判斷 else if 語句中的條件:!isReserved(key),該條件的意思是判斷定義在 data 中的 key 是否是保留鍵,大家可以在 core/util 目錄下的工具方法全解 中檢視對於 isReserved 函式的講解。isReserved 函式通過判斷一個字串的第一個字元是不是 $或 _ 來決定其是否是保留的,Vue 是不會代理那些鍵名以 $ 或 _ 開頭的欄位的,因為 Vue 自身的屬性和方法都是以 $ 或 _ 開頭的,所以這麼做是為了避免與 Vue 自身的屬性和方法相沖突。

如果 key 既不是以 $ 開頭,又不是以 _ 開頭,那麼將執行 proxy 函式,實現例項物件的代理訪問:

proxy(vm, `_data`, key)

其中關鍵點在於 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)
}

proxy 函式的原理是通過 Object.defineProperty 函式在例項物件 vm 上定義與 data 資料欄位同名的訪問器屬性,並且這些屬性代理的值是 vm._data 上對應屬性的值。舉個例子,比如 data 資料如下:

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

當我們訪問 ins.a 時實際訪問的是 ins._data.a。而 ins._data 才是真正的資料物件。

最後經過一系列的處理,initData 函式來到了最後一句程式碼:

// observe data
observe(data, true /* asRootData */)

呼叫 observe 函式將 data 資料物件轉換成響應式的,可以說這句程式碼才是響應系統的開始,不過在我們講解 observe 函式之前我們有必要總結一下 initData 函式所做的事情,通過前面分析 initData函式主要完成如下工作:

  • 根據 vm.$options.data 選項獲取真正想要的資料(注意:此時 vm.$options.data 是函式)
  • 校驗得到的資料是否是一個純物件
  • 檢查資料物件 data 上的鍵是否與 props 物件上的鍵衝突
  • 檢查 methods 物件上的鍵是否與 data 物件上的鍵衝突
  • 在 Vue 例項物件上新增代理訪問資料物件的同名屬性
  • 最後呼叫 observe 函式開啟響應式之路

#資料響應系統的基本思路

接下來我們將重點講解資料響應系統的實現,在具體到原始碼之前我們有必要了解一下資料響應系統實現的基本思路,這有助於我們更好的理解原始碼的目的,畢竟每一行程式碼都有它存在的意義。

在 Vue 中,我們可以使用 $watch 觀測一個欄位,當欄位的值發生變化的時候執行指定的觀察者,如下:

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

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

這樣當我們試圖修改 a 的值時:ins.a = 2,在控制檯將會列印 '修改了 a'。現在我們將這個問題抽象一下,假設我們有資料物件 data,如下:

const data = {
  a: 1
}

我們還有一個叫做 $watch 的函式:

function $watch () {...}

$watch 函式接收兩個引數,第一個引數是要觀測的欄位,第二個引數是當該欄位的值發生變化後要執行的函式,如下:

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

要實現這個功能,說複雜也複雜說簡單也簡單,複雜在於我們需要考慮的內容比較多,比如如何避免收集重複的依賴,如何深度觀測,如何處理陣列以及其他邊界條件等等。簡單在於如果不考慮那麼多邊界條件的話,要實現這樣一個功能還是很容易的,這一小節我們就從簡入手,致力於讓大家思路清晰,至於各種複雜情況的處理我們會在真正講解原始碼的部分一一為大家解答。

要實現上文的功能,我們面臨的第一個問題是,如何才能知道屬性被修改了(或被設定了)。這時候我們就要依賴 Object.defineProperty 函式,通過該函式為物件的每個屬性設定一對 getter/setter 從而得知屬性被讀取和被設定,如下:

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

這樣我們就實現了對屬性 a 的設定和獲取操作的攔截,有了它我們就可以大膽的思考一些事情,比如: 能不能在獲取屬性 a 的時候收集依賴,然後在設定屬性 a 的時候觸發之前收集的依賴呢? 嗯,這是一個好思路,不過既然要收集依賴,我們起碼需要一個”筐“,然後將所有收集到的依賴通通放到這個”筐”裡,當屬性被設定的時候將“筐”裡所有的依賴都拿出來執行就可以了,落實到程式碼如下:

// dep 陣列就是我們所謂的“筐”
const dep = []
Object.defineProperty(data, 'a', {
  set () {
    // 當屬性被設定的時候,將“筐”裡的依賴都執行一次
    dep.forEach(fn => fn())
  },
  get () {
    // 當屬性被獲取的時候,把依賴放到“筐”裡
    dep.push(fn)
  }
})

如上程式碼所示,我們定義了常量 dep,它是一個數組,這個陣列就是我們所說的“筐”,當獲取屬性 a的值時將觸發 get 函式,在 get 函式中,我們將收集到的依賴放入“筐”內,當設定屬性 a 的值時將觸發 set 函式,在 set 函式內我們將“筐”裡的依賴全部拿出來執行。

但是新的問題出現了,上面的程式碼中我們假設 fn 函式就是我們需要收集的依賴(觀察者),但 fn 從何而來呢? 也就是說如何在獲取屬性 a 的值時收集依賴呢? 為了解決這個問題我們需要思考一下我們現在都掌握哪些條件,這個時候我們就需要在 $watch 函式中做文章了,我們知道 $watch 函式接收兩個引數,第一個引數是一個字串,即資料欄位名,比如 'a',第二個引數是依賴該欄位的函式:

$watch('a', () => {
  console.log('設定了 a')
})

重點在於 $watch 函式是知道當前正在觀測的是哪一個欄位的,所以一個思路是我們在 $watch 函式中讀取該欄位的值,從而觸發欄位的 get 函式,同時將依賴收集,如下程式碼:

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

上面的程式碼中,首先我們定義了全域性變數 Target,然後在 $watch 中將 Target 的值設定為 fn 也就是依賴,接著讀取欄位的值 data[exp] 從而觸發被設定的屬性的 get 函式,在 get 函式中,由於此時 Target 變數就是我們要收集的依賴,所以將 Target 新增到 dep 陣列。現在我們新增如下測試程式碼:

$watch('a', () => {
  console.log('第一個依賴')
})
$watch('a', () => {
  console.log('第二個依賴')
})

此時當你嘗試設定 data.a = 3 時,在控制檯將分別列印字串 '第一個依賴' 和 '第二個依賴'。我們僅僅用十幾行程式碼就實現了這樣一個最基本的功能,但其實現在的實現存在很多缺陷,比如目前的程式碼僅僅能夠實現對欄位 a 的觀測,如果新增一個欄位 b 呢?所以最起碼我們應該使用一個迴圈將定義訪問器屬性的程式碼包裹起來,如下:

const data = {
  a: 1,
  b: 1
}

for (const key in data) {
  const dep = []
  Object.defineProperty(data, key, {
    set () {
      dep.forEach(fn => fn())
    },
    get () {
      dep.push(Target)
    }
  })
}

這樣我們就可以使用 $watch 函式觀測任意一個 data 物件下的欄位了,但是細心的同學可能早已發現上面程式碼的坑,即:

console.log(data.a) // undefined

直接在控制檯列印 data.a 輸出的值為 undefined,這是因為 get 函式沒有任何返回值,所以獲取任何屬性的值都將是 undefined,其實這個問題很好解決,如下:

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

只需要在使用 Object.defineProperty 函式定義訪問器屬性之前快取一下原來的值即 val,然後在 get 函式中將 val 返回即可,除此之外還要記得在 set 函式中使用新值(newVal)重寫舊值(val)。

但這樣就完美了嗎?當然沒有,這距離完美可以說還相差十萬八千里,比如當資料 data 是巢狀的物件時,我們的程式只能檢測到第一層物件的屬性,如果資料物件如下:

const data = {
  a: {
    b: 1
  }
}

對於以上物件結構,我們的程式只能把 data.a 欄位轉換成響應式屬性,而 data.a.b 依然不是響應式屬性,但是這個問題還是比較容易解決的,只需要遞迴定義即可:

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)

如上程式碼我們將定義訪問器屬性的邏輯放到了函式 walk 中,並增加了一段判斷邏輯如果某個屬性的值仍然是物件,則遞迴呼叫 walk 函式。這樣我們就實現了深度定義訪問器屬性。

但是雖然經過上面的改造 data.a.b 已經是訪問器屬性了,但是如下程式碼依然不能正確執行:

$watch('a.b', () => {
  console.log('修改了欄位 a.b')
})

來看看目前 $watch 函式的程式碼:

function $watch (exp, fn) {
  Target = fn
  // 讀取欄位值,觸發 get 函式
  data[exp]
}

讀取欄位值的時候我們直接使用 data[exp],如果按照 $watch('a.b', fn) 這樣呼叫 $watch 函式,那麼 data[exp] 等價於 data['a.b'],這顯然是不正確的,正確的讀取欄位值的方式應該是 data['a']['b']。所以我們需要稍微做一點小小的改造:

const data = {
  a: {
    b: 1
  }
}

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 函式做了一些改造,首先檢查要讀取的欄位是否包含 .,如果包含 . 說明讀取巢狀物件的欄位,這時候我們使用字串的 split('.') 函式將字串轉為陣列,所以如果訪問的路徑是 a.b那麼轉換後的陣列就是 ['a', 'b'],然後使用一個迴圈從而讀取到巢狀物件的屬性值,不過需要注意的是讀取到巢狀物件的屬性值之後應該立即 return,不需要再執行後面的程式碼。

下面我們再進一步,我們思考一下 $watch 函式的原理的是什麼?其實 $watch 函式所做的事情就是想方設法的訪問到你要觀測的欄位,從而觸發該欄位的 get 函式,進而收集依賴(觀察者)。現在我們傳遞給 $watch 函式的第一個引數是一個字串,代表要訪問資料的哪一個欄位屬性,那麼除了字串之外可不可以是一個函式呢?假設我們有一個函式叫做 render,如下

const data = {
  name: '霍春陽',
  age: 24
}

function render () {
  return document.write(`姓名:${data.name}; 年齡:${data.age}`)
}

可以看到 render 函式依賴了資料物件 data,那麼 render 函式的執行是不是會觸發 data.name 和 data.age 這兩個欄位的 get 攔截器呢?答案是肯定的,當然會!所以我們可以將 render 函式作為 $watch 函式的第一個引數:

$watch(render, render)

為了能夠保證 $watch 函式正常執行,我們需要對 $watch 函式做如下修改:

function $watch (exp, fn) {
  Target = fn
  let pathArr,
      obj = data
  // 如果 exp 是函式,直接執行該函式
  if (typeof exp === 'function') {
    exp()
    return
  }
  if (/\./.test(exp)) {
    pathArr = exp.split('.')
    pathArr.forEach(p => {
      obj = obj[p]
    })
    return
  }
  data[exp]
}

在上面的程式碼中,我們檢測了 exp 的型別,如果是函式則直接執行之,由於 render 函式的執行會觸發資料欄位的 get 攔截器,所以依賴會被收集。同時我們要注意傳遞給 $watch 函式的第二個引數:

$watch(render, render)

第二個引數依然是 render 函式,也就是說當依賴發生變化時,會重新執行 render 函式,這樣我們就實現了資料變化,並將變化自動應用到 DOM。其實這大概就是 Vue 的原理,但我們做的還遠遠不夠,比如上面這句程式碼,第一個引數中 render 函式的執行使得我們能夠收集依賴,當依賴變化時會重新執行第二個引數中的 render 函式,但不要忘了這又會觸發一次資料欄位的 get 攔截器,所以此時已經收集了兩遍重複的依賴,那麼我們是不是要想辦法避免收集冗餘的依賴呢?除此之外我們也沒有對陣列做處理,我們將這些問題留到後面,看看在 Vue 中它是如何處理的。

現在我們這個不嚴謹的實現暫時就到這裡,意圖在於讓大家明白資料響應系統的整體思路,為接下來真正進入 Vue 原始碼做必要的鋪墊。

#observe 工廠函式

瞭解了資料響應系統的基本思路,我們是時候回過頭來深入研究 Vue 的資料響應系統是如何實現的了,我們回到 initData 函式的最後一句程式碼:

// observe data
observe(data, true /* asRootData */)

呼叫了 observe 函式觀測資料,observe 函式來自於 core/observer/index.js 檔案,開啟該檔案找到 observe 函式:

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 函式的全部程式碼, observe 函式接收兩個引數,第一個引數是要觀測的資料,第二個引數是一個布林值,代表將要被觀測的資料是否是根級資料。在 observe 函式的一開始是一段 if 判斷語句:

if (!isObject(value) || value instanceof VNode) {
  return
}

用來判斷如果要觀測的資料不是一個物件或者是 VNode 例項,則直接 return 。接著定義變數 ob,該變數用來儲存 Observer 例項,可以發現 observe 函式的返回值就是 ob。緊接著又是一個 if...else 分支:

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 分支的判斷條件,首先使用 hasOwn 函式檢測資料物件 value 自身是否含有 __ob__ 屬性,並且 __ob__ 屬性應該是 Observer 的例項。如果為真則直接將資料物件自身的 __ob__ 屬性的值作為 ob 的值:ob = value.__ob__。那麼 __ob__ 是什麼呢?其實當一個數據物件被觀測之後將會在該物件上定義 __ob__ 屬性,所以 if 分支的作用是用來避免重複觀測一個數據物件。

接著我們再來看看 else...if 分支,如果資料物件上沒有定義 __ob__ 屬性,那麼說明該物件沒有被觀測過,進而會判斷 else...if 分支,如果 else...if 分支的條件為真,那麼會執行 ob = new Observer(value) 對資料物件進行觀測。也就是說只有當資料物件滿足所有 else...if 分支的條件才會被觀測,我們看看需要滿足什麼條件:

  • 第一個條件是 shouldObserve 必須為 true

shouldObserve 變數也定義在 core/observer/index.js 檔案內,如下:

/**
 * In some cases we may want to disable observation inside a component's
 * update computation.
 */
export let shouldObserve: boolean = true

export function toggleObserving (value: boolean) {
  shouldObserve = value
}

該變數的初始值為 true,在 shouldObserve 變數的下面定義了 toggleObserving 函式,該函式接收一個布林值引數,用來切換 shouldObserve 變數的真假值,我們可以把 shouldObserve 想象成一個開關,為 true 時說明打開了開關,此時可以對資料進行觀測,為 false 時可以理解為關閉了開關,此時資料物件將不會被觀測。為什麼這麼設計呢?原因是有一些場景下確實需要這個開關從而達到一些目的,後面我們遇到的時候再仔細來說。

  • 第二個條件是 !isServerRendering() 必須為真

isServerRendering() 函式的返回值是一個布林值,用來判斷是否是服務端渲染。也就是說只有當不是服務端渲染的時候才會觀測資料,關於這一點 Vue 的服務端渲染文件中有相關介紹,我們不做過多說明。

  • 第三個條件是 (Array.isArray(value) || isPlainObject(value)) 必須為真

這個條件很好理解,只有當資料物件是陣列或純物件的時候,才有必要對其進行觀測。

  • 第四個條件是 Object.isExtensible(value) 必須為真

也就是說要被觀測的資料物件必須是可擴充套件的。一個普通的物件預設就是可擴充套件的,以下三個方法都可以使得一個物件變得不可擴充套件:Object.preventExtensions()Object.freeze() 以及 Object.seal()

  • 第五個條件是 !value._isVue 必須為真

我們知道 Vue 例項物件擁有 _isVue 屬性,所以這個條件用來避免 Vue 例項物件被觀測。

當一個物件滿足了以上五個條件時,就會執行 else...if 語句塊的程式碼,即建立一個 Observer 例項:

ob = new Observer(value)

#Observer 建構函式

其實真正將資料物件轉換成響應式資料的是 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 類的例項物件將擁有三個例項屬性,分別是 valuedep 和 vmCount 以及兩個例項方法 walk 和 observeArrayObserver 類的建構函式接收一個引數,即資料物件。下面我們就從 constructor 方法開始,研究例項化一個 Observer 類時都做了哪些事情。

#資料物件的 __ob__ 屬性

如下是 constructor 方法的全部程式碼:

constructor (value: any) {
  this.value = value
  this.dep = new Dep()
  this.vmCount = 0
  def(value, '__ob__', this)
  if (Array.isArray(value)) {
    const augment = hasProto
      ? protoAugment
      : copyAugment
    augment(value, arrayMethods, arrayKeys)
    this.observeArray(value)
  } else {
    this.walk(value)
  }
}

constructor 方法的引數就是在例項化 Observer 例項時傳遞的引數,即資料物件本身,可以發現,例項物件的 value 屬性引用了資料物件:

this.value = value

例項物件的 dep 屬性,儲存了一個新建立的 Dep 例項物件:

this.dep = new Dep()

那麼這裡的 Dep 是什麼呢?就像我們在瞭解資料響應系統基本思路中所講到的,它就是一個收集依賴的“筐”。但這個“筐”並不屬於某一個欄位,後面我們會發現,這個框是屬於某一個物件或陣列的。

例項物件的 vmCount 屬性被設定為 0this.vmCount = 0

初始化完成三個例項屬性之後,使用 def 函式,為資料物件定義了一個 __ob__ 屬性,這個屬性的值就是當前 Observer 例項物件。其中 def 函式其實就是 Object.defineProperty 函式的簡單封裝,之所以這裡使用 def 函式定義 __ob__ 屬性是因為這樣可以定義不可列舉的屬性,這樣後面遍歷資料物件的時候就能夠防止遍歷到 __ob__ 屬性。

假設我們的資料物件如下:

const data = {
  a: 1
}

那麼經過 def 函式處理之後,data 物件應該變成如下這個樣子:

const data = {
  a: 1,
  // __ob__ 是不可列舉的屬性
  __ob__: {
    value: data, // value 屬性指向 data 資料物件本身,這是一個迴圈引用
    dep: dep例項物件, // new Dep()
    vmCount: 0
  }
}

#響應式資料之純物件的處理

接著進入一個 if...else 判斷分支:

if (Array.isArray(value)) {
  const augment = hasProto
    ? protoAugment
    : copyAugment
  augment(value, arrayMethods, arrayKeys)
  this.observeArray(value)
} else {
  this.walk(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])
  }
}

walk 方法很簡單,首先使用 Object.keys(obj) 獲取物件屬性所有可列舉的屬性,然後使用 for 迴圈遍歷這些屬性,同時為每個屬性呼叫了 defineReactive 函式。

#defineReactive 函式

那我們就看一看 defineReactive 函式都做了什麼,該函式也定義在 core/observer/index.js 檔案,內容如下:

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 () {
      // 省略...
    },
    set: function reactiveSetter (newVal) {
      // 省略...
    }
  })
}

defineReactive 函式的核心就是將資料物件的資料屬性轉換為訪問器屬性,即為資料物件的屬性設定一對 getter/setter,但其中做了很多處理邊界條件的工作。defineReactive 接收五個引數,但是在 walk 方法中呼叫 defineReactive 函式時只傳遞了前兩個引數,即資料物件和屬性的鍵名。我們看一下 defineReactive 的函式體,首先定義了 dep 常量,它是一個 Dep 例項物件:

const dep = new Dep()

我們在講解 Observer 的 constructor 方法時看到過,在 constructor 方法中為資料物件定義了一個 __ob__ 屬性,該屬性是一個 Observer 例項物件,且該物件包含一個 Dep 例項物件:

const data = {
  a: 1,
  __ob__: {
    value: data,
    dep: dep例項物件, // new Dep() , 包含 Dep 例項物件
    vmCount: 0
  }
}

當時我們說過 __ob__.dep 這個 Dep 例項物件的作用與我們在講解資料響應系統基本思路一節中所說的“筐”的作用不同。至於他的作用是什麼我們後面會講到。其實與我們前面所說過的“筐”的作用相同的 Dep 例項物件是在 defineReactive 函式一開始定義的 dep 常量,即:

const dep = new Dep()

這個 dep 常量所引用的 Dep 例項物件才與我們前面講過的“筐”的作用相同。細心的同學可能已經注意到了 dep 在訪問器屬性的 getter/setter 中被閉包引用,如下:

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  // 省略...

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        // 這裡閉包引用了上面的 dep 常量
        dep.depend()
        // 省略...
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // 省略...

      // 這裡閉包引用了上面的 dep 常量
      dep.notify()
    }
  })
}

如上面的程式碼中註釋所寫的那樣,在訪問器屬性的 getter/setter 中,通過閉包引用了前面定義的“筐”,即 dep 常量。這裡大家要明確一件事情,即每一個數據欄位都通過閉包引用著屬於自己的 dep 常量。因為在 walk 函式中通過迴圈遍歷了所有資料物件的屬性,並呼叫 defineReactive 函式,所以每次呼叫 defineReactive 定義訪問器屬性時,該屬性的 setter/getter 都閉包引用了一個屬於自己的“筐”。假設我們有如下資料欄位:

const data = {
  a: 1,
  b: 2
}

那麼欄位 data.a 和 data.b 都將通過閉包引用屬於自己的 Dep 例項物件,如下圖所示:

每個欄位的 Dep 物件都被用來收集那些屬於對應欄位的依賴。

在定義 dep 常量之後,是這樣一段程式碼:

const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
  return
}

首先通過 Object.getOwnPropertyDescriptor 函式獲取該欄位可能已有的屬性描述物件,並將該物件儲存在 property 常量中,接著是一個 if 語句塊,判斷該欄位是否是可配置的,如果不可配置(property.configurable === false),那麼直接 return ,即不會繼續執行 defineReactive 函式。這麼做也是合理的,因為一個不可配置的屬性是不能使用也沒必要使用 Object.defineProperty 改變其屬性定義的。

再往下是這樣一段程式碼:

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

這段程式碼的前兩句定義了 getter 和 setter 常量,分別儲存了來自 property 物件的 get 和 set函式,我們知道 property 物件是屬性的描述物件,一個物件的屬性很可能已經是一個訪問器屬性了,所以該屬性很可能已經存在 get 或 set 方法。由於接下來會使用 Object.defineProperty 函式重新定義屬性的 setter/getter,這會導致屬性原有的 set 和 get 方法被覆蓋,所以要將屬性原有的 setter/getter 快取,並在重新定義的 set 和 get 方法中呼叫快取的函式,從而做到不影響屬性的原有讀寫操作。

上面這段程式碼中比較難理解的是 if 條件語句:

(!getter || setter) && arguments.length === 2

其中 arguments.length === 2 這個條件好理解,當只傳遞兩個引數時,說明沒有傳遞第三個引數 val,那麼此時需要根據 key 主動去物件上獲取相應的值,即執行 if 語句塊內的程式碼:val = obj[key]。那麼 (!getter || setter) 這個條件的意思是什麼呢?要理解這個條件我們需要思考一些實際應用的場景,或者說邊界條件,但是現在還不適合給大家講解,我們等到講解完整個 defineReactive函式之後,再回頭來說。

在 if 語句塊的下面,是這句程式碼:

let childOb = !shallow && observe(val)

定義了 childOb 變數,我們知道,在 if 語句塊裡面,獲取到了物件屬性的值 val,但是 val 本身有可能也是一個物件,那麼此時應該繼續呼叫 observe(val) 函式觀測該物件從而深度觀測資料物件。但前提是 defineReactive 函式的最後一個引數 shallow 應該是假,即 !shallow 為真時才會繼續呼叫 observe 函式深度觀測,由於在 walk 函式中呼叫 defineReactive 函式時沒有傳遞 shallow 引數,所以該引數是 undefined,那麼也就是說預設就是深度觀測。其實非深度觀測的場景我們早就遇到過了,即 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 未必有值,因為必須在滿足條件 (!getter || setter) && arguments.length === 2 時,才會觸發取值的動作:val = obj[key],所以一旦不滿足條件即使屬性是有值的但是由於沒有觸發取值的動作,所以 val 依然是 undefined。這就會導致深度觀測無效。

#被觀測後的資料物件的樣子

現在我們需要明確一件事情,那就是一個數據物件經過了 observe 函式處理之後變成了什麼樣子,假設我們有如下資料物件:

const data = {
  a: {
    b: 1
  }
}

observe(data)

資料物件 data 擁有一個叫做 a 的屬性,且屬性 a 的值是另外一個物件,該物件擁有一個叫做 b的屬性。那麼經過 observe 處理之後, data 和 data.a 這兩個物件都被定義了 __ob__ 屬性,並且訪問器屬性 a 和 b 的 setter/getter 都通過閉包引用著屬於自己的 Dep 例項物件和 childOb 物件:

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,因為屬性 b 是基本型別值,並不是物件也不是陣列。

#在 get 函式中如何收集依賴

我們回過頭來繼續檢視 defineReactive 函式的程式碼,接下來是 defineReactive 函式的關鍵程式碼,即使用 Object.defineProperty 函式定義訪問器屬性:

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
    // 省略...
  },
  set: function reactiveSetter (newVal) {
    // 省略...
})

當執行完以上程式碼實際上 defineReactive 函式就執行完畢了,對於訪問器屬性的 get 和 set 函式是不會執行的,因為此時沒有觸發屬性的讀取和設定操作。不過這不妨礙我們研究一下在 get 和 set 函式中都做了哪些事情,這裡面就包含了我們在前面埋下伏筆的 if 條件語句的答案。我們先從 get 函式開始,看一看當屬性被讀取的時候都做了哪些事情,get 函式如下:

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
}

既然是 getter,那麼當然要能夠正確的返回屬性的值才能,我們知道依賴的收集時機就是屬性被讀取的時候,所以 get 函式做了兩件事:正確的返回屬性值以及收集依賴,我們具體看一下程式碼,get 函式的第一句程式碼如下:

const value = getter ? getter.call(obj) : val

首先判斷是否存在 getter,我們知道 getter 常量中儲存的屬性原型的 get 函式,如果 getter 存在那麼直接呼叫該函式,並以該函式的返回值作為屬性的值,保證屬性的原有讀取操作正常運作。如果 getter 不存在則使用 val 作為屬性的值。可以發現 get 函式的最後一句將 value 常量返回,這樣 get 函式需要做的第一件事就完成了,即正確的返回屬性值。

除了正確的返回屬性值,還要收集依賴,而處於 get 函式第一行和最後一行程式碼中間的所有程式碼都是用來完成收集依賴這件事兒的,下面我們就看一下它是如何收集依賴的,由於我們還沒有講解過 Dep 這個類,所以現在大家可以簡單的認為 dep.depend() 這句程式碼的執行就意味著依賴被收集了。接下來我們仔細看一下程式碼:

if (Dep.target) {
  dep.depend()
  if (childOb) {
    childOb.dep.depend()
    if (Array.isArray(value)) {
      dependArray(value)
    }
  }
}

首先判斷 Dep.target 是否存在,那麼 Dep.target 是什麼呢?其實 Dep.target 與我們在資料響應系統基本思路一節中所講的 Target 作用相同,所以 Dep.target 中儲存的值就是要被收集的依賴(觀察者)。所以如果 Dep.target 存在的話說明有依賴需要被收集,這個時候才需要執行 if 語句塊內的程式碼,如果 Dep.target 不存在就意味著沒有需要被收集的依賴,所以當然就不需要執行 if 語句塊內的程式碼了。

在 if 語句塊內第一句執行的程式碼就是:dep.depend(),執行 dep 物件的 depend 方法將依賴收集到 dep 這個“筐”中,這裡的 dep 物件就是屬性的 getter/setter 通過閉包引用的“筐”。

接著又判斷了 childOb 是否存在,如果存在那麼就執行 childOb.dep.depend(),這段程式碼是什麼意思呢?要想搞清楚這段程式碼的作用,你需要知道 childOb 是什麼,前面我們分析過,假設有如下資料物件:

const data = {
  a: {
    b: 1
  }
}

該資料物件經過觀測處理之後,將被新增 __ob__ 屬性,如下:

const data = {
  a: {
    b: 1,
    __ob__: {value, dep, vmCount}
  },
  __ob__: {value, dep, vmCount}
}

對於屬性 a 來講,訪問器屬性 a 的 setter/getter 通過閉包引用了一個 Dep 例項物件,即屬性 a 用來收集依賴的“筐”。除此之外訪問器屬性 a 的 setter/getter 還閉包引用著 childOb,且 childOb === data.a.__ob__ 所以 childOb.dep === data.a.__ob__.dep。也就是說 childOb.dep.depend() 這句話的執行說明除了要將依賴收集到屬性 a 自己的“筐”裡之外,還要將同樣的依賴收集到 data.a.__ob__.dep 這裡”筐“裡,為什麼要將同樣的依賴分別收集到這兩個不同的”筐“裡呢?其實答案就在於這兩個”筐“裡收集的依賴的觸發時機是不同的,即作用不同,兩個”筐“如下:

  • 第一個”筐“是 dep
  • 第二個”筐“是 childOb.dep

第一個”筐“裡收集的依賴的觸發時機是當屬性值被修改時觸發,即在 set 函式中觸發:dep.notify()。而第二個”筐“裡收集的依賴的觸發時機是在使用 $set 或 Vue.set 給資料物件新增新屬性時觸發,我們知道由於 js 語言的限制,在沒有 Proxy 之前 Vue 沒辦法攔截到給物件新增屬性的操作。所以 Vue 才提供了 $set 和 Vue.set 等方法讓我們有能力給物件新增新屬性的同時觸發依賴,那麼觸發依賴是怎麼做到的呢?就是通過資料物件的 __ob__ 屬性做到的。因為 __ob__.dep 這個”筐“裡收集了與 dep 這個”筐“同樣的依賴。假設 Vue.set 函式程式碼如下:

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

如上程式碼所示,當我們使用上面的程式碼給 data.a 物件新增新的屬性:

Vue.set(data.a, 'c', 1)

上面的程式碼之所以能夠觸發依賴,就是因為 Vue.set 函式中觸發了收集在 data.a.__ob__.dep 這個”筐“中的依賴:

Vue.set = function (obj, key, val) {
  defineReactive(obj, key, val)
  obj.__ob__.dep.notify() // 相當於 data.a.__ob__.dep.notify()
}

Vue.set(data.a, 'c', 1)

所以 __ob__ 屬性以及 __ob__.dep 的主要作用是為了新增、刪除屬性時有能力觸發依賴,而這就是 Vue.set 或 Vue.delete 的原理。

在 childOb.dep.depend() 這句話的下面還有一個 if 條件語句,如下:

            
           

相關推薦

vue原始碼揭開資料響應系統面紗

本文是學習vue原始碼,之所以轉載過來是方便自己隨時檢視,在這裡要感謝HcySunYang大神,提供的開源vue原始碼解析,寫的非常非常好,簡單易懂,比自己看要容易多了,他的文章連結地址是http://hcysun.me/vue-design/art/ 相信很多同學都對 Vue&nbs

微信小程式學習筆記本地資料快取

上一篇:微信小程式學習筆記(七) 【將資料儲存在本地快取】wx.setStorage 【讀取本地快取】wx.getStorage 以手機號+密碼登入為例,把登入成功返回的token值儲存在本地快取中,然後讀取快取中的token: login.php: <?php

lua原始碼分析揭開 table 的神祕面紗

       友情提醒:閱讀本文前請先確保自己對雜湊表有足夠深入的理解,雜湊表的詳解可以參見以下這篇文章:Redis底層詳解(一) 雜湊表和字典。        lua 底層 table (也叫陣列, 物件

淺析Vue原始碼——render到VNode的生成

前面我們用三片文章介紹了compile解析template,完成了 template --> AST --> render function 的過程。這篇我們主要介紹一下VNode的生成過程,在此之前,我們先來簡單的瞭解一下什麼是VNode?

白話Spring原始碼:Aware介面

我們知道spring框架中所有bean都是在工廠裡建立的,bean對自己是“無知覺”的,不知道自己叫什麼名字(bean的id或者name),從哪裡來(哪個工廠建立)。 一、為什麼需要Aware 大家看過黑客帝國電影吧,黑客帝國中機械工廠裡面“養殖”的人類,他們雖然能完成一定的功能,但是根本不

vue原始碼Vue 的初始化之開篇

本文是學習vue原始碼,之所以轉載過來是方便自己隨時檢視,在這裡要感謝HcySunYang大神,提供的開源vue原始碼解析,寫的非常非常好,簡單易懂,比自己看要容易多了,他的文章連結地址是http://hcysun.me/vue-design/art/ 用於初始化的最終選項 $options

vue原始碼Vue 選項的合併

本文是學習vue原始碼,之所以轉載過來是方便自己隨時檢視,在這裡要感謝HcySunYang大神,提供的開源vue原始碼解析,寫的非常非常好,簡單易懂,比自己看要容易多了,他的文章連結地址是http://hcysun.me/vue-design/art/ 上一章節我們瞭解了 Vue&nb

vue原始碼Vue 選項的規範化

本文是學習vue原始碼,之所以轉載過來是方便自己隨時檢視,在這裡要感謝HcySunYang大神,提供的開源vue原始碼解析,寫的非常非常好,簡單易懂,比自己看要容易多了,他的文章連結地址是http://hcysun.me/vue-design/art/ 注意:本節討論依舊沿用前文的例子 #弄

Thinking in BigData資料Hadoop核心架構HDFS+MapReduce+Hbase+Hive內部機理詳解

      純乾貨:Hadoop核心架構HDFS+MapReduce+Hbase+Hive內部機理詳解。       通過這一階段的調研總結,從內部機理的角度詳細分析,HDFS、MapReduce、Hbase、Hive是如何執行,以及基於Hadoop資料倉庫的構建和分散式資

Linux多執行緒基礎學習私有資料

/*============================================================================ // Name : thread_privateData.cpp // Author : Ryan // Version

vue.js8雙向資料繫結

雙向資料繫結:一定要有一個輸入的地方,一定要有一個輸出的地方//index.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <tit

MYSQL資料庫- 修改資料表新增約束

本章目錄 新增約束目錄 一、新增id列,無主鍵無約束,準備工作 二、給city2表新增主鍵約束(任何一張表只能有一個主鍵) 三、新增唯一約束 四、新增外來鍵約束 五、新增和/刪除預設約束 刪除約束目錄 一、刪除主鍵約束

Shiro介紹資料許可權的研究@RequiresData

Shiro幫我們實現的大多為操作許可權,那麼今天我想分享一個數據許可權的方案,主要採用的仍是註解+切面攔截。 思路大概是這樣的: 在controller的方法引數,約定包含一個Map型別的parameters 通過註解宣告一下當前使用者的某個成員屬性值

Android產品研發-->App資料統計

上一篇文章中我們介紹了Android社群中比較火的熱修復功能,並介紹了目前的幾個比較流行的熱修復框架,以及各自的優缺點,同時也介紹了一下自身專案中對熱修復功能的實踐。目前主流的熱修復原理上其實分為兩種,一種是通過利用dex的載入順序實現熱修復功能,一種是通過

Docker筆記資料管理

前面(哪個前面我也忘了)有說過,如果我們需要對資料進行持久化儲存,不應使其儲存在容器中,因為容器中的資料會隨著容器的刪除而丟失,而因通過將資料儲存於宿主機檔案系統的形式來持久化。在Docker容器中管理資料主要有資料卷、宿主機目錄掛載兩種方式。 1. 資料卷的方式 資料卷是一個特殊的檔案目錄(或檔案),具

Hive 系列—— Hive 資料查詢詳解

一、資料準備 為了演示查詢操作,這裡需要預先建立三張表,並載入測試資料。 資料檔案 emp.txt 和 dept.txt 可以從本倉庫的resources 目錄下載。 1.1 員工表 -- 建表語句 CREATE TABLE emp( empno INT, -- 員工表編號

SpringApplication物件是如何構建的? SpringBoot原始碼

**注:該原始碼分析對應SpringBoot版本為2.1.0.RELEASE** 本篇接 [SpringBoot的啟動流程是怎樣的?SpringBoot原始碼(七)](https://juejin.im/post/5e771657f265da574c569be1) # 1 溫故而知新 溫故而知新,我們來

Redis系列資料結構List雙向連結串列中阻塞版本之BLPOP、BRPOP和LINDEX、LINSERT、LRANGE命令詳解

1.BRPOP、BLPOP BLPOP: BLPOP 是阻塞式列表的彈出原語。 它是命令 LPOP 的阻塞版本,這是因為當給定列表內沒有任何元素可供彈出的時候, 連線將被 BLPOP 命令阻塞。 當給定多個 key 引數時,按引數 key 的先後順序依次檢查

linux基礎篇:基於Redhat7系統中的DHCP服務的設定

什麼是DHCP? DHCP,動態主機配置協議,前身是BOOTP協議,是一個區域網的網路協議,使用UDP協議工作,常用的2個埠:67(DHCP server),68(DHCP client)。DHCP通常被用於區域網環境,主要作用是集中的管理、分配IP地址,使client動態的獲得IP地址

Vue原始碼學習4——資料響應系統

Vue原始碼學習(4)——資料響應系統:通過initData() 看資料響應系統     下面,根據理解我們寫一個簡略的原始碼:參考 治癒watcher在:vm.$mount(vm.$options.el)    Function de