13.6 流程分析

13.6.1 重新渲染组件

再次渲染的流程从数据改变说起,在这个例子中,动态组件中chooseTabs数据的变化会引起依赖派发更新的过程(这个系列有三篇文章详细介绍了vue响应式系统的底层实现,感兴趣的同学可以借鉴)。简单来说,chooseTabs这个数据在初始化阶段会收集使用到该数据的相关依赖。当数据发生改变时,收集过的依赖会进行派发更新操作。

其中,父组件中负责实例挂载的过程作为依赖会被执行,即执行父组件的vm._update(vm._render(), hydrating);_render_update分别代表两个过程,其中_render函数会根据数据的变化为组件生成新的Vnode节点,而_update最终会为新的Vnode生成真实的节点。而在生成真实节点的过程中,会利用vitrual domdiff算法对前后vnode节点进行对比,使之尽可能少的更改真实节点,这一部分内容可以回顾深入剖析Vue源码 - 来,跟我一起实现diff算法!,里面详细阐述了利用diff算法进行节点差异对比的思路。

patch是新旧Vnode节点对比的过程,而patchVnode是其中核心的步骤,我们忽略patchVnode其他的流程,关注到其中对子组件执行prepatch钩子的过程中。

  1. function patchVnode (oldVnode,vnode,insertedVnodeQueue,ownerArray,index,removeOnly) {
  2. ···
  3. // 新vnode 执行prepatch钩子
  4. if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
  5. i(oldVnode, vnode);
  6. }
  7. ···
  8. }

执行prepatch钩子时会拿到新旧组件的实例并执行updateChildComponent函数。而updateChildComponent会对针对新的组件实例对旧实例进行状态的更新,包括props,listeners等,最终会调用vue提供的全局vm.$forceUpdate()方法进行实例的重新渲染。

  1. var componentVNodeHooks = {
  2. // 之前分析的init钩子
  3. init: function() {},
  4. prepatch: function prepatch (oldVnode, vnode) {
  5. // 新组件实例
  6. var options = vnode.componentOptions;
  7. // 旧组件实例
  8. var child = vnode.componentInstance = oldVnode.componentInstance;
  9. updateChildComponent(
  10. child,
  11. options.propsData, // updated props
  12. options.listeners, // updated listeners
  13. vnode, // new parent vnode
  14. options.children // new children
  15. );
  16. },
  17. }
  18. function updateChildComponent() {
  19. // 更新旧的状态,不分析这个过程
  20. ···
  21. // 迫使实例重新渲染。
  22. vm.$forceUpdate();
  23. }

先看看$forceUpdate做了什么操作。$forceUpdate是源码对外暴露的一个api,他们迫使Vue实例重新渲染,本质上是执行实例所收集的依赖,在例子中watcher对应的是keep-alivevm._update(vm._render(), hydrating);过程。

  1. Vue.prototype.$forceUpdate = function () {
  2. var vm = this;
  3. if (vm._watcher) {
  4. vm._watcher.update();
  5. }
  6. };

13.6.2 重用缓存组件

由于vm.$forceUpdate()会强迫keep-alive组件进行重新渲染,因此keep-alive组件会再一次执行render过程。这一次由于第一次对vnode的缓存,keep-alive在实例的cache对象中找到了缓存的组件。

  1. // keepalive组件选项
  2. var keepAlive = {
  3. name: 'keep-alive',
  4. abstract: true,
  5. render: function render () {
  6. // 拿到keep-alive下插槽的值
  7. var slot = this.$slots.default;
  8. // 第一个vnode节点
  9. var vnode = getFirstComponentChild(slot);
  10. // 拿到第一个组件实例
  11. var componentOptions = vnode && vnode.componentOptions;
  12. // keep-alive的第一个子组件实例存在
  13. if (componentOptions) {
  14. // check pattern
  15. //拿到第一个vnode节点的name
  16. var name = getComponentName(componentOptions);
  17. var ref = this;
  18. var include = ref.include;
  19. var exclude = ref.exclude;
  20. // 通过判断子组件是否满足缓存匹配
  21. if (
  22. // not included
  23. (include && (!name || !matches(include, name))) ||
  24. // excluded
  25. (exclude && name && matches(exclude, name))
  26. ) {
  27. return vnode
  28. }
  29. var ref$1 = this;
  30. var cache = ref$1.cache;
  31. var keys = ref$1.keys;
  32. var key = vnode.key == null ? componentOptions.Ctor.cid + (componentOptions.tag ? ("::" + (componentOptions.tag)) : '')
  33. : vnode.key;
  34. // ==== 关注点在这里 ====
  35. if (cache[key]) {
  36. // 直接取出缓存组件
  37. vnode.componentInstance = cache[key].componentInstance;
  38. // keys命中的组件名移到数组末端
  39. remove(keys, key);
  40. keys.push(key);
  41. } else {
  42. // 初次渲染时,将vnode缓存
  43. cache[key] = vnode;
  44. keys.push(key);
  45. // prune oldest entry
  46. if (this.max && keys.length > parseInt(this.max)) {
  47. pruneCacheEntry(cache, keys[0], keys, this._vnode);
  48. }
  49. }
  50. vnode.data.keepAlive = true;
  51. }
  52. return vnode || (slot && slot[0])
  53. }
  54. }

render函数前面逻辑可以参考前一篇文章,由于cache对象中存储了再次使用的vnode对象,所以直接通过cache[key]取出缓存的组件实例并赋值给vnodecomponentInstance属性。可能在读到这里的时候,会对源码中keys这个数组的作用,以及pruneCacheEntry的功能有疑惑,这里我们放到文章末尾讲缓存优化策略时解答。

13.6.3 真实节点的替换

执行了keep-alive组件的_render过程,接下来是_update产生真实的节点,同样的,keep-alive下有child1子组件,所以_update过程会调用createComponent递归创建子组件vnode,这个过程在初次渲染时也有分析过,我们可以对比一下,再次渲染时流程有哪些不同。

  1. function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  2. // vnode为缓存的vnode
  3. var i = vnode.data;
  4. if (isDef(i)) {
  5. // 此时isReactivated为true
  6. var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
  7. if (isDef(i = i.hook) && isDef(i = i.init)) {
  8. i(vnode, false /* hydrating */);
  9. }
  10. if (isDef(vnode.componentInstance)) {
  11. // 其中一个作用是保留真实dom到vnode中
  12. initComponent(vnode, insertedVnodeQueue);
  13. insert(parentElm, vnode.elm, refElm);
  14. if (isTrue(isReactivated)) {
  15. reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
  16. }
  17. return true
  18. }
  19. }
  20. }

此时的vnode是缓存取出的子组件vnode,并且由于在第一次渲染时对组件进行了标记vnode.data.keepAlive = true;,所以isReactivated的值为true,i.init依旧会执行子组件的初始化过程。但是这个过程由于有缓存,所以执行过程也不完全相同。

  1. var componentVNodeHooks = {
  2. init: function init (vnode, hydrating) {
  3. if (
  4. vnode.componentInstance &&
  5. !vnode.componentInstance._isDestroyed &&
  6. vnode.data.keepAlive
  7. ) {
  8. // 当有keepAlive标志时,执行prepatch钩子
  9. var mountedNode = vnode; // work around flow
  10. componentVNodeHooks.prepatch(mountedNode, mountedNode);
  11. } else {
  12. var child = vnode.componentInstance = createComponentInstanceForVnode(
  13. vnode,
  14. activeInstance
  15. );
  16. child.$mount(hydrating ? vnode.elm : undefined, hydrating);
  17. }
  18. },
  19. }

显然因为有keepAlive的标志,所以子组件不再走挂载流程,只是执行prepatch钩子对组件状态进行更新。并且很好的利用了缓存vnode之前保留的真实节点进行节点的替换。