Ask questions
- How is the value of the parent component passed to the child component via props
- How does the child component read props
- How does the props of the child component update when the data of the parent component changes
- Why not update props directly in child components
The actual scene
The scenario code is as follows:
new Vue({
el: '#app1'.components: {
child: {
props: {
num: {
type: Number}},template: `<strong>child: {{ num }}</strong>`}},template: `
father: {{ count }}
`.data() {
return {
count: 1}},methods: {
handleClick() {
this.count += 1}}})Copy the code
How does the parent component pass value to the child component props
Depending on the scenario setting, the child is a child component that receives a propsnum, and the parent passes its datacount to the child
The parent template component will compile into the following render function:
function anonymous() {
with (this) {
return _c(
'div',
[
_c('p', [_v('father: ' + _s(count))]),
_v(' '),
_c('button', { on: { click: handleClick } }, [_v('count+1')]),
_v(' '),
_c('child', { attrs: { num: count } }) // Generate child vnode].1)}}Copy the code
The render function uses the with operator to bind the scope to this, which points to the parent Vue instance;
For _c(‘child’, {attrs: {num: count}}), the count lookup is actually on the parent Vue instance, equivalent to: _c(‘child’, {attrs: {num: parent vm.count}});
_c(‘child’) passes in an object containing the attrs attribute evaluated to: _c(‘child’, {attrs: {num: 1}}), thus completing the parent component’s transmission to the child component
How does a subcomponent receive props
_c(‘child’) createComponent(Ctor, data, Context, children, tag)
Parameter Description:
- Ctor: Component constructor, here is the componentOptions object passed in
- Data:
_c('child')
Parameters passed in:{ attrs: { num: 1} }
- Context: refers to the parent Vue instance
- Children: child component, undefined here
- Tag: component name, in this case
'child'
CreateComponent source code:
function createComponent (
Ctor: Class<Component> | Function | Object | void, data: ? VNodeData,/ / property values
context: Component,
children: ?Array<VNode>, tag? : string) :VNode | void {
// If the Ctor argument is an object, it is converted to a constructor based on that object, referring to the global method vue.extend
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
data = data || {}
// Reparse Ctor options, since global-options was merged when the Ctor constructor was created
// This resolution prevents global-options from undergoing mixins changes after Ctor is created
resolveConstructorOptions(Ctor)
// Extract props, and make a match between the properties actually passed by the parent component and the properties declared by the child component
// Only properties declared by child components are retained
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// Event handling
const listeners = data.on
data.on = data.nativeOn
// Add four methods to data.hook
// init, prepatch, insert, destory
// Merge if data itself has one
// If data itself does not exist, add it
// These methods are useful in the subsequent patch
mergeHooks(data)
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? ` -${name}` : ' '}`,
data, undefined.undefined.undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
}
Copy the code
Among them and subcomponents the receive props is 20 lines of code, method extractPropsFromVNodeData, to have a look at:
function extractPropsFromVNodeData (data: VNodeData, Ctor: Class
, tag? : string
): ?Object {
const propOptions = Ctor.options.props // Here is the props declared when defining the child component
const res = {}
const { attrs, props } = data // Here are the attributes actually passed in by the parent, placed in attrs
if (isDef(attrs) || isDef(props)) {
for (const key in propOptions) {
const altKey = hyphenate(key) 'xxxYyy' --> 'xxxYyy'
Filter out the props defined in the child component from the props actually passed in by the parent component
checkProp(res, props, key, altKey, true) ||
checkProp(res, attrs, key, altKey, false)}}return res
}
// The filtering process is actually a copy of the value, a shallow copy
function checkProp (
res: Object,
hash: ?Object,
key: string,
altKey: string,
preserve: boolean
) :boolean {
if (isDef(hash)) {
if (hasOwn(hash, key)) {
res[key] = hash[key]
if(! preserve) {delete hash[key]
}
return true
} else if (hasOwn(hash, altKey)) {
res[key] = hash[altKey]
if(! preserve) {delete hash[altKey]
}
return true}}return false
}
Copy the code
Therefore, the child component filters the props actually passed by the parent component using its declared props as a template, and only accepts the predefined props. The filtering process is a shallow copy process. Therefore, for the props of the underlying data type, a modification to one of the props of a child component will not affect the parent component (it is not recommended). However, for the props of a reference data type, a modification to the props of a child component will affect the parent component
How does the parent component props update its data
To understand the parent component data changes, subcomponents props how to update, you should first understand how child component is initialized, it has already been created subcomponents vnode, but the vnode is actually a shell, because the child component constructor has not been instantiated, but the previous steps have been prepared for the constructor’s instantiation, The child component’s VNode should look like this before instantiation:
{
componentInstance: undefined.// Save the instantiated sub-vue instance
componentOptions: {propsData: {... },listeners: undefined.tag: "child".children: undefined.Ctor: ƒ n},context: parent Vue instance,data: {attrs: {... },on: undefined.hook: {... }},isComment: false.isStatic: false.key: undefined.parent: undefined.tag: "vue-component-1-child"
}
Copy the code
Instantiate the child component and store it in the componentInstance property. The instantiation process is similar to that of the parent component.
/* vm refers to the sub-vue instance propsOptions as declared props */
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {} // The actual received props value
const props = vm._props = {}
const keys = vm.$options._propKeys = [] // Save all props property names to an array for diff operations when updating
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm) // Verify that the received props and declared props are of the same type
defineReactive(props, key, value) // Reactive vm._props
Sub.prototype._props[key] // For props explicitly declared in subcomponents, props were propped when vue.extend created the constructor:
if(! (keyin vm)) { // If the vm does not contain the key attribute, then the proxy VM [key] --> vm._props[key]
proxy(vm, `_props`, key)
}
}
}
Copy the code
The initialization props in the instantiated subcomponent constructor does the following:
- Verify that the received props and declared props are of the same type
- The response sub vm._props object
- Propping vm[key] –> vm._props[key] for props that do not propping declarations at vue. extend
Subcomponent render:
function anonymous() {
with (this) {
return _c('strong', [_v('child: ' + _s(num))])
}
}
Copy the code
After initialization, a reference to props Num during the render execution of the subcomponent will trigger the getter for dependency collection of render. If num changes, render will be notified to update.
The parent Watcher is included in the subscriber list of the parent component’s data count. When it changes, the parent Watcher will be notified of the update and render will be executed again (the render function will be cached and will not be created again) :
function anonymous() {
with (this) {
return _c(
'div',
[
_c('p', [_v('father: ' + _s(count))]),
_v(' '),
_c('button', { on: { click: handleClick } }, [_v('count+1')]),
_v(' '),
_c('child', { attrs: { num: count } }) // Generate child vnode].1)}}Copy the code
The process of vnode creation is consistent. In the patch stage, diff is compared with the old and new vNodes for corresponding update, focusing on the update of the old and new child components:
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions // new-child-componentOptions, which contains the new props value
const child = vnode.componentInstance = oldVnode.componentInstance // Assign old-child-ComponentInstance to new-child-ComponentInstance
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children)}Copy the code
UpdateChildComponent source code:
export function updateChildComponent(
vm: Component, // old-vnode
propsData: ?Object.// updated props
listeners: ?Object.// updated listeners
parentVnode: VNode, // updated-vnode
renderChildren: ?Array<VNode> // new children
) {
consthasChildren = !! ( renderChildren ||// has new static slots
vm.$options._renderChildren || // has old static slots
parentVnode.data.scopedSlots || // has new scoped slotsvm.$scopedSlots ! == emptyObject// has old scoped slots
)
// All references to vNodes are updated as new vNodes
vm.$options._parentVnode = parentVnode
vm.$vnode = parentVnode
if (vm._vnode) {
vm._vnode.parent = parentVnode
}
vm.$options._renderChildren = renderChildren
// Update the props and listeners to point to the updated value
vm.$attrs = (parentVnode.data && parentVnode.data.attrs) || emptyObject
vm.$listeners = listeners || emptyObject
$options. Props is the declared props and its type
if (propsData && vm.$options.props) {
const props = vm._props
const propKeys = vm.$options._propKeys || [] // This is where all props keys were saved when the component was initialized for the first time
// Update all props keys
for (let i = 0; i < propKeys.length; i++) {
const key = propKeys[i]
props[key] = validateProp(key, vm.$options.props, propsData, vm) // Verify that the types of the updated props are the same as the declared props, and then update the props
}
vm.$options.propsData = propsData
}
/ / update the listeners
if (listeners) {
const oldListeners = vm.$options._parentListeners
vm.$options._parentListeners = listeners
updateComponentListeners(vm, listeners, oldListeners)
}
// Parse slots and force updates if there are children
if (hasChildren) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context)
vm.$forceUpdate()
}
if(process.env.NODE_ENV ! = ='production') {
isUpdatingChildComponent = false}}Copy the code
The source code for updating props starts at line 27, and when initProps is first used as a response to the child VM._props, updating the props value triggers a change notification in the corresponding setter, notifying child-render to re-execute
After the above analysis, we can sort out how to update the data of the parent component and the child component props:
- Notifies the parent of data changes
render-watcher
Update, that is, re-executerender
- To perform
render
Get the updated VNode - In the parent component
patch
Phase compares and updates the old and new VNodes - for
component-vnode
The diff will callprepatch
andupdateChildComponent
methods - in
prepatch
Method will be directly oldcomponentInstance
Assign the new vnode.componentInstance property, so the child component is not re-instantiated - in
updateChildComponent
The props method iterates through all the keys of the declared props, updating each prop one by one - An update to the props by a child component will trigger the setters defined when the props were first initialized to notify
Render - watcher
Update, triggerSon to render
Re-execute to update the child component view
Why not update props directly in child components
a
Because the props of a child component is a shallow copy of the data of the parent component, if the props value is the underlying data type, modifying the props value will not affect the parent component. However, if the props value is a reference type, modifying the parent component will affect the parent component. The data of the parent component may be shared by multiple child components. This allows changes in one child to have unintended effects on its siblings, which also violates the props one-way data flow design; A more important reason is that it causes inconsistencies, meaning that a direct update when the props value is the underlying data type does not cause an update to the parent component, but a reference type does cause an update to the parent component, which causes confusion.
How do I modify props in the child component?
The correct way to do this is to notify the parent component to update its own state, which drives the child component props to update its own state. In this way, the parent component can control the props completely, because the props are from the parent component and it is easier to control.