16

keep-alive实现原理

 4 years ago
source link: https://juejin.im/post/5e1ed2635188254c46131aaf
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

keep-alive实现原理

结合例子有以下情况

<keep-alive>
    <coma v-if="visible"></coma>
    <comb v-else></comb>
</keep-alive>
<button @click="visible = !visible">更改</button>
复制代码

例如在comacomb都有一个input都有对应的value,如果我们不用keep-alive,当更改visible的时候,这两个组件都会重新渲染,先前输入的内容就会丢失,会执行一遍完整的生命周期流程:beforeCreate => created...。
但是如果我们用了keep-alive,那么在次切换visible的时候,input对应的value为上次更改时候的值。 所以keep-alive主要是用于保持组件的状态,避免组件反复创建。

keep-alive的使用方法定在core/components/keep-alive

export default {
    abstract: true,
    props: {
        include: patternTypes, // 缓存白名单
        exclude: patternTypes,  // 缓存黑名单
        max: [String, Number] // 缓存的实例上限
    },
    created() {
        // 用于缓存虚拟DOM
        this.cache = Object.create(null);
        this.keys = [];
    },
    mounted() {
    // 用于监听i黑白名单,如果发生调用pruneCache
    // pruneCache更新vue的cache缓存
        this.$watch('include', val => {
            pruneCache(this, name => matches(val, name))
        })
        this.$watch('exclude', val => {
            pruneCache(this, name => !matches(val, name))
        })
    }
    render() {
        //...
    }
}
复制代码

上面代码中定义了多个声明周期的操作,最重要的render函数,下面来看看是如何实现的

render

render () {
    const slot = this.$slots.default
    const vnode: VNode = getFirstComponentChild(slot) // 找到第一个子组件对象
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) { // 存在组件参数
      // check pattern
      const name: ?string = getComponentName(componentOptions) // 组件名
      const { include, exclude } = this
      if ( // 条件匹配
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      const { cache, keys } = this
      const key: ?string = vnode.key == null // 定义组件的缓存key
        // same constructor may get registered as different local components
        // so cid alone is not enough (#3269)
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
      if (cache[key]) { // 已经缓存过该组件
        vnode.componentInstance = cache[key].componentInstance
        // make current key freshest
        remove(keys, key)
        keys.push(key) // 调整key排序
      } else {
        cache[key] = vnode // 缓存组件对象
        keys.push(key)
        // prune oldest entry
        if (this.max && keys.length > parseInt(this.max)) { // 超过缓存数限制,将第一个删除
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }

      vnode.data.keepAlive = true // 渲染和执行被包裹组件的钩子函数需要用到
    }
    return vnode || (slot && slot[0])
  }
复制代码

进行分步骤进行分析

  1. 获取keep-alive对象包括的第一个子组件对象
  2. 根据白黑名单是否匹配返回本身的vnode
  3. 根据vnodecidtag生成的key,在缓存对象中是否有当前缓存,如果有则返回,并更新keykeys中的位置
  4. 如果当前缓存对象不存在缓存,就往cache添加这个的内容,并且根据LRU算法删除最近没有使用的实例
  5. 设置为第一个子组件对象的keep-alivetrue

结合文章开头的文章进行分析当前例子,当页面首次渲染的时候,因为组件的渲染过程是先子组件后父组件的,所以这里就能拿到子组件的数据,然后把子组件的vnode信息存储到cache中,并且把coma组件的keepAlive的置为true。 这个有个疑问,为什么能拿到子组件的componentOptions,借助上面个例子,我们知道生成vnode是通过render函数,render函数是通过在platforms/web/entry-runtime-with-compiler中定义,通过compileToFunctionstemplate编译为render函数,看一下生成的对应render函数

<template>
    <div class="parent">
        <keep-alive>
            <coma v-if="visible"></coma>
        <comb v-else></comb>
        </keep-alive>
    </div>
</template>
<script>
(function anonymous() {
  with(this) {
    return _c('div', {
      staticClass: "parent"
    }, [
      _c('keep-alive', [(visibility) ? _c('coma') : _c('comb')], 1), 
      _c('button', {
      on: {
        "click": change
      }
    }, [_v("change")])], 1)
  }
})
</script>
复制代码

可以看到生成的render函数中有关keep-alive的生成过程

 _c('keep-alive', [(visibility) ? _c('coma') : _c('comb')], 1),
复制代码

keep-alive中先调用了_c('coma'),所以才能访问到到子组件的componentOptions,具体的_c是在vdom/create-element.js中定义,他判断是生成组件vnode还是其他的。

更改data,触发patch

在首次渲染的时候,我们更改coma中的input的值,看当visible再次更改为true的时候,input是否会记住先前的值。因为更改了visible的值后,会重新执行这段代码

updateComponent = () => {
    vm._update(vm._render())
}
复制代码

所以就会重新执行keep-aliverender函数,因为在首次渲染的时候已经把数据存入到cache中,所以这次数据直接从cache中获取执行。

vnode.componentInstance = cache[key].componentInstance
复制代码

在首次渲染的时候提到当key值不存在的时候会先将子组件的vnode缓存起来,如果通过打断点的方式可以看到首次渲染的时候componentInstanceundefinedcomponentInstance实际是在patch过程中调用组件的init钩子才生成的,那么为什么这个时候能拿到呢,这里通过一个例子来进行讲解例如有下面例子

a = {
    b: 1
}
c = a;
a.b = 5;
console.log(c.b) // 5
复制代码

object是引用类型,所以原对象发生更改的时候引用的地方也会发生改变
那么就把先前的状态信息重新赋值给了coma,然后为什么赋值给了comacoma的就不会执行组件的创建过程呢,看patch的代码,当执行到createComponent的时候,因为coma为组件,就会执行组件相关的逻辑

// core/vdom/patch.js
function createComponent(vnode, insertedVnodeQueue, parentElm, refELm) {
    let i = vnode.data;
    if (isDef(i)) {
        const isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
        if (isDef(i = i.hook) && isDef(i = i.init)) {
            i(vnode, false);
        }
    }
}
// core/vdom/create-component
init(vnode) {
    if (vnode.componentInstance && 
        !vnode.componentInstance._isDetroyed &&
        vnode.data.keepAlive) {
            const mountedNode: any = node;
            componentVnodeHooks.prepatch(mountedNode, mountedNode)
    } else {
        const child = vnode.componentInstance = createComponentInstanceForVnode(
            vnode,
            activeInstance
        )
        child.$mount(vnode.elm)
    }
}
复制代码

因为vnode.componentInstancekeep-alive已经进行了重新赋值,所以并且keepAlivetrue,所以只会执行prepatch,所以createdmounted钩子都不会执行。

keep-alive本身创建和patch过程

core/instance/render中,可以看到updateComponent的定义

updateComponent = () => {
    vm._update(vm._render())
}
复制代码

所以首先调用keep-aliverender函数生成vnode,然后调用vm._update执行patch操作,那么keep-alive和普通组件在首次创建的时候和patch过程中有什么差异呢?

不管keep-alive是不是抽象组件,他终究是个也是个组件,所以也会执行组件相应的逻辑,在首次渲染的时候执行patch操作,执行到core/vdom/patch

function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    if (isDef(i)) {
         const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
        if (isDef(i = i.hook) && isDef(i = i.init)) {
            i(vnode, false /* hydrating */)
        }
    }
}
复制代码

因为是首次渲染所以componentInstance并不存在,所以只执行了init钩子,init的具体作用就是创建子组件实例。
keep-alive毕竟是抽象组件,那抽象组件和正常组件区别体现在哪儿呢? 在core/instance/lifecycle中可以看到,不是抽象组件的时候才会往父组件中加入本身,,并且子组件也不会往抽象组件$children中加入自己。这个函数又是在vm._init中进行调用的

let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }
复制代码

更改数据后patch过程

结合上面的例子,当visible发生更改的时候,会影响到keep-alive组件吗,在patch的那片文章提到过当data中的值发生改变的时候,会触发updateComponent

updateComponent = () => {
    vm._update(vm._render())
}
复制代码

就会重新执行keep-aliverender函数,重新执行根组件的patch过程,具体的原理课参照Vue 源码patch过程详解,这里就直接执行了keep-alive组件的prepatch钩子

这里有个问题需要解决一下,每次到达下一个tick的时候都需要进行重新生成vnode,这里有什么办法优化吗,能不能用其他方式来替换,还是说必须这么做?小伙伴能想到什么好的办法吗?

keep-alive是否是必须的

可以看到keep-alive对于缓存数据是有巨大帮助的,并且可以防止组件反复创建。那么就有问题了,是否绝大多数组件都可以使用keep-alive用于提高性能。

  1. 什么场景使用
    在页面中,我们如果返回上一个页面是会刷新数据的,如果我们需要保留离开页面时候的状态,那么就需要使用keep-alive
  2. 什么场景不使用
    先思考使用keep-alive是否有必要,如果两个组件切换是不需要保存状态的,那还需要吗。你可能说用keep-alive能节省性能,那我们在需要在activated重置这些属性。这样做有几点风险
    1. 你能确定把所有的变量都进行了重置了吗,这个风险是可控的吗
    2. 所有的缓存都放在了cache中,当组件过多的时候内容过多,就导致这个对象巨大,还能起到提高性能的需求吗,这个表示怀疑态度

Vue的源码分析的文章会一直更新,麻烦关注一下我的github


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK