vue应用技巧集合

监听组件的生命周期

比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,常规的写法可能如下:

 // Parent.vue
<Child @mounted="doSomething"/>

// Child.vue
mounted() {
  this.$emit("mounted");
}

 

 此外,还有一种特别简单的方式,子组件不需要任何处理,只需要在父组件引用的时候通过@hook 来监听即可,代码如下:

<Child @hook:mounted="doSomething" /> 
<Child @hook:updated="doSomething" />

watch 的初始立即执行

当 watch 一个变量的时候,初始化时并不会执行,如下面的例子,你需要在 created 的时候手动调用一次:

created() {
  this.getList();
},
watch: {
  keyWord: 'getList',
}

 上面这样的做法可以使用,但很麻烦,我们可以添加 immediate 属性,这样初始化的时候就会自动触发(不用再写 created 去调用了),然后上面的代码就能简化为:

watch: {
  keyWord: {
    handler: 'getList',
    immediate: true
  }
}

 watch 有三个参数

  • handler:其值是一个回调函数。即监听到变化时应该执行的函数
  • deep:其值是 true 或 false;确认是否深入监听。
  • immediate:其值是 true 或 false,确认是否以当前的初始值执行 handler 的函数

路由参数变化组件不更新

同一path的页面跳转时路由参数变化,但是组件没有对应的更新。

原因:主要是因为获取参数写在了created或者mounted路由钩子函数中,路由参数变化的时候,这个生命周期不会重新执行。

解决方案1:watch监听路由

watch: {
 // 方法1 //监听路由是否变化
  '$route' (to, from) { 
   if(to.query.id !== from.query.id){
			this.id = to.query.id;
			this.init();//重新加载数据
		}
  }
}
//方法 2  设置路径变化时的处理函数
watch: {
'$route': {
    handler: 'init',
    immediate: true
  }
}

 解决方案2 :为了实现这样的效果可以给router-view添加一个不同的key,这样即使是公用组件,只要url变化了,就一定会重新创建这个组件。

<router-view :key="$route.fullpath"></router-view>

路由懒加载

Vue 项目中实现路由按需加载(路由懒加载)的 3 中方式:

// 1、Vue异步组件技术:
	{
		path: '/home',
		name: 'Home',
		component: resolve => reqire(['path路径'], resolve)
  }

// 2、es6提案的import()
  const Home = () => import('path路径')

// 3、webpack提供的require.ensure()
	{
		path: '/home',
		name: 'Home',
		component: r => require.ensure([],() =>  r(require('path路径')), 'demo')
	}

require.context()

require.context(directory,useSubdirectories,regExp)

  • directory:说明需要检索的目录
  • useSubdirectories:是否检索子目录
  • regExp: 匹配文件的正则表达式,一般是文件名

场景:如页面需要导入多个组件,原始写法:

import titleCom from '@/components/home/titleCom'
import bannerCom from '@/components/home/bannerCom'
import cellCom from '@/components/home/cellCom'
components: {
  titleCom, bannerCom, cellCom
}

 这样就写了大量重复的代码,利用 require.context 可以写成:

const path = require('path')
const files = require.context('@/components/home', false, /\.vue$/)
const modules = {}
files.keys().forEach(key => {
  const name = path.basename(key, '.vue')
  modules[name] = files(key).default || files(key)
})
components: modules

递归组件

  • 递归组件: 组件在它的模板内可以递归的调用自己,只要给组件设置 name 组件就可以了。
  • 不过需要注意的是,必须给一个条件来限制数量,否则会抛出错误: max stack size exceeded
  • 组件递归用来开发一些具体有未知层级关系的独立组件。比如:联级选择器和树形控件
<template>
  <div v-for="(item,index) in treeArr"> {{index}} <br/>
      <tree :item="item.arr" v-if="item.flag"></tree>
  </div>
</template>
<script>
export default {
  // 必须定义name,组件内部才能递归调用
  name: 'tree',
  data(){
    return {}
  },
  // 接收外部传入的值
  props: {
     item: {
      type:Array,
      default: ()=>[]
    }
  }
}
</script>

自定义路径别名

我们也可以在基础配置文件中添加自己的路径别名

resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      'vue$': 'vue/dist/vue.esm.js',
      '@': resolve('src'),
      'assets': resolve('src/assets')
    }
  }

 然后我们导入组件的时候就可以这样写:

// import YourComponent from '/src/assets/YourComponent'
import YourComponent from 'assets/YourComponent'

 这样既解决了路径过长的麻烦,又解决了相对路径的烦恼。

动态给修改dom的样式

原因:因为我们在写.vue文件中的样式都会追加scoped。这样针对模板dom中的样式就可以生效,但其生效后的最终样式并不是我们写的样式名,而是编码后的。比如:

<template>
  <div class="box">dom</div>
</template>
<style lang="scss" scoped>
  .box {
    background: red;
  }
</style>

 vue 将代码转译成如下,所以我们在js中拼接上的dom结构样式并不会生效。

.box[data-v-11c6864c]{ background:red; }
<template>
  <div class="box" data-v-11c6864c>dom</div>
</template>

 解决方法:将要改变的样式写在非scoped样式标签中。

长列表性能优化

我们应该都知道 vue 会通过 object.defineProperty 对数据进行劫持,来实现视图响应数据的变化,然而有些时候我们的组件就是纯粹的数据展示,不会有任何改变,我们就不需要 vue 来劫持我们的数据,在大量数据展示的情况下,这能够很明显的减少组件初始化的时间。

所以,我们可以通过 object.freeze 方法来冻结一个对象,这个对象一旦被冻结,vue就不会对数据进行劫持了。

export default {
  data: () => ({
    list: []
  }),
  async created() {
    const list = await axios.get('xxxx')
    this.list = Object.freeze(list)
  },
  methods: {
    // 此处做的操作都不能改变list的值
  }
}

 另外需要说明的是,这里只是冻结了 list 的值,引用不会被冻结,当我们需要 reactive 数据的时候,我们可以重新给 list 赋值。

内容分发(slot)

插槽 slot,也是组件的一块 HTML 模板,这一块模板显示不显示、以及怎样显示由父组件来决定。实际上,一个 slot 最核心的两个问题在这里就点出来了,是显示不显示和怎样显示。

默认插槽

又名单个插槽、匿名插槽,这类插槽没有具体名字,一个组件只能有一个该类插槽。

<!-- 父组件 parent.vue -->
<template>
  <div class="parent">
    <h1>父容器</h1>
    <child>
      <div class="tmpl">
        <span>菜单1</span>
      </div>
    </child>
  </div>
</template>

<!-- 子组件 child.vue -->
<template>
  <div class="child">
    <h1>子组件</h1>
    <slot></slot>
  </div>
</template>
具名插槽

匿名插槽没有 name 属性,所以叫匿名插槽。那么,插槽加了 name 属性,就变成了具名插槽。具名插槽可以在一个组件中出现 N 次,出现在不同的位置,只需要使用不同的 name 属性区分即可。

<!-- 父组件 parent.vue -->
<template>
  <div class="parent">
    <h1>父容器</h1>
    <child>
      <div class="tmpl" slot="up">
        <span>菜单up-1</span>
      </div>
      <div class="tmpl" slot="down">
        <span>菜单down-1</span>
      </div>
      <div class="tmpl">
        <span>菜单->1</span>
      </div>
    </child>
  </div>
</template>

<!-- 子组件 child.vue -->
<template>
  <div class="child">
    <!-- 具名插槽 -->
    <slot name="up"></slot>
    <h3>这里是子组件</h3>
    <!-- 具名插槽 -->
    <slot name="down"></slot>
    <!-- 匿名插槽 -->
    <slot></slot>
  </div>
</template>
作用域插槽

作用域插槽可以是默认插槽,也可以是具名插槽,不一样的地方是,作用域插槽可以为 slot 标签绑定数据,让其父组件可以获取到子组件的数据。

<!-- parent.vue -->
<template>
  <div class="parent">
    <h1>这是父组件</h1>
    <child
      >>
      <template slot="default" slot-scope="slotProps">
        {{ slotProps.user.name }}
      </template> </child
    >>
  </div>
</template>

<!-- 子组件 child.vue -->
<template>
  <div class="child">
    <h1>这是子组件</h1>
    <slot :user="user"></slot>
  </div>
</template>
<script>
  export default {
    data() {
      return {
        user: {
          name: '小赵'
        }
      }
    }
  }
</script>

 11、事件修饰符

Vue.js 为 v-on 提供了事件修饰符,修饰符是由点开头的指令后缀来表示的。

  • stop:阻止事件继续传播
  • prevent:阻止事件默认行为
  • capture:添加事件监听器时使用事件捕获模式
  • self:当前元素触发时才触发事件处理函数
  • once:事件只触发一次
  • passive:告诉浏览器你不想阻止事件的默认行为,不能和.prevent 一起使用。
<!-- 阻止单击事件继续传播 -->
<a v-on:click.stop="toDo"></a>

<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="toSubmit"></form>

<!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="toDo"></a>

<!-- 只有修饰符 -->
<form v-on:submit.prevent></form>

<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div v-on:click.capture="toDo">...</div>

<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<div v-on:click.self="toDo">...</div>

<!-- 点击事件将只会触发一次 -->
<a v-on:click.once="toDo"></a>

<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
<div v-on:scroll.passive="onScroll">...</div>

12、表单修饰符

  • .lazy 在输入框输入完内容,光标离开时才更新视图
  • .trim 过滤首尾空格
  • .number 如果先输入数字,那它就会限制你输入的只能是数字;如果先输入字符串,那就相当于没有加.number

13、生命周期函数

  • beforeCreate(创建前) vue 实例的挂载元素$el 和数据对象 data 都是 undefined, 还未初始化
  • created(创建后) 完成了 data 数据初始化, el 还未初始化
  • beforeMount(载入前) vue 实例的$el 和 data 都初始化了, 相关的 render 函数首次被调用
  • mounted(载入后) 此过程中进行 ajax 交互
  • beforeUpdate(更新前)
  • updated(更新后)
  • beforeDestroy(销毁前)
  • destroyed(销毁后)

Vue 的父组件和子组件生命周期钩子执行顺序是什么?

  1. 渲染过程:父组件挂载完成一定是等子组件都挂载完成后,才算是父组件挂载完,所以父组件的 mounted 在子组件 mouted 之后。
  • 父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted
  1. 子组件更新过程:
  • 影响到父组件:父 beforeUpdate -> 子 beforeUpdate->子 updated -> 父 updted
  • 不影响父组件:子 beforeUpdate -> 子 updated
  1. 父组件更新过程:
  • 影响到子组件:父 beforeUpdate -> 子 beforeUpdate->子 updated -> 父 updted
  • 不影响子组件:父 beforeUpdate -> 父 updated
  1. 销毁过程:
  • 父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

14、组件 attrs 和 listeners

attrs 获取子传父中未在 props 定义的值

// 父组件
<home title="这是标题" width="80" height="80" imgUrl="imgUrl"/>
// 子组件
mounted() {
  console.log(this.$attrs) //{title: "这是标题", width: "80", height: "80", imgUrl: "imgUrl"}
}

// 相对应的如果子组件定义了 props,打印的值就是剔除定义的属性
props: {
  width: {
    type: String,
    default: ''
  }
},
mounted() {
  console.log(this.$attrs) //{title: "这是标题", height: "80", imgUrl: "imgUrl"}
}

listeners:场景:子组件需要调用父组件的方法。 解决:父组件的方法可以通过 v-on="listeners" 传入内部组件——在创建更高层次的组件时非常有用

// 父组件
<home @change="change"/>

// 子组件
mounted() {
  console.log(this.$listeners) //即可拿到 change 事件
}

 

15、路由守卫

vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。

全局前置守卫 常用于判断登录状态和菜单权限校验

router.beforeEach((to, from, next) => {
  let isLogin = sessionStorage.getItem('isLogin') || ''
  if (!isLogin && to.meta.auth) {
    next('/login')
  } else {
    next()
  }
})
  • to: Route: 即将要进入的目标 路由对象
  • from: Route: 当前导航正要离开的路由
  • next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。

组件内的守卫

  • beforeRouteEnter
  • beforeRouteUpdate
  • beforeRouteLeave

16、路由缓存 keepalive

keep-alive 是 Vue 提供的一个抽象组件,用来对组件进行缓存,从而节省性能,由于是一个抽象组件,所以在 v 页面渲染完毕后不会被渲染成一个 DOM 元素。

<keep-alive>
  <router-view></router-view>
</keep-alive>

当组件在 keep-alive 内被切换时组件的 activateddeactivated 这两个生命周期钩子函数会被执行

使用参数include/exclude

  • include: 字符串或正则表达式。只有匹配的组件会被缓存。
  • exclude: 字符串或正则表达式。任何匹配的组件都不会被缓存。
<keep-alive include="a,b">
  <router-view></router-view>
</keep-alive>
<keep-alive exclude="c">
  <router-view></router-view>
</keep-alive>

使用$route.meta 的 keepAlive 属性

需要在 router 中设置 router 的元信息 meta

export default new Router({
  routes: [
    {
      path: '/',
      name: 'Hello',
      component: Hello,
      meta: {
        keepAlive: false // 不需要缓存
      }
    },
    {
      path: '/page1',
      name: 'Page1',
      component: Page1,
      meta: {
        keepAlive: true // 需要被缓存
      }
    }
  ]
})

在 app.vue 进行区别缓存和不用缓存的页面

<div id="app">
  <router-view v-if="!$route.meta.keepAlive"></router-view>
  <keep-alive>
    <router-view v-if="$route.meta.keepAlive"></router-view>
  </keep-alive>
</div>

 //==================================================/

以下内容出处

作者:lzg9527
链接:https://juejin.im/post/5e702c4c51882549052f6054
来源:掘金
 

17、Vue.use

我们使用的第三方 Vue.js 插件。如果插件是一个对象,必须提供install方法。如果插件是一个函数,它会被作为install方法。install方法调用时,会将Vue作为参数传入。该方法需要在调用new Vue()之前被调用。

我们在使用插件或者第三方组件库的时候用到Vue.use这个方法,比如

import iView from 'iview'
Vue.use(iView)

那么Vue.use到底做了些什么事情呢?我们先来看一下源码

import { toArray } from '../util/index'

export function initUse(Vue: GlobalAPI) {
  Vue.use = function(plugin: Function | Object) {
    const installedPlugins = this._installedPlugins || (this._installedPlugins = [])
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // additional parameters
    const args = toArray(arguments, 1)
    args.unshift(this)
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    installedPlugins.push(plugin)
    return this
  }
}

 

我们由以上可以看出,plugin参数为函数或者对象类型,首先Vue会去寻找这个插件在已安装的插件列表里有没有,如果没有,则进行安装插件,如果有则跳出函数,这保证插件只被安装一次。

接着通过toArray方法将参数变成数组,再判断plugininstall属性是否为函数,或者plugin本身就是函数,最后执行plugin.install或者plugin的方法。

举个例子

下面我们来举个实际例子
1、编写两个插件

const Plugin1 = {
  install(a) {
    console.log(a)
  }
}

function Plugin2(b) {
  console.log(b)
}

export { Plugin1, Plugin2 }

2、引入并 use 这两个插件

import Vue from 'vue'
import { Plugin1, Plugin2 } from './plugins'

Vue.use(Plugin1, '参数1')
Vue.use(Plugin2, '参数2')

此时我们运行项目代码就可以用到上面两个插件了。

18、Vue.mixin

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

1、定义一个 mixin.js

export default mixin {
 data() {
  return {
   name: 'mixin'
  }
 },
 created() {
  console.log('mixin...', this.name);
 },
 mounted() {},
 methods: {  //日期转换
   formatDate (dateTime, fmt = 'YYYY年MM月DD日 HH:mm:ss') {
     if (!dateTime) {
      return ''
     }
     moment.locale('zh-CN')
     dateTime = moment(dateTime).format(fmt)
     return dateTime
  }
 }
}

 

2、在vue文件中使用mixin

import '@/mixin'; // 引入mixin文件
export default {
 mixins: [mixin],  //用法
 data() {
  return {
   userName: "adimin",
   time: this.formatDate(new Date()) //这个vue文件的数据源data里面的time就是引用混入进来的方法
  }
 }
} 

 

或者在全局中使用在main.js中,所有页面都能使用了

import mixin from './mixin'
Vue.mixin(mixin)  

 

合并选项

当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。

  • data对象在内部会进行递归合并,并在发生冲突时以组件数据优先。
  • 同名钩子函数将合并为一个数组,因此都将被调用。混入对象的钩子将在组件自身钩子之前调用。
  • 值为对象的选项,例如 methodscomponents 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。

19、Vue.extend

Vue.extend 属于 Vue 的全局 API。它使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。如下:

<div id="app"></div>

var Profile = Vue.extend({
  template: '<p>{{firstName}} {{lastName}}</p>',
  data: function () {
    return {
      firstName: 'Walter',
      lastName: 'White'
    }
  }
})
// 创建 Profile 实例,并挂载到一个元素上。
new Profile().$mount('#app')

应用实例

我们常用 Vue.extend 封装一些全局插件,比如 toast, diolog 等。

下面以封装一个 toast 组件为例。

1、编写组件

  • 根据传入的 type 确定弹窗的类型(成功提示,失败提示,警告,加载,纯文字)
  • 设置弹窗消失的时间
<template>
  <div>
    <transition name="fade">
      <div class="little-tip" v-show="showTip">
        <img src="/success.png" alt="" width="36" v-if="type=='success'" />
        <img src="/fail.png" alt="" width="36" v-if="type=='fail'" />
        <img src="/warning.png" alt="" width="36" v-if="type=='warning'" />
        <img src="/loading.png" alt="" width="36" v-if="type=='loading'" class="loading" />
        <span>{{msg}}</span>
      </div>
    </transition>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        showTip: true,
        msg: '',
        type: ''
      }
    },
    mounted() {
      setTimeout(() => {
        this.showTip = false
      }, 1500)
    }
  }
</script>
<style lang="less" scoped>
  /* 样式略 */
</style>

2、利用 Vue.extend 构造器把 toast 组件挂载到 vue 实例下

import Vue from 'vue'
import Main from './toast.vue'

let Toast = Vue.extend(Main)

let instance
const toast = function(options) {
  options = options || {}
  instance = new Toast({
    data: options
  })
  instance.vm = instance.$mount()
  document.body.appendChild(instance.vm.$el)
  return instance.vm
}
export default toast

3、在 main.js 引入 toast 组价并挂载在 vue 原型上

import Vue from 'vue'
import toast from './components/toast'
Vue.prototype.$toast = toast

4、在项目中调用

this.$toast({ msg: '手机号码不能为空' })

this.$toast({
  msg: '成功提示',
  type: 'success'
})

Vue.extend 和 Vue.component 的区别

  • component是需要先进行组件注册后,然后在 template 中使用注册的标签名来实现组件的使用。Vue.extend 则是编程式的写法。
  • 控制component的显示与否,需要在父组件中传入一个状态来控制或者在组件外部用 v-if/v-show 来实现控制,而 Vue.extend 的显示与否是手动的去做组件的挂载和销毁。

20、Vue.directive

注册或获取全局指令。指令定义函数提供了几个钩子函数(可选):

  • bind: 只调用一次,指令第一次绑定到元素时调用,可以定义一个在绑定时执行一次的初始化动作。
  • inserted: 被绑定元素插入父节点时调用(父节点存在即可调用,不必存在于 document 中)。
  • update: 被绑定元素所在的模板更新时调用,而不论绑定值是否变化。通过比较更新前后的绑定值。
  • componentUpdated: 被绑定元素所在模板完成一次更新周期时调用。
  • unbind: 只调用一次, 指令与元素解绑时调用。

应用实例

下面封装一个复制粘贴文本的例子。

1、编写指令 copy.js

const vCopy = { 
  bind (el, { value }) {
    el.$value = value // 用一个全局属性来存传进来的值
    el.handler = () => {
      if (!el.$value) {
        alert('无复制内容')
        return
      }
      // 动态创建 textarea 标签
      const textarea = document.createElement('textarea')
      // 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域
      textarea.readOnly = 'readonly'
      textarea.style.position = 'absolute'
      textarea.style.left = '-9999px'
      // 将要 copy 的值赋给 textarea 标签的 value 属性
      textarea.value = el.$value
      // 将 textarea 插入到 body 中
      document.body.appendChild(textarea)
      // 选中值并复制
      textarea.select()
      // textarea.setSelectionRange(0, textarea.value.length);
      const result = document.execCommand('Copy')
      if (result) {
        alert('复制成功')
      }
      document.body.removeChild(textarea)
    }
    // 绑定点击事件,就是所谓的一键 copy 啦
    el.addEventListener('click', el.handler)
  },
  // 当传进来的值更新的时候触发
  componentUpdated (el, { value }) {
    el.$value = value
  },
  // 指令与元素解绑的时候,移除事件绑定
  unbind (el) {
    el.removeEventListener('click', el.handler)
  }
}

export default vCopy

2、注册指令

import copy from './copy'
// 自定义指令
const directives = {
  copy
}
// 这种写法可以批量注册指令
export default {
  install (Vue) {
    Object.keys(directives).forEach((key) => {
      Vue.directive(key, directives[key])
    })
  }
}

3、在 main.js 引入并 use

import Vue from 'vue'
import Directives from './JS/directives'
Vue.use(Directives)

这样就可以在项目直接用 vCopy 指令了。

posted @ 2020-03-18 09:47  晚安早安  阅读(462)  评论(0编辑  收藏  举报