Props 作为组件的核心特性之一,也是平时开发 Vue 项目中接触最多的特性之一,它可以让组件的功能变得丰富,也是父子组件通讯的一个渠道
规范化
在初始化props之前首先会对props做一次normalize,发生在mergeOptions时
定义在src/core/util/options.js中
/*** Merge two option objects into a new one.* Core utility used in both instantiation and inheritance.* 处理定义的组件的对象option,然后挂载到组件的实例this.$options中*/export function mergeOptions (parent: Object,child: Object,vm?: Component): Object {// ...normalizeProps(child, vm)// ...return options}/*** Ensure all props option syntax are normalized into the* Object-based format.* 把编写的props转成对象格式 也允许写成数组格式*/function normalizeProps (options: Object, vm: ?Component) {const props = options.propsif (!props) returnconst res = {}let i, val, name// 当props是一个数组,每一个数组元素prop只能是一个string,表示prop的key,转成驼峰格式,prop 的类型为空if (Array.isArray(props)) {i = props.lengthwhile (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)) { // 当props是一个对象,对于props中每个prop的key,会转驼峰格式,而它的value,如果不是一个对象,就把它规范成一个对象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') { // 如果props既不是数组也不是对象,就抛出一个警告warn(`Invalid value for option "props": expected an Array or an Object, ` +`but got ${toRawType(props)}.`,vm)}options.props = res}
例子
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) {vm._watchers = []const opts = vm.$optionsif (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 convertedif (!isRoot) {toggleObserving(false)}for (const key in propsOptions) {keys.push(key)// 校验// propsOptions 定义的props在规范后生成的options.props对象// propsData 父组件传递的prop数据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)}// 响应式// 把prop变成响应式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)}
检验
检查一下传递的数据是否满足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// 1.处理Boolean类型的数据const booleanIndex = getTypeIndex(Boolean, prop.type) // 拿到的是有Boolean构造的索引if (booleanIndex > -1) {// 判断如果父组件没有传递这个prop数据并且没有设置default的情况,则value值为falseif (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// 拿到匹配String构造的索引const stringIndex = getTypeIndex(String, prop.type)// 有值if (stringIndex < 0 || booleanIndex < stringIndex) {value = true}}}// check default value// 2.处理默认数据if (value === undefined) { // 父组件没有传这个prop// getPropDefaultValue去获取默认值value = getPropDefaultValue(vm, prop, key)// since the default value is a fresh copy,// make sure to observe it.const prevShouldObserve = shouldObservetoggleObserving(true)observe(value)toggleObserving(prevShouldObserve)}// 在开发环境且非weex的某种环境下if (process.env.NODE_ENV !== 'production' &&// skip validation for weex recycle-list child component props!(__WEEX__ && isObject(value) && ('@binding' in value))) {// 3.prop断言assertProp(prop, key, value, vm, absent)}// 返回prop的值return value}
处理Boolean类型的数据
getTypeIndex就是找到type和expectedTypes匹配的索引并返回
/*** Use function string name to check built-in types,* because a simple equality check will fail when running* across different vms / iframes.*/function getType (fn) {const match = fn && fn.toString().match(functionTypeCheckRE)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}
prop类型定义时可以是某个原生构造函数,也可以是原生构造函数的数组
比如
export default {props: {name: String,value: [String, Boolean]}}
如果expectedTypes是单个构造函数就执行isSameType去判断是否是一个类型
如果是数组那就遍历这个数组找到第一个同类型的,返回它的索引
例子说明处理Boolean类型的数据
// 组件Studentexport default {name: String,nickName: [Boolean, String]}
/* 父组件引入这个组件 */<template><div><student name="Kate" nick-name></student></div></template>/* 没有写属性的值,满足value==='' *//* 或者是 */<template><div><student name="Kate" nick-name="nick-name"></student></div></template>/* 满足value===hyphenate(key) */
nickName这个prop的类型是Boolean或者是String,并且满足booleanIndex < stringIndex,所以对nickName这个prop的value为true
处理默认数据
getPropDefaultValue
/*** Get the default value of a prop.*/function getPropDefaultValue (vm: ?Component, prop: PropOptions, key: string): any {// no default, return undefined// 没有定义default属性返回undefinedif (!hasOwn(prop, 'default')) {return undefined}const def = prop.default// warn against non-factory defaults for Object & Array// 开发环境下对prop的默认值是否为对象或者数组类型进行判断// 如果是的话会报警告,因为对象和数组类型的prop它们的默认值必须要返回一个工厂函数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// 如果上一次组件渲染父组件传递的prop的值是undefined,则直接返回上一次的默认值vm._props[key],这样可以避免触发不必要的watcher函数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// 判断def如果是工厂函数且prop的类型不是Function时返回工厂函数的返回值,否则直接返回defreturn typeof def === 'function' && getType(prop.type) !== 'Function'? def.call(vm): def}
除了Boolean类型的数据,其余没有设置default属性的prop默认值都是undefined
prop断言
assertProp做属性断言
/*** Assert whether a prop is valid.* 断言这个prop是否合法*/function assertProp (prop: PropOptions,name: string,value: any,vm: ?Component,absent: boolean) {// 如果prop定义了required属性但父组件没有传递这个prop数据的话会报一个警告if (prop.required && absent) {warn('Missing required prop: "' + name + '"',vm)return}// 如果value为空且prop没有定义required属性则直接返回if (value == null && !prop.required) {return}// 去对prop的类型做校验let type = prop.type // 先拿到peop中定义的类型typelet valid = !type || type === trueconst expectedTypes = []if (type) {// 如果不是数组,转成一个类型数组if (!Array.isArray(type)) {type = [type]}// 依次遍历这个数组,执行assertType(value, type[i], vm)去获取断言的结果for (let i = 0; i < type.length && !valid; i++) {const assertedType = assertType(value, type[i], vm)expectedTypes.push(assertedType.expectedType || '')valid = assertedType.valid}}const haveExpectedTypes = expectedTypes.some(t => t)// 循环结束后valid仍然为false,说明prop的值value与prop定义的类型都不匹配,就输入警告信息if (!valid && haveExpectedTypes) {warn(getInvalidTypeMessage(name, value, expectedTypes),vm)return}// 当prop自己定义了validator自定义校验器,则执行validator校验器方法,const validator = prop.validatorif (validator) {// 如果校验不通过则输出警告信息if (!validator(value)) {warn('Invalid prop: custom validator check failed for prop "' + name + '".',vm)}}}
assertType
const simpleCheckRE = /^(String|Number|Boolean|Function|Symbol|BigInt)$/function assertType (value: any, type: Function, vm: ?Component): {valid: boolean;expectedType: string;} {let valid// 获取prop期望的类型const expectedType = getType(type)// 根据几种不同的情况对比prop的值value是否和expectedType匹配if (simpleCheckRE.test(expectedType)) {const t = typeof valuevalid = t === expectedType.toLowerCase()// for primitive wrapper objectsif (!valid && t === 'object') {valid = value instanceof type}} else if (expectedType === 'Object') {valid = isPlainObject(value)} else if (expectedType === 'Array') {valid = Array.isArray(value)} else {try {valid = value instanceof type} catch (e) {warn('Invalid prop type: "' + String(type) + '" is not a constructor', vm);valid = false;}}// 返回匹配结果return {valid,expectedType}}
响应式
/* 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)}
在开发环境中会校验prop的key是否是HTML的保留属性
在defineReactive时会添加一个自定义的setter,当直接对prop赋值时会输出警告
关于 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) {// 当访问this.name时就相当于访问this._props.namesharedPropertyDefinition.get = function proxyGetter () {return this[sourceKey][key]}sharedPropertyDefinition.set = function proxySetter (val) {this[sourceKey][key] = val}Object.defineProperty(target, key, sharedPropertyDefinition)}
对于非根实例的子组件proxy的代理发生在Vue.extend阶段
定义在src/core/global-api/extend.js中
/*** Class inheritance*/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.propsfor (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 iconst data = vnode.dataif (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.componentOptionsconst child = vnode.componentInstance = oldVnode.componentInstance// 更新propsupdateChildComponent(child,options.propsData, // updated props 父组件的propDataoptions.listeners, // updated listenersvnode, // new parent vnodeoptions.children // new children)},
那么为什么 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 propsconst 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, // 父组件传递的props数据listeners: ?Object,parentVnode: MountedComponentVNode,renderChildren: ?Array<VNode>) {// ...// update propsif (propsData && vm.$options.props) {toggleObserving(false)const props = vm._props // vm._props指向的是子组件的props值const propKeys = vm.$options._propKeys || [] // propKeys是在之前initProps过程中缓存的子组件中定义的所有prop的key// 遍历propKeysfor (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) // 重新验证和计算新的prop数据,更新vm._props,也就是子组件的props}toggleObserving(true)// keep a copy of raw propsDatavm.$options.propsData = propsData}// ...}
子组件重新渲染
子组件的重新渲染有 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,进而触发子组件的重新渲染
toggleObserving
定义在src/core/observer/index.js中
/*** In some cases we may want to disable observation inside a component's* update computation.*/export let shouldObserve: boolean = true // 控制在observe的过程中是否需要把当前值变成一个Observer对象export function toggleObserving (value: boolean) {shouldObserve = value}
那么为什么在 props 的初始化和更新过程中,多次执行 toggleObserving(false) 呢
在initProps过程中
function initProps (vm: Component, propsOptions: Object) {// ...// root instance props should be convertedif (!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') {// ...} else {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 的过程中
export function validateProp (key: string,propOptions: Object,propsData: Object,vm?: Component): any {// ...// check default valueif (value === undefined) {value = getPropDefaultValue(vm, prop, key)// since the default value is a fresh copy,// make sure to observe it.const prevShouldObserve = shouldObservetoggleObserving(true)observe(value)toggleObserving(prevShouldObserve)}// ...}
这种是父组件没有传递 prop 值对默认值的处理逻辑,因为这个值是一个拷贝,所以需要 toggleObserving(true),然后执行 observe(value) 把值变成响应式
在 updateChildComponent 过程中
export function updateChildComponent (vm: Component,propsData: ?Object,listeners: ?Object,parentVnode: MountedComponentVNode,renderChildren: ?Array<VNode>) {// ...// update propsif (propsData && vm.$options.props) {toggleObserving(false)const props = vm._propsconst 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 propsDatavm.$options.propsData = propsData}// ...}
和 initProps 的逻辑一样,不需要对引用类型 props 递归做响应式处理,所以也需要 toggleObserving(false)
