Ask questions

  1. How is the value of the parent component passed to the child component via props
  2. How does the child component read props
  3. How does the props of the child component update when the data of the parent component changes
  4. 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:

  1. Ctor: Component constructor, here is the componentOptions object passed in
  2. Data:_c('child')Parameters passed in:{ attrs: { num: 1} }
  3. Context: refers to the parent Vue instance
  4. Children: child component, undefined here
  5. 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:

  1. Verify that the received props and declared props are of the same type
  2. The response sub vm._props object
  3. 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:

  1. Notifies the parent of data changesrender-watcherUpdate, that is, re-executerender
  2. To performrenderGet the updated VNode
  3. In the parent componentpatchPhase compares and updates the old and new VNodes
  4. forcomponent-vnodeThe diff will callprepatchandupdateChildComponentmethods
  5. inprepatchMethod will be directly oldcomponentInstanceAssign the new vnode.componentInstance property, so the child component is not re-instantiated
  6. inupdateChildComponentThe props method iterates through all the keys of the declared props, updating each prop one by one
  7. An update to the props by a child component will trigger the setters defined when the props were first initialized to notifyRender - watcherUpdate, triggerSon to renderRe-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.