为了组件的缓存优化而使用 组件

内置组件

定义在src/core/components/keep-alive.js中

  1. export default { // 一个对象
  2. name: 'keep-alive',
  3. abstract: true, // 抽象组件
  4. props: {
  5. // 字符串或者表达式
  6. include: patternTypes, // 只有匹配的组件会被缓存
  7. exclude: patternTypes, // 任何匹配的组件都不会被缓存
  8. max: [String, Number] // 缓存的大小
  9. // 缓存的是vnode对象,也会缓存DOM,当缓存很多时会比较占用内存,所以max允许配置指定缓存大小
  10. },
  11. methods: {
  12. cacheVNode() {
  13. const { cache, keys, vnodeToCache, keyToCache } = this
  14. if (vnodeToCache) {
  15. const { tag, componentInstance, componentOptions } = vnodeToCache
  16. cache[keyToCache] = {
  17. name: getComponentName(componentOptions),
  18. tag,
  19. componentInstance,
  20. }
  21. keys.push(keyToCache)
  22. // prune oldest entry
  23. if (this.max && keys.length > parseInt(this.max)) {
  24. pruneCacheEntry(cache, keys[0], keys, this._vnode)
  25. }
  26. this.vnodeToCache = null
  27. }
  28. }
  29. },
  30. created () {
  31. // 缓存已经创建过的vnode
  32. this.cache = Object.create(null)
  33. this.keys = []
  34. },
  35. destroyed () {
  36. for (const key in this.cache) {
  37. pruneCacheEntry(this.cache, key, this.keys)
  38. }
  39. },
  40. mounted () {
  41. this.cacheVNode()
  42. this.$watch('include', val => {
  43. pruneCache(this, name => matches(val, name))
  44. })
  45. this.$watch('exclude', val => {
  46. pruneCache(this, name => !matches(val, name))
  47. })
  48. },
  49. updated () {
  50. this.cacheVNode()
  51. },
  52. // 实现了render函数来执行组件渲染
  53. render () {
  54. // 获取第一个元素的vnode
  55. // 在<keep-alive>内部写DOM,可以先获取到它的默认插槽,然后在获取到它的第一个子节点
  56. // <keep-alive>只处理第一个子元素,所以一般和它搭配使用的有component动态组件或者是router-view
  57. const slot = this.$slots.default
  58. const vnode: VNode = getFirstComponentChild(slot)
  59. const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
  60. if (componentOptions) {
  61. // check pattern
  62. // 判断当前组件的名称和include、exclude的关系
  63. const name: ?string = getComponentName(componentOptions)
  64. const { include, exclude } = this
  65. // 传的include和exclude可以是数组、字符串、正则表达式三种中任意一种
  66. // 如果组件名满足了配置include且不匹配或者是配置了exclude且匹配就直接返回这个组件的vnode,否则走下一步缓存
  67. if (
  68. // not included
  69. (include && (!name || !matches(include, name))) ||
  70. // excluded
  71. (exclude && name && matches(exclude, name))
  72. ) {
  73. return vnode
  74. }
  75. const { cache, keys } = this
  76. const key: ?string = vnode.key == null
  77. // same constructor may get registered as different local components
  78. // so cid alone is not enough (#3269)
  79. ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
  80. : vnode.key
  81. // 命中缓存-直接从缓存中拿vnode的组件实例,并且重新调整key的顺序放在最后一个
  82. if (cache[key]) {
  83. vnode.componentInstance = cache[key].componentInstance
  84. // make current key freshest
  85. remove(keys, key)
  86. keys.push(key)
  87. } else {
  88. // delay setting the cache until update 将缓存设置延迟到更新
  89. this.vnodeToCache = vnode
  90. this.keyToCache = key
  91. }
  92. vnode.data.keepAlive = true
  93. }
  94. return vnode || (slot && slot[0])
  95. }
  96. }
  97. // 做匹配
  98. function matches (pattern: string | RegExp | Array<string>, name: string): boolean {
  99. if (Array.isArray(pattern)) { // 处理数组
  100. return pattern.indexOf(name) > -1
  101. } else if (typeof pattern === 'string') { // 处理字符串
  102. return pattern.split(',').indexOf(name) > -1
  103. } else if (isRegExp(pattern)) { // 处理正则表达式
  104. return pattern.test(name)
  105. }
  106. /* istanbul ignore next */
  107. return false
  108. }
  109. // 缓存vnode
  110. cacheVNode() {
  111. const { cache, keys, vnodeToCache, keyToCache } = this
  112. if (vnodeToCache) {
  113. const { tag, componentInstance, componentOptions } = vnodeToCache
  114. cache[keyToCache] = {
  115. name: getComponentName(componentOptions),
  116. tag,
  117. componentInstance,
  118. }
  119. keys.push(keyToCache)
  120. // prune oldest entry
  121. // 配置了max,并且缓存的长度超过了this.max,还要从缓存中删除第一个
  122. if (this.max && keys.length > parseInt(this.max)) {
  123. pruneCacheEntry(cache, keys[0], keys, this._vnode)
  124. }
  125. this.vnodeToCache = null
  126. }
  127. }
  128. // 删除缓存入口
  129. function pruneCacheEntry (
  130. cache: CacheEntryMap,
  131. key: string,
  132. keys: Array<string>,
  133. current?: VNode
  134. ) {
  135. const entry: ?CacheEntry = cache[key]
  136. // 如果要删除的缓存的组件tag不是当前渲染组件tag
  137. if (entry && (!current || entry.tag !== current.tag)) {
  138. // 执行删除缓存的组件实例的$destroy方法
  139. entry.componentInstance.$destroy()
  140. }
  141. cache[key] = null
  142. remove(keys, key)
  143. }

在组件实例建立父子关系的时候会被忽略,发生在 initLifecycle 的过程中

  1. // locate first non-abstract parent
  2. var parent = options.parent;
  3. if (parent && !options.abstract) {
  4. while (parent.$options.abstract && parent.$parent) {
  5. parent = parent.$parent;
  6. }
  7. parent.$children.push(vm);
  8. }
  9. vm.$parent = parent;

注意, 组件也是为观测 include 和 exclude 的变化,对缓存做处理

  1. // 观测变化执行pruneCache函数
  2. this.$watch('include', val => {
  3. pruneCache(this, name => matches(val, name))
  4. })
  5. this.$watch('exclude', val => {
  6. pruneCache(this, name => !matches(val, name))
  7. })
  8. function pruneCache (keepAliveInstance: any, filter: Function) {
  9. const { cache, keys, _vnode } = keepAliveInstance
  10. // 对cache做遍历
  11. for (const key in cache) {
  12. const entry: ?CacheEntry = cache[key]
  13. // 发现缓存的节点名称和新的规则没有匹配上时
  14. if (entry) {
  15. const name: ?string = entry.name
  16. if (name && !filter(name)) {
  17. // 把这个缓存节点从缓存中摘除
  18. pruneCacheEntry(cache, key, keys, _vnode)
  19. }
  20. }
  21. }
  22. }

组件渲染

了解了 的组件实现,但并不知道它包裹的子组件渲染和普通组件有什么不一样

例子

  1. let A = {
  2. template: '<div class="a">' +
  3. '<p>A Comp</p>' +
  4. '</div>',
  5. name: 'A'
  6. }
  7. let B = {
  8. template: '<div class="b">' +
  9. '<p>B Comp</p>' +
  10. '</div>',
  11. name: 'B'
  12. }
  13. let vm = new Vue({
  14. el: '#app',
  15. template: '<div>' +
  16. '<keep-alive>' +
  17. '<component :is="currentComp">' +
  18. '</component>' +
  19. '</keep-alive>' +
  20. '<button @click="change">switch</button>' +
  21. '</div>',
  22. data: {
  23. currentComp: 'A'
  24. },
  25. methods: {
  26. change() {
  27. this.currentComp = this.currentComp === 'A' ? 'B' : 'A'
  28. }
  29. },
  30. components: {
  31. A,
  32. B
  33. }
  34. })

首次渲染

Vue 的渲染最后都会到 patch 过程,而组件的 patch 过程会执行 createComponent 方法,它的定义在 src/core/vdom/patch.js 中

  1. function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  2. let i = vnode.data
  3. if (isDef(i)) {
  4. // 第一次渲染时vnode.componentInstance为undefined和vnode.data.keepAlive为true
  5. // 因为它的父组件 <keep-alive> 的 render 函数会先执行,那么该 vnode 缓存到内存中,并且设置 vnode.data.keepAlive 为 true
  6. const isReactivated = isDef(vnode.componentInstance) && i.keepAlive // false
  7. // 走正常的init钩子函数执行组件的mount
  8. if (isDef(i = i.hook) && isDef(i = i.init)) {
  9. i(vnode, false /* hydrating */)
  10. }
  11. // after calling the init hook, if the vnode is a child component
  12. // it should've created a child instance and mounted it. the child
  13. // component also has set the placeholder vnode's elm.
  14. // in that case we can just return the element and be done.
  15. if (isDef(vnode.componentInstance)) {
  16. // 当vnode已经执行完patch后执行initComponent函数
  17. initComponent(vnode, insertedVnodeQueue)
  18. insert(parentElm, vnode.elm, refElm)
  19. if (isTrue(isReactivated)) {
  20. reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
  21. }
  22. return true
  23. }
  24. }
  25. }

initComponent

  1. function initComponent (vnode, insertedVnodeQueue) {
  2. if (isDef(vnode.data.pendingInsert)) {
  3. insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
  4. vnode.data.pendingInsert = null
  5. }
  6. // 缓存了vnode创建生成的DOM节点
  7. vnode.elm = vnode.componentInstance.$el
  8. if (isPatchable(vnode)) {
  9. invokeCreateHooks(vnode, insertedVnodeQueue)
  10. setScope(vnode)
  11. } else {
  12. // empty component root.
  13. // skip all element-related modules except for ref (#3455)
  14. registerRef(vnode)
  15. // make sure to invoke the insert hook
  16. insertedVnodeQueue.push(vnode)
  17. }
  18. }

对于首次渲染而言,除了在 中建立缓存,和普通组件渲染没什么区别
对于例子,初始化渲染 A 组件以及第一次点击 switch 渲染 B 组件,都是首次渲染

缓存渲染

当从 B 组件再次点击 switch 切换到 A 组件,就会命中缓存渲染
当数据发送变化,在 patch 的过程中会执行 patchVnode 的逻辑,它会对比新旧 vnode 节点,甚至对比它们的子节点去做更新逻辑,但是对于组件 vnode 而言,是没有 children 的,那么对于 组件而言,如何更新它包裹的内容呢?
原来 patchVnode 在做各种 diff 之前,会先执行 prepatch 的钩子函数,
定义在 src/core/vdom/create-component 中

  1. const componentVNodeHooks = {
  2. // ...
  3. prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
  4. const options = vnode.componentOptions
  5. const child = vnode.componentInstance = oldVnode.componentInstance
  6. updateChildComponent(
  7. child,
  8. options.propsData, // updated props
  9. options.listeners, // updated listeners
  10. vnode, // new parent vnode
  11. options.children // new children
  12. )
  13. },
  14. // ...
  15. }

updateChildComponent定义在src/core/instance/lifecycle.js中

  1. // 更新组件实例的一些属性
  2. export function updateChildComponent (
  3. vm: Component,
  4. propsData: ?Object,
  5. listeners: ?Object,
  6. parentVnode: MountedComponentVNode,
  7. renderChildren: ?Array<VNode>
  8. ) {
  9. // ...
  10. // Any static slot children from the parent may have changed during parent's
  11. // update. Dynamic scoped slots may also have changed. In such cases, a forced
  12. // update is necessary to ensure correctness.
  13. const needsForceUpdate = !!(
  14. renderChildren || // has new static slots
  15. vm.$options._renderChildren || // has old static slots
  16. hasDynamicScopedSlot
  17. )
  18. // ...
  19. // resolve slots + force update if has children
  20. if (needsForceUpdate) {
  21. vm.$slots = resolveSlots(renderChildren, parentVnode.context)
  22. vm.$forceUpdate()
  23. }
  24. // ...
  25. }

由于 组件本质上支持了 slot,所以它执行 prepatch 的时候,需要对自己的 children,也就是这些 slots 做重新解析,并触发 组件实例 $forceUpdate 逻辑,也就是重新执行 的 render 方法,这个时候如果它包裹的第一个组件 vnode 命中缓存,则直接返回缓存中的 vnode.componentInstance
在例子中就是缓存的 A 组件,接着又会执行 patch 过程,再次执行到 createComponent 方法

  1. function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  2. let i = vnode.data
  3. if (isDef(i)) {
  4. const isReactivated = isDef(vnode.componentInstance) && i.keepAlive // true
  5. if (isDef(i = i.hook) && isDef(i = i.init)) {
  6. i(vnode, false /* hydrating */)
  7. }
  8. // after calling the init hook, if the vnode is a child component
  9. // it should've created a child instance and mounted it. the child
  10. // component also has set the placeholder vnode's elm.
  11. // in that case we can just return the element and be done.
  12. if (isDef(vnode.componentInstance)) {
  13. initComponent(vnode, insertedVnodeQueue)
  14. insert(parentElm, vnode.elm, refElm)
  15. if (isTrue(isReactivated)) {
  16. reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
  17. }
  18. return true
  19. }
  20. }
  21. }

在执行 init 钩子函数的时候不会再执行组件的mount过程了,相关逻辑在src/core/vdom/create-component.js中

  1. const componentVNodeHooks = {
  2. init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
  3. if (
  4. vnode.componentInstance &&
  5. !vnode.componentInstance._isDestroyed &&
  6. vnode.data.keepAlive
  7. ) {
  8. // kept-alive components, treat as a patch
  9. const mountedNode: any = vnode // work around flow
  10. componentVNodeHooks.prepatch(mountedNode, mountedNode)
  11. } else {
  12. const child = vnode.componentInstance = createComponentInstanceForVnode(
  13. vnode,
  14. activeInstance
  15. )
  16. child.$mount(hydrating ? vnode.elm : undefined, hydrating)
  17. }
  18. },
  19. }

这也就是被 包裹的组件在有缓存的时候就不会在执行组件的 created、mounted 等钩子函数的原因
reactivateComponent

  1. function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  2. let i
  3. // hack for #4339: a reactivated component with inner transition
  4. // does not trigger because the inner node's created hooks are not called
  5. // again. It's not ideal to involve module-specific logic in here but
  6. // there doesn't seem to be a better way to do it.
  7. // 解决对 reactived 组件 transition 动画不触发的问题
  8. let innerNode = vnode
  9. while (innerNode.componentInstance) {
  10. innerNode = innerNode.componentInstance._vnode
  11. if (isDef(i = innerNode.data) && isDef(i = i.transition)) {
  12. for (i = 0; i < cbs.activate.length; ++i) {
  13. cbs.activate[i](emptyNode, innerNode)
  14. }
  15. insertedVnodeQueue.push(innerNode)
  16. break
  17. }
  18. }
  19. // unlike a newly created component,
  20. // a reactivated keep-alive component doesn't insert itself
  21. // 把缓存的DOM对象直接插入到目标元素中,这样就完成了数据更新的情况下渲染的过程
  22. insert(parentElm, vnode.elm, refElm)
  23. }

生命周期

组件一旦被 缓存,那么再次渲染的时候就不会执行 created、mounted 等钩子函数,但是很多业务场景都是希望被缓存的组件再次被渲染的时候做一些事情,好在 Vue 提供了 activated 钩子函数,它的执行时机是 包裹的组件渲染的时候
在渲染的最后一步,会执行 invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) 函数执行 vnode 的 insert 钩子函数,它的定义在 src/core/vdom/create-component.js 中

  1. const componentVNodeHooks = {
  2. // ...
  3. insert (vnode: MountedComponentVNode) {
  4. const { context, componentInstance } = vnode
  5. if (!componentInstance._isMounted) {
  6. componentInstance._isMounted = true
  7. callHook(componentInstance, 'mounted')
  8. }
  9. if (vnode.data.keepAlive) {
  10. if (context._isMounted) { // 判断被<keep-alive>包裹的组件已经mounted
  11. // vue-router#1212
  12. // During updates, a kept-alive component's child components may
  13. // change, so directly walking the tree here may call activated hooks
  14. // on incorrect children. Instead we push them into a queue which will
  15. // be processed after the whole patch process ended.
  16. queueActivatedComponent(componentInstance)
  17. } else {
  18. activateChildComponent(componentInstance, true /* direct */)
  19. }
  20. }
  21. },
  22. // ...
  23. }

activateChildComponent

  1. export function activateChildComponent (vm: Component, direct?: boolean) {
  2. if (direct) {
  3. vm._directInactive = false
  4. if (isInInactiveTree(vm)) {
  5. return
  6. }
  7. } else if (vm._directInactive) {
  8. return
  9. }
  10. if (vm._inactive || vm._inactive === null) {
  11. vm._inactive = false
  12. // 递归执行它的所有子组件的activated钩子函数
  13. for (let i = 0; i < vm.$children.length; i++) {
  14. activateChildComponent(vm.$children[i])
  15. }
  16. // 执行组件的activated钩子函数
  17. callHook(vm, 'activated')
  18. }
  19. }

queueActivatedComponent定义在 src/core/observer/scheduler.js 中

  1. /**
  2. * Queue a kept-alive component that was activated during patch.
  3. * The queue will be processed after the entire tree has been patched.
  4. */
  5. export function queueActivatedComponent (vm: Component) {
  6. // setting _inactive to false here so that a render function can
  7. // rely on checking whether it's in an inactive tree (e.g. router-view)
  8. vm._inactive = false
  9. // 把当前vm实例添加到activatedChildren数组中,等所有的渲染完毕后在nextTick后会执行flushSchedulerQueue
  10. activatedChildren.push(vm)
  11. }

flushSchedulerQueue

  1. **
  2. * Flush both queues and run the watchers.
  3. */
  4. function flushSchedulerQueue () {
  5. // ...
  6. // keep copies of post queues before resetting state
  7. const activatedQueue = activatedChildren.slice()
  8. const updatedQueue = queue.slice()
  9. resetSchedulerState()
  10. // call component updated and activated hooks
  11. // 遍历所有的activatedChildren
  12. callActivatedHooks(activatedQueue)
  13. // ...
  14. }
  15. function callActivatedHooks (queue) {
  16. // 执行activateChildComponent方法,通过队列的方式就是把整个activated时机延后了
  17. for (let i = 0; i < queue.length; i++) {
  18. queue[i]._inactive = true
  19. activateChildComponent(queue[i], true /* true */)
  20. }
  21. }

有 activated 钩子函数,也就有对应的 deactivated 钩子函数,它是发生在 vnode 的 destory 钩子函数,定义在 src/core/vdom/create-component.js 中

  1. const componentVNodeHooks = {
  2. // ...
  3. destroy (vnode: MountedComponentVNode) {
  4. const { componentInstance } = vnode
  5. if (!componentInstance._isDestroyed) {
  6. if (!vnode.data.keepAlive) {
  7. componentInstance.$destroy()
  8. } else {
  9. deactivateChildComponent(componentInstance, true /* direct */)
  10. }
  11. }
  12. }
  13. }

对于 包裹的组件而言,它会执行 deactivateChildComponent(componentInstance, true) 方法,定义在 src/core/instance/lifecycle.js 中

  1. export function deactivateChildComponent (vm: Component, direct?: boolean) {
  2. if (direct) {
  3. vm._directInactive = true
  4. if (isInInactiveTree(vm)) {
  5. return
  6. }
  7. }
  8. if (!vm._inactive) {
  9. vm._inactive = true
  10. // 递归执行它的所有子组件的deactivated钩子函数
  11. for (let i = 0; i < vm.$children.length; i++) {
  12. deactivateChildComponent(vm.$children[i])
  13. }
  14. // 执行组件的deactivated钩子函数
  15. callHook(vm, 'deactivated')
  16. }
  17. }

组件是一个抽象组件,它的实现通过自定义 render 函数并且利用了插槽,并且知道了 缓存 vnode,了解组件包裹的子元素——也就是插槽是如何做更新的。且在 patch 过程中对于已缓存的组件不会执行 mounted,所以不会有一般的组件的生命周期函数但是又提供了 activated 和 deactivated 钩子函数
另外还知道了 的 props 除了 include 和 exclude 还有文档中没有提到的 max,它能控制缓存的个数