Vue Source Code Learning (6) Props Attribute Analysis

As one of the core features of the component, ‘Props’ is also one of the features we usually contact the most in the development of Vue projects. It can enrich the functions of the component and is also a channel for communication between parent and child components.

Here thanksustbhuangyiBoss massive open online course.

Standardization

Before initializing’props’, ‘props’ will first be’normalized ‘once, which happens when’mergeOptions’, in’src/core/util/options.js’:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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
}

Merge configuration We talked about it in the component section, which mainly deals with the object’option ‘that we define the component, and then mounts it to the instance’this. $options’ of the component.

We next focus on the implementation of’normalizeProps’. In fact, the main purpose of this function is to convert the’props’ we have written into object format, because in fact’props’ allows to be written in array format in addition to object format.

When’props’ is an array, each array element’prop ‘can only be a’string’, representing the’key ‘of’prop’, converted to hump format, and the type of’prop 'is null.

When’props’ is an object, for the’key ‘of each’prop’ in’props’, we convert the camel format, and its’value ', if not an object, we normalize it to an object.

If’props’ is neither an array nor an object, a warning is thrown.

For example:

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

After normalizeProps, it will be normalized to:

1
2
3
4
options.props = {
name: { type: null },
nickName: { type: null }
}
1
2
3
4
5
6
7
8
export default {
props: {
name: String,
nickName: {
type: Boolean
}
}
}

After normalizeProps, it will be normalized to:

1
2
3
4
options.props = {
name: { type: String },
nickName: { type: Boolean }
}

Since props in object form can specify the type of each prop and define other properties, it is recommended to define props in object form.

Initialization

The initialization of’Props’ mainly occurs in the’initState ‘stage in’new Vue’, in’src/core/instance/state.js’:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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’ mainly does three things: validation, responsiveness, and proxying.

Verification

The logic of the verification is simple, traverse’propsOptions’ and execute the’validateProp (key, propsOptions, propsData, vm) ‘method. The’propsOptions’ here is the’options.props’ object generated by the’props’ we defined after the specification, and’propsData ‘is the’prop’ data passed from the parent component. The purpose of the so-called verification is to check whether the data we pass meets the definition specification of’prop ‘. Let’s take a look at the’validateProp’ method, which is defined in’src/core/util/props.js’:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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
}

ValidateProp does three main things: handles Boolean data, handles default data, asserts prop, and ultimately returns the value of prop.

Let’s first look at the processing logic of’Boolean 'type data.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
}
}
}

First, use’const booleanIndex = getTypeIndex (Boolean, prop.type) ‘to determine whether the definition of’prop’ is of type’Boolean '.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
}

The getTypeIndex function finds the index that matches type and expectedTypes and returns it.

The’prop 'type can be defined as a native constructor function or an array of native constructors, for example:

1
2
3
4
5
6
export default {
props: {
name: String,
value: [String, Boolean]
}
}

If expectedTypes is a single constructor function, execute isSameType to determine whether it is the same type; if it is an array, then traverse the array, find the first one of the same type, and return its index.

Returning to the’validateProp ‘function, get’booleanIndex’ by’const booleanIndex = getTypeIndex (Boolean, prop.type) ‘, if’prop.type’ is a’Boolean ‘type, then use’absent & &! hasOwn (prop,’ default ') ’ to determine if the parent component did not pass this’prop ‘data and did not set’default’, then’value 'is false.

Then judge the value. = ‘’ || value In the case of = hyphenate (key) ‘, if it is satisfied, first obtain the index matching the type of’String’ through’const stringIndex = getTypeIndex (String, prop.type) ‘, and then judge the value of’stringIndex < 0 | | booleanIndex < stringIndex’ to determine whether the value of’value ‘is’true’. This logic is a bit confusing, we give 2 examples to illustrate:

For example, if you define a component’Student ':

1
2
3
4
export default {
name: String,
nickName: [Boolean, String]
}

Then introduce this component in the parent component.

1
2
3
4
5
<template>
<div>
<student name="Kate" nick-name></student>
</div>
</template>

Or:

1
2
3
4
5
<template>
<div>
<student name="Kate" nick-name="nick-name"></student>
</div>
</template>

The first case does not write the value of the attribute, satisfying 'value = ‘’,第二种满足 value = hyphenate (key) ‘, in addition, the’prop’ of’nickName ‘is of type’Boolean’ or’String ‘, and satisfies’booleanIndex < stringIndex’, so the’value ‘of’nickName’ is’true ‘for’prop’.

Next, let’s take a look at the default data processing logic.

1
2
3
4
5
6
7
8
9
10
// 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)
}

When the value of’value ‘is’undefined’, it means that the parent component did not pass this’prop ‘at all, so we need to get the default value of this’prop’ through’getPropDefaultValue (vm, prop, key) ‘. We only focus on the implementation of’getPropDefaultValue’ here, the role of’toggleObserving ‘and’observe’ we will talk about later.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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
}

Check if’prop ‘does not define the’default’ property, then return’undefined ‘. Through this logic, we know that except for data of type’Boolean’, the default value of’prop ‘without setting the’default’ property is’undefined '.

Next is the determination of whether the default value of’prop ‘in the Development Environment is an object or array type. If so, a warning will be issued, because the default values of’prop’ of object and array type must return a factory function.

The next judgment is that if the value of’prop ‘passed by the parent component of the previous component rendering is’undefined’, it will directly return the default value of’vm._props [key] ‘from the previous time, which can avoid triggering unnecessary’watcher’ updates.

Finally, it is judged that if’def ‘is a factory function and the type of’prop’ is not’Function ‘, return the return value of the factory function, otherwise return’def’ directly.

At this point, we have covered the processing logic of the’Boolean ‘type data of the’validateProp’ function and the default data processing logic, and finally look at the assertion logic of’prop '.

1
2
3
4
5
6
7
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)
}

In the Development Environment and some environment other than weex, execute assertProp to make attribute assertions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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
)
}
}
}

The purpose of the assertProp function is to assert whether the prop is valid.

First, if’prop ‘defines the’required’ attribute but the parent component does not pass this’prop 'data, a warning will be issued.

Then it is determined that if’value ‘is empty and’prop’ does not define the’required 'attribute, it will be returned directly.

Then go to check the type of’prop ‘, first get the type’type’ defined in’prop ', and try to convert it into an array of types, and then traverse this array in turn, execute’assertType (value, type [i]) ’ to get the result of the assertion, until the traversal is completed or’valid ‘is’true’ when the loop jumps out.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
}
}

The logic of assertType is very simple. First, get the expected type of prop expectedType through getType (type), then compare the value of prop value and expectedType according to several different situations, and finally return the matching result.

If “valid” is still “false” after the loop ends, then the value “value” of “prop” does not match the type defined by “prop”, then a warning message generated by "getInvalidTypeMessage (name, value, expectedTypes) " will be output, without going into detail.

Finally, it is determined that when’prop ‘defines its own’validator’ custom validator, the’validator 'validator method is executed, and a warning message is output if the verification does not pass.

Responsive

Returning to the’initProps’ method, when we verify the’prop 'with’const value = validateProp (key, propsOptions, propsData, vm) ’ and get the value of’prop ‘, we need to use’defineReactive’ to turn’prop 'into a response.

We have already introduced’defineReactive ‘before. It should be noted here that in the Development Environment, we will check whether the’key’ of’prop ‘is a reserved attribute of’HTML’, and we will add a custom’setter ‘when’defineReactive’. When we assign a value to’prop 'directly, a warning will be output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
)
}
})
}

The one difference about the response of’prop ‘is that when’vm’ is not the root instance, it will perform’toggleObserving (false) 'first. Its purpose is to optimize the response. We will skip it first and explain it in detail later.

Agent

After reactive processing, we will add the value of’prop 'to’vm.props’, such as’prop ‘with key’name’, its value is stored in’vm. props.name ‘, but we can access this’prop’ in the component through 'this.name ', which is what the proxy does.

1
2
3
4
5
6
// 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)
}

The above requirements are achieved through the’proxy 'function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)
}

When visiting this.name is equivalent to visiting _ props.name.

In fact, for subcomponents that are not root instances, the proxy for’prop ‘occurs in the’Vue.extend’ stage, in’src/core/global-api/extende.js’:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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)
}
}

The advantage of this is that there is no need to do a layer of’proxy 'for each component instance, which is an optimization method.

Props

We know that when the’props’ value passed by the parent component to the child component changes, the corresponding value of the child component will also change, and it will trigger the re-rendering of the child component. Then we will analyze these two processes from the source code point of view.

Subassembly

First of all, the value of the’prop ‘data changes in the parent component. We know that the’prop’ data will be accessed during the’render ‘process of the parent component, so when the’prop’ data changes, it will definitely trigger the re-rendering of the parent component, so how does the re-rendering update the value of the’prop 'corresponding to the child component?

At the end of the parent component re-rendering, the’patch ‘process will be executed, and then the’patchVnode’ function will be executed. The’patchVnode ‘is usually a recursion process. When it encounters the component’vnode’, it will execute the’prepatch ‘hook function of the component update process, in’src/core/vdom/patch.js’:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)
}
// ...
}

The prepatch function is defined in src/core/vdom/create-component .js:

1
2
3
4
5
6
7
8
9
10
11
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
)
}

The’updateChildComponent ‘method will be called internally to update the’props’. Note that the second parameter is the’propData ‘of the parent component, so why is’vnode.assementOptions.propsData’ the’prop ‘data passed by the parent component to the child component? (This also explains the source of the’propsData’ for the first rendering)? It turns out that during the’render ‘process of the component, the component node will be created through the’createComponent’ method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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
}

In the process of creating the component vnode, first extract the propData from the data, and then in the new VNode, as the seventh parameter VNodeComponentOptions in a property passed, so we can get the prop data through the vnode.assementOptions.propsData.

Then look at the’updateChildComponent ‘function, which is defined in’src/core/instance/lifecycle.js’:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
}

// ...
}

Let’s focus on the relevant logic of updating’props’. Here’propsData ‘is the’props’ data passed by the parent component, and’vm ‘is the instance of the child component.’ vm._props ‘points to the’props’ value of the child component, and’propKeys’ is the’key ‘of all the’props’ defined in the cached child component during the previous’initProps’ process. The main logic is to traverse’propKeys’, and then execute’props [key] = validateProp (key, propOptions, propsData, vm) ‘to revalidate and calculate the new’prop’ data, and update’vm._props ‘, that is, the’props’ of the subcomponent, which is the update process of the subcomponent’props’.

Subcomponent re-rendering

In fact, there are two cases of re-rendering of subcomponents, one is the value of’prop ‘is modified, and the other is the change of the internal property of’prop’ of the object type.

Let’s take a look at the situation where the value of’prop 'is modified. When executing’props [key] = validateProp (key, propOptions, propsData, vm) ’ to update the subcomponent’prop ‘, it will trigger the’setter’ process of’prop ‘. As long as the’prop’ value is accessed when rendering the subcomponent, then according to the principle of responsiveness, the rerendering of the subcomponent will be triggered.

Let’s take a look again. When the internal properties of the object type’prop ‘change, the update of the subcomponent’prop’ is not actually triggered at this time. However, during the rendering process of the child component, this object’prop ‘has been accessed, so this object’prop’ will collect the’render watcher ‘of the child component into the dependency when it triggers the’getter’, and then when we update the parent component When a property of this object’prop ‘is used, the’setter’ process will be triggered, and the’update ‘of the child component’render watcher’ will be notified, which in turn triggers the re-rendering of the child component.

These are the two situations when the parent component’props’ is updated, triggering a child component to re-render.

toggleObserving

Finally, let’s talk about’toggleObserving ‘, which is defined in’src/core/observer/index.js’:

1
2
3
4
5
export let shouldObserve: boolean = true

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

It defines the’shouldObserve ‘variable in the current module to control whether the current value needs to be turned into an’Observer’ object during the’observe 'process.

So why do we execute toggleObserving (false) multiple times during the initialization and update of props? Next, we will analyze these situations.

In the process of’initProps’:

1
2
3
4
5
6
7
8
9
10
11
12
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)

For non-root instances, we do toggleObserving (false), and then for each prop value, we do defineReactive (props, key, value) to turn it into a response.

Recall the definition of’defineReactive ':

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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) {
// ...
}
})
}

Usually the observe function is executed for the value val, and then the recursion is executed when val is an object or array, and the defineReactive is executed to make their child properties responsive, but since the value of shouldObserve becomes false, this recursion process is omitted. Why is this so?

Because as we analyzed earlier, for the’prop ‘value of the object, the’prop’ value of the child component always points to the’prop ‘value of the parent component, and as long as the’prop’ value of the parent component changes, it will trigger the re-rendering of the child component, so this’observe 'process can be omitted.

Finally, execute toggleObserving (true) to restore shouldObserve to true.

In the process of validateProp:

1
2
3
4
5
6
7
8
9
10
// 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)
}

This is the logic of the parent component not passing the’prop 'value to the default value, because this value is a copy, so we need’toggleObserving (true) ', and then execute’observe (value) 'to turn the value into a response.

During the updateChildComponent process:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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
}

In fact, like the logic of’initProps’, there is no need to do reactive processing on the reference type’props’ recursion, so’toggleObserving (false) 'is also needed.

Summary

Through the analysis of this section, we understand the implementation principle of the normalization, initialization, update and other processes of’props’; also understand how to optimize’props’ responsively within Vue; at the same time, we also understand how the change of’props’ triggers the update of subcomponents. Understanding these will be of great help to our usual application of’props’ and location tracking when encountering problems.