Vue开发小技巧

小技巧

1、页面刷新

传统刷新

this.$router.go(0) 、location.reload()

弊端:

  • 刷新整个应用而不是页面
  • 如果有应用初始化时调用的接口,方法等也会执行,如根据用户信息获取账号权限,浪费资源,影响体验
  • 页面会有一瞬间的白屏

新方案1:router-view添加v-if + (provide/inject 或 event-bus)

通过属性控制router-view的重新渲染,达到页面刷新的效果。

// App.vue
<template>
    <router-view v-if="isRouterAlive"/>
</template>

export default {
  provide() {
    return {
      AppReload: this.AppReload
    };
  },
  data() {
    return {
      isRouterAlive: true   // 控制页面刷新字段
    }
  },
  methods: {
    // 刷新页面的方法
    AppReload() {
      this.isRouterAlive = false;
      this.$nextTick( () => {
        this.isRouterAlive = true;
      });
    },
  }
}
复制代码
// 子路由页面
export default {
  inject: ["AppReload"],
  methods: {
    pageReload() {
      // 调用注入的刷新方法
      this.AppReload();
    }
  }
}
复制代码

也可用event-bus替换provide/inject,方式同理

新方案2:redirect

路由的重定向,适用于菜单类操作,点击菜单可能是跳转到新路由,也可能还是当前路由(用户的习惯如果点击当前路由应该是刷新页面),而在vue中相同路由的跳转并不会刷新。通过重定向的方式可以解决这一问题

// 先注册一个名为 `redirect` 的路由
<script>
export default {
  beforeCreate() {
    const { params, query } = this.$route
    const { path } = params
    this.$router.replace({ path: '/' + path, query })
  },
  render: function(h) {
    return h() // avoid warning message
  }
}
</script>
复制代码
// 页面跳转时,跳转到 '/redirect' 页面,带上目标页面的参数即可
const { fullPath } = this.$route
this.$router.replace({
  path: '/redirect' + fullPath
})
复制代码

2、Vue.set使用场景

向响应式对象中添加一个 新的 响应式的 property,且触发视图更新

为什么使用

由于Vue 无法检测到对象属性的添加或删除。而Vue 会在实例初始化时会对data对象属性执行 getter/setter 转化,所以初始化时data对象中存在数据才为响应式数据。那如何在后续为对象添加新的响应式数据,可通过Vue.set。

使用方法:

Vue.set( target, propertyName/index, value )  
// 或 vm.$set
复制代码

其中target为: 响应式对象或数组,且不能是vue实例(不允许动态添加根级响应式 property)

常见场景:

// 初始数据
export default {
  data () {
    return {
      person: {
        name: 'zs'
      },
      list: [
        {
          year: '2020',
          name: 'create'
        },
        {
          year: '2021',
          name: 'grow'
        }
      ]
    }
  }
}

/*
* 对象的操作
*/
// 给对象设置新属性时
this.person.age = 18  // 非响应式,不触发视图更新

// 响应式设置
this.$set(this.person, 'age', 18) // 响应式,视图更新
this.person.age = 20 // set之后,数据为响应式,直接赋值也可触发更新

/*
* 数组的操作
*/
// 非响应式设置:利用索引直接设置一个数组项时
this.list[0] = {}

// 响应式设置
this.$set(this.list, 0, {})
// 或
this.list.splice(0, 1, {})
// 直接给数组的某一项设置新属性时,同理
this.$set(this.list[0], 'time', 1) // 响应式

// 响应式数据的直接赋值,也会触发视图更新,并且新值也为响应式
this.person = Object.assign({}, this.person, { a: 1, b: 2 })
// 代替 `Object.assign(this.person, { a: 1, b: 2 })` 
复制代码

set失败场景

set方法基本原理 set方法执行时,内部是会先判断 target 中是否已存在property属性,如果已存在,则是直接对target中property进行简单赋值。

// .set内部执行机制(这里只是简单说明,实际会更复杂)
function set(target, property, value) {
    if (target[property]) {
        target[property] = value
    } else {
        // 执行新属性的赋值同时getter/setter化,设为响应式数据
        ...
    }
}
复制代码

基于该机制,部分场景下会出现set失败场景。

如下,person初始化无age属性时,先直接进行了赋值操作,后续再执行set,set则只是赋值,不会将数据 getter/setter 化,导致失败。

// 某些操作中,直接给对象设置新属性
this.person.age = 18

// 后续再执行set,则age依旧为非响应式
this.$set(this.person, 'age', 18) 
复制代码

如:v-model 中set失败

v-model绑定对象形式属性时(如a.b),在更改值的回调事件中,会默认调用set方法来对值进行更新;

// html
<input v-model="a.b" />

// 输入框内容变更时,默认是调用的set方法进行值的更新
_vm.$set(_vm.a, "b", value)
复制代码

所以我们通常通过 v-model 绑定响应式对象不存在属性时,给我们的感觉就是响应式。通常情况下这是没有问题的。但是如果在v-model更改值的回调前,手动赋值了a.b数据,后续表单值再更改也不会触发视图更新(即上面的set失败场景)。如下:

// data初始化
data {
  person: {}
}

// html
<input v-model="person.age" />

// 如果在表单值更改前,已经执行了直接赋值,则表单更改时不会再触发视图更新。
// 比如在页面初始化后,获取了age参数,并进行赋值,此时再去修改input值,并不会触发视图更新
this.person.age = 18
复制代码

3、非响应式数据的“伪视图更新”:

非响应式数据更新时,事件循环中当前组件视图有依赖的响应式数据更新,则视图中非响应式数据也会更新

<template>
  <div>
    {{c}}
    {{a.b}}
  </div>
</template>

...
data {
  a: {},
  c: 1
}
...
// setValue 方法执行前4次,视图中c、a.b数据都会更新,4次后不再更新视图
setValue() {
  if (this.c < 5) {
    this.c += 1;
  }
  this.a.b = this.a.b ? this.a.b + 1 : 1;
}
复制代码

原因分析

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖(依赖收集)。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染(派发更新)。

而Vue 在更新 DOM 时是 异步执行 的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。

然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。这也是为什么如果要获取更新后的DOM,通常在Vue.nextTick()回调中处理

而上方的例子中,setValue 方法前4次执行时,都触发了“c”属性的setter,将通知当前组件的watcher,重新渲染组件。而更新DOM是异步的,真正去更新DOM时,a.b值也变更了,则视图中会连同a.b一起更新。而第4次后执行的事件,“c”值不会变更,即使a.b值变更了,也不会触发组件的重新渲染,视图不会再更新。

4、style scoped原理及穿透

CSS模块化,避免组件间样式互相影响。

为什么添加scoped后父子组件间样式不再被继承、覆写(子组件的根节点还是可以被父组件样式影响)。因为添加该属性后每个css选择器语句及html标签都会添加自定义属性hash,通过该hash来控制样式影响指定的元素! 如下:

// 源代码

// 父组件
<template>
  <div class="a">
    <div class="b">
      <comp />
    </div>
  </div>
</template>

<style scoped>
.a{}
.a .b{}
.a .c{}
.a .d{}
.a >>> .c{}
.a >>> .d{}
.a >>> .c .d{}
</style>

// comp子组件
<template>
  <p class="c">
    <span class="d"></span>
  </p>
</template>

<style scoped>
...
</style>
复制代码
// 编译过后

// 如果组件style无scoped属性,则html、css不会添加data-v属性
<template>
  <div data-v-6364f53c class="a">
    <div data-v-6364f53c class="b">
      <!--子组件根元素会同时添加父组件、子组件data-v属性-->
      <p data-v-537e2781 data-v-6364f53c class="c">
        <span data-v-537e2781 class="d"></span>
      </p>
    </div>
  </div>
</template>

<style>
  <!-- 组件本身元素样式不需穿透 -->
  .a{}            →  .a[data-v-6364f53c]{}
  .a .b{}         →  .a .b[data-v-6364f53c] {}
  <!-- 设置子组件样式 -->
  .a .c{}         →  .a .c[data-v-6364f53c] {} <!-- 不加穿透也可控制子组件根元素样式-->
  .a .d{}         →  .a .d[data-v-6364f53c] {} <!-- 无效,无法影响子组件的非根元素-->
  <!-- 设置穿透,可影响子组件所有元素-->
  .a >>> .c{}     →  .a[data-v-6364f53c] .c {} 
  .a >>> .d{}     →  .a[data-v-6364f53c] .d {}
  .a >>> .c .d{}  →  .a[data-v-6364f53c] .c .d {}
</style>
复制代码

注意事项:

  • 原生css使用>>>进行样式穿透; scss、less使用/deep/;
  • 不需要嵌套使用穿透语法,在父组件设置穿透可影响所有子孙组件。嵌套使用会导致后续层级穿透语法解析失败,导致出错。
  • 不要在全局样式中使用穿透语法(.vue文件中style不带scope也是全局样式)。全局样式本身就能影响所有组件,不受scoped影响,添加后反而可能导致穿透语法编译失败,样式无法正常生效。

5、组件通信

Vue组件通信中几种主要的场景:父子组件、隔代组件、兄弟组件

  1. props / $emit:父子组件
  2. ref 与 $parent / $children:父子组件
    • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例
    • $parent / $children:访问父 / 子实例
  3. EventBus:均可,通过一个空的 Vue 实例作为事件中心,用它来触发事件和监听事件
  4. $attrs / $listeners:隔代组件
  5. provide / inject:隔代组件。祖先组件通过 provider 提供变量,子孙组件通过 inject 注入变量
  6. Vuex:均可。只保存全局数据,不要滥用,否则反而影响性能。如:父子组件通信使用父子组件通信方式就好

如:父组件调用子组件的方法

通过$refs或者$chilren来拿到对应的实例,从而操作

<comp ref="comp"></comp>

// 调用子组件方法
this.$refs.comp.eventName()
复制代码

6、.sync && v-model

prop 的“双向绑定”。都是语法糖。单个组件可以将.sync作用于多个属性,而v-model只能使用一个

v-model

作用于自定义组件时,默认会利用名为 value 的 prop 和名为 input 的事件(可在子组件中通过model属性更改属性和事件名)。

// 父组件
<comp v-model="bar.name"></comp>
// 基本等同于 ↓
<comp :value="bar.name" @input="bar.name = $event"></comp>
// 但是不单单只是做了类语法糖的这种处理,还有其他的比如:
// 绑定的如果是对象属性,回调为set的赋值处理等

// 子组件:
<div>{{value}}</div>

// 接收参数
props: ["value"]
// 更新方式
this.$emit("input", newValue)
复制代码

当作用于表单项时,v-model 在内部为不同的元素使用不同的属性并抛出不同的事件。

  • text、textarea:value 属性、input 事件;
  • checkbox、radio:checked 属性、change 事件;
  • select:value 属性、 change 事件。

.sync

实现机制和v-model是类似的。当有需要在子组件修改父组件值的时候这个方法很好用

// 父组件
<comp :foo.sync="bar.name"></comp>
// 等同于 ↓
<comp :foo="bar.name" @update:foo="bar.name = $event"></comp>

// 子组件内更新方式
this.$emit("update:foo", newValue)

// 同时设置一个对象的全部属性
<comp v-bind.sync="bar"></comp>
// 把 bar 对象中的每一个 property 都作为一个独立的 prop 传进去,然后各自添加用于更新的 v-on 监听器
复制代码

7、事件修饰符

.native

用于在某个组件上注册一个原生事件。因为直接通过@click不加该修饰符注册的事件,触发的实际是组件内部通过 $emit传递过来的事件。

// 组件内部没有$emit传递click事件,则事件无法生效。
<vButton @click="clickHandler">按钮</vButton> 

// 事件作为原生click事件注册生效
<vButton @click.native="clickHandler">按钮</vButton>   
复制代码

而部分UI组件库,在组件上做了处理,如element-ui的 Button组件,点击按钮时,会将点击事情传递出来。

// Button组件内部处理
<button
  class="el-button"
  @click="handleClick"
</button>

...

handleClick: function handleClick(evt) {
  this.$emit('click', evt);
}
复制代码

所以我们在使用这种组件时,是否添加了.native,点击事件都能生效

.stop

阻止事件冒泡

.prevent

拦截默认事件

.passive

不拦截默认事件。

通俗点说就是每次事件产生,浏览器都会去查询一下是否有preventDefault阻止该次事件的默认动作。我们加上passive就是为了告诉浏览器,不用查询了,我们没用preventDefault阻止默认动作。

常同于滚动监听 @scoll,@touchmove事件,提高效率。因为滚动监听过程中,移动每个像素都会产生一次事件,每次都使用内核线程查询prevent会使滑动卡顿。我们通过passive将内核线程查询跳过,可以大大提升滑动的流畅度。

<div v-on:scroll.passive="onScroll">...</div>
复制代码

注:passive和prevent冲突,不能同时绑定在一个监听器上。

8、生命周期(路由切换,父子组件)

路由切换

路由A->B

B beforeCreate -> B created -> B beforeMount -> A beforeDestroy -> A destroyed -> B mounted
复制代码

可能出现的问题

// A、B组件中绑定了同名的全局事件,A路由切换到B路由时,需要销毁A路由中事件,初始化B路由中事件。
// 如我之前做过的一次,echarts图表点击的自定义事件,A、B页面初始化时都需要注册一个这样的全局事件,页面离开时销毁。

// A组件
beforeMount(){
    window.gotoPageChart = () => {
      ...
    }
}
destroyed() {
    window.gotoPageChart = null;
}

// B组件
beforeMount(){
    window.gotoPageChart = () => {
      ...
    }
}
destroyed() {
    window.gotoPageChart = null;
}
复制代码

最终切换到B路由后,触发事件时找不到window.gotoPageChart事件。就是因为这里的生命周期顺序,B组件的beforeMount执行赋值后,再执行了A组件的destroyed内方法,将window.gotoPageChart设置了null,导致与预期不一致。

解决方案:将beforeMount生命周期换为mounted即可

父子组件

加载渲染过程

父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted
复制代码

子组件更新过程

父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

父组件更新过程

父 beforeUpdate -> 父 updated

销毁过程

父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

9、动态参数

<v-time :[setData]="valueDate"></v-time>

...

data() {
  return {
    valueDate: new Date(),
    setData: "startDate"     // 动态的属性名,如果这里的值为 endDate,则为endDate属性绑定值。
  }
}
复制代码

这里会将 setData 作为JavaScript表达式动态求值,结果作为最终的参数来使用。上述绑定将等价于 v-bind:startDate

同样地,你可以使用动态参数作为事件名绑定处理函数:

<button @[eventName]="handler"></button>
复制代码

当 eventName 的值为 focus 时,v-on:[eventName] 将等价于 v-on:focus。

动态参数预期结果为字符串。当值为null时,可以被显性地用于移除绑定

同样可以适用于插槽绑定:

<foo>
    <template #[name]>
        Default slot
    </template>
</foo>
复制代码

10、组件库中未暴露的API

除了API中暴露出来的属性、方法,部分组件还有其他支持的属性或方法可以使用。通常这在是API中提供的不满足我们需求时。

如:element ui 下拉菜单:我们希望点击菜单项后不被隐藏,执行自定义操作后再隐藏

// 下拉菜单默认在点击菜单项后会被隐藏,将hide-on-click属性默认为false可以关闭此功能
<el-dropdown ref="dropdown" :hide-on-click="false"></el-dropdown>

...
// 而在执行其他操作后,可调用此方法再将下拉菜单隐藏
this.$refs.dropdown.hide();  // 手动隐藏

// 也有显示方法
this.$refs.dropdown.show();  // 手动显示
复制代码

查找方式

  • 可以查看组件源码,如 el-dropdown 的 methods
methods: {
  show() {
    if (this.triggerElm.disabled) return;
    clearTimeout(this.timeout);
    this.timeout = setTimeout(() => {
      this.visible = true;
    }, this.trigger === 'click' ? 0 : this.showTimeout);
  },
  hide() {
    if (this.triggerElm.disabled) return;
    this.removeTabindex();
    if (this.tabindex >= 0) {
      this.resetTabindex(this.triggerElm);
    }
    clearTimeout(this.timeout);
    this.timeout = setTimeout(() => {
      this.visible = false;
    }, this.trigger === 'click' ? 0 : this.hideTimeout);
  },
  ...
}
复制代码
  • 查看组件实例对象,如: console.log(this.$refs.dropdown),通过属性或方法名称查看是否有需要的方法。通常普通的方法可以通过方法名判断出基本用途

11、别名的多场景使用(template/js/css)

别名的使用场景,如已经在配置文件中配置好 ./src 目录别名为 @。

// 1、JS文件中,直接使用 @
import chartItem from "@/components/report";

// 2、css(scss等预处理)文件中,使用 ~@ 开头
@import '~@/assets/css/global.less';
background-image: url("~@/assets/img/login/user.png");

// 3、template模板中,@、~@ 均可
<img src="@/assets/img/login/logo.png">
复制代码

12、computed的 get、set

  • 在处理传入数据和目标数据格式不一致的时候很有用,如时间格式;
// 前端显示时间单位为 秒
<input v-model.number="customTime">

...
data() {
    return {
        // 后端返回及传递时间为 毫秒
        form: {
            time: 3000
        }
    }
}

computed: {
    customTime: {
        get() {
            return this.form.time / 1000
        },
        set(val) {
            this.form.time = val * 1000
        }
    }
}
复制代码
  • 可以在获取数据的同时,监听数据变化,当发生变化时,做一些额外的操作。

最经典的用法就是v-model上绑定一个 vuex 值的时候,input 发生变化时,通过 commit更新存在 vuex 里面的值。

<input v-model="name">

...

computed: {
    name: {
        get() {
            return this.$store.state.name
        },
        set(val) {
            this.$store.commit('updateName', val)
        }
    }
}
复制代码
  • 还有如模态框的二次封装,设置显示、隐藏
<div>
    <el-dialog :visible.sync="dialogVisible">
        ...
    </el-dialog>
</div>


props: {
    // 模态框是否显示
    dialogViewVisible: {
        required: true,
        type: Boolean,
        default: false
    }
}
computed: {
    dialogVisible: {
        get() {
            return this.dialogViewVisible;
        },
        set(val) {
            this.$emit("update:dialogViewVisible", val);
        }
    }
}
复制代码

13、watch的常用参数

immediate

当 watch 一个变量的时候,组件初始化时默认并不会执行,需要在created的时候手动调用一次。

// bad
created() {
  this.fetchUserList();
},
watch: {
    searchText: 'fetchUserList'
}
复制代码

可以添加immediate属性,这样初始化的时候也会触发

// good
watch: {
  searchText: {
    handler: 'fetchUserList',
    immediate: true,
  }
}
复制代码

deep

当设置为true时,它会进行深度监听。比如有一个数组或对象,里面任意一项变更都会触发watch。

watch: {
  searchTextObj: {
    handler: 'fetchUserList',
    deep: true
  }
}
复制代码

14、$attrs && $listeners

组件二次封装的神器

属性的说明

  • 非 prop 的 attribute:传向一个组件,但是该组件并没有定义相应 prop 的 attribute。

显式定义的 prop 适用于向一个子组件传入信息。而其他非 prop的attribute 会被添加到这个组件的根元素上(class 和 style会和子组件本身的合并,而其他的大部分属性会进行替换),子组件设置inheritAttrs: false,可以阻止这一默认行为,根元素不继承 attribute(不影响class和style)

  • $attrs:非 prop 的 attribute( class 和 style 除外 )。可以通过 v-bind="$attrs" 传入内部组件,手动决定这些 attribute 赋予到哪个元素。通常配合子组件 inheritAttrs 选项一起使用。
  • $listeners:向子组件绑定 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="listeners" 传入内部组件 如我们基于第三方组件进行二次封装时,可能会加入一些业务逻辑,但是第三方组件本身可能支持几十个配置参数,不可能所有参数都自定义通过props传递,并且第三方组件后期也可能加入新参数。这时候我们就可以使用v-bind="\attrs"传递所有属性、v-on="$listeners"传递所有方法。

如下:基于Element-UI的el-pagination组件的简单二次封装

// 自定义的pagination组件
// 基于el-pagination组件二次封装,封装公共属性、业务等; 支持父组件传入的el-pagination属性、绑定事件
<div class="custom-pagination">
    <div>{{custom-prop}}</div>
    <el-pagination
      background
      :layout="layout"
      v-bind="$attrs"
      v-on="$listeners"
    />
</div>

export default {
    inheritAttrs: false, // 根元素不继承 非prop的attribute
    props: ['custom-prop'],
    data() {
        return {
          layout: 'prev, pager, next, total'
        }
    },
}

// 父组件
// 引入自定义pagination组件,可直接按照el-pagination规则传递参数,绑定监听事件
<pagination
    :custom-prop="_customProp"
    :current-page="currentPage"
    :page-size="limit"
    :total="total"
    @current-change="handleCurrentChange"
    @size-change="handleSizeChange"
/>
复制代码

如上,我们在自定义的pagination组件中,只显式声明了custom-prop的prop属性,其他定义的属性如current-page、page-size、total将通过pagination组件中定义的v-bind="$attrs"直接透传到el-pagination组件中,而不用在把这些props全部显式定义一遍。事件的传递同理。

我们没有在pagination组件props中声明的属性,给pagination组件绑定的方法,会通过$attrs、$listeners直接传递到el-pagination组件中

组件显式定义接收的 props ,也可以通过this.$props获取,通过v-bind="$props"直接向下传递

15、v-show && v-if

v-if

DOM 区域没有生成,没有插入文档,等条件成立的时候才动态插入到页面!

v-show

DOM 区域在组件渲染的时候同时渲染了,只是单纯用 css 隐藏了

使用

  • 频繁切换显隐的用v-show,条件判断渲染内容不怎么切换的用v-if
  • DOM结构不怎么变化的用v-show, 数据需要改动很大或者布局改动的用v-if

16、自动注册全局组件、指令、过滤器等

传统的注册全局组件方式(指令和过滤器基本也是这样):

// 依次引入全局组件,再依次进行注册
import baseButton from '@/globalComponents/baseButton.vue'
import baseDialog from '@/globalComponents/baseDialog.vue'
Vue.component('baseButton', baseButton)
Vue.component('baseDialog', baseDialog)
复制代码

以上方式,如果组件一旦过多,代码就会非常长,并且后续再添加新的全局组件时,需要再次改写这里的引入、注册代码。不够优雅

我们可以基于 webpack 的require.context()来实现自动引入组件并注册。

例:创建一个globalComponents文件夹,将想要注册到全局的组件都放在这个文件夹里。在入口文件main.js中引入如下:

// require.context:接收3个参数:要搜索的文件夹目录,是否搜索子目录,匹配文件的正则表达式;返回一个根据request获取模块内容的函数。

// 找到globalComponents文件跟目录下以.vue命名的文件
const requireComponent = require.context(
    './globalComponents', false, /\.vue$/
)

// 循环找到的文件map对象
requireComponent.keys().forEach(fileName => {
    // 获取对应文件的模块
    const componentConfig = requireComponent(fileName)
    
    //因为得到的filename格式是: './baseButton.vue', 所以这里我们去掉头和尾,只保留真正的文件名(baseButton)
    const componentName = fileName.replace(/^\.\//, '').replace(/\.\w+$/, '')
    
    Vue.component(componentName, componentConfig.default)
})
复制代码

后续再添加组件到globalComponents目录中,就会自动进行注册了,指令、过滤器等注册方法同理。

并且不单是可以用来进行全局的注册,也可以根据自己的需要,获取指定文件夹下的文件后做其他的处理。

具体关于require.context的使用说明,可以参考另外一篇文章:webpack的require.context详解

17、子组件生命周期监听

hook

监听生命周期。

  • 常用场景1:组件销毁时销毁全局事件或销毁定时器。
// 传统的事件注册、销毁。有2点弊端
// 1.需要在实例中保存要销毁的事件或定时器
// 2.在不同的option中维护,option中内容较多时,维护起来较麻烦,并且容易忘记
mounted() {
  window.addEventListener('resize', this.debounceHeight)
},
beforeDestroy() {
  window.removeEventListener('resize', this.debounceHeight)
}
复制代码
// 使用hook销毁,不需要在实例中绑定事件,代码一处维护
mounted() {
  window.addEventListener('resize', _debounceHeight)
  // 也可以使用this.$on
  this.$once('hook:beforeDestroy', function() {
    window.removeEventListener('resize', _debounceHeight)
  })
},
复制代码
  • 常用场景2:监听子组件生命周期。
// 传统方式

// 自组件指定生命周期抛出事件
mounted() {
  this.$emit("mounted")
}
// 父组件监听事件
<comp @mounted="handleEvent"/>
复制代码
// hook方式
<comp @hook:mounted="handleEvent"/>
复制代码

18、动态组件

根据传入的 'is' 参数,动态判断需要渲染哪一个组件

如装修、搭建类页面的JSON数据解析,根据数据中组件类型决定当前使用哪一个组件来渲染

<div v-for="currentComponent in pageList">
    // :is 属性值需要和组件定义名称对应上
    <component
      :is="currentComponent.type"
      :key="currentComponent.id"
      :parmes="currentComponent.data"
    />
</div>
复制代码

19、异步组件

组件的懒加载,只有在这个组件需要被渲染的时候才会加载资源。 与路由的懒加载同样的原理,工厂函数可以返回一个对象配置加载时组件、加载失败组件等

通常用来一个页面中,组件内容过多,过大,但是部分组件并不是一进入页面就会被使用。如:点击页面查看按钮后弹出的查看组件,默认进入页面只会加载其他内容的资源文件,点击查看按钮时才会加载查看组件资源。

components: {
    // 异步引入查看组件
    ViewPage: () => ({
        // 需要加载的组件,该属性必填,其他都是非必填
        component: import("./view"),
        // 异步组件加载时使用的组件
        loading: PageLoading
        // 加载失败时使用的组件
        error: ErrorComponent,
        // 展示加载时组件(loading组件)的延时时间。默认值是 200 (毫秒)
        delay: 200,
        // 如果提供了超时时间且组件加载也超时了,
        // 则使用加载失败时使用的组件。默认值是:`Infinity`
        timeout: 3000
    })
}
复制代码

20、递归组件

组件在自己的模板中调用自身

  • 需要设置组件的name属性
  • 需有结束的阙值,递归调用是条件性的(使用一个最终会得到 false 的 v-if)
  • 不需要import引入自身
// 组件递归用来开发一些具体有未知层级关系的独立组件。比如:
// 联级选择器和树形控件

<template>
    <div v-for="(item,index) in treeArr">
        <!-- 递归调用自身, 需有一个最终为false的v-if -->
        <tree :item="item.arr" v-if="item.flag"></tree>
    </div>
</template>

<script>
export default {
    // 必须定义name,组件内部才能递归调用
    name: 'tree',
    data(){
        return {
            treeArr: []
        }
    }
}
</script>
复制代码

21、Vue.observable()

让一个对象变为响应式数据。Vue 内部就是用它来处理 data 函数返回的对象,让data中数据成为响应式数据。 返回的对象可以直接用于渲染函数和计算属性内,并且会在发生改变时触发相应的更新; 也可以作为最小化的跨组件状态存储器,用于简单的场景。

通讯原理实质上是利用Vue.observable实现一个简易的 vuex

// 文件路径 - /store/store.js
import Vue from 'vue'

export const store = Vue.observable({ count: 0 })
export const mutations = {
  setCount (count) {
    store.count = count
  }
}

//使用,每次数据变更时,视图就会自动更新
<template>
    <div>
        <label for="bookNum">数 量</label>
        <button @click="setCount(count+1)">+</button>
        <span>{{count}}</span>
        <button @click="setCount(count-1)">-</button>
    </div>
</template>

<script>
import { store, mutations } from '../store/store'

export default {
    name: 'Add',
    computed: {
        count () {
            return store.count
        }
    },
    methods: {
        setCount: mutations.setCount
    }
}
</script>
复制代码

22、keep-alive

缓存组件。组件移除时,进行暂存,下次激活时,再直接渲染。

使用场景

  • 保持页面状态(记录当前页面滚动位置、翻页页数等),结合路由一起使用
  • 避免重复渲染(提高性能),在部分组件高频切换的场景使用

注意事项

  • 提供 include 和 exclude 属性,两者都支持字符串或正则表达式, include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高;
  • 被缓存的组件,在生命周期中多2个钩子函数 activated 和 deactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated。

不能无限制缓存,只缓存需要缓存的,不需要的缓存及时释放。否则占用内存太多可能影响性能

23、router key

常用于相同路由,不同参数的页面跳转。

如/product/1,跳转到/product/2,跳转后页面不会更新,因为vue-router发现这是同一个组件,会复用这个组件。在页面跳转后created、mounted组件初始化生命周期内的方法没有执行。

通常的解决方案是监听$route的变化来初始化数据。虽然可以解决问题但是不够优雅

更好的解决方案:给router-view添加一个路由路径的key值,只要url变化,就会重新创建组件,重新获取数据

<router-view :key="$route.fullpath"></router-view>
复制代码

24、作用域插槽

让插槽的内容能够访问子组件的数据

常用于组件多处使用时,插槽基于子组件数据自定义处理,类似

// 子组件 todo-list
<div>
    {{ user.firstName }}
    <slot name="todo" :user="user" :text="text">
        {{ user.lastName }}
    </slot>
</div>
 
data() {
    return {
        user:{
            lastName:"Zhang",
            firstName:"yue",
            sex:"男",
        },
        text: "其他内容"
    }
}
复制代码
// 父组件

// 直接接取数据
<todo-list>
    //slotProps 可以随意命名
    //slotProps 接取的是子组件标签slot上属性数据的集合
    <template #todo="slotProps" >
        {{slotProps.user.sex}}
    </template>
</todo-list>

// 或解构
<todo-list>
    <template #todo="{ user, text }" >
        {{text}}
    </template>
</todo-list>
复制代码

基于这个,我们可以将插槽转换为可复用的模板,这些模板可以基于输入的 prop 渲染出不同的内容。这在设计封装数据逻辑的同时,允许父级组件自定义部分布局的可复用组件时是很有用的。在组件库中也经常会遇到这种用法

25、生产环境的检查、调试

在生产环境如何查看Vue实例,获取、修改实例对象的属性?

  1. 首先,找到我们组件跟元素对应的DOM元素,2种方式
  • 在控制台中通过DOM查找方法(如querySelector或getElementById等)选择元素。
  • 使用Chrome devtools 的 elements面板选中组件的根元素,然后在控制台中输入$0,$0表示最后选择的元素。$1是之前选择的元素,依此类推.它记得最后五个元素$0 – $4.
  1. 通过DOM元素的__vue__属性,获取Vue实例的详细信息
// 方式1
document.querySelector('[data-v-1f10e70e]').__vue__

// 方式2
$0.__vue__

// 查看组件属性
$0.__vue__.msg

// 更改组件属性
$0.__vue__.msg = "获取到组件实例了"
复制代码

性能优化

1、webpack-bundle-analyzer - 构建结果输出分析

可以更简单、直观地分析输出结果,Vue-cli中默认集成了该插件。其他场景需要的话也可以自定义引入:

如在Vue-cli的项目中 执行 $ npm run build --report 后生成分析报告:可以查看我们各个包的大小。引入了哪些资源,从而进一步分析可优化项

2、Object.freeze

大数据优化,数据量特别大的时候,使用容易卡顿,如果这些数据并不需要响应式变化,冻结对象,禁止响应式。

当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter,它们让 Vue 能进行追踪依赖,在属性被访问和修改时通知变化。 使用了 Object.freeze 之后,不仅可以减少 observer 的开销,还能减少不少内存开销

使用方式:this.obj = Object.freeze(Object.assign({}, obj))

注意:冻结只是冻结对象里面的属性,对象本身还是可以更改

new Vue({
    data: {
        // 这样vue不会对list里的对象属性做getter、setter绑定
        list: Object.freeze([
            { value: 1 },
            { value: 2 }
        ])
    },
    mounted () {
        // 界面不会有响应,因为单个属性被冻结
        this.list[0].value = 100;

        // 重新赋值,数据重新变成响应式
        this.list = [
            { value: 100 },
            { value: 200 }
        ];
    }
})
复制代码

3、v-for && v-if

v-for 遍历必须为 item 添加 key。且避免同时使用 v-if,在vue2.x中,同时使用这2个指令时,依旧会把所有数据循环一遍,再控制是否显示(相当于先执行的v-for,再执行的v-if)。在Vue3 中针对这一点做了优化

4、尽量避免全局操作

  • 选择DOM,通过this.refs.xxx.$el的方式,而不是 document.querySelector()等全局选择器。将操作局限在当前组件内
  • 全局事件需要销毁。如某个组件监听窗口变化,window.addEventListener('resize', this.__resizeHandler),一定要在 beforeDestroy或者destroyed生命周期注销。包括定时器
  • 避免过多的全局状态,不是所有的的状态都需要存在Vuex中,需要根据业务进行取舍。只是部分业务的状态处理,可以考虑使用Event Bus或模块化等其他机制。
  • css也尽量避免写过多的全局样式,除了全局公共的样式。其他各个组件的样式都应该使用scoped写法。部分组件内部的差异化可通过样式穿透或使用命名空间。

5、无限列表性能

如果应用存在非常长或者无限滚动的列表,那么需要采用窗口化的技术来优化性能,只需要渲染少部分区域的内容,减少重新渲染组件和创建 dom 节点的时间。 可以参考以下开源项目 vue-virtual-scroll-listvue-virtual-scroller 来优化这种无限列表的场景。

6、防抖节流

涉及到对性能有影响的高频操作,使用防抖节流控制频率,提高性能。

比如我之前做的可视化搭建系统,为了实现配置的所见即所得,每次后台配置组件的展示属性时,都会通过postmessage实时向组件展示区域(另外一个h5系统)发送配置数据,特别是涉及到slider滑块类的配置时,这种高频的传输是比较消耗性能的,此时使用防抖或节流就是很好的选择。

7、组件库的按需加载

对于大型组件库的使用,特别是只用到其中一部分的内容,只引入需要的部分(一般大点的组件库都支持按需引入)

如:echarts

// 方式1: 引入整个包
import echarts from 'echarts'
复制代码
// 方式2: 按需引入

// 引入 ECharts 主模块
var echarts = require('echarts/lib/echarts');
// 按需引入用到的模块
require("echarts/lib/chart/line");
require("echarts/lib/chart/pie");
require("echarts/lib/component/grid");
require("echarts/lib/component/legend");
复制代码

可以对比下上面2种引入方式。可以发现,最终打包的代码,按需引入的echarts部分代码包的体积,比全部引入明显小很多。当然如果我们组件库中大部分内容都会被使用,那还是可以整个引入,可以根据实际情况判断。

Vue3中使用注意事项

以上所有内容都是基于vue2.x版本,由于在vue3中对部分api做了改造、移除等处理,包括vue内部处理的改动。所以有些内容并不适用于vue3。 如下:

  • 数据响应式的处理由原来的Object.defineProperty更改为使用Proxy,所以对于数据的响应式不再是getter、setter化处理,Proxy 代理的是对象,性能更好,新增属性也不需要做特殊处理。
  • Vue.observable,改为用 Vue.reactive 替换
  • $on,$off 和 $once 实例方法已被移除
  • vue3中 v-model 合并了原来的v-model & .sync 一个组件也定义多个 v-model,并指定不同的属性名
  • 过滤器(Filters)的移除
  • 异步组件新的定义方法:defineAsyncComponent
  • v-if 与 v-for 作用在同一元素上的优先级修改。2.x中v-for 会优先作用,3.x中v-if 会优先作用
  • 生命周期的重命名。destroyed重命名为unmounted,beforeDestroy重命名为beforeUnmount

包括还有其他更多的改动,这里只列举出了本文档中涉及到的内容。

参考资料

手摸手,带你用vue撸后台

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值