前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

Vue 生命周期八个钩子完整拆解(五)

首页2018-08-26 17:21:32Front-End
Vue生命周期前端基础

面试问「Vue 的生命周期有哪些」,背出八个钩子的名字不难。难的是下一个问题:请求应该发在 created 还是 mounted,为什么?这时候光背名字就不够了,得知道每个钩子跑的那一刻,实例上到底有什么、没有什么。这篇的做法是往八个钩子里各塞一段 console.log,把 $el、$data、message 三个东西在每个阶段的值都打出来,对着输出一段段解释。看完你不光能说清顺序,还能自己判断某段代码该往哪个钩子里放。

每个 Vue 实例在被创建之前都要经过一系列的初始化过程,这个过程就是 Vue 的生命周期。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • 八个钩子的完整顺序,以及每个阶段实例上有什么
  • beforeCreate 和 created 之间到底发生了什么
  • 为什么没有 el 选项时生命周期会停在 created
  • render、template、outer HTML 三者的优先级
  • 数据更新时 beforeUpdate 和 updated 的触发时机
  • 销毁阶段该清理什么,不清理会怎样
  • 请求、DOM 操作、定时器分别该放在哪个钩子里
  • Vue 3 的钩子改名和组合式 API 的写法

# 一、生命周期钩子函数

Vue 2 常用的八个钩子按执行顺序是这样的:

  • beforeCreate
  • created
  • beforeMount
  • mounted
  • beforeUpdate
  • updated
  • beforeDestroy
  • destroyed

除了这八个,Vue 2 还有 activated / deactivated(配合 keep-alive 用)和 errorCaptured(捕获子孙组件错误),日常用得少但真到场景里没有替代品。

下面这段代码把八个钩子全填上了,每个钩子里打印 $el、$data、message 三个值。直接扔进浏览器跑一遍,控制台的输出比任何图都直观:

var vm = new Vue({
    el: '#app',
    data: {
      message: 'Vue的生命周期'
    },
    beforeCreate: function() {
      console.group('------beforeCreate创建前状态------');
      console.log("%c%s", "color:red" , "el     : " + this.$el); //undefined
      console.log("%c%s", "color:red","data   : " + this.$data); //undefined 
      console.log("%c%s", "color:red","message: " + this.message) 
    },
    created: function() {
      console.group('------created创建完毕状态------');
      console.log("%c%s", "color:red","el     : " + this.$el); //undefined
      console.log("%c%s", "color:red","data   : " + this.$data); //已被初始化 
      console.log("%c%s", "color:red","message: " + this.message); //已被初始化
    },
    beforeMount: function() {
      console.group('------beforeMount挂载前状态------');
      console.log("%c%s", "color:red","el     : " + (this.$el)); //已被初始化
      console.log(this.$el);
      console.log("%c%s", "color:red","data   : " + this.$data); //已被初始化  
      console.log("%c%s", "color:red","message: " + this.message); //已被初始化  
    },
    mounted: function() {
      console.group('------mounted 挂载结束状态------');
      console.log("%c%s", "color:red","el     : " + this.$el); //已被初始化
      console.log(this.$el);    
      console.log("%c%s", "color:red","data   : " + this.$data); //已被初始化
      console.log("%c%s", "color:red","message: " + this.message); //已被初始化 
    }
  })
@前端进阶之旅: 代码已经复制到剪贴板

更新和销毁那四个钩子的写法完全一样,只是打印的分组名不同:

var vm = new Vue({
    // ...上面的选项省略
    beforeUpdate: function () {
      console.group('beforeUpdate 更新前状态===============》');
      console.log("%c%s", "color:red","el     : " + this.$el);
      console.log(this.$el);   
      console.log("%c%s", "color:red","data   : " + this.$data); 
      console.log("%c%s", "color:red","message: " + this.message); 
    },
    updated: function () {
      console.group('updated 更新完成状态===============》');
      console.log("%c%s", "color:red","el     : " + this.$el);
      console.log(this.$el); 
      console.log("%c%s", "color:red","data   : " + this.$data); 
      console.log("%c%s", "color:red","message: " + this.message); 
    },
    beforeDestroy: function () {
      console.group('beforeDestroy 销毁前状态===============》');
      console.log("%c%s", "color:red","el     : " + this.$el);
      console.log(this.$el);    
      console.log("%c%s", "color:red","data   : " + this.$data); 
      console.log("%c%s", "color:red","message: " + this.message); 
    },
    destroyed: function () {
      console.group('destroyed 销毁完成状态===============》');
      console.log("%c%s", "color:red","el     : " + this.$el);
      console.log(this.$el);  
      console.log("%c%s", "color:red","data   : " + this.$data); 
      console.log("%c%s", "color:red","message: " + this.message)
    }
  })
@前端进阶之旅: 代码已经复制到剪贴板

官方那张生命周期图把整条链路画得很完整,后面每一节讲的都是这张图上的一小段:

Vue 实例生命周期完整流程图

# 二、beforeCreate 和 created 之间

先看这一段的输出。beforeCreate 里 el 和 data 都是 undefined,message 也拿不到;到了 created,data 已经初始化好了,message 有值了,但 el 还是 undefined。

这中间 Vue 干的事情是初始化事件系统和进行数据的观测。所谓数据观测,就是把 data 里的每个属性用 Object.defineProperty 改造成带 getter/setter 的响应式属性,之后改这些属性视图才会跟着变。这个改造过程的细节,我在 实现数据的双向绑定 mvvm 里从零写过一遍。

所以在 created 这一刻,数据是活的,DOM 还没有。

这条结论直接决定了一个高频问题的答案:请求放哪儿?放 created 就够了。数据已经能读能写,请求发出去、回来赋值给 data,视图渲染时自然带上数据,比放 mounted 早一步,能少一次「先渲染空白再渲染数据」的抖动。

反过来,任何需要操作真实 DOM 的代码(获取元素宽高、初始化第三方图表库、绑原生事件)都不能放在 created 里,因为这时候 $el 是 undefined。

# 三、created 到 beforeMount

这一段是整个生命周期里分支最多的地方:

created 到 beforeMount 之间的判断分支

Vue 首先会判断实例上有没有 el 选项。有就继续向下编译;没有 el 选项,编译就停在这里,生命周期也跟着停住,直到你手动在这个实例上调用 vm.$mount(el)。

把上面那段代码里的 el: '#app', 注释掉再跑,可以看到控制台输出到 created 就没了:

注释掉 el 选项后生命周期停在 created

如果后面继续调用 vm.$mount(el),代码就会接着往下执行。这里的 el 参数就是要挂载的 DOM 节点。

vm.$mount('#app')
@前端进阶之旅: 代码已经复制到剪贴板

这个设计在实际项目里是有用的。异步场景下你可能希望实例先创建、数据先备好,等某个条件满足了再挂到页面上,$mount 就是那个开关。

# 3.1 template 选项对生命周期的影响

过了 el 这一关,Vue 接着判断有没有 template 选项:

  • 有 template 参数选项,就把它作为模板编译成 render 函数
  • 没有 template 选项,就把外部 HTML(也就是 el 指向的那个元素本身的 HTML)作为模板编译
  • 所以 template 中的模板优先级要高于 outer HTML

改一下代码验证。在 HTML 结构里写一串内容,同时在 Vue 对象里也给一个 template:

<div id="app">
  <!--html 中修改的-->
  <h1>{{message + '这是在outer HTML中的'}}</h1>
</div>
@前端进阶之旅: 代码已经复制到剪贴板
var vm = new Vue({
  el: '#app',
  // 在 vue 配置项中修改的
  template: "<h1>{{message +'这是在template中的'}}</h1>",
  data: {
    message: 'Vue的生命周期'
  }
})
@前端进阶之旅: 代码已经复制到剪贴板

页面输出的是 Vue的生命周期这是在template中的。把 template 选项注释掉再跑,输出变成 Vue的生命周期这是在outer HTML中的。原文这段示例的实例代码少了收尾的括号,直接复制会报语法错误,这里补上了。

顺便说一句,el 的判断为什么要排在 template 前面?因为 Vue 需要先通过 el 找到对应的 outer template,el 都没有的话,「外部 HTML」这个兜底选项根本无从谈起。

除了 template,Vue 实例里还可以直接写 render 函数,它以 createElement 作为参数,返回虚拟节点,而且可以直接嵌入 JSX:

new Vue({
    el: '#app',
    render: function(createElement) {
        return createElement('h1', 'this is createElement')
    }
})
@前端进阶之旅: 代码已经复制到剪贴板
fe
  • 一、生命周期钩子函数
  • 二、beforeCreate 和 created 之间
  • 三、created 到 beforeMount
    • 3.1 template 选项对生命周期的影响
  • 四、beforeMount 到 mounted
  • 五、beforeUpdate 到 updated
  • 六、beforeDestroy 到 destroyed
  • 七、把八个钩子按用途归个类
  • 八、Vue 3 里生命周期变了什么
  • 总结
  • 参考

← Vue 事件处理与事件修饰符详解(六)Vue 基本指令用法详解与常见坑(四) →