vue源码解读十七: props更新如何触发子组件重新渲染?
本文主要内容摘抄自黄轶老师的慕课网课程Vue.js 源码全方位深入解析 全面深入理解Vue实现原理,主要用于个人学习和复习,不用作其他用途。
Props
作为组件的核心特性之一,也是平时开发 Vue 项目中接触最多的特性之一,它可以让组件的功能变得丰富,也是父子组件通讯的一个渠道。那么它的实现原理是怎样的,我们来一探究竟。
规范化
在初始化 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
}
这个函数的主要目的就是把我们编写的 props
转成对象格式,因为实际上 props
除了对象格式,还允许写成数组格式。
当 props
是一个数组,每一个数组元素 prop
只能是一个 string
,表示 prop
的 key
,转成驼峰格式,prop
的类型为空。
当 props
是一个对象,对于 props
中每个 prop
的 key
,我们会转驼峰格式,而它的 value
,如果不是一个对象,我们就把它规范成一个对象。
举个例子:
数组格式
export default {
props: ['name', 'nick-name']
}
经过 normalizeProps
后,会被规范成:
options.props = {
name: { type: null },
nickName: { type: null }
}
对象格式
export default {
props: {
name: String,
nickName: {
type: Boolean,
default: false
}
}
}
经过 normalizeProps
后,会被规范成:
options.props = {
name: { type: String },
nickName: { type: Boolean, default: false }
}
初始化
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 || {}
// 这个就是最后能访问的props真实值,同时在vm上挂载了_props
const props = vm._props = {}
// 这用于props更新时用的
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
// 校验求值
// propsOptions是组件定义的props定义
// propsData是父组件传递过来的prop数据
const value = validateProp(key, propsOptions, propsData, vm)
if (process.env.NODE_ENV !== 'production') {
...
// 把props变成响应式,并把父组件传过来的值赋值给props
defineReactive(props, key, value, () => {
// 不能直接修改组件内部的props值,否则弹出警告
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 (!(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]
const booleanIndex = getTypeIndex(Boolean, prop.type)
if (booleanIndex > -1) {
if (absent && !hasOwn(prop, 'default')) {
value = false
} else if (value === '' || value === hyphenate(key)) {
const stringIndex = getTypeIndex(String, prop.type)
if (stringIndex < 0 || booleanIndex < stringIndex) {
value = true
}
}
}
// check default value
if (value === undefined) {
value = getPropDefaultValue(vm, prop, key)
const prevShouldObserve = shouldObserve
toggleObserving(true)
observe(value)
toggleObserving(prevShouldObserve)
}
if (
process.env.NODE_ENV !== 'production' &&
!(__WEEX__ && isObject(value) && ('@binding' in value))
) {
assertProp(prop, key, value, vm, absent)
}
return value
}
validateProp
主要就做 3 件事情:处理 Boolean
类型的数据,处理默认数据,prop
断言,并最终返回 prop
的值。
先来看 Boolean
类型数据的处理逻辑:
const prop = propOptions[key]
const absent = !hasOwn(propsData, key)
let value = propsData[key]
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
。
上面这一点逻辑主要是用来处理boolean类型,因为boolean的值只能是true和false。如果你定义了某个key是boolean类型,当它不传值的时候默认为true,比如:<student nick-name></student>
,如果都不传这个key,如<student></student>
默认为false。
接下来看一下默认数据处理逻辑:
if (value === undefined) {
value = getPropDefaultValue(vm, prop, key)
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 {
// 如果没有default,直接返回undefined
if (!hasOwn(prop, 'default')) {
return undefined
}
const def = prop.default
// 如果你传递的是一个对象或者数组,需要使用工厂函数的形式,因为对象是引用类型,否则会报如下的警告
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
)
}
// 这是一段更新的优化逻辑
if (vm && vm.$options.propsData &&
vm.$options.propsData[key] === undefined &&
vm._props[key] !== undefined
) {
return vm._props[key]
}
// 调用工厂函数返回值
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
的更新。可以看下面的例子:
const CompB = {
template: `
<div>
<p>age: {{age}}</p>
<p>sex: {{sex}}</p>
<p>ball: {{hobby.ball}}</p>
<p>game: {{hobby.game}}</p>
</div>
`,
props: {
age: Number,
sex: {
type: String,
default: 'female',
validator(value) {
return value === 'male' || value === 'female'
}
},
hobby: {
type: Object,
default() {
return {
ball: 'basketball',
game: 'dota'
}
}
}
},
watch: {
hobby(newValue, oldValue) {
console.log(newValue, oldValue)
}
}
}
new Vue({
el: '#app',
components: { CompA, CompB },
data() {
return {
age: 18
}
},
methods: {
change() {
this.age++
}
},
template: `
<div>
<comp-b :age="age" sex="male"></comp-b>
<button @click="change">change</button>
</div>
`
})
父组件没有传hobby的值,那么第一次渲染的时候这个hobby就去默认的值,当点击change事件更新age字段的时候,子组件会重新渲染,在重新渲染的过程中,会访问hobby的default函数,那么就会走到这里,此时vm.$options.propsData[key] === undefined
,同时vm._props[key] !== undefined
(第一次渲染的时候已经挂载了),所以会直接返回第一次的值。
if (vm && vm.$options.propsData &&
vm.$options.propsData[key] === undefined &&
vm._props[key] !== undefined
) {
return vm._props[key]
}
如果这个时候有watch监听,因为值没有变化,所以不会执行。但是如果把上面这段逻辑去掉,再次执行工厂函数,返回一个新的值,两个值不同就会执行watch,这样就浪费了。
watch: {
hobby(newValue, oldValue) {
console.log(newValue, oldValue)
}
}
至此,我们讲完了 validateProp
函数的 Boolean
类型数据的处理逻辑和默认数据处理逻辑,最后来看一下 prop
断言逻辑。
function assertProp (
prop: PropOptions,
name: string,
value: any,
vm: ?Component,
absent: boolean
) {
// 如果存在required,但是父组件没有传,则报错
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++) {
// 对type 和 value的类型匹配,如果匹配成功assertedType.valid为true,同时跳出循环
const assertedType = assertType(value, type[i], vm)
expectedTypes.push(assertedType.expectedType || '')
valid = assertedType.valid
}
}
// 下面是给用户提示用的,当组件定义的类型与你传递的类型不匹配的时候,valid为false,则给出提示
const haveExpectedTypes = expectedTypes.some(t => t)
if (!valid && haveExpectedTypes) {
warn(
getInvalidTypeMessage(name, value, expectedTypes),
vm
)
return
}
// 如果用户定义了validator,则执行
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
// getType返回一个字符串,表示prop定义的类型,比如String类型,则返回String字符串
const expectedType = getType(type)
if (simpleCheckRE.test(expectedType)) {
// 根据value的值判断value的类型
const t = typeof value
// 进行比较
valid = t === expectedType.toLowerCase()
...
} 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
的保留属性, 比如 style class
等就不能作为props的key
,并且在 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
中(但是它不能直接访问,是通过代理的方式访问的),比如 key 为 name
的 prop
,它的值保存在 vm._props.name
中,但是我们在组件中可以通过 this.name
访问到这个 prop
,这就是代理做的事情。
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
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)
}
// ...
Sub.options = mergeOptions(
Super.options,
extendOptions
)
...
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
数据。
在参数合并的时候,会把父组件传递过来的props挂载到组件实例options上:
// 组件的参数合并
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// 组件的占位符vnode
const parentVnode = options._parentVnode
// 占位符节点有一个componentOptions选项
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = vnodeComponentOptions.propsData
}
接着看 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进行赋值
props[key] = validateProp(key, propOptions, propsData, vm)
}
toggleObserving(true)
// keep a copy of raw propsData
vm.$options.propsData = propsData
}
}
我们重点来看更新 props
的相关逻辑,这里的 propsData
是父组件传递的 props
数据,vm
是子组件的实例。vm._props
指向的就是子组件的 props
值,propKeys
就是在之前 initProps
过程中,缓存的子组件中定义的所有 prop
的 key
。主要逻辑就是遍历 propKeys
,然后执行 props[key] = validateProp(key, propOptions, propsData, vm)
重新验证和计算新的 prop
数据,更新 vm._props
,也就是子组件的 props
,这个就是子组件 props
的更新过程。
在更新props[key]
的过程中,就会触发setter函数:
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
// 如果新值和旧值一样,直接返回
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
...
childOb = !shallow && observe(newVal);
// 重新渲染
dep.notify();
}
子组件重新渲染
其实子组件的重新渲染有 2 种情况,一个是 prop
值被修改,另一个是对象类型的 prop
内部属性的变化。
先来看一下 prop
值被修改的情况,当执行 props[key] = validateProp(key, propOptions, propsData, vm)
更新子组件 prop
的时候,会触发 prop
的 setter
过程,只要在渲染子组件的时候访问过这个 prop
值,那么根据响应式原理,就会触发子组件的重新渲染。
再来看一下当对象类型的 prop
的内部属性发生变化的时候,这个时候其实并没有触发子组件 prop
的更新。但是在子组件的渲染过程中,访问过这个对象 prop
的内部属性,所以这个对象 prop
的内部属性在触发 getter
的时候会把子组件的 render watcher
收集到依赖中,然后当我们在父组件更新这个对象 prop
的某个属性的时候,因为子组件的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
里面的每个属性在初始化的时候都已经添加上getter
,当子组件访问的到某个具体的属性的时候,那么这个属性就会收集子组件的render watcher
。
当父组件 prop
的某个属性值变化,就会触发这个属性的setter
方法,然后就执行dep.notity
,所以这个 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)
。
转载自:https://juejin.cn/post/7125724822499229727