经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » HTML/CSS » HTML5 » 查看文章
Vue.js 源码分析(三十一) 高级应用 keep-alive 组件 详解
来源:cnblogs  作者:大沙漠  时间:2019/8/5 10:08:59  对本文有异议

当使用is特性切换不同的组件时,每次都会重新生成组件Vue实例并生成对应的VNode进行渲染,这样是比较花费性能的,而且切换重新显示时数据又会初始化,例如:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Document</title>
  6. <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
  7. </head>
  8. <body>
  9.  
  10. <div id="app">
  11. <button @click="currentComp=currentComp=='A'?'B':'A'">切换</button> <!--动态组件-->
  12. <component :is="currentComp"/>
  13. </div>
  14. <script>
  15. with(Vue.config){productionTip=false;devtools=false;}
  16. var app = new Vue({
  17. el: '#app',
  18. components:{
  19. A:{
  20. template:"<div><input type='text'/></div>",
  21. name:'A',
  22. mounted:function(){console.log('Comp A mounted');}
  23. },
  24. B:{
  25. template:"<div>B组件</div>",
  26. name:'B',
  27. mounted:function(){console.log('Comp B mounted');}
  28. }
  29. },
  30. data:{
  31. currentComp:"A"
  32. }
  33. })
  34. </script>
  35.  
  36. </body>
  37. </html>

渲染结果为:

控制台输出:

当我们在输入框输入内容后再点击切换将切换到B组件后控制台输出:

然后再次点击切换,将显示A组件,此时控制台输出:

渲染出的A组件内容是空白的,我们之前在输入框输入的内容将没有了,这是因为使用is特性切换不同的组件时,每次都会重新生成组件Vue实例并生成对应的VNode进行渲染,数据会丢失的

解决办法是可以用Kepp-alive组件对子组件内的组件实例进行缓存,子组件激活时将不会再创建一个组件实例,而是从缓存里拿到组件实例,直接挂载即可,

使用keep-alive组件时,可以给该组件传递以下特性:
    include         ;只有名称匹配的组件会被缓存        ;只可以是字符串数组、字符串(以逗号分隔,分隔后每个内容就是要缓存的组件名)、正则表达式
    exclude         ;任何名称匹配的组件都不会被缓存        ;只可以是字符串数组、字符串(以逗号分隔,分隔后每个内容就是要缓存的组件名)、正则表达式
    max            ;数字。最多可以缓存多少组件实例

keep-alive对应的子组件有两个生命周期函数,这两个生命周期是keep-alive特有的,如下:
    activated        ;该子组件被激活时调用
    deactivated         ;该子组件被停用时调用

例如:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Document</title>
  6. <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
  7. </head>
  8. <body>
  9. <div id="app">
  10. <button @click="currentComp=currentComp=='A'?'B':'A'">切换</button>
  11. <keep-alive>
  12. <component :is="currentComp"/>
  13. </keep-alive>
  14. </div>
  15. <script>
  16. with(Vue.config){productionTip=false;devtools=false;}
  17. var app = new Vue({
  18. el: '#app',
  19. components:{
  20. A:{
  21. template:"<div><input type='text'/></div>",
  22. name:'A',
  23. mounted:function(){console.log('Comp A mounted');}, //挂载事件
  24. activated:function(){console.log("Comp A activated");}, //激活时的事件,Kepp-alive独有的生命周期函数
  25. deactivated:function(){console.log("Comp A deactivated");} //停用时的事件,Kepp-alive独有的生命周期函数
  26. },
  27. B:{
  28. template:"<div>B组件</div>",
  29. name:'B',
  30. mounted:function(){console.log('Comp B mounted');},
  31. activated:function(){console.log("Comp B activated");},
  32. deactivated:function(){console.log("Comp B deactivated");}
  33. }
  34. },
  35. data:{
  36. currentComp:"A"
  37. }
  38. })
  39. </script>
  40. </body>
  41. </html>

这样组件在切换时之前的数据就不会丢失了。

 

源码分析


 对于keep-alive来说,是通过initGlobalAPI()函数注册的,如下:

  1. var builtInComponents = { //第5059行,KeppAlive组件的定义
  2. KeepAlive: KeepAlive
  3. }
  4. function initGlobalAPI (Vue) { //第5015行
  5. /**/
  6. extend(Vue.options.components, builtInComponents); //第5051行
  7. /**/
  8. }

Keep-alive组件的定义如下:

  1. var KeepAlive = { //第4928行
  2. name: 'keep-alive',
  3. abstract: true,
  4. props: {
  5. include: patternTypes,
  6. exclude: patternTypes,
  7. max: [String, Number]
  8. },
  9. created: function created () { //创建时的周期函数
  10. this.cache = Object.create(null); //用于缓存KeepAlive的VNode
  11. this.keys = []; //设置this.keys为空数组
  12. },
  13. destroyed: function destroyed () { //销毁生命周期
  14. var this$1 = this;
  15. for (var key in this$1.cache) {
  16. pruneCacheEntry(this$1.cache, key, this$1.keys);
  17. }
  18. },
  19. mounted: function mounted () { //挂载时的生命周期函数
  20. var this$1 = this;
  21. this.$watch('include', function (val) {                        //监视include的变化
  22. pruneCache(this$1, function (name) { return matches(val, name); });
  23. });
  24. this.$watch('exclude', function (val) {                         //监视exclude的变化
  25. pruneCache(this$1, function (name) { return !matches(val, name); });
  26. });
  27. },
  28. render: function render () { //render函数
  29. /**/
  30. }
  31. }

Keep-alive也是一个抽象组件(abstract属性为true),mounted挂载时会监视include和exclude的变化,也就是说程序运行时可以通过修改include或exclude来对keep-alive里缓存的子组件进行移除操作。

Keep-alive组件的render函数如下:

  1. render: function render () { //第4926行 keepalive组件的render函数
  2. var slot = this.$slots.default; //获取所有的子节点,是个VNode数组
  3. var vnode = getFirstComponentChild(slot); //拿到第一个组件VNode
  4. var componentOptions = vnode && vnode.componentOptions; //该组件的配置信息
  5. if (componentOptions) {
  6. // check pattern
  7. var name = getComponentName(componentOptions); //获取组件名称,优先获取name属性,如果没有则获取tag名称
  8. var ref = this; //当前KeppAlive组件的Vue实例
  9. var include = ref.include; //获取include属性
  10. var exclude = ref.exclude; //获取exclude属性
  11. if (
  12. // not included
  13. (include && (!name || !matches(include, name))) ||
  14. // excluded
  15. (exclude && name && matches(exclude, name)) //执行matches进行匹配,如果该组件不满足条件
  16. ) {
  17. return vnode //则直接返回vnode,即不做处理
  18. }
  19. var ref$1 = this;
  20. var cache = ref$1.cache;
  21. var keys = ref$1.keys;
  22. var key = vnode.key == null
  23. // same constructor may get registered as different local components
  24. // so cid alone is not enough (#3269)
  25. ? componentOptions.Ctor.cid + (componentOptions.tag ? ("::" + (componentOptions.tag)) : '')
  26. : vnode.key; //为子组件定义一个唯一的key值 如果该子组件没有定义key则拼凑一个,值为该组件对应的Vue实例的cid::tag,例如:1::A 同一个构造函数可以注册为不同的组件,所以单凭一个cid作为凭证是不够的
  27. if (cache[key]) { //如果该组件被缓存了
  28. vnode.componentInstance = cache[key].componentInstance; //直接将该组件的实例保存到vnode.componentInstance里面
  29. // make current key freshest
  30. remove(keys, key);
  31. keys.push(key);
  32. } else { //如果当前组件没有被缓存
  33. cache[key] = vnode; //先将VNode保存到缓存cache里
  34. keys.push(key); //然后将key保存到keys里
  35. // prune oldest entry
  36. if (this.max && keys.length > parseInt(this.max)) { //如果指定了max且当前的keys里存储的长度大于this.max
  37. pruneCacheEntry(cache, keys[0], keys, this._vnode); //则移除keys[0],这是最不常用的子组件
  38. }
  39. }
  40. vnode.data.keepAlive = true; //设置vnode.data.keepAlive为true,即设置一个标记
  41. }
  42. return vnode || (slot && slot[0]) //最后返回vnode(即第一个组件子节点)
  43. }

matches用于匹配传给Kepp-alive的include或exclude特性是否匹配,如下:

  1. function matches (pattern, name) { //第4885行 //查看name这个组件是否匹配pattern
  2. if (Array.isArray(pattern)) { //pattern可以是数组格式
  3. return pattern.indexOf(name) > -1
  4. } else if (typeof pattern === 'string') { //也可以是字符串,用逗号分隔
  5. return pattern.split(',').indexOf(name) > -1
  6. } else if (isRegExp(pattern)) { //也可以是正则表达式
  7. return pattern.test(name)
  8. }
  9. /* istanbul ignore next */
  10. return false
  11. }

初次渲染时,keep-alive下的组件和普通组件是没有区别的,当一个组件从被激活变为激活状态时,和Keep-alive相关的逻辑如下:

执行patch()将VNode渲染成真实节点时会执行createElm()函数,又会优先执行createComponent创建组件实例,如下:

  1. function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { //第5589行 创建组件节点
  2. var i = vnode.data; //获取vnode的data属性
  3. if (isDef(i)) { //如果存在data属性(组件vnode肯定存在这个属性,普通vnode有可能存在)
  4. var isReactivated = isDef(vnode.componentInstance) && i.keepAlive; //是否为激活操作 如果vnode.componentInstance为true(组件实例存在)且存在keepAlive属性则表示为keepalive组件
  5. if (isDef(i = i.hook) && isDef(i = i.init)) {
  6. i(vnode, false /* hydrating */, parentElm, refElm); //执行组件的init钩子函数
  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); //将子组件的VNode push到insertedVnodeQueue里面,
  14. if (isTrue(isReactivated)) { //如果是keep-alive激活的状态
  15. reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm); //执行reactivateComponent()函数
  16. }
  17. return true
  18. }
  19. }
  20. }

init是组件的钩子函数,用于创建组件的实例,如下:

  1. init: function init ( //第4109行 组件的安装
  2. vnode,
  3. hydrating,
  4. parentElm,
  5. refElm
  6. ) {
  7. if (
  8. vnode.componentInstance &&
  9. !vnode.componentInstance._isDestroyed &&
  10. vnode.data.keepAlive
  11. ) { //如果vnode.componentInstance和vnode.data.keepAlive都存在,则表示是一个keep-alive组件的激活状态
  12. // kept-alive components, treat as a patch
  13. var mountedNode = vnode; // work around flow
  14. componentVNodeHooks.prepatch(mountedNode, mountedNode); //执行该组件的prepatch方法
  15. } else {
  16. var child = vnode.componentInstance = createComponentInstanceForVnode(
  17. vnode,
  18. activeInstance,
  19. parentElm,
  20. refElm
  21. );
  22. child.$mount(hydrating ? vnode.elm : undefined, hydrating);
  23. }
  24. },

对于Keep-alive子组件的激活过程来说,它是不会调用createComponentInstanceForVnode去创建一个新的组件实例的,而是直接从VNode的componentInstance拿到组件实例即可

回到createComponent()函数,最后会执行reactivateComponent()函数,该函数就比较简单了,就是将子组件vnode.elm插入到DOM中,如下:

  1. function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) { //第5628行 激活一个组件
  2. var 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. var innerNode = vnode;
  8. while (innerNode.componentInstance) {
  9. innerNode = innerNode.componentInstance._vnode;
  10. if (isDef(i = innerNode.data) && isDef(i = i.transition)) {
  11. for (i = 0; i < cbs.activate.length; ++i) {
  12. cbs.activate[i](emptyNode, innerNode);
  13. }
  14. insertedVnodeQueue.push(innerNode);
  15. break
  16. }
  17. }
  18. // unlike a newly created component,
  19. // a reactivated keep-alive component doesn't insert itself
  20. insert(parentElm, vnode.elm, refElm); //调用insert将vnode.elm插入到parentElm里
  21. }

insert会调用原生的insertBefore或者appendChild这去插入DOM,最后返回到patch()函数内,就把之前的B组件从DOM树中移除,并执行相关生命周期函数。

原文链接:http://www.cnblogs.com/greatdesert/p/11287509.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号