Skip to content

渲染函数render与脚手架说明(Lesson7 3/3) ​

本页对应 Lesson07,共 5 段视频,约 52 分钟。

学习目标 ​

  1. 能够说明 beforeUpdate 与 updated 的触发条件、执行顺序,并区分二者执行时数据状态与视图更新状态。
  2. 能够使用 $refs 或原生 DOM 查询验证更新阶段中视图是否已完成渲染。
  3. 能够描述卸载阶段 beforeDestroy 与 destroyed 的执行过程,并完成解绑事件、清除定时器等清理操作。
  4. 能够分析父子组件生命周期钩子的执行顺序,并判断父组件数据变化时子组件是否触发更新或卸载。
  5. 能够使用回调函数和 Promise 两种方式调用 $nextTick,说明其回调执行时机。
  6. 能够运用 $nextTick 解决数据更新后获取最新 DOM、切换输入框后自动聚焦等实际场景问题。

知识点 ​

先说明:标题与转写内容的关系 ​

本节笔记标题是“渲染函数 render”,但转写稿实际梳理的是 Vue2 生命周期中的更新阶段、卸载阶段、父子组件钩子执行顺序,以及 $nextTick 的用法。下面严格按转写稿内容扩写,不额外编造 render 函数知识;本节未附图片,因此不虚构图片。

更新阶段:beforeUpdate 与 updated ​

为什么需要:挂载完成后用户会操作数据,数据变化后视图要更新。我们需要知道“数据已变但视图还没变”和“视图已经更新完”两个时机。

怎么想:转写稿反复强调,beforeUpdate 不是“数据更新之前”,而是“视图更新之前”。执行 beforeUpdate 时,数据已经是最新值,但真实 DOM 还没重新渲染;updated 执行时视图已经更新完成。

怎么做:可写一个按钮让 name 自增,并在两个钩子里打印,同时用 $refs 读取按钮文本验证。

js
data() {
  return { name: 1 }
},
methods: {
  add() { this.name++ }
},
beforeUpdate() {
  console.log('beforeUpdate', this.$refs.btn.innerText)
},
updated() {
  console.log('updated', this.$refs.btn.innerText)
}

调试结论来自转写稿:点击后 beforeUpdate 里读到的是旧视图值,updated 里读到的是新视图值。易错点:不要以为 beforeUpdate 里数据是旧的;两个钩子里 this.name 都已是新值。若数据改成原值,视图不变化,更新钩子不会执行。

卸载阶段:beforeDestroy 与 destroyed ​

为什么需要:组件销毁前要处理后事,例如清除定时器、解绑事件,否则会造成内存泄漏或销毁后仍在执行。

怎么想:转写稿用 vm.$destroy() 演示。执行销毁时先走 beforeDestroy,此时实例和 DOM 的关联还在;随后清理实例与其他实例的连接、解绑指令和事件监听,最后执行 destroyed。销毁的是“数据与视图之间的关联”,不是数据对象本身。

怎么做:在 mounted 中启动定时器,销毁前清理。

js
mounted() {
  this.timer = setInterval(() => {
    this.name++
  }, 1000)
},
beforeDestroy() {
  clearInterval(this.timer)
},
destroyed() {
  console.log('destroyed')
}

易错点:只调用 $destroy() 不会自动清理你自己写的定时器;destroyed 后再点按钮不再触发更新,因为视图关联已断开。

父子组件钩子顺序与更新条件 ​

为什么需要:真实页面是组件树,必须知道父组件挂载时子组件何时挂载,以及父组件更新是否会引起子组件更新。

怎么想:转写稿最终确认:父组件先执行 beforeCreate、created、beforeMount;进入模板渲染时子组件完成 beforeCreate、created、beforeMount、mounted;最后父组件 mounted。因为父模板里使用了子组件,只有子组件挂载完,父组件才算挂载完。

更新时,父组件数据变化若没有影响子组件视图,子组件不会执行 beforeUpdate/updated;只有通过 prop 传入且子组件模板真正使用,父更新影响到子视图时,子组件才进入更新阶段。易错点:v-if 为 false 会卸载组件,再为 true 会重新挂载;v-show 只是切换显示,不会销毁组件。

$nextTick:等视图更新后再拿 DOM ​

为什么需要:转写稿用“点击编辑后让输入框获取焦点”说明:数据改变后视图不会立刻更新,直接 $refs.input.focus() 拿到的还是更新前状态,焦点设置会失败。

怎么想:Vue 会把同一轮数据变更排队,等所有同步代码执行完,再一次性更新视图。$nextTick 的回调会在 DOM 更新后执行;同一轮改多个数据,更新钩子也只执行一次。

怎么做:两种写法。

js
this.isEdit = true
this.$nextTick(() => {
  this.$refs.input.focus()
})

// 或
this.$nextTick().then(() => {
  this.$refs.input.focus()
})

转写稿还验证了在 mounted 中调用 $nextTick,它会在 mounted 执行完之后调用。易错点:不要把 $nextTick 当成“延迟固定毫秒”,它等的是视图更新时机;需要最新 DOM 时再使用。

本节笔记的边界与同音字纠正 ​

本节笔记的主题是“脚手架说明”,但转写稿主体讲的是生命周期中的更新阶段、卸载阶段、组件钩子以及 $nextTick,其中“脚肉架”是“脚手架”的同音误写。两者不是同一知识点,不能把 beforeUpdate、updated、$destroy、$nextTick 的调试过程硬说成脚手架配置推导,否则就是编造。本节笔记也没有附图片和代码片段,因此下面不虚构截图或命令,只围绕笔记中的脚手架定义、版本、文档和官方标准化说明扩写;生命周期的推导应放到对应章节。

知识点一:Vue CLI 是什么,为什么需要 ​

为什么需要: 如果直接基于 webpack 创建工程化 Vue 项目,开发者要自己处理构建工具、默认配置和工具衔接,学习和维护成本高。Vue CLI 的价值是把这套工程化过程标准化、简化。

怎么想: 笔记明确说,Vue 脚手架是 Vue 官方提供的标准化开发工具(开发平台),它简化了程序员基于 webpack 创建工程化 Vue 项目的过程。要抓住两个关键词:一是“官方”,说明它是官方工具链的一部分;二是“标准化开发平台”,说明它不是某个业务组件,而是项目创建和开发的基础设施。

怎么做: 学习时先把它定位为“创建和管理工程化 Vue 项目的工具”,再结合官方文档理解它如何用默认配置承接各种构建工具。不要把脚手架等同于 Vue 运行时本身。

易错点: 容易把“脚手架”误写成“脚肉架”,也容易把 Vue CLI 和 Vue 框架本体混为一谈。另一个常见误区是以为调整配置必须 eject,但笔记中的官方说明已经指出,它提供了调整配置的灵活性,无需 eject。

知识点二:版本与文档入口 ​

为什么需要: 版本不同,文档入口和使用方式可能不同。先确认版本,能避免照着旧资料操作。

怎么想: 笔记给出两个硬信息:最新的版本是 5.x;文档地址是 https://cli.vuejs.org/zh/。学习时应先以这两个信息为入口,而不是凭记忆写配置。

怎么做: 打开官方文档,先确认当前阅读的是 5.x 相关说明,再查看“标准化”“默认配置”“调整配置灵活性”等描述。本节只要求理解脚手架说明,不要求在这里展开完整安装命令。

易错点: 不要把旧版本文档里的写法直接套到 5.x 上;也不要把文档链接抄错。版本信息是本节笔记明确给出的,不能自行改成其他版本。

知识点三:官方“标准化”到底标准化什么 ​

为什么需要: 只记住“Vue CLI 是脚手架”不够,还要理解它解决的是工具链衔接问题,而不是单纯提供一个命令。

怎么想: 笔记中的官方原文说:“Vue CLI 致力于将 Vue 生态中的工具基础标准化。它确保了各种构建工具能够基于智能的默认配置即可平稳衔接,这样你可以专注在撰写应用上,而不必花好几天去纠结配置的问题。与此同时,它也为每个工具提供了调整配置的灵活性,无需 eject。”这里有两层:默认配置要能平稳衔接;同时保留调整配置的灵活性。

怎么做: 学习时先接受“智能默认配置”,把注意力放在应用开发上;当默认配置不满足需求时,再按官方文档调整,而不是一开始就陷入配置细节。

易错点: 不要把“标准化”理解成“不能改配置”;也不要把“无需 eject”理解成“永远不需要了解构建工具”。官方强调的是默认配置可用、调整配置有灵活性,而不是鼓励盲目改配置。

知识点四:如何把本节内容用于后续学习 ​

为什么需要: 本节是脚手架说明,不是完整操作课。先建立正确认知,后续进入脚手架创建项目时才不会把工具链和生命周期混在一起。

怎么想: 可以把本节当成“地图”:知道 Vue CLI 是官方标准化开发工具,知道当前是 5.x,知道文档入口,知道官方强调默认配置和无需 eject 的灵活性。转写稿里关于 beforeUpdate、updated、$destroy、$nextTick 的内容属于生命周期调试,不应拿来证明脚手架如何配置。

怎么做: 后续学习脚手架时,先打开官方文档,确认版本,再按文档创建和调整项目;遇到配置问题,优先查官方说明,而不是凭转写稿中的生命周期代码推测。

易错点: 最大的易错点是把两个章节的知识混在一起:看到转写稿讲“更新阶段”“卸载阶段”,就以为本节要讲这些。实际上本节笔记只给了脚手架的定义、版本、文档和官方标准化说明。另一个易错点是同音字,如“脚肉架”应纠正为“脚手架”。

从生命周期练习过渡到脚手架准备 ​

这一节笔记很短,但位置很关键:前面我们一直在单实例里验证生命周期钩子,转写稿里讲师也明确说“今天应该是涉及不到脚手架了,明天开始讲脚手架”。也就是说,这一节不是讲业务代码,而是为进入工程化项目做准备。前面反复练习的挂载、更新、销毁、beforeUpdate、updated、$nextTick 等,后面在脚手架项目里都会继续用到;而要让终端能创建和管理 Vue 项目,第一步就是安装 @vue/cli。

CLI 是什么,为什么需要它 ​

CLI 是 Command Line Interface,即命令行接口。笔记里给的定义是:@vue/cli 是一个全局安装的 npm 包,提供了终端里的 vue 命令。

为什么需要它?因为后面不能只在一个 HTML 文件里写实例,而是要用脚手架创建项目、运行开发服务、构建项目。怎么想?先不要把 CLI 当成“另一个框架”,它本质是一个全局命令工具;装好以后,终端里能识别 vue 命令,才算准备完成。怎么做?使用 npm 或 yarn 进行全局安装。易错点是:这里安装的是 @vue/cli,命令里的 @、/ 不能漏;-g 或 global 表示全局,不是装到某个项目目录里。

安装命令:npm 与 yarn 二选一 ​

笔记给出两种等价写法,任选一种即可:

shell
npm install -g @vue/cli
# OR
yarn global add @vue/cli

为什么需要两种写法?因为不同团队可能使用 npm 或 yarn。怎么想?如果你机器上主要用 npm,就用第一条;如果使用 yarn,就用第二条。怎么做?在终端中执行其中一条即可,不需要两条都执行。易错点是:npm 的全局参数是 -g,yarn 的全局写法是 global add,不要混写成 yarn add -g;另外全局安装可能需要相应权限,遇到权限问题应按自己系统的 npm 全局目录配置处理,而不是反复重复安装。

验证安装:版本检查 ​

安装完成后要检查版本:

shell
vue --version
# OR
vue -V

为什么需要验证?因为“安装命令执行完”不等于“终端一定能找到命令”。怎么想?最直接的调试就是让终端输出 vue 的版本:能输出版本号,说明全局命令可用;不能输出,就先回头检查安装是否成功、终端是否重新打开、全局路径是否生效。怎么做?执行 vue --version 或 vue -V,两者都是检查版本。易错点是:-V 是大写 V,不要写成小写 -v;--version 是两个短横线。验证这一步看似简单,但它能把“装没装上”和“命令能不能用”分开排查。

和后续生命周期的衔接 ​

转写稿大量内容在讲生命周期和 $nextTick,表面上和安装 CLI 不是同一件事,但它解释了为什么现在要装:讲师说“明天开始讲脚手架”,并且后面“也会用到这些钩子”。前面我们已经验证过:数据改变后视图不会立刻更新,beforeUpdate 和 updated 的时机不同,$nextTick 可以拿到视图更新后的结果。这些结论进入脚手架项目后仍然是理解组件行为的基础。因此这一节的操作目标很明确:先把全局 vue 命令装好并验证通过,再进入脚手架阶段;不要在这一步提前展开项目配置,也不要因为笔记短就跳过版本检查。

创建项目命令与交互入口 ​

为什么需要:Vue CLI 把项目骨架、构建配置和依赖安装串成一条命令,避免手工搭目录。怎么想:先在一个空目录或工作目录中执行创建命令,项目名就是目录名。怎么做:

shell
vue create first

执行后进入交互式选择界面。先遇到配置模式选择,本节笔记记录为选择自动配置:

选择自动配置还是手动配置

易错点:终端交互要用上下键移动、回车确认;如果命令不存在,先确认 Vue CLI 已安装。笔记文字写“选择自动配置”,但后面又出现插件列表,说明实际交互可能进入了特性选择流程;以终端提示为准,出现插件列表就按下一节选择。

选择插件与 Vue 版本 ​

为什么需要:插件决定项目具备 Babel、TypeScript、路由、Vuex、CSS 预编译等能力。怎么想:本节目标是把常用能力一次配齐。怎么做:在插件列表中用上下键切换,空格单选/取消,按 a 全选,回车进入下一步。笔记要求选择 Babel、TypeScript、路由、Vuex、CSS 预编译:

选择插件

随后选择 Vue 版本,本节选 2.x:

选择 Vue 版本

易错点:笔记中的 typscript 应理解为 TypeScript;全选会选中终端列出的全部特性,若只想保留笔记列出的几项,就用空格逐项确认,不要误按 a。

路由模式与 CSS 预编译 ​

为什么需要:路由模式影响 URL 形态,CSS 预编译影响样式写法。怎么想:本节要求 history 风格路由和 Less。怎么做:出现是否使用 history 风格路由时输入 y 并回车:

选择 history 风格路由

CSS 预编译选择 Less 并回车:

选择 CSS 预编译

易错点:输入 y 后要回车确认;CSS 预编译不要选错,否则后续 .less 文件无法按本节预期编译。

配置保存方式与预设 ​

为什么需要:插件配置可以集中放,也可以拆成独立文件。怎么想:本节选择各配置单独文件,便于查看和维护。怎么做:选择 In dedicated config files:

选择配置保存方式

是否保存这套配置为预设,本节选择 N:

是否保存配置

易错点:选择 N 表示本次不把配置保存为预设;后面 .vuerc 中出现的 presets 示例是已保存配置的形态,用于说明字段,不要和本次选择混淆。

依赖下载、启动与浏览器验证 ​

为什么需要:创建完成后必须安装依赖并启动开发服务器,才能看到页面。怎么想:等待依赖下载完成,再进入目录启动。怎么做:依赖下载完成页面如下:

依赖下载完成页面

进入项目目录:

shell
cd first

启动项目二选一:

shell
yarn serve

或:

shell
npm run serve

浏览器查看:

浏览器查看项目页面

易错点:yarn serve 与 npm run serve 不要混着排查;先确认依赖下载完成,再看端口和启动日志。转写稿后面会讲 beforeCreate、mounted、beforeUpdate、updated 等生命周期钩子,但前提是脚手架能成功启动,否则后续钩子调试没有运行环境。

.vuerc 与配置调整 ​

为什么需要:Vue CLI 会把预设和包管理器等信息写入用户目录配置,便于复用。怎么想:需要调整已保存配置时,可查看 C:\Users\zhangpeiyue\.vuerc。怎么做:其内容形如:

json
{
  "useTaobaoRegistry": true,
  "packageManager": "yarn",
  "presets": {
    "221110": {
      "useConfigFiles": true,
      "plugins": {
        "@vue/cli-plugin-babel": {},
        "@vue/cli-plugin-router": {
          "historyMode": true
        },
        "@vue/cli-plugin-vuex": {}
      },
      "vueVersion": "2",
      "cssPreprocessor": "less"
    }
  }
}

易错点:手改 JSON 要保证逗号、引号合法;不要把示例中的预设名 221110 当成每次创建都会自动生成的名字。

先看目录树:建立项目全局地图 ​

本节笔记先给出完整项目结构,学习时不要急着逐个文件背,而要先建立“哪些是依赖、哪些是源码、哪些是配置”的地图。

shell
├── node_modules 
├── public
│   ├── favicon.ico: 页签图标
│   └── index.html: 主页面
├── src
│   ├── assets: 存放静态资源
│   │   └── logo.png
│   │── components: 存放组件
│   │   └── HelloWorld.vue
│   │── App.vue: 汇总所有组件
│   │── main.js: 入口文件
├── .gitignore: git版本管制忽略的配置
├── babel.config.js: babel的配置文件
├── jsconfig.json: js配置文件
├── package.json: 应用包配置文件 
├── vue.config.js: vue配置文件
├── README.md: 应用描述文件
├── package-lock.json:包版本控制文件

为什么需要:项目文件一多,最容易出现“改了文件但不知道谁在生效”的问题。目录树能帮助你判断入口、组件、静态资源和配置各自负责什么。
怎么想:先分三层——node_modules 是安装依赖;public 和 src 是运行时代码;根目录配置文件是工具链配置。
怎么做:看项目时先找 main.js,再找 App.vue,最后进入 components 和 assets。
易错点:不要手改 node_modules;不要以为 package-lock.json 和 package.json 是一回事,前者锁定版本,后者描述依赖与脚本。

public 与 src:静态资源和源码分开 ​

public/index.html 是主页面,public/favicon.ico 是页签图标;src/assets/logo.png 存放静态资源,src/components/HelloWorld.vue 存放组件。
为什么需要:public 里的文件通常按原样提供,src 里的内容会参与构建和模块解析。把图片、组件、入口分开,项目才可维护。
怎么想:主页面只提供挂载容器,真正的页面由 main.js 创建实例并挂到 index.html 上,再由 App.vue 汇总组件。
怎么做:组件放 components,图片放 assets,页面入口保持精简。
易错点:不要把业务组件随手丢进 public;assets 中的资源通常通过模块方式引用,路径写错时先检查相对路径。

入口与汇总:main.js、App.vue、components ​

main.js 是入口文件,App.vue 汇总所有组件,HelloWorld.vue 是具体组件。转写稿里反复调试“挂载完成后再点按钮”,前提就是入口先创建实例、挂载视图。
为什么需要:没有入口,Vue 不知道从哪里启动;没有 App.vue,组件缺少统一汇总位置。
怎么想:把 main.js 看成启动器,把 App.vue 看成根组件,把 components 看成可复用零件。
怎么做:调试生命周期时,在入口挂载后观察页面;在组件内写 beforeCreate、created、beforeMount、mounted 等钩子输出顺序。
易错点:转写稿里“比form”应纠正为 beforeUpdate,“up提的”是 updated,“do”是 $destroy,“ntic”是 $nextTick。术语写错,排查时会找错方向。

配置文件:让工具知道怎么跑 ​

.gitignore 控制 Git 忽略项;babel.config.js 配置 Babel;jsconfig.json 提供 JS 配置;package.json 描述应用包;vue.config.js 配置 Vue 项目;README.md 写应用说明;package-lock.json 锁定包版本。
为什么需要:现代前端不是只写 .vue 文件,编译、依赖安装、版本锁定、Git 提交都需要配置。
怎么想:先判断问题是“代码问题”还是“工具链问题”。例如依赖版本漂移看锁文件,路径别名看 jsconfig.json 或 vue.config.js。
怎么做:安装依赖后保留锁文件;提交代码前检查 .gitignore 是否漏掉 node_modules。
易错点:不要随意删除 package-lock.json;不要把所有配置都塞进 vue.config.js,各配置文件职责不同。

用生命周期验证结构:更新阶段 ​

转写稿的调试过程是:挂载完成后页面可见,点击按钮修改 name,先执行 beforeUpdate,再执行 updated。关键结论是:beforeUpdate 不是“数据更新之前”,而是“视图更新之前”;此时数据已经变了,但 DOM 还没更新。
为什么需要:理解更新阶段,才能知道什么时候读数据、什么时候操作 DOM。
怎么想:数据变 → 虚拟 DOM 重新渲染并比较 → 执行 updated。
怎么做:可写调试输出:

js
beforeUpdate() {
  console.log('beforeUpdate', this.name)
},
updated() {
  console.log('updated', this.name)
}

易错点:在 beforeUpdate 里读 DOM,读到的往往是旧视图;在 updated 里也不要无限改数据,否则可能造成循环更新。

卸载阶段与组件钩子:销毁的是关联 ​

调用 vm.$destroy() 后,先执行 beforeDestroy,再做清理,最后执行 destroyed。转写稿强调:销毁的是“数据与视图之间的关联”,不是把数据对象本身删掉。
为什么需要:定时器、事件监听、订阅如果不清理,组件销毁后仍可能运行。
怎么想:组件卸载前做“后事处理”。
怎么做:在 beforeDestroy 中清理定时器:

js
beforeDestroy() {
  clearInterval(this.timer)
}

组件钩子顺序上,子组件挂载完成后,父组件的 mounted 才最后执行;v-if 会销毁和重建组件,v-show 只切换显示隐藏。
易错点:把 v-if 当 v-show 用,会误以为组件没有销毁;销毁后再次点击原按钮无反应是正常现象。

$nextTick:等视图更新后再操作 DOM ​

转写稿用“点击编辑后让输入框获取焦点”说明:数据改变后视图不会立刻更新,所以紧接着 this.$refs.input.focus() 可能拿不到已渲染的输入框。
为什么需要:Vue 会合并更新,多个数据变化只触发一次视图更新;DOM 操作必须等更新完成。
怎么想:要拿“更新后的视图”,就放进 $nextTick 回调。
怎么做:

js
this.isEdit = true
this.$nextTick(() => {
  this.$refs.input.focus()
})

也可以用 await this.$nextTick()。
易错点:不要在改完数据后立刻操作 DOM;$nextTick 的回调在视图更新后执行,适合获取最新 DOM,但不要把它当成任意延迟工具。

转写稿未涉及 dev.config.js 的具体配置,以下按转写稿实际内容整理生命周期钩子与 $nextTick,不补写未出现的配置项;转写稿也未包含图片,故不虚构图片。

更新阶段:beforeUpdate 与 updated ​

为什么需要:挂载完成后用户会改数据,我们需要在“视图更新前/后”做不同的事,例如记录旧 DOM、等新 DOM 出现后再操作。怎么想:数据先变,视图后更新;beforeUpdate 是视图更新之前,updated 是视图更新之后。转写稿反复强调:beforeUpdate 里数据已经是新值,但 DOM 还是旧的。怎么做:写一个按钮让 name++,在两个钩子里用 ref 取按钮文本对比。调试时点击按钮,beforeUpdate 输出旧文本,updated 输出新文本;把值改回原值则两个钩子都不执行。易错点:不要把 beforeUpdate 当成“数据更新前”;只有状态改变且影响视图才触发;同一轮多个数据变化只更新一次。

html
<div id="app">
  <button ref="btnRef" @click="name++">{{ name }}</button>
  <button @click="destroy">销毁</button>
</div>
js
new Vue({
  el: '#app',
  data: { name: 1, timer: null },
  beforeCreate() { console.log('beforeCreate') },
  created() { console.log('created') },
  beforeMount() { console.log('beforeMount') },
  mounted() {
    console.log('mounted')
    this.timer = setInterval(() => { this.name++ }, 1000)
  },
  beforeUpdate() {
    console.log('beforeUpdate', this.$refs.btnRef.innerText)
  },
  updated() {
    console.log('updated', this.$refs.btnRef.innerText)
  },
  beforeDestroy() {
    console.log('beforeDestroy')
    clearInterval(this.timer)
  },
  destroyed() { console.log('destroyed') },
  methods: {
    destroy() { this.$destroy() }
  }
})

卸载阶段:beforeDestroy 与 destroyed ​

为什么需要:组件销毁时要清理定时器、解绑事件,否则“人死了计时器还活着”。怎么想:vm.$destroy() 销毁的是实例与视图之间的关联,不是把数据对象删掉;销毁前还能访问 DOM,销毁后关联断开。怎么做:按钮里调用 this.$destroy(),在 beforeDestroy 中 clearInterval(this.timer)。转写稿调试:mounted 里定时器让 name++,点击销毁后按钮不再有反应;若不清理定时器,控制台仍会继续加。易错点:销毁后数据还在,但视图不再更新;beforeDestroy 里还能拿到 $refs,destroyed 后不要再依赖 DOM;定时器、事件监听要在销毁前清理。

父子组件钩子与 v-if ​

为什么需要:组件嵌套后要弄清谁先挂载、谁先更新、谁先销毁。怎么想:父组件 beforeMount 后开始准备渲染模板,子组件先完成挂载,父组件 mounted 最后执行;更新时若父传 props 且子视图用到,子组件才执行 beforeUpdate/updated;销毁时 v-if=false 会卸载子组件并触发其 beforeDestroy/destroyed。怎么做:在父子钩子里打日志,点击父组件按钮改 userName,观察子组件是否更新;再用 v-if 切换显示。易错点:v-show 只改 display,不会销毁组件;父组件更新若没影响子组件视图,子组件更新钩子不执行;props 传了但子组件视图没用,也不会触发子组件更新。

$nextTick:等视图更新后再拿 DOM ​

为什么需要:数据改变后视图不会立刻更新,直接 focus() 或取 innerText 会拿到旧 DOM。怎么想:Vue 会把同一轮数据变更批量处理,$nextTick 的回调在 DOM 更新后执行,也可用 await this.$nextTick()。怎么做:编辑案例中点击“编辑”把 isEdit 改为 true,立刻让输入框聚焦会失败,放进 $nextTick 后成功。易错点:$nextTick 不是普通延时器,而是等更新队列;不要在改数据后立刻取最新 DOM;回调里可拿最新视图,updated 之后也会调用。

html
<div id="app">
  <button @click="edit">{{ isEdit ? '完成' : '编辑' }}</button>
  <input v-if="isEdit" ref="inputRef" v-model="str">
  <span v-else>{{ str }}</span>
</div>
js
new Vue({
  el: '#app',
  data: { isEdit: false, str: '一会儿下课,晚上吃什么' },
  methods: {
    edit() {
      this.isEdit = !this.isEdit
      this.$nextTick(() => {
        this.$refs.inputRef && this.$refs.inputRef.focus()
      })
    }
  }
})

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

示例一:更新阶段与卸载阶段的钩子(含定时器清理、$refs 验证视图) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>生命周期:更新阶段 / 卸载阶段</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body>
  <div id="app">
    <h3>{{ name }} --- {{ userName }}</h3>

    <button ref="btnRef" @click="changeName">点击 +1</button>
    <button @click="destroy">内部销毁 this.$destroy()</button>
    <button @click="fn">外部销毁 vm.$destroy()</button>
  </div>

  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        name: 1,
        userName: '张三'
      },
      methods: {
        changeName() {
          // 一次改两个数据,beforeUpdate / updated 也只执行一次
          this.name += 1;
          this.userName += '!';
        },
        destroy() {
          this.$destroy();
        },
        fn() {
          vm.$destroy();
        }
      },
      beforeCreate() { console.log('1 beforeCreate'); },
      created() { console.log('2 created'); },
      beforeMount() { console.log('3 beforeMount'); },
      mounted() {
        console.log('4 mounted');
        // 挂载完成后再开定时器
        this.timer = setInterval(() => { this.name += 1; }, 1000);
      },
      beforeUpdate() {
        console.log('beforeUpdate:数据已更新,视图还没更新');
        console.log('按钮上的文字:', this.$refs.btnRef.innerText);
      },
      updated() {
        console.log('updated:视图已经更新完毕');
        console.log('按钮上的文字:', this.$refs.btnRef.innerText);
      },
      beforeDestroy() {
        console.log('beforeDestroy:还能拿到 $refs', this.$refs.btnRef);
        clearInterval(this.timer); // 处理后事
      },
      destroyed() {
        console.log('destroyed:$refs 已断开', this.$refs.btnRef);
      }
    });
  </script>
</body>
</html>

要点解释:

  • beforeUpdate 里通过 this.$refs.btnRef.innerText 拿到的是更新前的按钮文字(例如 1),updated 里拿到的是更新后的(例如 2)——这正是“数据已更新、视图还没更新”的证明。
  • 一次事件里改多个数据(name 和 userName),beforeUpdate / updated 只会执行一次,因为 Vue 是把数据变化排队后一次性渲染视图的。
  • this.$destroy() 销毁的是数据与视图之间的关联,不是数据本身:销毁后 vm.name 还在,但按钮点了没有任何反应(指令、事件监听器都被解绑)。
  • 易错点:mounted 里开的 setInterval 必须放在 beforeDestroy 里 clearInterval,否则组件销毁后定时器还在跑,控制台会一直打印。
  • 易错点:如果 changeName 里把值改成和原来一样(比如把“张三”还赋成“张三”),视图没变,beforeUpdate / updated 都不会执行。

示例二:父子组件钩子执行顺序 + v-if 触发卸载/重新挂载 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>父子组件钩子顺序 / v-if 与 v-show</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body>
  <div id="app">
    <h3>父组件:{{ userName }}</h3>
    <button @click="userName += '!'">修改父数据</button>
    <button @click="isShow = !isShow">显示/隐藏子组件</button>
    <hr />
    <one v-if="isShow" :user-name="userName"></one>
  </div>

  <script>
    Vue.component('one', {
      props: ['userName'],
      template: `<div><h4>one 组件:{{ userName }}</h4></div>`,
      beforeCreate() { console.log('子 beforeCreate'); },
      created() { console.log('子 created'); },
      beforeMount() { console.log('子 beforeMount'); },
      mounted() { console.log('子 mounted'); },
      beforeUpdate() { console.log('子 beforeUpdate'); },
      updated() { console.log('子 updated'); },
      beforeDestroy() { console.log('子 beforeDestroy'); },
      destroyed() { console.log('子 destroyed'); }
    });

    new Vue({
      el: '#app',
      data: {
        userName: '张三',
        isShow: true
      },
      beforeCreate() { console.log('父 beforeCreate'); },
      created() { console.log('父 created'); },
      beforeMount() { console.log('父 beforeMount'); },
      mounted() { console.log('父 mounted'); },
      beforeUpdate() { console.log('父 beforeUpdate'); },
      updated() { console.log('父 updated'); }
    });
  </script>
</body>
</html>

要点解释:

  • 首次挂载顺序:父 beforeCreate → 父 created → 父 beforeMount → 子 beforeCreate → 子 created → 子 beforeMount → 子 mounted → 父 mounted。beforeMount 与 mounted 之间才是“用编译好的 HTML 替换 el”的渲染过程,渲染过程中子组件要先挂载完,父的 mounted 才最后执行。
  • 父数据 userName 变了、并且通过 props 传给了子组件、子组件模板里也用了它,子组件的 beforeUpdate / updated 才会执行;只改父数据而子组件没用到该 props,子组件的更新钩子不会触发。
  • v-if 为 false 时子组件被销毁,会执行 beforeDestroy / destroyed;再改回 true 会重新创建并挂载,四个挂载钩子重新走一遍。
  • 易错点:v-show 只是把 display 改成 none,不会卸载组件,所以不会触发销毁钩子——v-if 与 v-show 要分清。

示例三:$nextTick 的典型应用——点击编辑后让输入框获取焦点 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>$nextTick 应用:编辑后自动聚焦</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body>
  <div id="app">
    <span v-show="!isEdit">{{ str }}</span>
    <input v-show="isEdit" ref="inputRef" v-model="str" />
    <button ref="btnRef" @click="edit">{{ isEdit ? '完成' : '编辑' }}</button>
  </div>

  <script>
    new Vue({
      el: '#app',
      data: {
        str: '一会下课,晚上吃什么',
        isEdit: false
      },
      methods: {
        edit() {
          this.isEdit = !this.isEdit;

          if (this.isEdit) {
            // 错误写法:此时视图还没更新,input 还是隐藏的,focus 无效
            // this.$refs.inputRef.focus();

            // 正确写法:等视图更新完成后再聚焦
            this.$nextTick(() => {
              this.$refs.inputRef.focus();
            });

            // 另一种写法(返回 Promise):
            // this.$nextTick().then(() => {
            //   this.$refs.inputRef.focus();
            // });
          }
        }
      },
      beforeUpdate() { console.log('beforeUpdate'); },
      updated() { console.log('updated'); }
    });
  </script>
</body>
</html>

要点解释:

  • 数据改变后视图不会立即更新,所以紧接着写 this.$refs.inputRef.focus() 时 input 还处于隐藏状态,聚焦无效;放进 this.$nextTick(回调) 里,回调会在视图更新完成(updated 之后)执行,聚焦才生效。
  • $nextTick 有两种写法:传回调函数,或直接 .then()(返回 Promise);两种都是等视图更新后再执行。
  • 想验证顺序,可以在 updated 里也打一条日志,会看到 beforeUpdate → updated → $nextTick 回调。
  • 易错点:$nextTick 拿到的是最新视图,但不要在 beforeUpdate 里做 DOM 相关操作,那时视图还是旧的;也别用 setTimeout 硬等来替代 $nextTick。

常见错误 ​

  1. 现象:在 beforeUpdate 里读取按钮/输入框 DOM,拿到的还是旧内容,误以为数据没更新。
    原因:beforeUpdate 时数据已更新,但视图尚未完成重新渲染。
    修法:需要读取更新后的 DOM 时放到 updated 或 $nextTick 回调中。

  2. 现象:在 updated 中修改与视图相关的数据,页面卡顿甚至无限更新。
    原因:updated 触发后再次改数据,会再次进入更新流程。
    修法:避免在 updated 中无条件改状态;必须改时加明确条件判断,或改用 $nextTick/事件回调处理。

  3. 现象:组件销毁后定时器仍在跑,控制台继续输出或请求继续发送。
    原因:beforeDestroy/destroyed 只解除实例与视图关联,不会自动清理外部副作用。
    修法:在销毁钩子中手动 clearInterval、解绑事件、取消订阅/请求,并置空引用。

  4. 现象:父组件数据变了,子组件的 beforeUpdate/updated 却没执行,误判生命周期失效。
    原因:传入子组件的数据没有影响子组件视图,子组件不需要重新渲染。
    修法:确认子组件是否真正使用该 prop;需要响应变化时在子组件模板/计算属性中实际消费它。

  5. 现象:点击“编辑”后立刻 focus() 输入框无效。
    原因:数据刚改完,DOM 还未替换/渲染完成,拿不到目标输入框。
    修法:把聚焦逻辑放进 this.$nextTick(() => { ... }) 或 await this.$nextTick() 后执行。

  6. 现象:用 v-show 控制子组件显隐,以为会触发子组件销毁钩子;或混用 v-if 后钩子执行不符合预期。
    原因:v-show 只切换显示样式,不销毁组件;v-if 为假才会卸载组件。
    修法:需要销毁/重建组件用 v-if;只是显示隐藏用 v-show,不要依赖销毁钩子做清理。

练习 ​

  1. 改错题:下面代码希望点击后立刻让输入框获得焦点,但无效。请指出问题并改写。

    js
    this.isEdit = true
    this.$refs.inputRef.focus()
  2. 场景题:一个商品详情组件在 mounted 中启动轮询计时器。用户切换路由后计时器仍在请求。请说明应在哪些钩子中做什么处理,并写出关键代码。

  3. 判断题:父组件修改了一个传给子组件的 userName,但子组件模板中并未使用它。子组件会执行 beforeUpdate 和 updated 吗?说明理由。

  4. 实践题:用 v-if 控制一个子组件显示/隐藏,观察子组件创建、挂载、销毁钩子的执行。请写出你会重点验证的 3 个现象,并说明 v-show 下结果有何不同。

原视频对照 ​

#视频时长
1更新阶段08:38
2卸载阶段09:06
3组件钩子函数15:59
4$nextTick11:35
5$nextTick应用07:28

小结 ​

  1. 更新阶段要区分“数据已变”和“DOM 已更新”,读 DOM 的时机选错是高频错误。
  2. updated 适合做依赖新 DOM 的收尾操作,不适合无 koşullu 改状态。
  3. 组件销毁不等于自动清理副作用,定时器、事件、订阅必须在销毁钩子中手动释放。
  4. 子组件更新钩子是否触发,取决于父组件变化是否真正影响子组件视图。
  5. $nextTick 的核心价值是拿到本次数据变更后最终渲染完成的 DOM/状态。
  6. v-if 与 v-show 对组件生命周期的影响完全不同,选错会直接影响销毁清理逻辑。
  7. 脚手架创建项目时,Vue 版本、路由模式、CSS 预处理器等选择要在创建阶段确认,避免后续返工。