porps
了解props源码主要是解决两个疑问
- 父组件怎么将值传递给子组件的
- 父组件的值更新后,子组件如何更新
父组件怎么将值传递给子组件的
答:我们写的节点模板会被解析并生成render
函数,函数执行时会从父组件中获取到传参,子组件获取到传参后,会对其外层进行响应式绑定,并代理到vm上
详细的说,可以从两个部分来解释,分别是
父组件怎么传给子组件
子组件怎么处理传过来的参数
- 父组件怎么传给子组件
我们写的节点模板会被解析并生成render
函数、然后通过render
函数最终生成一个vnode
虚拟节点,render
函数大概长这个样子
(function anonymous(
) {
with(this){return _c('div',[_c('children-dom',{attrs:{"qwe":value}})])}
})
这段代码看着很头疼,我们只需要简单了解一下,就是通过with
方法将{}
内的代码的作用域绑定为this
(父级),目的是为了取到父级中的传参
然后我的节点是
<div><children-dom :qwe='value'></children-dom></div>
对应我的节点,内部调用了两次_c
函数,并传了标签名
和attrs
(标签上的属性,例子中会把qwe
传过去)
_c
函数就是createElement
函数, 这个函数的作用就是生成一个 VNode
虚拟节点节点,我们只需要关注在该函数处理组件时,会在attrs
中筛选出props
的内容并存在VNode
虚拟节点中
/**
* 提取父组件通过props传递的数据
*/
export function extractPropsFromVNodeData (
data: VNodeData,
Ctor: Class<Component>,
tag?: string
): ?Object {
// 我们这里只提取原始值。
// 验证和默认值在子级中处理组件本身。
const propOptions = Ctor.options.props // xxx : {type: string,...}
if (isUndef(propOptions)) {
return
}
const res = {}
const { attrs, props } = data // {key: value}
if (isDef(attrs) || isDef(props)) { // attrs|props有值
for (const key in propOptions) {
const altKey = hyphenate(key)
if (process.env.NODE_ENV !== 'production') {
const keyInLowerCase = key.toLowerCase()
}
checkProp(res, props, key, altKey, true) || checkProp(res, attrs, key, altKey, false) // 监测属性是否合法
}
}
return res
}
执行结束后就生成了VNode
- 子组件怎么处理传过来的参数
接上边的流程,开始创建真实节点,通过_render
、createElm
、_init
等函数(跳过,只关注props
相关的),在init
中,会调用initInternalComponent
创建$options并赋值父级传过来的数据,然后开始初始化属性,走到了处理props
的函数initProps
,在这里对props
属性的外层进行响应式绑定,并将其代理到vm
上,这样,我们就可以使用this.xxx
获取组件的属性了
详见代码:
// 创建$options并赋值父级传过来的数据
function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
const parentVnode = options._parentVnode
...
opts.propsData = parentVnode.componentOptions.propsData // 组件属性
...
}
...
// 初始化props
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._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
// 在此此处判断,如果当前vm不是根vm,就跳过递归绑定响应式(因为非根传过来的props本身就是响应式的数据)
if (!isRoot) {
// 把shouldObserve设为false 用于禁用递归绑定响应式
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm) // 校验value
if (process.env.NODE_ENV !== 'production') { // 非开发环境抛出错误
// 如果是保留属性抛出错误
...
defineReactive(props, key, value,set时候抛出不允许修改的错误)
} else {
defineReactive(props, key, value)
}
// 将props代理到vm上,允许我们通过this[key]访问到props里的属性
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
// 把shouldObserve设为true 用于开启递归绑定响应式,对应上边关闭
toggleObserving(true)
}
initInternalComponent
会判断如果是组件,就把上边说的props
中的内容存到vm.$options
上,此时你在子组件中this.$options
就可以看到了~
initProps
跳过前边的变量创建,该函数主要干了以下几件事
toggleObserving(false)
首先看toggleObserving(false)
这个函数的作用,这个函数会将响应式绑定中shouldObserve
设为false
,作用是改为false
后再调用响应式绑定的时候,不会递归其子集进行响应式绑定
比如我们从父级传到子集一个对象,对象在父级创建的时候就已经做过响应式绑定了,在传输的过程中,只是原封不动的拿了过来,然后对最外层做了响应式代理(外层做响应式代理目的是用于更新,下边会说道)
if (!isRoot)
的判断是因为如果是根vm的数据可能不是响应式的,要转换成响应式防止修改属性无法更新- 将父组件传过来的值绑定到vm._props中,并对其调用defineReactive进行响应式绑定
- 将props代理到vm上,允许我们通过this[key]直接访问到props里的属性
- 再次调用
toggleObserving(true)
把shouldObserve
还原为true
父组件的值更新后,子组件如何更新
答:父传子的实现思路就是获取到父级传过来的属性并定义在自己的props上边,当父组件传过来的参数更新时,会重新执行render函数生成虚拟dom,然后diff对比更新子组件
代码流程:
在父组件中更新属性,首先我们看diff算法
首先我们要知道diff算法会根据新老两套vnode
(虚拟dom)对比,排查节点是否可以复用,在排查到组件时,会找到老组件vnode
和对应的新组建vnode
,通过sameVnode
判断得出该组件可以复用,走patchVnode
,进行新老节点更替
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
...
// 在diff算法中
if () {
...
} else if (sameVnode(oldStartVnode, newStartVnode)) { // 新老节点相同 --- 头头比较:老头<->新头
// 走patchVnode(递归),进行新老节点更替
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}
...
}
patchVnode
方法中,会调用prepatch
方法,并在内部调用了updateChildComponent
方法,用于更新vm上的一系列属性(包括props
)
两个函数都放在下边
// patchVnode 的作用就是把新的 vnode patch 到旧的 vnode 上
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
...
/**
* 只有组件才会调用该函数
* prepatch函数创建位置:create-component.js
* prepatch函数内部调用了updateChildComponent方法(函数位置在lifecycle.js)
* 最终更新了组件实例(oldVnode.componentInstance)中的一系列属性,并更新vnode的组件实例
* (在内部会调用$forceUpdate())
*/
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
...
}
// prepatch 方法就是拿到新的 vnode 的组件配置以及组件实例,去执行 updateChildComponent 方法
// updateChildComponent是在instance/lifecycle.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
函数中会首先拿到子组件的vm._props
,注意上边有说过,这个_props
已经做过toggleObserving(false)
(禁用递归绑定响应式)状态下的响应式绑定了,然后在这里直接对其进行赋值操作,就可以通过响应式方法更新子组件中的数据了!(在这段代码中也有toggleObserving(false)
,在上边已经说过原因,这里就不再重复了:)
export function updateChildComponent (
vm: Component,
propsData: ?Object,
listeners: ?Object,
parentVnode: MountedComponentVNode,
renderChildren: ?Array<VNode>
) {
...
// 更新props
if (propsData && vm.$options.props) {
// 把shouldObserve设为false 用于禁用递归绑定响应式
toggleObserving(false)
const props = vm._props // 注意这里的_props是已经进行过响应式绑定的(外层与子组件watcher绑定)
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
}
...
}
---------end---------