Vue源码 深入响应式原理 (六)Props
Vue源码 深入响应式原理 (六)Props
学习内容和文章内容来自 黄轶老师
黄轶老师的慕课网视频教程地址:
《Vue.js2.0 源码揭秘》、
黄轶老师拉钩教育教程地址:
《Vue.js 3.0 核心源码解析》
这里分析的源码是Runtime + Compiler 的 Vue.js
调试代码在:node_modules\vue\dist\vue.esm.js 里添加
vue版本:Vue.js 2.5.17-beta
你越是认真生活,你的生活就会越美好
——弗兰克·劳埃德·莱特
《人生果实》经典语录
Props (v2.6.11)
Props
作为组件的核心特性之一,也是我们平时开发 Vue 项目中接触最多的特性之一,它可以让组件的功能变得丰富,也是父子组件通讯的一个渠道
。那么它的实现原理是怎样的,我们来一探究竟。
单步调试代码
// src/main.js
import Vue from 'vue'
// import App from './App.vue'
const CompA = {
template:
`
<div>
<p>Name: {{name}}</p>
<p>NickName: {{nickName}}</p>
</div>
`,
props: {
name: String,
nickName: [Boolean, String]
}
}
const CompB = {
template:
`
<div>
<p>Age: {{age}}</p>
<p>Sex: {{sex}}</p>
</div>
`,
props: {
age: Number,
sex: {
type: String,
defaule: 'feamle',
validator(value) {
return value === 'mele' || value === 'female'
}
}
}
}
new Vue({
el: '#app',
components: {CompA, CompB},
template: `
<div>
<comp-a name="Jackson" nick-name></comp-a>
<comp-b :age="18" sex="male"></comp-b>
</div>
`
})
规范化
在初始化 props
之前,首先会对 props
做一次 normalize
,它发生在 mergeOptions
的时候,在 src/core/util/options.js
中:
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
}
合并配置
我们在组件化章节讲过,它主要就是处理我们定义组件的对象 option
,然后挂载到组件的实例 this.$options
中。
我们接下来重点看 normalizeProps
的实现,其实这个函数的主要目的就是把我们编写的 props
转成对象格式
,因为实际上 props
除了对象格式
,还允许写成数组格式
。
当 props
是一个数组,每一个数组元素 prop
只能是一个 string
,表示 prop
的 key
,转成驼峰格式,prop
的类型为空。
当 props
是一个对象,对于 props
中每个 prop
的 key
,我们会转驼峰格式
,而它的 value
,如果不是一个对象,我们就把它规范成一个对象
。
如果 props
既不是数组也不是对象,就抛出一个警告。
举个例子:
export default {
props: ['name', 'nick-name']
}
经过 normalizeProps
后,会被规范成:
options.props = {
name: { type: null },
nickName: { type: null }
}
export default {
props: {
name: String,
nickName: {
type: Boolean
}
}
}
经过 normalizeProps
后,会被规范成:
options.props = {
name: { type: String },
nickName: { type: Boolean }
}
由于对象形式的 props
可以指定每个 prop
的类型和定义其它的一些属性,推荐用对象形式定义 props
。
初始化
Props
的初始化主要发生在 new Vue
中的 initState
阶段,在 src/core/instance/state.js
中:
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
主要做 3 件事情:
- 校验
- 响应式
- 代理。
校验
校验的逻辑很简单,遍历 propsOptions
,执行 validateProp(key, propsOptions, propsData, vm)
方法。
这里的 propsOptions
就是我们定义的 props
在规范后
生成的 options.props
对象,propsData
是从父组件传递的 prop
数据。
所谓校验的目的就是检查一下我们传递的数据是否满足 prop
的定义规范。再来看一下 validateProp
方法,它定义在 src/core/util/props.js
中:
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
}
PS:
先跳过toggleObserving方法
,后面单独讲
validateProp
主要就做 3 件事情:处理 Boolean
类型的数据,处理默认数据,prop
断言,并最终返回 prop
的值。
先来看 Boolean
类型数据的处理逻辑:
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
}
}
}
先通过 const booleanIndex = getTypeIndex(Boolean, prop.type)
来判断 prop
的定义是否是 Boolean
类型的。
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
}
getTypeIndex
函数就是找到 type
和 expectedTypes
匹配的索引并返回。
prop
类型定义的时候可以是某个原生构造函数,也可以是原生构造函数的数组,比如:
export default {
props: {
name: String,
value: [String, Boolean]
}
}
如果 expectedTypes
是单个构造函数,就执行 isSameType
去判断是否是同一个类型;
如果是数组,那么就遍历这个数组,找到第一个同类型的,返回它的索引。
回到 validateProp
函数,通过 const booleanIndex = getTypeIndex(Boolean, prop.type)
得到 booleanIndex
,如果 prop.type
是一个 Boolean
类型,则通过 absent && !hasOwn(prop, 'default')
来判断
如果父组件没有传递这个 prop
数据并且没有设置 default
的情况,则 value
为 false。
接着判断value === '' || value === hyphenate(key)
的情况,如果满足则先通过 const stringIndex = getTypeIndex(String, prop.type)
获取匹配 String
类型的索引,然后判断 stringIndex < 0 || booleanIndex < stringIndex
的值来决定 value
的值是否为 true
。
这块逻辑稍微有点绕,我们举 2 个例子来说明:
例如你定义一个组件 Student
:
export default {
name: String,
nickName: [Boolean, String]
}
然后在父组件中引入这个组件:
<template>
<div>
<student name="Kate" nick-name></student>
</div>
</template>
或者是:
<template>
<div>
<student name="Kate" nick-name="nick-name"></student>
</div>
</template>
第一种情况没有写属性的值,满足 value === ''
,第二种满足 value === hyphenate(key)
的情况,另外 nickName
这个 prop
的类型是 Boolean
或者是 String
,并且满足 booleanIndex < stringIndex
,所以对 nickName
这个 prop
的 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)
}
当 value
的值为 undefined
的时候,说明父组件根本就没有传这个 prop
,那么我们就需要通过 getPropDefaultValue(vm, prop, key)
获取这个 prop
的默认值。
我们这里只关注 getPropDefaultValue
的实现,toggleObserving
和 observe
的作用我们之后会说。
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
}
检测如果 prop
没有定义 default
属性,那么返回 undefined
,通过这块逻辑我们知道除了 Boolean
类型的数据,其余没有设置 default
属性的 prop
默认值都是 undefined
。
接着是开发环境下对 prop
的默认值是否为对象或者数组类型的判断,如果是的话会报警告,因为对象和数组类型的 prop
,他们的默认值必须要返回一个工厂函数。
接下来的判断是如果上一次组件渲染父组件传递的 prop
的值是 undefined
,则直接返回 上一次的默认值 vm._props[key]
,这样可以避免触发不必要的 watcher
的更新。
最后就是判断 def
如果是工厂函数且 prop
的类型不是 Function
的时候,返回工厂函数的返回值,否则直接返回 def
。
至此,我们讲完了 validateProp
函数的 Boolean
类型数据的处理逻辑和默认数据处理逻辑,最后来看一下 prop
断言逻辑。
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)
}
在开发环境且非 weex
的某种环境下,执行 assertProp
做属性断言。
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
)
}
}
}
assertProp
函数的目的是断言这个 prop
是否合法。
首先判断如果 prop
定义了 required
属性但父组件没有传递这个 prop
数据的话会报一个警告。
接着判断如果 value
为空且 prop
没有定义 required
属性则直接返回。
然后再去对 prop
的类型做校验,先是拿到 prop
中定义的类型 type
,并尝试把它转成一个类型数组,然后依次遍历这个数组,执行 assertType(value, type[i])
去获取断言的结果,直到遍历完成或者是 valid
为 true
的时候跳出循环。
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
}
}
assertType
的逻辑很简单,先通过 getType(type)
获取 prop
期望的类型 expectedType
,然后再去根据几种不同的情况对比 prop
的值 value
是否和 expectedType
匹配,最后返回匹配的结果。
如果循环结束后 valid
仍然为 false
,那么说明 prop
的值 value
与 prop
定义的类型都不匹配,那么就会输出一段通过 getInvalidTypeMessage(name, value, expectedTypes)
生成的警告信息,就不细说了。
最后判断当 prop
自己定义了 validator
自定义校验器,则执行 validator
校验器方法,如果校验不通过则输出警告信息。
响应式
回到 initProps
方法,当我们通过 const value = validateProp(key, propsOptions, propsData, vm)
对 prop
做验证并且获取到 prop
的值后,接下来需要通过 defineReactive
把 prop
变成响应式。
defineReactive
我们之前已经介绍过,这里要注意的是,在开发环境中我们会校验 prop
的 key
是否是 HTML
的保留属性,并且在 defineReactive
的时候会添加一个自定义 setter
,当我们直接对 prop
赋值的时候会输出警告:
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
)
}
})
}
关于 prop
的响应式有一点不同的是当 vm
是非根实例的时候,会先执行 toggleObserving(false)
,它的目的是为了响应式的优化
,我们先跳过,之后会详细说明。
代理
在经过响应式处理后,我们会把 prop
的值添加到 vm._props
中,比如 key 为 name
的 prop
,它的值保存在 vm._props.name
中,但是我们在组件中可以通过 this.name
访问到这个 prop
,这就是代理做的事情。
// 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)
}
通过 proxy
函数实现了上述需求。
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)
}
当访问 this.name
的时候就相当于访问 this._props.name
。
其实对于非根实例的子组件而言
,prop
的代理发生在 Vue.extend
阶段,在 src/core/global-api/extend.js
中:
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)
}
}
这么做的好处是不用为每个组件实例都做一层 proxy
,是一种优化手段。
Props 更新
我们知道,当父组件传递给子组件的 props
值变化,子组件对应的值也会改变,同时会触发子组件的重新渲染。
那么接下来我们就从源码角度来分析这两个过程。
子组件 props 更新
首先,prop
数据的值变化在父组件,我们知道在父组件的 render
过程中会访问到这个 prop
数据,所以当 prop
数据变化一定会触发父组件的重新渲染,那么重新渲染是如何更新子组件对应的 prop
的值呢?
在父组件重新渲染的最后,会执行 patch
过程,进而执行 patchVnode
函数,patchVnode
通常是一个递归过程,当它遇到组件 vnode
的时候,会执行组件更新过程的 prepatch
钩子函数,在 src/core/vdom/patch.js
中:
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)
}
// ...
}
prepatch
函数定义在 src/core/vdom/create-component.js
中:
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
)
}
内部会调用 updateChildComponent
方法来更新 props
,注意第二个参数就是父组件的 propData
,那么为什么 vnode.componentOptions.propsData
就是父组件传递给子组件的 prop
数据呢(这个也同样解释了第一次渲染的 propsData
来源)?原来在组件的 render
过程中,对于组件节点会通过 createComponent
方法来创建组件 vnode
:
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
}
在创建组件 vnode
的过程中,首先从 data
中提取出 propData
,然后在 new VNode
的时候,作为第七个参数 VNodeComponentOptions
中的一个属性传入,所以我们可以通过 vnode.componentOptions.propsData
拿到 prop
数据。
接着看 updateChildComponent
函数,它的定义在 src/core/instance/lifecycle.js
中:
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
}
// ...
}
我们重点来看更新prop
的相关逻辑,这里的 propsData
是父组件传递的 props
数据,vm
是子组件的实例。
vm._props
指向的就是子组件的 props
值,propKeys
就是在之前 initProps
过程中,缓存的子组件中定义的所有 prop
的 key
。
主要逻辑就是遍历 propKeys
,然后执行 props[key] = validateProp(key, propOptions, propsData, vm)
重新验证和计算新的 prop
数据,更新 vm._props
,也就是子组件的 props
,这个就是子组件 props
的更新过程。
子组件重新渲染
其实子组件的重新渲染有 2 种情况,一个是 prop
值被修改,另一个是对象类型的 prop
内部属性的变化。
先来看一下 prop
值被修改的情况,当执行 props[key] = validateProp(key, propOptions, propsData, vm)
更新子组件 prop
的时候,会触发 prop
的 setter
过程,只要在渲染子组件的时候访问过这个 prop
值,那么根据响应式原理,就会触发子组件的重新渲染。
再来看一下当对象类型的 prop
的内部属性发生变化的时候,这个时候其实并没有触发子组件 prop
的更新。
但是在子组件的渲染过程中,访问过这个对象 prop
,所以这个对象 prop
在触发 getter
的时候会把子组件的 render watcher
收集到依赖中,然后当我们在父组件更新这个对象 prop
的某个属性的时候,会触发 setter
过程,也就会通知子组件 render watcher
的 update
,进而触发子组件的重新渲染。
以上就是当父组件 props
更新,触发子组件重新渲染的 2 种情况。
toggleObserving
最后我们在来聊一下 toggleObserving
,它的定义在 src/core/observer/index.js
中:
export let shouldObserve: boolean = true
export function toggleObserving (value: boolean) {
shouldObserve = value
}
它在当前模块中定义了 shouldObserve
变量,用来控制在 observe
的过程中是否需要把当前值变成一个 Observer
对象。
那么为什么在 props
的初始化和更新过程中,多次执行 toggleObserving(false)
呢,接下来我们就来分析这几种情况。
在 initProps
的过程中:
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)
对于非根实例
的情况,我们会执行 toggleObserving(false)
,然后对于每一个 prop
值,去执行 defineReactive(props, key, value)
去把它变成响应式。
回顾一下 defineReactive
的定义:
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) {
// ...
}
})
}
通常对于值 val
会执行 observe
函数,然后遇到 val
是对象或者数组的情况会递归执行 defineReactive
把它们的子属性都变成响应式的,但是由于 shouldObserve
的值变成了 false
,这个递归过程被省略了。为什么会这样呢?
因为正如我们前面分析的,对于对象的 prop
值,子组件的 prop
值始终指向父组件的 prop
值,只要父组件的 prop
值变化,就会触发子组件的重新渲染,所以这个 observe
过程是可以省略的。
最后再执行 toggleObserving(true)
恢复 shouldObserve
为 true
。
在 validateProp
的过程中:
// 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)
}
这种是父组件没有传递 prop
值对默认值的处理逻辑,因为这个值是一个拷贝,所以我们需要 toggleObserving(true)
,然后执行 observe(value)
把值变成响应式。
在 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)
// keep a copy of raw propsData
vm.$options.propsData = propsData
}
其实和 initProps
的逻辑一样,不需要对引用类型 props
递归做响应式处理,所以也需要 toggleObserving(false)
。
总结
通过这一节的分析,我们了解了 props的规范化、初始化、更新等过程的实现原理
;也了解了 Vue 内部对 props
如何做响应式的优化;同时还了解到 props
的变化是如何触发子组件的更新。
了解这些对我们平时对 props
的应用,遇到问题时的定位追踪会有很大的帮助。
Vue源码学习目录
组件化 (一) createComponent
组件化 (二) patch
组件化 (三) 合并配置
组件化 (四) 生命周期
组件化(五) 组件注册
深入响应式原理(一) 响应式对象
深入响应式原理 (二)依赖收集 & 派发更新
深入响应式原理 (三)nextTick & 检测变化的注意事项
深入响应式原理 (四)计算属性 VS 侦听属性
深入响应式原理 (五)深入响应式原理 (五)组件更新
深入响应式原理 (六)Props (v2.6.11)
深入响应式原理 (七)原理图总结
谢谢你阅读到了最后~
期待你关注、收藏、评论、点赞~
让我们一起 变得更强