天天看點

Vue源碼 深入響應式原理 (六)Props (v2.6.11)Vue源碼 深入響應式原理 (六)PropsProps (v2.6.11)Vue源碼學習目錄

Vue源碼 深入響應式原理 (六)Props

  • Vue源碼 深入響應式原理 (六)Props
  • Props (v2.6.11)
    • 單步調試代碼
    • 規範化
    • 初始化
      • 校驗
      • 響應式
      • 代理
    • Props 更新
      • 子元件 props 更新
      • 子元件重新渲染
    • toggleObserving
    • 總結
  • Vue源碼學習目錄

Vue源碼 深入響應式原理 (六)Props

學習内容和文章内容來自 黃轶老師

黃轶老師的慕課網視訊教程位址:

《Vue.js2.0 源碼揭秘》、

黃轶老師拉鈎教育教程位址:

《Vue.js 3.0 核心源碼解析》

這裡分析的源碼是Runtime + Compiler 的 Vue.js

調試代碼在:node_modules\vue\dist\vue.esm.js 裡添加

vue版本:Vue.js 2.5.17-beta

你越是認真生活,你的生活就會越美好

——弗蘭克·勞埃德·萊特

《人生果實》經典語錄

點選回到 Vue源碼學習完整目錄

Props (v2.6.11)

Props

作為元件的核心特性之一,也是我們平時開發 Vue 項目中接觸最多的特性之一,它可以讓元件的功能變得豐富,也是

父子元件通訊的一個管道

。那麼它的實作原理是怎樣的,我們來一探究竟。

單步調試代碼

// src/main.js
import Vue from 'vue'
// import App from './App.vue'

const CompA = {
  template: 
    `
    <div>
      <p>Name: {{name}}</p>
      <p>NickName: {{nickName}}</p>
    </div>
    `,
    props: {
      name: String,
      nickName: [Boolean, String]
    }
}

const CompB = {
  template: 
    `
    <div>
      <p>Age: {{age}}</p>
      <p>Sex: {{sex}}</p>
    </div>
    `,
    props: {
      age: Number,
      sex: {
        type: String,
        defaule: 'feamle',
        validator(value) {
          return value === 'mele' || value === 'female'
        }
      }
    }
}

new Vue({
  el: '#app',
  components: {CompA, CompB},
  template: `
    <div>
      <comp-a name="Jackson" nick-name></comp-a>
      <comp-b :age="18" sex="male"></comp-b>
    </div>
  `
})
           

規範化

在初始化

props

之前,首先會對

props

做一次

normalize

,它發生在

mergeOptions

的時候,在

src/core/util/options.js

中:

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  // ...
  normalizeProps(child, vm)
  // ...
}

function normalizeProps (options: Object, vm: ?Component) {
  const props = options.props
  if (!props) return
  const res = {}
  let i, val, name
  if (Array.isArray(props)) {
    i = props.length
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
        name = camelize(val)
        res[name] = { type: null }
      } else if (process.env.NODE_ENV !== 'production') {
        warn('props must be strings when using array syntax.')
      }
    }
  } else if (isPlainObject(props)) {
    for (const key in props) {
      val = props[key]
      name = camelize(key)
      res[name] = isPlainObject(val)
        ? val
        : { type: val }
    }
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `Invalid value for option "props": expected an Array or an Object, ` +
      `but got ${toRawType(props)}.`,
      vm
    )
  }
  options.props = res
}
           
Vue源碼 深入響應式原理 (六)Props (v2.6.11)Vue源碼 深入響應式原理 (六)PropsProps (v2.6.11)Vue源碼學習目錄

合并配置

我們在元件化章節講過,它主要就是處理我們定義元件的對象

option

,然後挂載到元件的執行個體

this.$options

中。

我們接下來重點看

normalizeProps

的實作,其實這個函數的主要目的就是把我們編寫的

props

轉成

對象格式

,因為實際上

props

除了

對象格式

,還允許寫成

數組格式

props

是一個數組,每一個數組元素

prop

隻能是一個

string

,表示

prop

key

,轉成駝峰格式,

prop

的類型為空。

Vue源碼 深入響應式原理 (六)Props (v2.6.11)Vue源碼 深入響應式原理 (六)PropsProps (v2.6.11)Vue源碼學習目錄

props

是一個對象,對于

props

中每個

prop

key

,我們會

轉駝峰格式

,而它的

value

,如果不是一個對象,我們就把它

規範成一個對象

Vue源碼 深入響應式原理 (六)Props (v2.6.11)Vue源碼 深入響應式原理 (六)PropsProps (v2.6.11)Vue源碼學習目錄
Vue源碼 深入響應式原理 (六)Props (v2.6.11)Vue源碼 深入響應式原理 (六)PropsProps (v2.6.11)Vue源碼學習目錄

如果

props

既不是數組也不是對象,就抛出一個警告。

舉個例子:

export default {
  props: ['name', 'nick-name']
}
           

經過

normalizeProps

後,會被規範成:

options.props = {
  name: { type: null },
  nickName: { type: null }
}
           
export default {
  props: {
    name: String,
    nickName: {
      type: Boolean
    }
  }
}
           

經過

normalizeProps

後,會被規範成:

options.props = {
  name: { type: String },
  nickName: { type: Boolean }
}
           

由于對象形式的

props

可以指定每個

prop

的類型和定義其它的一些屬性,

推薦用對象形式定義 props

初始化

Props

的初始化主要發生在

new Vue

中的

initState

階段,在

src/core/instance/state.js

中:

export function initState (vm: Component) {
  // ....
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  // ...
}


function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  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)
  }
  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 {
      defineReactive(props, key, value)
    }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}
           

initProps

主要做 3 件事情:

  • 校驗
  • 響應式
  • 代理。

校驗

校驗的邏輯很簡單,周遊

propsOptions

,執行

validateProp(key, propsOptions, propsData, vm)

方法。

這裡的

propsOptions

就是我們定義的

props

規範後

生成的

options.props

對象,

propsData

是從父元件傳遞的

prop

資料。

所謂校驗的目的就是檢查一下我們傳遞的資料是否滿足

prop

的定義規範。再來看一下

validateProp

方法,它定義在

src/core/util/props.js

中:

Vue源碼 深入響應式原理 (六)Props (v2.6.11)Vue源碼 深入響應式原理 (六)PropsProps (v2.6.11)Vue源碼學習目錄
export function validateProp (
  key: string,
  propOptions: Object,
  propsData: Object,
  vm?: Component
): any {
  const prop = propOptions[key]
  const absent = !hasOwn(propsData, key)
  let value = propsData[key]
  // boolean casting
  const booleanIndex = getTypeIndex(Boolean, prop.type)
  if (booleanIndex > -1) {
    if (absent && !hasOwn(prop, 'default')) {
      value = false
    } else if (value === '' || value === hyphenate(key)) {
      // only cast empty string / same name to boolean if
      // boolean has higher priority
      const stringIndex = getTypeIndex(String, prop.type)
      if (stringIndex < 0 || booleanIndex < stringIndex) {
        value = true
      }
    }
  }
  // check default value
  if (value === undefined) {
    value = getPropDefaultValue(vm, prop, key)
    // since the default value is a fresh copy,
    // make sure to observe it.
    const prevShouldObserve = shouldObserve
    toggleObserving(true)
    observe(value)
    toggleObserving(prevShouldObserve)
  }
  if (
    process.env.NODE_ENV !== 'production' &&
    // skip validation for weex recycle-list child component props
    !(__WEEX__ && isObject(value) && ('@binding' in value))
  ) {
    assertProp(prop, key, value, vm, absent)
  }
  return value
}
           

PS:

先跳過toggleObserving方法

,後面單獨講

validateProp

主要就做 3 件事情:處理

Boolean

類型的資料,處理預設資料,

prop

斷言,并最終傳回

prop

的值。

先來看

Boolean

類型資料的處理邏輯:

const prop = propOptions[key]
const absent = !hasOwn(propsData, key)
let value = propsData[key]
// boolean casting
const booleanIndex = getTypeIndex(Boolean, prop.type)
if (booleanIndex > -1) {
  if (absent && !hasOwn(prop, 'default')) {
    value = false
  } else if (value === '' || value === hyphenate(key)) {
    // only cast empty string / same name to boolean if
    // boolean has higher priority
    const stringIndex = getTypeIndex(String, prop.type)
    if (stringIndex < 0 || booleanIndex < stringIndex) {
      value = true
    }
  }
}
           

先通過

const booleanIndex = getTypeIndex(Boolean, prop.type)

來判斷

prop

的定義是否是

Boolean

類型的。

function getType (fn) {
  const match = fn && fn.toString().match(/^\s*function (\w+)/)
  return match ? match[1] : ''
}

function isSameType (a, b) {
  return getType(a) === getType(b)
}

function getTypeIndex (type, expectedTypes): number {
  if (!Array.isArray(expectedTypes)) {
    return isSameType(expectedTypes, type) ? 0 : -1
  }
  for (let i = 0, len = expectedTypes.length; i < len; i++) {
    if (isSameType(expectedTypes[i], type)) {
      return i
    }
  }
  return -1
}
           

getTypeIndex

函數就是找到

type

expectedTypes

比對的索引并傳回。

prop

類型定義的時候可以是某個原生構造函數,也可以是原生構造函數的數組,比如:

export default {
  props: {
    name: String,
    value: [String, Boolean]
  }
}
           

如果

expectedTypes

是單個構造函數,就執行

isSameType

去判斷是否是同一個類型;

如果是數組,那麼就周遊這個數組,找到第一個同類型的,傳回它的索引。

回到

validateProp

函數,通過

const booleanIndex = getTypeIndex(Boolean, prop.type)

得到

booleanIndex

,如果

prop.type

是一個

Boolean

類型,則通過

absent && !hasOwn(prop, 'default')

來判斷

如果父元件沒有傳遞這個

prop

資料并且沒有設定

default

的情況,則

value

為 false。

接着判斷

value === '' || value === hyphenate(key)

的情況,如果滿足則先通過

const stringIndex = getTypeIndex(String, prop.type)

擷取比對

String

類型的索引,然後判斷

stringIndex < 0 || booleanIndex < stringIndex

的值來決定

value

的值是否為

true

這塊邏輯稍微有點繞,我們舉 2 個例子來說明:

例如你定義一個元件

Student

:

export default {
  name: String,
  nickName: [Boolean, String]
}
           

然後在父元件中引入這個元件:

<template>
  <div>
    <student name="Kate" nick-name></student>
  </div>
</template>
           

或者是:

<template>
  <div>
    <student name="Kate" nick-name="nick-name"></student>
  </div>
</template>
           

第一種情況沒有寫屬性的值,滿足

value === ''

,第二種滿足

value === hyphenate(key)

的情況,另外

nickName

這個

prop

的類型是

Boolean

或者是

String

,并且滿足

booleanIndex < stringIndex

,是以對

nickName

這個

prop

value

true

接下來看一下預設資料處理邏輯:

// check default value
if (value === undefined) {
  value = getPropDefaultValue(vm, prop, key)
  // since the default value is a fresh copy,
  // make sure to observe it.
  const prevShouldObserve = shouldObserve
  toggleObserving(true)
  observe(value)
  toggleObserving(prevShouldObserve)
}
           

value

的值為

undefined

的時候,說明父元件根本就沒有傳這個

prop

,那麼我們就需要通過

getPropDefaultValue(vm, prop, key)

擷取這個

prop

的預設值。

我們這裡隻關注

getPropDefaultValue

的實作,

toggleObserving

observe

的作用我們之後會說。

function getPropDefaultValue (vm: ?Component, prop: PropOptions, key: string): any {
  // no default, return undefined
  if (!hasOwn(prop, 'default')) {
    return undefined
  }
  const def = prop.default
  // warn against non-factory defaults for Object & Array
  if (process.env.NODE_ENV !== 'production' && isObject(def)) {
    warn(
      'Invalid default value for prop "' + key + '": ' +
      'Props with type Object/Array must use a factory function ' +
      'to return the default value.',
      vm
    )
  }
  // the raw prop value was also undefined from previous render,
  // return previous default value to avoid unnecessary watcher trigger
  if (vm && vm.$options.propsData &&
    vm.$options.propsData[key] === undefined &&
    vm._props[key] !== undefined
  ) {
    return vm._props[key]
  }
  // call factory function for non-Function types
  // a value is Function if its prototype is function even across different execution context
  return typeof def === 'function' && getType(prop.type) !== 'Function'
    ? def.call(vm)
    : def
}
           

檢測如果

prop

沒有定義

default

屬性,那麼傳回

undefined

,通過這塊邏輯我們知道除了

Boolean

類型的資料,其餘沒有設定

default

屬性的

prop

預設值都是

undefined

接着是開發環境下對

prop

的預設值是否為對象或者數組類型的判斷,如果是的話會報警告,因為對象和數組類型的

prop

,他們的預設值必須要傳回一個工廠函數。

接下來的判斷是如果上一次元件渲染父元件傳遞的

prop

的值是

undefined

,則直接傳回 上一次的預設值

vm._props[key]

,這樣可以避免觸發不必要的

watcher

的更新。

最後就是判斷

def

如果是工廠函數且

prop

的類型不是

Function

的時候,傳回工廠函數的傳回值,否則直接傳回

def

至此,我們講完了

validateProp

函數的

Boolean

類型資料的處理邏輯和預設資料處理邏輯,最後來看一下

prop

斷言邏輯。

if (
process.env.NODE_ENV !== 'production' &&
// skip validation for weex recycle-list child component props
!(__WEEX__ && isObject(value) && ('@binding' in value))
) {
  assertProp(prop, key, value, vm, absent)
}
           

在開發環境且非

weex

的某種環境下,執行

assertProp

做屬性斷言。

function assertProp (
  prop: PropOptions,
  name: string,
  value: any,
  vm: ?Component,
  absent: boolean
) {
  if (prop.required && absent) {
    warn(
      'Missing required prop: "' + name + '"',
      vm
    )
    return
  }
  if (value == null && !prop.required) {
    return
  }
  let type = prop.type
  let valid = !type || type === true
  const expectedTypes = []
  if (type) {
    if (!Array.isArray(type)) {
      type = [type]
    }
    for (let i = 0; i < type.length && !valid; i++) {
      const assertedType = assertType(value, type[i])
      expectedTypes.push(assertedType.expectedType || '')
      valid = assertedType.valid
    }
  }

  if (!valid) {
    warn(
      getInvalidTypeMessage(name, value, expectedTypes),
      vm
    )
    return
  }
  const validator = prop.validator
  if (validator) {
    if (!validator(value)) {
      warn(
        'Invalid prop: custom validator check failed for prop "' + name + '".',
        vm
      )
    }
  }
}
           

assertProp

函數的目的是斷言這個

prop

是否合法。

首先判斷如果

prop

定義了

required

屬性但父元件沒有傳遞這個

prop

資料的話會報一個警告。

接着判斷如果

value

為空且

prop

沒有定義

required

屬性則直接傳回。

然後再去對

prop

的類型做校驗,先是拿到

prop

中定義的類型

type

,并嘗試把它轉成一個類型數組,然後依次周遊這個數組,執行

assertType(value, type[i])

去擷取斷言的結果,直到周遊完成或者是

valid

true

的時候跳出循環。

const simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/
function assertType (value: any, type: Function): {
  valid: boolean;
  expectedType: string;
} {
  let valid
  const expectedType = getType(type)
  if (simpleCheckRE.test(expectedType)) {
    const t = typeof value
    valid = t === expectedType.toLowerCase()
    // for primitive wrapper objects
    if (!valid && t === 'object') {
      valid = value instanceof type
    }
  } else if (expectedType === 'Object') {
    valid = isPlainObject(value)
  } else if (expectedType === 'Array') {
    valid = Array.isArray(value)
  } else {
    valid = value instanceof type
  }
  return {
    valid,
    expectedType
  }
}
           

assertType

的邏輯很簡單,先通過

getType(type)

擷取

prop

期望的類型

expectedType

,然後再去根據幾種不同的情況對比

prop

的值

value

是否和

expectedType

比對,最後傳回比對的結果。

如果循環結束後

valid

仍然為

false

,那麼說明

prop

的值

value

prop

定義的類型都不比對,那麼就會輸出一段通過

getInvalidTypeMessage(name, value, expectedTypes)

生成的警告資訊,就不細說了。

最後判斷當

prop

自己定義了

validator

自定義校驗器,則執行

validator

校驗器方法,如果校驗不通過則輸出警告資訊。

響應式

回到

initProps

方法,當我們通過

const value = validateProp(key, propsOptions, propsData, vm)

prop

做驗證并且擷取到

prop

的值後,接下來需要通過

defineReactive

prop

變成響應式。

defineReactive

我們之前已經介紹過,這裡要注意的是,在開發環境中我們會校驗

prop

key

是否是

HTML

的保留屬性,并且在

defineReactive

的時候會添加一個自定義

setter

,當我們直接對

prop

指派的時候會輸出警告:

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

關于

prop

的響應式有一點不同的是當

vm

是非根執行個體的時候,會先執行

toggleObserving(false)

,它的目的是

為了響應式的優化

,我們先跳過,之後會詳細說明。

代理

在經過響應式處理後,我們會把

prop

的值添加到

vm._props

中,比如 key 為

name

prop

,它的值儲存在

vm._props.name

中,但是我們在元件中可以通過

this.name

通路到這個

prop

,這就是代理做的事情。

// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (!(key in vm)) {
  proxy(vm, `_props`, key)
}
           

通過

proxy

函數實作了上述需求。

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

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

當通路

this.name

的時候就相當于通路

this._props.name

其實

對于非根執行個體的子元件而言

prop

的代理發生在

Vue.extend

階段,在

src/core/global-api/extend.js

中:

Vue.extend = function (extendOptions: Object): Function {
  // ...
  const Sub = function VueComponent (options) {
    this._init(options)
  }
  // ...

  // For props and computed properties, we define the proxy getters on
  // the Vue instances at extension time, on the extended prototype. This
  // avoids Object.defineProperty calls for each instance created.
  if (Sub.options.props) {
    initProps(Sub)
  }
  if (Sub.options.computed) {
    initComputed(Sub)
  }

  // ...
  return Sub
}

function initProps (Comp) {
  const props = Comp.options.props
  for (const key in props) {
    proxy(Comp.prototype, `_props`, key)
  }
}
           

這麼做的好處是不用為每個元件執行個體都做一層

proxy

,是一種優化手段。

Props 更新

我們知道,當父元件傳遞給子元件的

props

值變化,子元件對應的值也會改變,同時會觸發子元件的重新渲染。

那麼接下來我們就從源碼角度來分析這兩個過程。

子元件 props 更新

首先,

prop

資料的值變化在父元件,我們知道在父元件的

render

過程中會通路到這個

prop

資料,是以當

prop

資料變化一定會觸發父元件的重新渲染,那麼重新渲染是如何更新子元件對應的

prop

的值呢?

在父元件重新渲染的最後,會執行

patch

過程,進而執行

patchVnode

函數,

patchVnode

通常是一個遞歸過程,當它遇到元件

vnode

的時候,會

執行元件更新過程的 prepatch

鈎子函數,在

src/core/vdom/patch.js

中:

function patchVnode (
  oldVnode,
  vnode,
  insertedVnodeQueue,
  ownerArray,
  index,
  removeOnly
) {
  // ...

  let i
  const data = vnode.data
  if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    i(oldVnode, vnode)
  }
  // ...
}
           

prepatch

函數定義在

src/core/vdom/create-component.js

中:

prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
  const options = vnode.componentOptions
  const child = vnode.componentInstance = oldVnode.componentInstance
  updateChildComponent(
    child,
    options.propsData, // updated props
    options.listeners, // updated listeners
    vnode, // new parent vnode
    options.children // new children
  )
}
           

内部會調用

updateChildComponent

方法來更新

props

,注意第二個參數就是父元件的

propData

,那麼為什麼

vnode.componentOptions.propsData

就是父元件傳遞給子元件的

prop

資料呢(這個也同樣解釋了第一次渲染的

propsData

來源)?原來在元件的

render

過程中,對于元件節點會通過

createComponent

方法來建立元件

vnode

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  // ...

  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // ...
  
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  // ...
  
  return vnode
}
           

在建立元件

vnode

的過程中,首先從

data

中提取出

propData

,然後在

new VNode

的時候,作為第七個參數

VNodeComponentOptions

中的一個屬性傳入,是以我們可以通過

vnode.componentOptions.propsData

拿到

prop

資料。

接着看

updateChildComponent

函數,它的定義在

src/core/instance/lifecycle.js

中:

export function updateChildComponent (
  vm: Component,
  propsData: ?Object,
  listeners: ?Object,
  parentVnode: MountedComponentVNode,
  renderChildren: ?Array<VNode>
) {
  // ...

  // update props
  if (propsData && vm.$options.props) {
    toggleObserving(false)
    const props = vm._props
    const propKeys = vm.$options._propKeys || []
    for (let i = 0; i < propKeys.length; i++) {
      const key = propKeys[i]
      const propOptions: any = vm.$options.props // wtf flow?
      props[key] = validateProp(key, propOptions, propsData, vm)
    }
    toggleObserving(true)
    // keep a copy of raw propsData
    vm.$options.propsData = propsData
  }

  // ...
}
           

我們重點來看

更新prop

的相關邏輯,這裡的

propsData

是父元件傳遞的

props

資料,

vm

是子元件的執行個體。

vm._props

指向的就是子元件的

props

值,

propKeys

就是在之前

initProps

過程中,緩存的子元件中定義的所有

prop

key

主要邏輯就是周遊

propKeys

,然後執行

props[key] = validateProp(key, propOptions, propsData, vm)

重新驗證和計算新的

prop

資料,更新

vm._props

,也就是子元件的

props

,這個就是子元件

props

的更新過程。

子元件重新渲染

其實子元件的重新渲染有 2 種情況,一個是

prop

值被修改,另一個是對象類型的

prop

内部屬性的變化。

先來看一下

prop

值被修改的情況,當執行

props[key] = validateProp(key, propOptions, propsData, vm)

更新子元件

prop

的時候,會觸發

prop

setter

過程,隻要在渲染子元件的時候通路過這個

prop

值,那麼根據響應式原理,就會觸發子元件的重新渲染。

再來看一下當對象類型的

prop

的内部屬性發生變化的時候,這個時候其實并沒有觸發子元件

prop

的更新。

但是在子元件的渲染過程中,通路過這個對象

prop

,是以這個對象

prop

在觸發

getter

的時候會把子元件的

render watcher

收集到依賴中,然後當我們在父元件更新這個對象

prop

的某個屬性的時候,會觸發

setter

過程,也就會通知子元件

render watcher

update

,進而觸發子元件的重新渲染。

以上就是當父元件

props

更新,觸發子元件重新渲染的 2 種情況。

toggleObserving

最後我們在來聊一下

toggleObserving

,它的定義在

src/core/observer/index.js

中:

export let shouldObserve: boolean = true

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

它在目前子產品中定義了

shouldObserve

變量,用來控制在

observe

的過程中是否需要把目前值變成一個

Observer

對象。

那麼為什麼在

props

的初始化和更新過程中,多次執行

toggleObserving(false)

呢,接下來我們就來分析這幾種情況。

initProps

的過程中:

const isRoot = !vm.$parent
// root instance props should be converted
if (!isRoot) {
  toggleObserving(false)
}
for (const key in propsOptions) {
  // ...
  const value = validateProp(key, propsOptions, propsData, vm)
  defineReactive(props, key, value)
  // ...
}
toggleObserving(true)
           

對于

非根執行個體

的情況,我們會執行

toggleObserving(false)

,然後對于每一個

prop

值,去執行

defineReactive(props, key, value)

去把它變成響應式。

回顧一下

defineReactive

的定義:

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // ...
  
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // ...
    },
    set: function reactiveSetter (newVal) {
      // ...
    }
  })
}
           

通常對于值

val

會執行

observe

函數,然後遇到

val

是對象或者數組的情況會遞歸執行

defineReactive

把它們的子屬性都變成響應式的,但是由于

shouldObserve

的值變成了

false

,這個遞歸過程被省略了。為什麼會這樣呢?

因為正如我們前面分析的,對于對象的

prop

值,子元件的

prop

值始終指向父元件的

prop

值,隻要父元件的

prop

值變化,就會觸發子元件的重新渲染,是以這個

observe

過程是可以省略的。

最後再執行

toggleObserving(true)

恢複

shouldObserve

true

validateProp

的過程中:

// check default value
if (value === undefined) {
  value = getPropDefaultValue(vm, prop, key)
  // since the default value is a fresh copy,
  // make sure to observe it.
  const prevShouldObserve = shouldObserve
  toggleObserving(true)
  observe(value)
  toggleObserving(prevShouldObserve)
}
           

這種是父元件沒有傳遞

prop

值對預設值的處理邏輯,因為這個值是一個拷貝,是以我們需要

toggleObserving(true)

,然後執行

observe(value)

把值變成響應式。

updateChildComponent

過程中:

// update props
if (propsData && vm.$options.props) {
  toggleObserving(false)
  const props = vm._props
  const propKeys = vm.$options._propKeys || []
  for (let i = 0; i < propKeys.length; i++) {
    const key = propKeys[i]
    const propOptions: any = vm.$options.props // wtf flow?
    props[key] = validateProp(key, propOptions, propsData, vm)
  }
  toggleObserving(true)
  // keep a copy of raw propsData
  vm.$options.propsData = propsData
}
           

其實和

initProps

的邏輯一樣,不需要對引用類型

props

遞歸做響應式處理,是以也需要

toggleObserving(false)

總結

通過這一節的分析,我們了解了

props的規範化、初始化、更新等過程的實作原理

;也了解了 Vue 内部對

props

如何做響應式的優化;同時還了解到

props

的變化是如何觸發子元件的更新。

了解這些對我們平時對

props

的應用,遇到問題時的定位追蹤會有很大的幫助。

Vue源碼學習目錄

元件化 (一) createComponent

元件化 (二) patch

元件化 (三) 合并配置

元件化 (四) 生命周期

元件化(五) 元件注冊

深入響應式原理(一) 響應式對象

深入響應式原理 (二)依賴收集 & 派發更新

深入響應式原理 (三)nextTick & 檢測變化的注意事項

深入響應式原理 (四)計算屬性 VS 偵聽屬性

深入響應式原理 (五)深入響應式原理 (五)元件更新

深入響應式原理 (六)Props (v2.6.11)

深入響應式原理 (七)原理圖總結

點選回到 Vue源碼學習完整目錄

謝謝你閱讀到了最後~

期待你關注、收藏、評論、點贊~

讓我們一起 變得更強