跳到主要内容

组件的重新渲染

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