总结vue的6大高级特性——及浅谈一下nextTickkeep-alive的原理 __Vue.js
发布于 4 年前 作者 banyungong 1314 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

额,说实话前面状态不好水了两篇简单的东西有点惭愧。。。 所以周末刷了两天何同学的视频,嗯。。。深刻感觉到自己是个垃圾之后决定痛改前非(最起码现在还是这样想的)

一个堕落者的自我救赎之路

有些概念问题可能自己以前叫习惯了一时没改过来, 如有误,望指教,感恩

吐槽:刚到了一本D3的书打算学习一下,随着这一期的技术专题就是可视化。枯了呀,整天看大佬刷礼品眼馋啊、我太酸了。唉现在又不能把主要精力放到D3上去,又与杯子无缘咯

1. 自定义v-model

相信大家对于v-model这个指令都很熟悉了。它其实就是一个<input type="text" :value="num" @input="num=$event.target.value">的语法糖。

但是呢,我们也发现了这个指令只能应用在少数几个表单控件上。但是v-model的这种数据绑定的功能又很好用,我们怎么将这种东西应用到我们自己的组件上呢?

初步

来简单实现以下吧

就利用两个东西:

1,vue中父组件可以监听子组件的事件

2,子组件利用$event以触发其父组件所监听的事件

我们先来写一个my-input组件

<template>
    <div>
       
       <input type="text" :value="value"  @input="$emit('input',$event.target.value)">
    </div>
</template>
<script>
export default {
    props:['value']
}
</script>

父组件

<template>
  <div id="app">
    {{value}}
     <my-input :value='value' @input="value=$event"></my-input>
  </div>
 
</template>


<script>
import myInput from './components/myInput'
export default {
  data() {
    return {
      value:'gxb'
    }
  },
  components: {
   myInput
  }
}
</script>

改进

我也想向vue的指令一样,就直接在组件中使用v-model。语法糖那么简练,这谁不爱用呢,我该怎么改进呢?

来了来了:一个组件上的v-model会默认利用名为valueprop和名为input事件,故利用model选项指定属性和事件即可

故:对于上面的栗子,直接把<my-input :value='value' @input="value=$event"></my-input>换成<my-input v-model="value"></my-input>即可

也就是说这个v-model,就是会向子组件传一个叫做value的属性,同时也会监听这个子组件所触发的input事件

但是还有些表单控件,它所需要的可不是value和input。像复选框,它要的是 checked 。同时事件也变成了change。

那要改怎么搞呢?

既然v-model默认从父组件传过来的是value,监听的是input事件,默认就是说明我们还能指定嘛

来吧,看栗子

先看父组件(几乎没啥变动,就是把传过去的值变成布尔值了)

<template>
  <div id="app">
    {{value}}
     <my-input v-model="value"></my-input>
  </div>
 
</template>


<script>
import myInput from './components/myInput'
export default {
  data() {
    return {
      value:true
    }
  },
  components: {
   myInput
  }
}
</script>



子组件,加一个model选项,用于指定传过来的属性名和指定本次v-model要利用的事件。

即原来默认v-model传过来的东西就叫value,所以你可以props:[‘value’]这个样子接收,但现在不是了你得指定一下吧

原来v-model默认利用的是input事件,即它原来默认就监听input事件,这里你是要触发change事件,你得告诉它一声啊

<template>
    <div>
       
       <input type="checkbox" :checked='checked' @change="$emit('change',$event.target.checked)">
    </div>
</template>
<script>
export default {
    model:{
        prop:'checked',
        event:'change'
    },
    props:['checked'],
}
</script>

2. nextTick

vue异步DOM更新

首先我们应该都了解:vue为了性能的保证,故它的DOM更新操作是异步执行的。

多说无益,来看栗子

<template>
  <div id="app">
    <ul ref="ul">
      <li v-for="(item, index) in arr" :key="index">{{item}}</li>
    </ul>
    <button @click="add">add</button>
  </div>
</template>


<script>
export default {
  data() {
    return {
      arr: [1, 2, 3, 4],
    };
  },
  methods: {
    add() {
      this.arr.push(Math.random());
      this.arr.push(Math.random());
      this.arr.push(Math.random());
      console.log(this.arr);
      console.log(this.$refs.ul.childNodes.length);
    },
  },
};
</script>

观察效果,即使在代码中我们的打印语句是放在添加后面的,但是因为即使此时数据已经放进arr中了,但是vue显然并没有将新增的立即渲染出来。故此时DOM中ul还是只有原来的那几个li节点

那问题就来了,有时候我们就是要类似于同步渲染的那种效果。我们就是希望能够及时拿到先要的数据该怎么做呢?

nextTick基本使用

故vue在全局(还有实例)中提供了nextTickAPI,它就是来帮助我们来做这件事的

先来了解一下用法:首先接收一个回调作为参数,即这个回调会在DOM更新之后执行。如果没有提供回调且此时所处的环境支持Promise,则会返回一个Promise

改进上面代码

<script>
export default {
  data() {
    return {
      arr: [1, 2, 3, 4],
    };
  },
  methods: {
    add() {
      this.arr.push(Math.random());
      this.arr.push(Math.random());
      this.arr.push(Math.random());
      console.log(this.arr);
      this.$nextTick(() => {
        console.log(this.$refs.ul.childNodes.length);
      });
    },
  },
};
</script>

看效果

对于一个好用的API,让小白只停留在会用的方面显然是不可能的。来让我们看一下它的原理吧

nextTick原理分析

这里就有点精彩了

首先知识铺垫

希望你能够数据响应式的完整原理(主要是依赖收集与派发更新,最重要的是你要知道watch内部做的事情)->js的事件循环

我之前写过一篇手写vue响应式和diff的文章,来看一下watch里面写的一个东西(这里我写的不好,应该把他们放进微任务队列中)

开始我是这样写的当一个属性的状态发生了改变,watch直接去更新

但是想要一下,在vue2.x的版本中开始使用虚拟DOM进行渲染,此时的变化侦测只能通知到组件级别。什么意思呢,也就是说这个组件中所有状态仅由一个watcher进行检测。

这又啥关系呢?要知道依赖收集的时候,我们这个组件的所有状态(属性)均在每一个dep中了。在状态改变也即数据set时一个组件中的所有的需要派发更新的dep,它们最终是调的同一个watcher的更新方法

再白话一点,也就是有个数据变动了就掉一下更新方法。但是有时候因为某种需求我们可能是对一个属性操作了好几次

像这样

methods: {
    test() {
        for (let i = 0; i < 10; i++) {
            this.test = i;
        }
    },
}

难道我们就要掉用十次更新方法嘛,可要知道虽然是虚拟DOM比真实DOM性能强太多了,但是不要忘了它还得走十次diff呢。

这时候使用异步更新的好处就出来了,因为是异步更新的,虽然这个属性的dep还是通知了watcher10次,但是你通知是同步代码我只取你最后一次的

当然这是对同一个属性多次变动来说的,多个属性的变动也是一样;数据变动时watcher会它这些通知更新的操作放到一个异步更新队列中,在同步代码执行完成后执行更新操作

再一句话总结:一个组件中,不管你数据怎么变动,这个组件所对应的watcher也只会在最后集中更新一次

分析它的原理

看到这你可能有一点nextTick的实现思路了吧,最简单的想法就是将nextTick接收的回调也放到一个异步队列中(这里我们并没有打算将源码拿出来解释一遍,有兴趣的可以去看看源码,也就1000多行。它的主要思想其是并占不了多少所以赶上懒也就没想写一便)

但是有了上面的基础和事件循环我想你现在也应该明白了它的核心了吧。

通过上面所说vue的DOM更新操作被放在微任务队列中,根据浏览器中宏微任务的执行顺序,你完全可以将你的nextTick回调直接放进一个微宏任务中。

放进微任务中好说,因为本身存储这些任务的数据结构就是队列,先进先出吗

看栗子:将处理函数放进微任务队列

<script>
export default {
  data() {
    return {
      arr: [1, 2, 3, 4],
    };
  },
  methods: {
    add() {
      this.arr.push(Math.random());
      this.arr.push(Math.random());
      this.arr.push(Math.random());
      console.log(this.arr);

      new Promise(resole => {
        resole();
      }).then(() => {
        console.log(this.$refs.ul.childNodes.length);
      });
    },
  },
};
</script>

再看栗子:将处理函数放进宏任务队列

<script>
export default {
  data() {
    return {
      arr: [1, 2, 3, 4],
    };
  },
  methods: {
    add() {
      this.arr.push(Math.random());
      this.arr.push(Math.random());
      this.arr.push(Math.random());
      console.log(this.arr);

      setTimeout(()=> {
        console.log(this.$refs.ul.childNodes.length);
      }, 0);
    },
  },
};
</script>

注意派发优先级

这里是因为简单,这两种均可实现。但是一个稍微完整的小项目中,此时浏览器中异步队列(无论是宏还是微)可是还用很多其他的任务的

故这块还有一个重点,就是这个处理函数派发问题。因为微任务的优先级比较高容易出问题,故下面就单举一个使用微任务派发失败的栗子

有个数据是在宏任务中才可以拿到的(这个情况也有时候会遇到吧)

<script>
export default {
  data() {
    return {
      arr: [1, 2, 3, 4],
    };
  },
  methods: {
    add() {
      this.arr.push(Math.random());
      this.arr.push(Math.random());

      setTimeout(() => {
        this.arr.push(Math.random());
      }, 0);
      new Promise((resole) => {
        resole();
      }).then(() => {
        console.log(this.$refs.ul.childNodes.length);
      });
      this.arr.push(Math.random());
    },
  },
};
</script>

效果,又有一个数据不能及时拿到了

此时派发就可以采用宏任务了吧

3. 插槽

相识

这个玩意,我们就熟悉的多了

即我们的组件标签中的数据是默认被忽略的

如这样

<template>
  <div id="app">
    <test>这是插槽内容</test>
  </div>
</template>
<script>
import Test from './components/test01'
export default {
  components:{
    Test
  }
};
</script>

可是页面是还是只有test组件里面的东西

但是我们就是要这个组件标签内的数据内。

vue为我们提供了一个插槽标签<slot></slot>,父组件我们的组件标签内的东西就会跑到这里面来

test组件

<template>
  <div>
  test
  <slot></slot>
  </div>
</template>
<script>
export default {
 
};
</script>

相知

为插槽设置默认内容

上面在组件test里面我们直接扔进去一个slot标签,这个slot标签内也是可以放置数据的。

即如果test标签内有数据,数据就会放进slot里面进行展示

如果没有,则看看slot标签内是否有东西

栗子

test组件

<template>
  <div>
  
  <slot>插槽默认内容</slot>
  </div>
</template>
<script>
export default {
 data() {
   return {
     test:'son'
   }
 },
};
</script>

父组件中,test标签内放置东西

 <test>插槽数据</test>

test标签内没有东西

<test></test>

具名插槽

要知道插槽内不仅可以放字符串,它的数据也可以是html甚至是别的组件

这就又来问题了,如果我们打算往插槽中放置的数据如一个主页排版,它可能要有header,body,footer。我怎么保证通过插槽放进去的数据可进入到组件的指定位置

这块功能的实现就要依靠具名插槽了

栗子

插槽写法:使用template标签将所要放置数据包裹,并以v-slot的参数形式帮到要放置到那个具体test组件slot "坑位"中去。

注意没有使用template包裹并指定slot名字的数据,相当于<template v-slot:default> 身体 </template>

同样会放到test组件中没有名字的slot坑位中去

 <test>
      <template v-slot:header>
        头部
      </template>
      <template v-slot:body>
        身体
      </template>
      <template v-slot:footer>
        尾部
      </template>
    </test>

test组件:给坑位slot指定name属性

template>
  <div>
  
  <header>
    <slot name="header"></slot>
    </header>
  <main>
    <slot name="body"></slot>
    </main>
  <font>
    <slot name="footer"></slot>
  </font>
  </div>
</template>

在父组件中动态指定插槽名

 <test>
      <template v-slot:[名字]>
        头部
       <template
                 
   <test>

作用域插槽

先来看

<template>
  <div id="app">
    <test>
      {{test}}
    </test>
  </div>
</template>
<script>
import Test from './components/test01'
export default {
  data() {
    return {
      test:'fa'
    }
  },
  components:{
    Test
  }
};
</script>

你在test的插槽中这样{{test}}取得的数据,肯定仅是当前组件的数据(一个组件可理解为有一个组件作用域)

但是呢,我又想test标签内的空间也可以算是和test组件有一些关系(毕竟这块空间属于人家的插槽嘛)

那么就像,能不能在这个test插槽内取到我test组件自身的数据呢?

作用域插槽帮我们解决了这一问题

看栗子

先来看test组件怎么把数据传出去

功能类似于组件中间的父子通信,它也是给slot绑属性的方式传递过去的(组件是父子,这里有点逆向的意思)

<template>
  <div>
    <slot :test="test"></slot>
  </div>
</template>
<script>
export default {
  data() {
    return {
      test: "son",
    };
  },
};
</script>

再来看一下父组件中怎么接收

还是使用v-slot指定,因为因为传过来数据的那个坑没有名字。故我们直接用v-slot="testProps",此时传过来的数据就放到了testProps中(这个变量我们自定义命名)。数据格式test:“son”(以绑定test属性传过来的嘛)

有名字仿照下面,就是再以参数形式指定名字呗

v-slot="testProps"等价于v-slot:default=“testProps”(推荐一一对应)

<template>
  <div id="app">
    <test>
      <template v-slot="testProps">{{testProps.test}}</template>
    </test>
  </div>
</template>
<script>
import Test from "./components/test01";
export default {
  data() {
    return {
      test: "fa",
    };
  },
  components: {
    Test,
  },
};
</script>

同时因为作用域插槽的工作原理是将插槽内容作为有个传入一个参数的函数

怎么理解呢

<template v-slot:default="testProps">
    插槽内容
    {{testProps.test}}
</template>
function (testProps){
    //插槽内容
}

即你就可以这样理解,这个你自定义的testProps就是一个函数的形参,下面的插槽内容就是这个函数的函数体

故这时你就可以做些小的操作了

解构props

<template v-slot:default="{test}">{{test}}</template>

还有其他的函数参数相关的操作,默认值重命名也均可以在这使用了

最后

v-slot的缩写形式

将参数之前的所有内容( v-slot: )替换为#。(有参数时也即需要指定名字时使用)

   <test>
      <template #default="{test}">{{test}}</template>
    </test>

4. 动态组件和异步组件

动态组件

先来看一下动态组件最为基础的一个栗子tab栏切换

我们分别切换组件test01 、02 、03

总不能和以前一样使用显示隐藏吧

vue中为我们提供了一个很简单的方法去实现这个操作

即使用 <component> 元素再给它绑一个is属性去实现。is的值可以是注册过的组件名或是一个组件的选项对象

上面效果的代码

<template>
  <div id="app">
    <ul @click="change">
      <li>test01</li>
      <li>test02</li>
      <li>test03</li>
    </ul>

    <div class="zhan"></div>

    <component :is="componentId"></component>
  </div>
</template>
<script>
import Test01 from "./components/test01";
import Test02 from "./components/test02";
import Test03 from "./components/test03";
export default {
  data() {
    return {
      componentId: "Test01",
    };
  },
  methods: {
    change(e) {
      this.componentId = e.target.innerText;
    },
  },
  components: {
    Test01,
    Test02,
    Test03,
  },
};
</script>
<style lang="css">
.zhan {
  height: 50px;
}
ul{
  margin: 0;
  padding: 0;
}
li {
  float: left;
  margin-left: 5ox;
  width: 50px;
  list-style: none;
}
</style>

异步组件

异步加载的问题我们遇到的好像都数不清了,前天水的那篇vue-router,里面的路由对象中组件也是搞了一下异步加载

它的好处还是老样子,用到的时候就用用不到就不用,不能一开始就把所有的组件资源全拿拿过来出来

比如上面的tab案例,你请求的是这个放着tab的根组件,没有使用异步组件的情况是你同时要拿这个tab栏中包含的所有组件,test01,02,03。首先是数量的问题,这里少还好说,如果一个大型网站呢。再其次可能有些就根本不用请求过来,比如test03这个组件就没有去用,你把它开始就拿过来是不是浪费

这导致的问题也很是严重,一是时间问题,二是不必要的开销

按需拿也即用到再呢,就好多了吗

它的使用也比较简单

  components: {
    Test01:()=>import('./components/test01'),//返回的是promise
    Test02:()=>import('./components/test02'),
    Test03:()=>import('./components/test03'),
  },

5. keep-alive

初识

还得从上面的tab栏栗子说起,这回我们为每个栗子加上几个钩子函数

像这样

<template>
  <div>
   test01
  </div>
</template>
<script>
export default {
 beforeCreate() {
   console.log('beforeCreate')
 },
 created() {
   console.log('created')
 },
 beforeMount() {
   console.log('beforeMount')
 },
 mounted() {
    console.log('mounted')
 },
};
</script>

看效果

它们的每个组件都是都是又走了一遍创建流程,但是这个方式对性能肯定不好啊。我明明拿过来一次了,下次使用我还得跑去拿一遍。累傻小子呢?

我就不能拿过来放自己兜里啊,下次用我从兜里拿多好啊

keep-alive就是来做这件事情的

它的使用更为简单,用<keep-alive>标签将动态目标包裹一下即可

 <keep-alive>
      <component :is="componentId"></component>
    </keep-alive>

看效果:后面是不是就走缓存了呢,走缓存还有一个好处,可以记录原来组件的浏览信息

keep-alive组件还能接收三个属性

  • include - 字符串或正则表达式。只有名称匹配的组件会被缓存。
  • exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
  • max - 数字。最多可以缓存多少组件实例。

这个就比较简单了,直接看官网的栗子

<!-- 逗号分隔字符串 -->
<keep-alive include="a,b">
  <component :is="view"></component>
</keep-alive>

<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
  <component :is="view"></component>
</keep-alive>

<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
  <component :is="view"></component>
</keep-alive>

值得注意的是: 匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components 选项的键值)。匿名组件不能被匹配。

实现原理

keep-alive是vue的一个内置组件,它的原理也不算是太复杂。

主要就是要实现组件缓存呗,下面就来看一下它的基本原理吧(一些工具函数我只解释它是做什么的,就不具体写出来了。具体参照vue这块的源码即可)

先写一下这个组件的基本架子

export default {
    name: 'keep-alive',
    props: {
        include: [String, RegExp, Array],
        exclude: [String, RegExp, Array],
        max: [String, Number]
    },
    created() {},
    mounted() {},
    destroyed() {},
    render() {}

}

首先这个组件是要接收那传过来的三个属性参数的,首先这里我们使用不了模板语法(和vue-router的实现以下,那需要带编译器的版本)故我们需要使用render函数

我们最终的目的就是储存组件,那么我们就要考虑使用什么数据结构保存、在什么时候创建这个创建结构、传过来的三个属性值变化了我们在哪个钩子里做处理、keep-alive组件销毁之后,所有存储的组件也要进行销毁

created 钩子做的事情

首先,我们选择在created钩子里创建储存组件的数据结构,就采用对象存储即可

created () {
    this.cache = Object.create(null)//[key:组件]
    this.keys = []//只保存所有需要缓存组件key的数组
  },

mounted钩子做的事情

首先我们知道mounted钩子执行完就完成了挂载,并且这个钩子是在更新钩子的后面它肯定是在再次触发该钩子时可以拿到最新传过来的三个属性的值。

故我们在这里处理属性变化的逻辑

mounted () {
    this.$watch('include', val => {
        pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
        pruneCache(this, name => !matches(val, name))
    })
}

pruneCache这个函数,就是把以储存的组件,根据include或exclude的最新变化进行判断是否还需要储存,不需要剔除缓存对象中

destroyed钩子做的事

这个就是把缓存中的所有组件也销毁

最最重要的render

首先要知道 keep-alive 只处理第一个子组件 ,我们可以通过插槽拿到其对应的vnode

拿到这个组件的名称去进行和include和exclude匹配(可能还有不存name的时候,那就拿tag)

即和exclude匹配上了就表示不缓存,直接返回vnode即可

和include匹配上了,表示就需要放进缓存对象中了

那么以后进行展示keep-alive内部东西时,就会先根据插槽拿到的组件名称去缓存里面找有没有,有就直接用。没有那就不好意思

到这里最最基本的思想就结束了,但是还有一点。像开始的那个tab栗子测试的时候你会发现,一开始的渲染的那个组件它没有缓存

这里它用了一个LRU的缓存淘汰策略(可以自动去了解一下。忘了这个和组原还是os,的一个啥的命中差不多记不太清了)

6. 混入

这个玩意我们前面使用太多次了吧

先看一下它的概念:

混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。

这个玩意还是比较简单的,就是可以搞一个和组件选项同名的对象。它里面的数据可以混合到组件中去

多说无益,看栗子。

混入data

写一个mixin对象,将这个对象里的data函数中的数据混入到组件中去。(同名的一组件的为主)

<template>
  <div>test01</div>
</template>
<script>
const mixin = {
  data() {
    return {
      foo: "foo",
      name: "zs",
    };
  },
};
export default {
  mixins: [mixin],
  data() {
    return {
      foo: "foo组件",
    };
  },
  created() {
    console.log(this.$data)
  },
};
</script>

混入钩子

同名的构造函数会被合并到一个数组中,就是不管是组件上的还是混进来的对象上的两个同名钩子都会被调用(混进来的那个钩子先与组件上的)

<template>
  <div>test01</div>
</template>
<script>
const mixin = {
  created() {
    console.log('混进来的')
  },
};
export default {
  mixins: [mixin],
 
  created() {
    console.log('组件上的')
  },
};
</script>

其它的那些值是对象的选项

methodscomponents 那些和data中的数据一样。组件没有的直接混进来,组件有的(同名的)还是用组件的

全局混入也是这样,不过它会影响在它后面所创建的每一个vue实例。

这个的原理就是各种合并的问题了,什么时候合并?如何合并?权重问题?

这个就留到后面再说了,因为我是打算在后面的总结中写一篇关于vue的11个全局API的实现,故这里就不想再重复写了

写到最后

其实还是按以前说的那几个内容系列总结的,只不过js的东西写的也不算少了,而轮子方面还没有几篇。故总结完数据结构后就在整轮子的一些知识,js还有几块非常重要的知识点也会马上去写 后面紧值着的应该会是js剩下的几块知识,vue的应用上的一些东西和vue的一些比较重要的API的原理实现

总之:星光不问赶路人,时光不负有心人 让我们一起努力 期待着我们的下一次邂逅

致谢:

vue官网

keep-alive内部原理

vue异步组件(高级异步组件)使用场景及实践

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 宫小白 原文链接:https://juejin.im/post/6864570298767769607

回到顶部