「这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战」。
一、前情回顾 & 背景
前面一篇文章从整体上梳理了一遍 _init 方法都做了哪些工作,总结起来就是:
mergeOptions合并Vue.options和options- 设置 _renderProxy
initLifetcylce初始化$parent、$root、$childreninitEvents初始化自定义事件initRender解析组件中插槽信息,初始化vm._c和vm.$createElement- 触发
beforeCreated initInjections处理inject并设置其为响应式数据initState数据响应initProvide初始化Provide- 触发
created - 根据
options.el有无决定是否调用$mount
这些都说了,就剩一个 initState 没有说,这一篇算是填个大坑吧,这个 initState 可以说是第二复杂的,第一复杂的是 $mount 这一部分后面还要接着讲。
接下来请本篇小作文的主角 initState 登场!
二、initState
方法位置:src/core/instance/state.js -> initState
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
// .... 暂时省略后面的过程
}
initState 方法作为数据响应式的入口,分别要处理:
props对应initPropsmethods对应initMethodsdata对应initDatacomputed对应initComputed方法watch对应initWtach方法
其实这里有必要说一下,我们的 test.html 中,new Vue 创建根实例的时候并没有传入 props/computed/watch/methods,所以事实上第一次执行的时候并不会执行对应的处理方法,所以这里需要更新一下 test.html 创建 Vue 实例部分的代码:
test.html 的 script 标签部分代码
<script>
const sub = { /* sub details */ };
debugger
new Vue({
el: '#app',
data: {
msg: 'hello vue'
},
props: {
someProp: {
type: String,
default: function () { return { sp: ''somePropDefaultVal'' } }
}
},
computed: {
someComputed () {
return this.msg + this.someProp
}
},
watch: {
msg (nv, ov) {
console.log(nv, ov)
}
},
hahaha: 'hahahahahha',
provide: {
foo: 'bar'
},
components: {
someCom: sub
}
})
</script>
三、initProps
方法位置:src/core/instance/state.js -> initProps
该方法的主要作用:
- 获取或者初始化
vm.$options.propsData和vm._props属性,值为{} for in循环遍历vm.$options.propsOptions,处理props选项,把props的每个key都转化成响应式,并将_props代理到vm上
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
if (!isRoot) {
toggleObserving(false)
}
// 遍历 props 对象
for (const key in propsOptions) {
keys.push(key)
// 获取 props[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) {}
})
} else {
// 为 props 的每个 key 设置数据响应式
defineReactive(props, key, value)
}
if (!(key in vm)) {
// 代理 key 到 到 vm 上
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
3.1 vm.$options.propsData
在 initProps 的开头,我们获取了 vm.$options.propsData 对象,在初始化根实例的时候这个对象是个无关紧要的对象,因为他是个 undefined。
但是当初始化子组件实例时,经过在 _init 的时候变不会走 mergeOptions 这个 else 分支合并选项了,而是会走前面的 if 选项,进而执行 initInternalComponent(vm, options) 方法,这个方法会在 options 上挂载 propsData 这里先有个印象,因为这里讲的是 new Vue 时的 initState 中的 initProps;
Vue.prototype._init = function () {
if (options && options._isComponent) {
// 组件时走这里
initInternalComponent(vm, options)
} else {
mergeOptions(.....)
}
}
// ....
export function initInternalComponent (vm, options) {
// ...
opts.propsData =vnodeComponentOptions.propsData // 初始化 组件 options.propsData
// ...
}
3.2 const props = vm._props = {}
用于接收处理响应式数据的 prop 的对象,它里面的数据都变成了 getter 和 setter ,当然 set 是要抛出错误的。
3.3 for in 遍历 propsOptions
propsOptions 就是我们上面 demo 中定义好的 props 对象:
在遍历的过程中主要做了以下几件事:
- 缓存
propsOptions中的key - 通过
validateProp(key, propsOptions, propsData, vm)方法获取key对应的值value - 把
key设置到props变量即vm._props,且为这个key设置响应式
3.3.1 validateOptions
方法位置:src/core/util/props.js -> validateOptions
方法作用:处理 prop 类型为 Boolean 的情形的默认值,另外当 propsData 中没有这个 prop 的时候,使用默认值,当然,你的默认值还不是响应式的数据类型,所以要用 observe 方法对这个默认值进行处理,简单来说 observe 就是递归调用 defineReactive 把默认值的子属性,甚至子属性的子属性都变成响应式数据。
此外,如果默认值是对象或者数组类型时,则会要求用一个工厂函数返回默认值所对应的对象或者数组,这和组件中的 data 要求用工厂函数时一个道理,防止组件间共享数据发生意外。
在我们的例子中,props 是在 new Vue 时传入的,所以并没有给 someProp 传值,所以这里就是取用的默认值;
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 的默认值
const booleanIndex = getTypeIndex(Boolean, prop.type)
if (booleanIndex > -1) {
}
// check default value
if (value === undefined) {
value = getPropDefaultValue(vm, prop, key) // 这里会检查对象或者数组默认值提示必须使用工厂函数返回默认值
// 默认值是一个全新的值,所以要设置数据响应式
const prevShouldObserve = shouldObserve
toggleObserving(true)
observe(value)
toggleObserving(prevShouldObserve)
}
// ....
return value
}
3.3.2 defineReactive
在一个对象上定义一个响应式的属性,其核心就是大家熟知的 Obeject.definePorperty 方法来拦截对象属性的读取和设置,当读取时收集依赖,当重新设置时派发更新,告知那些依赖方重新计算就可以得到新的值。
举个例子,我们 new Vue 传入的 props 是这样的:
new Vue({
//
props: {
someProp: {
type: Object,
default: function () { return { sp: 'special offer' } }
}
},
})
此时如果 someProp 变成了一个新值,那肯定是要派发更新的,但是如果只是 someProp.sp 这个属性发生变化,这个大家都知道也是要派发更新的,所以从这里大家就要有个认识,这里面一定是递归处理过了才能让 sp 值被读取时依赖被收集,更新时更感知到并且派发依赖。
defeineReactive 也是一个大块内容,我们下个主题再讨论他。
四、总结
本文主要回顾了一下 _init 做了哪些事儿,着重讲了 initState 中的 initProps 的概况,for in 遍历 propsOptions,获取每个 key 对应的默认值,如果这个默认值是对象或者数组,则递归的将其属性或者子项都变成数据响应式,最后通过 defineProperty 将这个 prop 代理到 vm._props 属性上实现数据响应式