Vue2总结(十)异步

本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

原文链接:blog.ouyangsihai.cn >> Vue2总结(十)异步

目前社区有很多 Vue.js 的源码解读文章,但是质量层次不齐,不够系统和全面,最近我将从各个细枝末节解读 Vue.js 的实现原理,针对 vue3 版本之前,让同学们可以彻底掌握 Vue.js。 友情提示:阅读本文大概需要** 34****分钟**

前言

随着项目越来越大,性能问题已经成为了困扰业务发展的重要因素。功能不停地累加后,核心页面已经不堪重负,访问速度愈来愈慢。业务发展、用户体验都非常迫切的需要释放页面的负载,提高页面加载速度,今天就一起来谈谈 vue 项目中的异步与keep-alive缓存。

Vue2总结(十)异步

异步组件

在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。例如:


// 官网示例
Vue.component('async-example', function (resolve, reject) {
  setTimeout(function () {
    // 向 `resolve` 回调传递组件定义
    resolve({
      template: 'divI am async!/div'
    })
  }, 1000)
})

如你所见,这个工厂函数会收到一个 resolve 回调,这个回调函数会在你从服务器得到组件定义的时候被调用。你也可以调用 reject(reason) 来表示加载失败。这里的 setTimeout 是为了演示用的,如何获取组件取决于你自己。一个推荐的做法是将异步组件和 webpack 的 code-splitting 功能一起配合使用:


// 官网示例
Vue.component('async-webpack-example', function (resolve) {
  // 这个特殊的 `require` 语法将会告诉 webpack
  // 自动将你的构建代码切割成多个包,这些包
  // 会通过 Ajax 请求加载
  require(['./my-async-component'], resolve)
})

你也可以在工厂函数中返回一个 Promise,所以把 webpack 2 和 ES2015 语法加在一起,我们可以写成这样:


// 官网示例
Vue.component(
  'async-webpack-example',
  // 这个 `import` 函数会返回一个 `Promise` 对象。
  () = import('./my-async-component')
)

当使用局部注册的时候,你也可以直接提供一个返回 Promise 的函数:


// 官网示例
new Vue({
  // ...
  components: {
    'my-component': () = import('./my-async-component')
  }
})

如果你是一个 Browserify 用户同时喜欢使用异步组件,很不幸这个工具的作者明确表示异步加载“并不会被 Browserify 支持”,至少官方不会。Browserify 社区已经找到了一些变通方案,这些方案可能会对已存在的复杂应用有帮助。对于其它的场景,我们推荐直接使用 webpack,以拥有内置的头等异步支持。

处理加载状态

在vue2.3中新增了处理加载状态,这里的异步组件工厂函数也可以返回一个如下格式的对象:


const AsyncComponent = () = ({
  // 需要加载的组件 (应该是一个 `Promise` 对象)
  component: import('./MyComponent.vue'),
  // 异步组件加载时使用的组件
  loading: LoadingComponent,
  // 加载失败时使用的组件
  error: ErrorComponent,
  // 展示加载时组件的延时时间。默认值是 200 (毫秒)
  delay: 200,
  // 如果提供了超时时间且组件加载也超时了,
  // 则使用加载失败时使用的组件。默认值是:`Infinity`
  timeout: 3000
})

keep-alive

keep-alive是Vue.js的一个内置组件。它能够不活动的组件实例保存在内存中,而不是直接将其销毁,它是一个抽象组件,不会被渲染到真实DOM中,也不会出现在父组件链中。它提供了include与exclude两个属性,允许组件有条件地进行缓存。

用法

// 这里的component组件会被缓存起来
keep-alive
    component/component
/keep-alive
举个栗子

keep-alive
    coma v-if="test"/coma
    comb v-else/comb
/keep-alive
button @click="test=handleClick"请点击/button

export default {
    data () {
        return {
            test: true
        }
    },
    methods: {
        handleClick () {
            this.test = !this.test;
        }
    }
}

在点击button时候,coma与comb两个组件会发生切换,但是这时候这两个组件的状态会被缓存起来,比如说coma与comb组件中都有一个input标签,那么input标签中的内容不会因为组件的切换而消失。

props

keep-alive组件提供了include与exclude两个属性来允许组件有条件地进行缓存,二者都可以用逗号分隔字符串、正则表达式或一个数组来表示。


// 将缓存name为a的组件
keep-alive include="a"
  component/component
/keep-alive

// name为a的组件将不会被缓存
keep-alive exclude="a"
  component/component
/keep-alive
生命钩子

keep-alive提供了两个生命钩子,分别是activated与deactivated。因为keep-alive会将组件保存在内存中,并不会销毁以及重新创建,所以不会重新调用组件的created等方法,需要用activated与deactivated这两个生命钩子来得知当前组件是否处于活动状态。

keep-alive源码

现在初级前端面试什么幺蛾子题目都会有,处处都要深入了解,现在我们一起从源码角度看一下keep-alive组件究竟是如何实现组件的缓存的。

1.created

created钩子会创建一个cache对象,用来作为缓存容器,保存vnode节点。


created () {
    /* 缓存对象 */
    this.cache = Object.create(null)
},
2.destroyed

destroyed钩子则在组件被销毁的时候清除cache缓存中的所有组件实例。


/* destroyed钩子中销毁所有cache中的组件实例 */
destroyed () {
    for (const key in this.cache) {
        pruneCacheEntry(this.cache[key])
    }
},
3.render

接下来是render函数。


render () {
    /* 得到slot插槽中的第一个组件 */
    const vnode: VNode = getFirstComponentChild(this.$slots.default)

    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
        // check pattern
        /* 获取组件名称,优先获取组件的name字段,否则是组件的tag */
        const name: ?string = getComponentName(componentOptions)
        /* name不在inlcude中或者在exlude中则直接返回vnode(没有取缓存) */
        if (name && (
        (this.include && !matches(this.include, name)) ||
        (this.exclude && matches(this.exclude, name))
        )) {
            return vnode
        }
        const key: ?string = vnode.key == null
        // 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
        /* 如果已经做过缓存了则直接从缓存中获取组件实例给vnode,还未缓存过则进行缓存 */
        if (this.cache[key]) {
            vnode.componentInstance = this.cache[key].componentInstance
        } else {
            this.cache[key] = vnode
        }
        /* keepAlive标记位 */
        vnode.data.keepAlive = true
    }
    return vnode
}

首先通过getFirstComponentChild获取第一个子组件,获取该组件的name(存在组件名则直接使用组件名,否则会使用tag)。接下来会将这个name通过include与exclude属性进行匹配,匹配不成功(说明不需要进行缓存)则不进行任何操作直接返回vnode,vnode是一个VNode类型的对象。


/* 检测name是否匹配 */
function matches (pattern: string | RegExp, name: string): boolean {
  if (typeof pattern === 'string') {
    /* 字符串情况,如a,b,c */
    return pattern.split(',').indexOf(name)  -1
  } else if (isRegExp(pattern)) {
    /* 正则 */
    return pattern.test(name)
  }
  /* istanbul ignore next */
  return false
}

检测include与exclude属性匹配的函数很简单,include与exclude属性支持字符串如”a,b,c”这样组件名以逗号隔开的情况以及正则表达式。matches通过这两种方式分别检测是否匹配当前组件。


//
if (this.cache[key]) {
    vnode.componentInstance = this.cache[key].componentInstance
} else {
    this.cache[key] = vnode
}

接下来的事情很简单,根据key在this.cache中查找,如果存在则说明之前已经缓存过了,直接将缓存的vnode的componentInstance(组件实例)覆盖到目前的vnode上面。否则将vnode存储在cache中。

最后返回vnode(有缓存时该vnode的componentInstance已经被替换成缓存中的了)。

4.watch

用watch来监听pruneCache与pruneCache这两个属性的改变,在改变的时候修改cache缓存中的缓存数据。


watch: {
    /* 监视include以及exclude,在被修改的时候对cache进行修正 */
    include (val: string | RegExp) {
        pruneCache(this.cache, this._vnode, name = matches(val, name))
    },
    exclude (val: string | RegExp) {
        pruneCache(this.cache, this._vnode, name = !matches(val, name))
    }
},

来看一下pruneCache的实现。


/* 修正cache */
function pruneCache (cache: VNodeCache, current: VNode, filter: Function) {
  for (const key in cache) {
    /* 取出cache中的vnode */
    const cachedNode: ?VNode = cache[key]
    if (cachedNode) {
      const name: ?string = getComponentName(cachedNode.componentOptions)
      /* name不符合filter条件的,同时不是目前渲染的vnode时,销毁vnode对应的组件实例(Vue实例),并从cache中移除 */
      if (name && !filter(name)) {
        if (cachedNode !== current) {
          pruneCacheEntry(cachedNode)
        }
        cache[key] = null
      }
    }
  }
} 

/* 销毁vnode对应的组件实例(Vue实例) */
function pruneCacheEntry (vnode: ?VNode) {
  if (vnode) {
    vnode.componentInstance.$destroy()
  }
}

遍历cache中的所有项,如果不符合filter指定的规则的话,则会执行pruneCacheEntry。pruneCacheEntry则会调用组件实例的$destroy方法来将组件销毁。

移除缓存

业务场景

在实际项目中,我们会想要记录一些用户的浏览页面的记录并适当缓存,以一个记账项目举例,常见的场景有首页、记到账页面、选择合同、新建合同、选择客户、新建客户这些页面。在这些页面中,很显然,用户的浏览行为应该是逐渐深入的,通俗得讲就是浏览页面在不断前进。

而且这些页面之间还是有互动性存在的,两种互动行为:

  • 用户前进时,总是进入新的页面。(比如在合同列表页反复加载多次列表之后,进入其中一个合同详情,再返回时,应该仍停留之前里列表页同一个位置,而不是重新刷新列表页。)
  • 用户后退时,需要能保留前一页数据并继续操作。(比如,记到账时需要选择合同,选择合同时可以新建合同,新建合同时填了一堆数据可以去选择客户,在选择客户时又去创建了客户,那么这一堆操作下来应该能够做到:创建完客户后继续新建合同,建完合同后继续记该合同的到账)。
  • Vue2总结(十)异步

    上图是 demo 项目中的真实效果,目前常见的 vue 开发方案里,一般都会引入 vuex 或 localStorage ,在各个页面不断的存储和调用页面内的数据,这样肯定不妥。

    业务问题

    vue 支持 keep-alive 组件,如果启用,页面内的所有数据都会被保留,所以,上文的互动行为二后退时保留前一页数据继续操作没有问题。

    问题出在互动行为一用户前进时总是进入新页面,然而一旦缓存,你就没法总是进新页面了,你总是进入缓存页,这就很让人头疼了。官方提供了 include 和 exclude 特性,说你可以决定哪些页面使用缓存哪些页面不用缓存。链接然而问题又回到了原点,并没有解决我们酌情决定是否使用已缓存的缓存这一需求。

    所以很多人想到了一个方法在离开页面时销毁这个页面是不是就可以了,然而并不能,这里出现了 bug ,组件销毁了缓存还在。于是,就有人提出希望keep-alive能增加可以动态删除已缓存组件的功能,之前一直没有进展,核心原因就在于 keep-alive 不能正确处理已销毁的组件。

    解决方法

    如果能实现动态使用缓存这一功能,那么所有问题也就迎刃而解。 在 keep-alive 的代码,发现了这么一段代码:

    
    const { cache, keys } = this
    const key: ?string = vnode.key == null
      // 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)
    } 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)
      }
    }
    

    通过在浏览器上测试并打印组件变量的时候,发现了这么个眼熟的字段,在 parent 的 componentInstance 的节点下,可以找到 父级的 keep-alive 组件的 cache,我们可以通过操控其中的 cache 列表,强行删除其中的缓存,问题也就迎刃而解(暴力解法)。

    参考

    [1].wanyaxing,https://juejin.im/post/5b610da4e51d45195c07720d

    [2].vue官网,https://cn.vuejs.org

    总结

    Vue.js内部将DOM节点抽象成了一个个的VNode节点,keep-alive组件的缓存也是基于VNode节点的而不是直接存储DOM结构。它将满足条件(pruneCache与pruneCache)的组件在cache对象中缓存起来,在需要重新渲染的时候再将vnode节点从cache对象中取出并渲染。

    但是keep-alive 默认不支持动态销毁已缓存的组件,所以此处给出的解决方案是通过直接操控 keep-alvie 组件里的 cahce 列表,暴力移除缓存:

    
    //使用Vue.mixin的方法拦截了路由离开事件,并在该拦截方法中实现了销毁页面缓存的功能。
    Vue.mixin({
        beforeRouteLeave:function(to, from, next){
            if (from && from.meta.rank && to.meta.rank && from.meta.rankto.meta.rank)
            {//此处判断是如果返回上一层,你可以根据自己的业务更改此处的判断逻辑,酌情决定是否摧毁本层缓存。
                if (this.$vnode && this.$vnode.data.keepAlive)
                {
                    if (this.$vnode.parent && this.$vnode.parent.componentInstance && this.$vnode.parent.componentInstance.cache)
                    {
                        if (this.$vnode.componentOptions)
                        {
                            var key = this.$vnode.key == null
                                        ? this.$vnode.componentOptions.Ctor.cid + (this.$vnode.componentOptions.tag ? `::${this.$vnode.componentOptions.tag}` : '')
                                        : this.$vnode.key;
                            var cache = this.$vnode.parent.componentInstance.cache;
                            var keys  = this.$vnode.parent.componentInstance.keys;
                            if (cache[key])
                            {
                                if (keys.length) {
                                    var index = keys.indexOf(key);
                                    if (index  -1) {
                                        keys.splice(index, 1);
                                    }
                                }
                                delete cache[key];
                            }
                        }
                    }
                }
                this.$destroy();
            }
            next();
        },
    });
    

    最后

    今天的 Vue2总结(十)异步与缓存 就分享到这里,我的公众号没有留言功能哈,有问题大家心里默念,我能感受到,谢谢 ~ 

    原文始发于微信公众号(程序员思语):

    本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

    本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

    原文链接:blog.ouyangsihai.cn >> Vue2总结(十)异步


     上一篇
    vue生命周期 vue生命周期
    首先,每个Vue实例在被创建之前都要经过一系列的初始化过程,这个过程就是vue的生命周期。 所有的钩子 beforeCreate —-创建前状态 created —  创建完毕状态 beforeMount — 挂载前状态 mounted
    2021-04-05
    下一篇 
    Vue2总结(九)虚拟DOM Vue2总结(九)虚拟DOM
    目前社区有很多 Vue.js 的源码解读文章,但是质量层次不齐,不够系统和全面,最近我将从各个细枝末节解读 Vue.js 的实现原理,针对 vue3 版本之前,让同学们可以彻底掌握 Vue.js。 友情提示:阅读本文大概需要** 65*
    2021-04-05