组件的重新渲染
Vue 的响应式更新精确到组件级别?
举个例子:
我们在触发 this.msg = 'Hello, Changed~'的时候,会触发组件的更新,视图的重新渲染。
但是 <ChildComponent />
这个组件其实是不会重新渲染的,这是 Vue 刻意而为之的。
<template>
<div>
{{ msg }}
<ChildComponent />
</div>
</template>
因为之前我看React源码的时候发现他是一种递归的更新(内部jsx解析成虚拟树对象,diff的时候有一个tag可以区分是普通标签还是组件,是组件就dfs),以为Vue也是这么干的,但是在之后发现官网说更新粒度是组件级别,就很疑惑,如何办到的,都是比对虚拟树。
React中的Diff粒度
React 中假如 ChildComponent 里还有十层嵌套子元素,那么所有层次都会递归的重新render(在不进行手动优化的情况下),这是性能上的灾难。 React有两个不就措施,一个是手动写shouldComponentUpdate(memo)对比更新前后的props是否改变,还有一个就是facebook的React团队重构了大量代码加上了异步渲染的fiber架构。
Vue中的粒度
注意:Vue不会深入到组件内部进行更新!本来这里应该递归深入Vue却没有,那如果这个组件props改变了视图应该更新怎么办?
在diff的过程中,Vue会对 component 上声明的 props、listeners等属性进行更新,而不会深入到组件内部进行更新。那是如何办到props改变的组件才更新的?
每个组件都有自己的渲染 watcher,它掌管了当前组件的视图更新,这个看过响应式原理的应该都知道,但是并不会掌管 ChildComponent 的更新,而是通过对props的响应式处理来更新。
msg 在传给子组件的时候,会被保存在子组件实例的 _props 上,并且被定义成了响应式属性,而子组件的模板中对于 msg 的访问其实是被代理到 _props.msg 上去的,所以自然也能精确的收集到依赖,只要 ChildComponent 在模板里也读取了这个属性,ChildComponent的watcher会被父组件Dep收集。
详解Props对组件更新的影响
Props 的初始化主要发生在 new Vue 中的 initState 阶段,在 src/core/instance/state.js 中:
initProps 主要做 3 件事情:校验、响应式和代理。
子组件重新渲染
我们知道,当父组件传递给子组件的 props 值变化,子组件对应的值也会改变,同时会触发子组件的重新渲染。
其实子组件的重新渲染有 2 种情况,一个是 prop 值被修改,另一个是对象类型的 prop 内部属性的变化。
子组件 prop 值被修改
先来看一下 prop 值被修改的情况,当执行 props[key] = validateProp(key, propOptions, propsData, vm) 更新子组件 prop 的时候,会触发 prop 的 setter 过程,只要在渲染子组件的时候访问过这个 prop 值,那么根据响应式原理,就会触发子组件的重新渲染。
父组件prop改变
- 当对象类型的 prop 的内部属性发生变化的时候,这个时候其实并没有触发子组件 prop 的更新。
- 但是在子组件的渲染过程中,访问过这个对象 prop,所以这个对象 prop 在触发 getter 的时候会把子组件的 render watcher 收集到依赖中(全局的Dep.target在子组件渲染是肯定指向子组件的watcher),现在父组件和子组件的watcher都被收集了
- 然后当我们在父组件更新这个对象 prop 的某个属性的时候,会触发 setter 过程,也就会通知子组件 render watcher 的 update,进而触发子组件的重新渲染。
源码角度分析
在父组件重新渲染的最后,会执行 patch 过程,进而执行 patchVnode 函数,patchVnode 通常是一个递归过程,当它遇到组件 vnode 的时候,会执行组件更新过程的 prepatch 钩子函数,在 src/core/vdom/patch.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
)
}
这里的 propsData 是父组件传递的 props 数据,vm 是子组件的实例。vm._props 指向的就是子组件的 props 值,propKeys 就是在之前 initProps 过程中,缓存的子组件中定义的所有 prop 的 key。
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
}
// ...
}
主要逻辑就是遍历 propKeys,然后执行 props[key] = validateProp(key, propOptions, propsData, vm) 重新验证和计算新的 prop 数据,更新 vm._props,也就是子组件的 props,这个就是子组件 props 的更新过程。
总结父节点更新 子组件如何更新
- 父节点更新对比两颗虚拟树
- 在patch的过程中对应子组件类型会执行prePatch钩子函数
- prePatch内部执行updateChildComponent
- updateChildComponent内部会拿到新的props,然后赋值给子组件的props
- 在这个props赋值给子组件的props时,触发setter响应式
- 然后就会触发子组件的渲染Watcher的update
- 然后就执行到了子组件的patch