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:
props
I could write it as an array, but in an arraykey
The element must bestring
Type.
export default {
props: ['name'.'age']}Copy the code
- Key values are not objects: this is common when only definitions are needed
key
The type ofprops
.
export default {
props: {
name: String}}Copy the code
- Standard format: This mode is
Vue.js
acceptprops
The best format, for a demanding component, will be written strictlyprops
Rules, 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: when
props
If it’s an array, it first goes through the array in reverse order, and then usestypeof
To determine the type of the array elements. If it is notstring
Type, an error is reported in the development environment, if yesstring
Type, firstkey
Put it in the hump form, and then take thiskey
Assign to temporaryres
Object, 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 object
for-in
Iterate over the object and then use it as an array formcamelize
Come and takekey
Turn it into a hump form and useisPlainObject()
Method to determine whether it is a normal object. If not, convert to{ type: Type }
Object form, whereType
To define thekey
At the time of theType
If 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 with
Boolean
The type ofprops
. - To deal with
default
Default data. props
Assertions.
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:
- In order to
Component A
Component for example, itsprops
It’s not an array but it isBoolean
Type, so the index is returned0
. - In order to
Component B
Component, for example, because of itsprops
It’s all an array, so I’m going to walk through that array and returnBoolean
The index of the type in the arrayi
. - In order to
Component C
Component, 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:
- in
if
Conditional judgmentabsent
The representation is defined in the child componentprops
But the parent component does not pass any value, and then&
The condition determines the child componentprops
Does it providedefault
Default 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
- in
else if
In the conditional judgment, two particular kinds ofprops
Delivery 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
- for
ChildComponentA
As a result ofstringIndex
A value of1
.booleanIndex
A value of0
.booleanIndex < stringIndex
So it can be argued thatBoolean
Has a higher priorityvalue
The value oftrue
. - for
ChildComponentB
As a result ofstringIndex
A value of0
.booleanIndex
A value of1
.stringIndex < booleanIndex
So it can be argued thatString
Has a higher priorityvalue
The 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:
- First, determine whether the child component is provided
default
Default value option, if no, return directlyundefined
. - And then judged
default
If it is a reference type, the prompt must bedefault
Write it as a function:
default: {}
default: []
// must be written
default () {
return{}}default () {
return[]}Copy the code
- Finally according to
default
The function is called if it is a function type, or used directly if it is not a function type. - The following section of code does not explain and analyze what it does here, but rather
props
Update 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 componentprops
providesrequired
The choice is thisprops
The 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 definitions
type
, will iterate through the array of types, as long as the currentprops
If 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 user
validator
The 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:
- The above
vm
Instance is a child component,propsData
Passed in the parent componentprops
The value of and_propKeys
Is beforeprops
All cached during initializationprops
The key. - After the parent component value is updated, traversal is passed
propsKey
To repair the child componentsprops
forCheck evaluation, and then assign a value.
(props) props (props) props (props) props (props) props
- ordinary
props
The value is modified: Whenprops
After 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 triggeredsetter
And then the child component can be rerendered. - object
props
Internal property change: When this happens, no child component is triggeredprop
But was read while the subcomponent was renderingprops
, so this will be collectedprops
therender watcher
When the objectprops
When the internal property changes, it still triggers according to the responsive principlesetter
And 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.