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
}
合并配置
我們在元件化章節講過,它主要就是處理我們定義元件的對象
option
,然後挂載到元件的執行個體
this.$options
中。
我們接下來重點看
normalizeProps
的實作,其實這個函數的主要目的就是把我們編寫的
props
轉成
對象格式
,因為實際上
props
除了
對象格式
,還允許寫成
數組格式
。
當
props
是一個數組,每一個數組元素
prop
隻能是一個
string
,表示
prop
的
key
,轉成駝峰格式,
prop
的類型為空。
當
props
是一個對象,對于
props
中每個
prop
的
key
,我們會
轉駝峰格式
,而它的
value
,如果不是一個對象,我們就把它
規範成一個對象
。
如果
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
中:
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源碼學習完整目錄
謝謝你閱讀到了最後~
期待你關注、收藏、評論、點贊~
讓我們一起 變得更強