A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

结合例子有以下情况
[JavaScript] 纯文本查看 复制代码
<keep-alive>
    <coma v-if="visible"></coma>
    <comb v-else></comb>
</keep-alive>
<button @click="visible = !visible">更改</button>

例如在coma和comb都有一个input都有对应的value,如果我们不用keep-alive,当更改visible的时候,这两个组件都会重新渲染,先前输入的内容就会丢失,会执行一遍完整的生命周期流程:beforeCreate => created...。
但是如果我们用了keep-alive,那么在次切换visible的时候,input对应的value为上次更改时候的值。所以keep-alive主要是用于保持组件的状态,避免组件反复创建。
原理keep-alive的使用方法定在core/components/keep-alive中
[JavaScript] 纯文本查看 复制代码
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
[JavaScript] 纯文本查看 复制代码
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])
  }

进行分步骤进行分析
  • 获取keep-alive对象包括的第一个子组件对象
  • 根据白黑名单是否匹配返回本身的vnode
  • 根据vnode的cid和tag生成的key,在缓存对象中是否有当前缓存,如果有则返回,并更新key在keys中的位置
  • 如果当前缓存对象不存在缓存,就往cache添加这个的内容,并且根据LRU算法删除最近没有使用的实例
  • 设置为第一个子组件对象的keep-alive为true
首次渲染结合文章开头的文章进行分析当前例子,当页面首次渲染的时候,因为组件的渲染过程是先子组件后父组件的,所以这里就能拿到子组件的数据,然后把子组件的vnode信息存储到cache中,并且把coma组件的keepAlive的置为true。这个有个疑问,为什么能拿到子组件的componentOptions,借助上面个例子,我们知道生成vnode是通过render函数,render函数是通过在platforms/web/entry-runtime-with-compiler中定义,通过compileToFunctions将template编译为render函数,看一下生成的对应render函数
[JavaScript] 纯文本查看 复制代码
<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的生成过程
[JavaScript] 纯文本查看 复制代码
 _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的值后,会重新执行这段代码
[JavaScript] 纯文本查看 复制代码
updateComponent = () => {
    vm._update(vm._render())
}

所以就会重新执行keep-alive的render函数,因为在首次渲染的时候已经把数据存入到cache中,所以这次数据直接从cache中获取执行。
[JavaScript] 纯文本查看 复制代码
vnode.componentInstance = cache[key].componentInstance

在首次渲染的时候提到当key值不存在的时候会先将子组件的vnode缓存起来,如果通过打断点的方式可以看到首次渲染的时候componentInstance为undefined,componentInstance实际是在patch过程中调用组件的init钩子才生成的,那么为什么这个时候能拿到呢,这里通过一个例子来进行讲解例如有下面例子
[JavaScript] 纯文本查看 复制代码
a = {
    b: 1
}
c = a;
a.b = 5;
console.log(c.b) // 5

object是引用类型,所以原对象发生更改的时候引用的地方也会发生改变
那么就把先前的状态信息重新赋值给了coma,然后为什么赋值给了coma,coma的就不会执行组件的创建过程呢,看patch的代码,当执行到createComponent的时候,因为coma为组件,就会执行组件相关的逻辑
[JavaScript] 纯文本查看 复制代码
// 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.componentInstance在keep-alive已经进行了重新赋值,所以并且keepAlive为true,所以只会执行prepatch,所以created、mounted钩子都不会执行。
keep-alive本身创建和patch过程在core/instance/render中,可以看到updateComponent的定义
[JavaScript] 纯文本查看 复制代码
updateComponent = () => {
    vm._update(vm._render())
}

所以首先调用keep-alive的render函数生成vnode,然后调用vm._update执行patch操作,那么keep-alive和普通组件在首次创建的时候和patch过程中有什么差异呢?
首次渲染不管keep-alive是不是抽象组件,他终究是个也是个组件,所以也会执行组件相应的逻辑,在首次渲染的时候执行patch操作,执行到core/vdom/patch中
[JavaScript] 纯文本查看 复制代码
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中进行调用的
[JavaScript] 纯文本查看 复制代码
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
[JavaScript] 纯文本查看 复制代码
updateComponent = () => {
    vm._update(vm._render())
}

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


文章转载自:https://juejin.im/post/5e1ed2635188254c46131aaf

0 个回复

您需要登录后才可以回帖 登录 | 加入黑马