vue 拓展

vue 生命周期详解

vue 生命周期

生命周期响应类型实践
beforeCreate1. 完成实例初始化,初始化非响应式变量
2. this 指向创建的实例;
3. 可以在里加个 loading 事件;
4. data,computed,watch,methods 上的数据和方法均不能访问
5. $route 对象是存在的,可以根据路由信息进行重定向之类的操作
常用于初始化非响应式变量
created1. 实例创建完成
2. 完成数据(data,props,computed,watch,methods)的初始化 导入依赖项。
3. 可访问 data,computed,watch,methods 上的方法和数据
4. 未挂载 DOM,不能访问 $el, $ref 为空数组
5. 可以对 data 数据进行操作,可进行请求,请求不应过多,避免白屏时间太长。
6. 若在此阶段进行的 DOM 操作需要放在 Vue.nextTick() 的回调函数中
常用于简单的 ajax 请求, 页面的初始化
beforeMount1. $el 已被初始化,数据已加载完成,可以修改数据并更新,不会触发 beforeUpdate,updated.
2. 在挂载开始之前被调用,beforeMount 之前,会找到对应的 template,并编译成 render 函数
-
mounted1. 完成创建 vm.$el,和数据的双向绑定
2. 完成 DOM 挂载和渲染;可在 mounted 钩子对挂载的 dom 进行操作
3. $ref 属性可以访问
4. 可在这里发起请求,获取数据,配合路由钩子(beforeEach,afterEach 等)完成需求
常用于获取 vnode 信息和操作,ajax 请求
beforeUpdate响应式数据更新前调用,发生在虚拟 DOM 打补丁之前适合在更新之前访问现有的 DOM,例如手动移除已添加的事件监视器
updated1. 完成虚拟 DOM 的重新渲染和打补丁
2. 组件 DOM 已完成更新;
3. 可执行依赖的 dom 操作
注意:不要在此函数中操作数据,可能会陷入死循环的。
actived1. 在使用 vue-router 时有时需要使用<keep-alive></keep-alive>来缓存组件状态,这个时候 created 钩子就不会被重复调用
2. 如果我们的子组件需要在每次加载的时候进行某些操作,可以使用 activated 钩子触发
-
deactivated被 keep-alive 缓存的组件停用时调用。-
beforeDestroy实例销毁之前调用, 在这个钩子里, 实例仍然可以使用, this 依旧可以获取到实例常用于销毁定时器,解绑全局事件,销毁插件对象等操作
destroyed实例销毁后调用, 调用后, 当前组件已被删除,销毁监听事件, 组件, 事件, 子实例也被销毁-

!> 不要在选项 property 或回调上使用箭头函数,比如 created: () => console.log(this.a)vm.$watch('a', newValue => this.myMethod()) 。因为箭头函数并没有 thisthis 会作为变量一直向上级词法作用域查找,直至找到为止,经常导致 Uncaught TypeError: Cannot read property of undefinedUncaught TypeError: this.myMethod is not a function 之类的错误。

接口请求放在那个生命周期?

接口请求一般放在 created()mounted(),根据上表分析:

  • 如果请求结果不需要操作 dom,可以放在 created()
  • 如果请求结果需要操作 dom,可以放在 mounted()

为什么要手动销毁定时器,解绑全局事件?

组件销毁只会销毁与实例相关的事件,如果将事件绑在文档或文档中其他与实例不相关的节点上是必须取消监听的,因为该事件与实例无关,因此不会自动销毁。

程序化的事件侦听器

你已经知道了 $emit 的用法,它可以被 v-on 侦听,但是 Vue 实例同时在其事件接口中提供了其它的方法。我们可以:

  • 通过 $on(eventName, eventHandler) 侦听一个事件
  • 通过 $once(eventName, eventHandler) 一次性侦听一个事件
  • 通过 $off(eventName, eventHandler) 停止侦听一个事件

你通常不会用到这些,但是当你需要在一个组件实例上手动侦听事件时,它们是派得上用场的。它们也可以用于代码组织工具。例如,你可能经常看到这种集成一个第三方库的模式:

// 一次性将这个日期选择器附加到一个输入框上 // 它会被挂载到 DOM 上。

mounted: function () {
  // Pikaday 是一个第三方日期选择器的库
  this.picker = new Pikaday({
    field: this.$refs.input,
    format: 'YYYY-MM-DD'
  })
},
// 在组件被销毁之前,
// 也销毁这个日期选择器。
beforeDestroy: function () {
  this.picker.destroy()
}

这里有两个潜在的问题:

它需要在这个组件实例中保存这个 picker,如果可以的话最好只有生命周期钩子可以访问到它。这并不算严重的问题,但是它可以被视为杂物。 我们的建立代码独立于我们的清理代码,这使得我们比较难于程序化地清理我们建立的所有东西。 你应该通过一个程序化的侦听器解决这两个问题:

mounted: function () {
  var picker = new Pikaday({
    field: this.$refs.input,
    format: 'YYYY-MM-DD'
  })

  this.$once('hook:beforeDestroy', function () {
    picker.destroy()
  })
}

使用了这个策略,我甚至可以让多个输入框元素同时使用不同的 Pikaday,每个新的实例都程序化地在后期清理它自己:

mounted: function () {
  this.attachDatepicker('startDateInput')
  this.attachDatepicker('endDateInput')
},
methods: {
  attachDatepicker: function (refName) {
    var picker = new Pikaday({
      field: this.$refs[refName],
      format: 'YYYY-MM-DD'
    })

    this.$once('hook:beforeDestroy', function () {
      picker.destroy()
    })
  }
}

查阅这个示例open in new window可以了解到完整的代码。注意,即便如此,如果你发现自己不得不在单个组件里做很多建立和清理的工作,最好的方式通常还是创建更多的模块化组件。在这个例子中,我们推荐创建一个可复用的 <input-datepicker> 组件。

想了解更多程序化侦听器的内容,请查阅 vue 实例方法 / 事件相关的 APIopen in new window

!> 注意 Vue 的事件系统不同于浏览器的 EventTarget API。尽管它们工作起来是相似的,但是 $emit$on, 和 $off 并不是 dispatchEventaddEventListenerremoveEventListener 的别名。

v-once 使组件只渲染一次

只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。

<!-- 单个元素 -->
<span v-once>This will never change: {{msg}}</span>
<!-- 有子元素 -->
<div v-once>
  <h1>comment</h1>
  <p>{{msg}}</p>
</div>
<!-- 组件 -->
<my-component v-once :comment="msg"></my-component>
<!-- `v-for` 指令-->
<ul>
  <li v-for="i in list" v-once>{{i}}</li>
</ul>

keep-alive

主要用于保留组件状态或避免重新渲染。

1. Props:

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

2. 用法

<keep-alive> 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和 <transition> 相似,<keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。

当组件在 <keep-alive> 内被切换,它的 activated 和 deactivated 这两个生命周期钩子函数将会被对应执行。

在 2.2.0 及其更高版本中,activated 和 deactivated 将会在 <keep-alive> 树内的所有嵌套组件中触发。

<!-- 基本 -->
<keep-alive>
  <component :is="view"></component>
</keep-alive>

<!-- 多个条件判断的子组件 -->
<keep-alive>
  <comp-a v-if="a > 1"></comp-a>
  <comp-b v-else></comp-b>
</keep-alive>

<!-- 和 `<transition>` 一起使用 -->
<transition>
  <keep-alive>
    <component :is="view"></component>
  </keep-alive>
</transition>

!> 注意,<keep-alive> 是用在其一个直属的子组件被开关的情形。如果你在其中有 v-for 则不会工作。如果有上述的多个条件性的子元素,<keep-alive> 要求同时只有一个子元素被渲染。

3. include and exclude

2.1.0 新增

includeexclude prop 允许组件有条件地缓存。二者都可以用逗号分隔字符串、正则表达式或一个数组来表示:

<!-- 逗号分隔字符串 -->
<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 选项的键值)。匿名组件不能被匹配。

4. max

2.5.0 新增

最多可以缓存多少组件实例。一旦这个数字达到了,在新实例被创建之前,已缓存组件中最久没有被访问的实例会被销毁掉。

<keep-alive :max="10">
  <component :is="view"></component>
</keep-alive>
<keep-alive> 不会在函数式组件中正常工作,因为它们没有缓存实例。</keep-alive>

5. 使用场景

如果未使用 keep-alive 组件,则在页面回退时仍然会重新渲染页面,触发 created 钩子,使用体验不好。 在以下场景中使用 keep-alive 组件会显著提高用户体验,菜单存在多级关系,多见于列表页+详情页的场景如:

  • 商品列表页点击商品跳转到商品详情,返回后仍显示原有信息
  • 订单列表跳转到订单详情,返回,等等场景。

6. keep-alive 的生命周期

初次进入时:created > mounted > activated ;退出后触发 deactivated 再次进入:会触发 activated ;事件挂载的方法等

只执行一次的方法放在 mounted 中;组件每次进去执行的方法放在 activated

7. 实战

实现前进刷新,后退不刷新

结合第 5,6 点, 商品列表页首次加载请求数据渲染列表, 这样只有第一次进入到列表页的时候才会请求数据,当从列表页跳到详情页,再从详情页回来的时候,列表页就不会刷新了。

App.vue

<keep-alive include="list">
  <router-view />
</keep-alive>

router.js

{
    path: '/list',
    name: 'list',
    component: () => import('../view/list.vue'),
    meta: {
      keepAlive: true
      }
},
{
    path: '/detail',
    name: 'detail',
    component: () => import('../view/detail.vue'),
    meta: {
      keepAlive: false
      }
},
watch:{
  $route(to, from) {
    const fname = from.name
    const tname = to.name
    if (from.meta.isRefresh || (fname != 'detail' && tname == 'list')) {
      // 在这里重新请求数据
      from.meta.isRefresh = false
    }
  }
}
记录页面滚动位置

keep-alive 并不会记录页面的滚动位置,所以我们在跳转时需要记录当前的滚动位置,在触发 activated 钩子时重新定位到原有位置。 具体设计思路:

在deactivated钩子中记录当前滚动位置,使用localStorage:
deactivated () {
 window.localStorage.setItem(this.key, JSON.stringify({
 listScrollTop: this.scrollTop
 }))
}

在 activated 钩子中滚动:

this.cacheData = window.localStorage.getItem(this.key)JSON.parse(window.localStorage.getItem(this.key)) : null
$('.sidebar-item').scrollTop(this.cacheData.listScrollTop)

$nextTick

建议先了解: js 单线程机制

使用场景: 在进行获取数据后,需要对新视图进行下一步操作或者其他操作时,发现获取不到 DOM。

原因:

  • 这里就涉及到 Vue 一个很重要的概念:异步更新队列(JS 运行机制 、 事件循环)。
  • Vue 在观察到数据变化时并不是直接更新 DOM,而是开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。
  • 在缓冲时会去除重复数据,从而避免不必要的计算和 DOM 操作。
  • 然后,在下一个事件循环 tick 中,Vue 刷新队列并执行实际(已去重的)工作。
  • 所以如果用 for 循环来动态改变数据 100 次,其实它只会应用最后一次改变,如果没有这种机制,DOM 就要重绘 100 次,是一个很大的开销,损耗性能。

一个 this.$nextTick 的实现

// 首先,定义变量:
var callbacks = []; // 缓存函数的数组
var pending = false; // 是否正在执行
var timerFunc; // 保存着要执行的函数

// 然后,创建 $nextTick 内实际调用的函数

function nextTickHandler() {
  pending = false;
  //  拷贝出函数数组副本
  var copies = callbacks.slice(0);
  //  把函数数组清空
  callbacks.length = 0;
  // 依次执行函数
  for (var i = 0; i < copies.length; i++) {
    copies[i]();
  }
}

其次, Vue 会根据当前浏览器环境优先使用原生的 Promise.thenMutationObserver ,如果都不支持,就会采用 setTimeout 代替,目的是:延迟函数到 DOM 更新后再使用

Last Updated:
Contributors: zerojs