Series of articles:

  • Vue source code interpretation (Design)
  • Vue source code interpretation (Rollup)
  • Vue source code interpretation (entry into the overall process of the constructor)

introduce

The initState() method handles props, methods, data, and so on:

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)}if (opts.computed) initComputed(vm, opts.computed)
  if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }Copy the code

Then the in-depth introduction to reactive principles will start with the initState() method and gradually analyze the principle of reactive in Vue. The following diagram can show the principle of reactive.

Pre-core concept

Object. DefineProperty is introduced

Object.defineproperty (obj, key, descriptor); object.defineProperty (obj, key, descriptor);

  • obj: The object whose properties are to be defined.
  • key: The name of the property to define or modify.
  • descriptor: The descriptor to define or modify the property.

There are a number of optional keys for descriptor, but the most important ones for Vue reactives are the get and set methods, which trigger getters when getting property values and setters when setting property values, respectively. Before introducing the principles, let’s use object.defineProperty () to implement a simple responsive example:

function defineReactive (obj, key, val) {
  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.get: function reactiveGetter () {
      console.log('get msg')
      return val
    },
    set: function reactiveSetter (newVal) {
      console.log('set msg')
      val = newVal
    }
  })
}
const vm = {
  msg: 'hello, Vue.js'
}
let msg = ' '
defineReactive(vm, 'msg', vm.msg)
msg = vm.msg          // get msg
vm.msg = 'Hello, Msg' // set msg
msg = vm.msg          // get msg
Copy the code

To make it easy to use the object.defineProperty () method elsewhere, wrap it as a defineReactive function.

The proxy agent

This is because Vue uses the default proxy for props and data (props and data). To understand what a proxy is, let’s look at a simple example:

this._data = {
  name: 'AAA'.age: 23
}
/ / agent before
console.log(this._data.name) // AAA
proxy(vm, '_data', key)
/ / agent
console.log(this.name)       // AAA
Copy the code

The proxy() method is defined in the instance/state.js file. The code is simple:

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)
}
Copy the code

As you can see from the code above, the proxy method mainly hijacks the get and set methods of the property.

const name = this.name
this.name = 'BBB'
/ / equivalent to the
const name = this._data.name
this._data.name = 'BBB'
Copy the code

The $options properties

In the previous introduction, we know that the options passed when initializing the Vue instance will be configured and merged according to different circumstances. We will discuss the specific options merging policy in the later section. At this stage, we only need to know that $options can get all the merged properties. Examples include props, methods, data, and so on.

Suppose the following example is defined:

const vm = new Vue({
  el: '#app'.props: {
    msg: ' '
  },
  data () {
    return {
      firstName: 'AAA'.lastName: 'BBB'.age: 23}},methods: {
    sayHello () {
      console.log('Hello, Vue.js')}},computed: {
    fullName () {
      return this.firstName + this.lastName
    }
  }
})
Copy the code

These properties can then be retrieved in the following manner.

const opts = this.$options
const props = opts.props
const methods = opts.methods
const data = opts.data
const computed = opts.computed
const watch = opts.watch
/ /... , etc.
Copy the code

Props to deal with

The first thing you need to learn after introducing these pre-core concepts is how vue.js handles the logic associated with props. The logic related to props is divided into three parts: props canonicalization, props initialization, and props update.

Props standardization

There are several ways to write component props in everyday development.

  • Array form:propsI could write it as an array, but in an arraykeyThe element must bestringType.
export default {
  props: ['name'.'age']}Copy the code
  • Key values are not objects: this is common when only definitions are neededkeyThe type ofprops.
export default {
  props: {
    name: String}}Copy the code
  • Standard format: This mode isVue.jsacceptpropsThe best format, for a demanding component, will be written strictlypropsRules, which are the most common among open source UI frameworks.
export default {
  props: {
    name: {
      type: String.default: ' '
    },
    age: {
      type: Number.default: 0,
      validator (value) {
        return value >= 0 && value <= 100}}}}Copy the code

What the props canonization does is formalize various forms that are not the props format into the props format, making it easier for vue.js to handle props later. Next, let’s examine how vue.js normalizes props.

The canonicalization of props occurs in the mergeOptions merge configuration in the this._init() method:

import { mergeOptions } from '.. /util/index'
export function _init (Vue) {
  const vm = this
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  )
}
Copy the code

The mergeOptions() method is defined in the SRC /core/util/options.js file, which has a section of method calls like this:

export function mergeOptions (
  parent: Object,
  child: Object, vm? : Component) :Object {
  // Omit the code
  normalizeProps(child, vm)
  return options
} 
Copy the code

NormalizeProps () ¶ normalizeProps() ¶ normalizeProps() ¶ normalizeProps() ¶ normalizeProps();

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
}
Copy the code

To better understand the normalizeProps() method, write a few examples to illustrate it in detail:

  • Form of an array: whenpropsIf it’s an array, it first goes through the array in reverse order, and then usestypeofTo determine the type of the array elements. If it is notstringType, an error is reported in the development environment, if yesstringType, firstkeyPut it in the hump form, and then take thiskeyAssign to temporaryresObject, where the key value is fixed to{ type: null }
// Before normalization
export default {
  props: ['age'.'nick-name']}// After normalization
export default {
  props: {
    age: {
      type: null
    },
    nickName: {
      type: null}}}Copy the code
  • Object form: Used when it is an objectfor-inIterate over the object and then use it as an array formcamelizeCome and takekeyTurn it into a hump form and useisPlainObject()Method to determine whether it is a normal object. If not, convert to{ type: Type }Object form, whereTypeTo define thekeyAt the time of theTypeIf yes, the object is used directly.
// Before normalization
export default {
  props: {
    name: String.age: Number}}// After normalization
export default {
  props: {
    name: {
      type: String
    },
    age: {
      type: Number}}}Copy the code
  • Neither array nor object: error reported
Invalid value for option "props": expected an Array or an Object, but got String
export default {
  props: 'name, age'
}
Copy the code

Props to initialize

Now that you know how to normalize props, let’s look at the initialization process of props. The props initialization process also occurs in the this._init() method, which is handled during initState:

export function initState (vm) {
  // Omit the code
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
}
Copy the code

Then take a closer look at the initProps code:

function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  const keys = vm.$options._propKeys = []
  constisRoot = ! vm.$parentif(! isRoot) { toggleObserving(false)}for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    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)
    }
    if(! (keyin vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)}Copy the code

After reading the initProps() method carefully, you can summarize the initProps() method. It does three main things: props verification and evaluation, props response, and props proxy.

Props responsive

Let’s take a look at the simplest props response. This part of the process uses the defineReactive method we introduced earlier:

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
    )
  }
})
Copy the code

The only thing to note is that in the development environment, the response of props hijks the setter method in order to ensure that the props are stand-alone data streams: neither can you modify the props passed by the parent component directly in the child component.

Props agent

_props object. To obtain the props value, you need to create a layer of proxy for the props. The implementation of proxy has been described in previous chapters.

this._props = {
  name: ' '.age: 0
}

/ / agent before
console.log(this._props.name)
proxy(vm, `_props`, key)
/ / agent
console.log(this.name)
Copy the code

Props check evaluation

Finally, let’s look at the slightly more complicated props validation. This part of the function occurs in validateProp, which looks like this:

export function validateProp (
  key: string,
  propOptions: Object,
  propsData: Object, vm? : Component) :any {
  const prop = propOptions[key]
  constabsent = ! 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
}
Copy the code

Code analysis: validateProp does not throw an error that prevents validateProp() from returning a value, but it does indicate the validateProp() method as clearly as possible. The validateProp() method essentially returns the value, but it also handles different situations depending on how you write the props. The validateProp() method can be summarized as doing several things:

  • To deal withBooleanThe type ofprops.
  • To deal withdefaultDefault data.
  • propsAssertions.

Then the following will be a detailed description of each of these things.

Handling Boolean types

Here are some examples of props passing Boolean:

// Component A
export default {
  props: {
    fixed: Boolean}}// Component B
export default {
  props: {
    fixed: [Boolean.String]}}// Component C
export default {
  props: {
    fixed: []}}Copy the code

Then back in the source code where the Boolean getTypeIndex is handled, the code for this function looks like this:

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
}
Copy the code

The implementation logic of this function is fairly clear:

  1. In order toComponent AComponent for example, itspropsIt’s not an array but it isBooleanType, so the index is returned0.
  2. In order toComponent BComponent, for example, because of itspropsIt’s all an array, so I’m going to walk through that array and returnBooleanThe index of the type in the arrayi.
  3. In order toComponent CComponent, for example, although it is an array, does not have any elements in the array and therefore returns the index- 1.

After you get the booleanIndex, you need to go through the following code logic:

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}}}Copy the code

Code analysis:

  • inifConditional judgmentabsentThe representation is defined in the child componentpropsBut the parent component does not pass any value, and then&The condition determines the child componentpropsDoes it providedefaultDefault value option, if not, then its value can only befalse.
// The parent component does not pass fixed
export default {
  name: 'ParentComponent'
  template: `<child-component />`
}

// The fixed value of the child component is false
export default {
  name: 'ChildComponent'.props: {
    fixed: Boolean}}Copy the code
  • inelse ifIn the conditional judgment, two particular kinds ofpropsDelivery mode:
// Parent Component A
export default {
  name: 'ParentComponentA'.template: `<child-component fixed />`
}

// Parent Component B
export default {
  name: 'ParentComponentB'.template: `<child-component fixed="fixed" />`
}
Copy the code

In the first case stringIndex is -1 and booleanIndex is 0, so value is true. In the second case, we need to make a distinction according to the definition of props:

// Child Component A
export default {
  name: 'ChildComponentA'
  props: {
    fixed: [Boolean.String]}}// Child Component B
export default {
  name: 'ChildComponentB'.props: [String.Boolean]}Copy the code
  1. forChildComponentAAs a result ofstringIndexA value of1.booleanIndexA value of0.booleanIndex < stringIndexSo it can be argued thatBooleanHas a higher priorityvalueThe value oftrue.
  2. forChildComponentBAs a result ofstringIndexA value of0.booleanIndexA value of1.stringIndex < booleanIndexSo it can be argued thatStringHas a higher priorityvalueThe value of is not processed.

Process default Default data

After handling the Boolean type, we handle the default value, as mentioned in cases where the child component defines props but the parent component does not pass it.

// The parent component does not pass fixed
export default {
  name: 'ParentComponent'
  template: `<child-component />`
}

// The child component provides the default option
export default {
  name: 'ChildComponent'.props: {
    fixed: {
      type: Boolean.default: false}}}Copy the code

For the above example, the following code logic would follow:

if (value === undefined) {
  value = getPropDefaultValue(vm, prop, key)
}

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
}
Copy the code

Code analysis:

  1. First, determine whether the child component is provideddefaultDefault value option, if no, return directlyundefined.
  2. And then judgeddefaultIf it is a reference type, the prompt must bedefaultWrite it as a function:
default: {}
default: []

// must be written
default () {
  return{}}default () {
  return[]}Copy the code
  1. Finally according todefaultThe function is called if it is a function type, or used directly if it is not a function type.
  2. The following section of code does not explain and analyze what it does here, but ratherpropsUpdate the section to introduce.
if (vm && vm.$options.propsData &&
  vm.$options.propsData[key] === undefined&& vm._props[key] ! = =undefined
) {
  return vm._props[key]
}
Copy the code

Props assertion

Finally, the props assertion.

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
  letvalid = ! 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
      )
    }
  }
}
Copy the code

There are three situations to assert in an assertProp:

  • required: If the child componentpropsprovidesrequiredThe choice is thispropsThe value must be passed in the parent component, or an error message is thrown if it is notMissing required prop: fixed.
  • For those with multiple definitionstype, will iterate through the array of types, as long as the currentpropsIf the type of the array matches an element in the array. Otherwise, an error message is thrown.
// Parent Component
export default {
  name: 'ParentComponent'.template: `<child-component :age="true" />`
}
// Chil Component
export default {
  name: 'ChilComponent'.props: {
    age: [Number.String]}}Invalid prop: type check failed for prop age, Expected Number, String, got with value true
Copy the code
  • Provided by the uservalidatorThe validator also needs to assert:
// Parent Component
export default {
  name: 'ParentComponent'.template: `<child-component :age="101" />`
}
// Chil Component
export default {
  name: 'ChilComponent'.props: {
    age: {
      type: Number,
      validator (value) {
        return value >=0 && value <=100}}}}Invalid prop: Custom validator check failed for prop age
Copy the code

Props to update

It is known that the props value of a child component is derived from its parent component. When the parent component’s value is updated, the child component’s value changes, triggering a re-rendering of the child component. (root_props) props (root_props) props (props) props (props) props (props) props (props) props (props) props (props) props (props) props (props) props (props) props

export function updateChildComponent (
  vm: Component,
  propsData: ?Object,
  listeners: ?Object,
  parentVnode: MountedComponentVNode,
  renderChildren: ?Array<VNode>
) {
  // Omit the code
  // 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
  }
}
Copy the code

Code analysis:

  1. The abovevmInstance is a child component,propsDataPassed in the parent componentpropsThe value of and_propKeysIs beforepropsAll cached during initializationpropsThe key.
  2. After the parent component value is updated, traversal is passedpropsKeyTo repair the child componentspropsforCheck evaluation, and then assign a value.

(props) props (props) props (props) props (props) props

  • ordinarypropsThe value is modified: WhenpropsAfter the value is modified, there is a piece of code in itprops[key] = validateProp(key, propOptions, propsData, vm)According to the principle of responsiveness, the property will be triggeredsetterAnd then the child component can be rerendered.
  • objectpropsInternal property change: When this happens, no child component is triggeredpropBut was read while the subcomponent was renderingprops, so this will be collectedpropstherender watcherWhen the objectpropsWhen the internal property changes, it still triggers according to the responsive principlesetterAnd then the child component can be rerendered.

ToggleObserving role

ToggleObserving is defined in SRC/core/observer/index. A function of js file, the code is simple:

export let shouldObserve: boolean = true
export function toggleObserving (value: boolean) {
  shouldObserve = value
}
Copy the code

This changes the current module’s shouldObserve variable, which controls whether to change the current value to an Observer object during observe.

export function observe (value: any, asRootData: ? boolean) :Observer | void {
  if(! isObject(value) || valueinstanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if( shouldObserve && ! isServerRendering() && (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) && ! value._isVue ) { ob =new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
Copy the code

Next, in handling props, when toggleObserving(true), when toggleObserving(false), and why do you need to do this?

function initProps (vm: Component, propsOptions: Object) {
  if(! isRoot) { toggleObserving(false)}// Omit the defineReactive process
  toggleObserving(true)}Copy the code

Props = props = props = props = props = props = props = props = props When props is an object or an array, we recursively iterate over the child property and then perform observe(val). Since props is derived from the parent, this process has already been performed in the parent. If no restrictions are imposed, the process will be repeated in the child. This is why toggleObserving(false) is needed, to avoid cases where the props attribute is recursively used, as a means of reactive optimization. At the end of the code, we call toggleObserving(true) to restore the shouldObserve value.

Props (props) : props (props) : props (props) : props (props)

export default {
  props: {
    point: {
      type: Object.default () {
        return {
          x: 0.y: 0}}},list: {
      type: Array.default () {
        return[]}}}}Copy the code

ToggleObserving (true) = object_object (object_object) = object_object (object_object) = object_object (object_object) = object_object (object_object) = object_object (object_object) = object_object (object_object) = object_object (object_object) = object_object (object_object) = object_object (object_object) = object_object (object_object) = object_object (object_object)

export function validateProp () {
  // Omit the code
  if (value === undefined) {
    value = getPropDefaultValue(vm, prop, key)
    const prevShouldObserve = shouldObserve 
    toggleObserving(true)
    observe(value)
    toggleObserving(prevShouldObserve)
  }
}
Copy the code

In the props update: When the parent component is updated, updateChildComponent() is called to update the props of the child component. This method uses the same logic as props. There is no need to recurse to the object or array that points to the parent component. This is why toggleObserving(false) is needed.

export function 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)
    vm.$options.propsData = propsData
  }
}
Copy the code

Overall flow chart

After analyzing all the props related logic, you can summarize the flowchart as follows.