主题
渲染函数render与脚手架说明(Lesson7 3/3)
本页对应 Lesson07,共 5 段视频,约 52 分钟。
学习目标
- 能够说明
beforeUpdate与updated的触发条件、执行顺序,并区分二者执行时数据状态与视图更新状态。 - 能够使用
$refs或原生 DOM 查询验证更新阶段中视图是否已完成渲染。 - 能够描述卸载阶段
beforeDestroy与destroyed的执行过程,并完成解绑事件、清除定时器等清理操作。 - 能够分析父子组件生命周期钩子的执行顺序,并判断父组件数据变化时子组件是否触发更新或卸载。
- 能够使用回调函数和 Promise 两种方式调用
$nextTick,说明其回调执行时机。 - 能够运用
$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:

易错点:笔记中的 typscript 应理解为 TypeScript;全选会选中终端列出的全部特性,若只想保留笔记列出的几项,就用空格逐项确认,不要误按 a。
路由模式与 CSS 预编译
为什么需要:路由模式影响 URL 形态,CSS 预编译影响样式写法。怎么想:本节要求 history 风格路由和 Less。怎么做:出现是否使用 history 风格路由时输入 y 并回车:

CSS 预编译选择 Less 并回车:

易错点:输入 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()
})
}
}
})课堂画面





代码示例
示例一:更新阶段与卸载阶段的钩子(含定时器清理、$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。
常见错误
现象:在
beforeUpdate里读取按钮/输入框 DOM,拿到的还是旧内容,误以为数据没更新。
原因:beforeUpdate时数据已更新,但视图尚未完成重新渲染。
修法:需要读取更新后的 DOM 时放到updated或$nextTick回调中。现象:在
updated中修改与视图相关的数据,页面卡顿甚至无限更新。
原因:updated触发后再次改数据,会再次进入更新流程。
修法:避免在updated中无条件改状态;必须改时加明确条件判断,或改用$nextTick/事件回调处理。现象:组件销毁后定时器仍在跑,控制台继续输出或请求继续发送。
原因:beforeDestroy/destroyed只解除实例与视图关联,不会自动清理外部副作用。
修法:在销毁钩子中手动clearInterval、解绑事件、取消订阅/请求,并置空引用。现象:父组件数据变了,子组件的
beforeUpdate/updated却没执行,误判生命周期失效。
原因:传入子组件的数据没有影响子组件视图,子组件不需要重新渲染。
修法:确认子组件是否真正使用该 prop;需要响应变化时在子组件模板/计算属性中实际消费它。现象:点击“编辑”后立刻
focus()输入框无效。
原因:数据刚改完,DOM 还未替换/渲染完成,拿不到目标输入框。
修法:把聚焦逻辑放进this.$nextTick(() => { ... })或await this.$nextTick()后执行。现象:用
v-show控制子组件显隐,以为会触发子组件销毁钩子;或混用v-if后钩子执行不符合预期。
原因:v-show只切换显示样式,不销毁组件;v-if为假才会卸载组件。
修法:需要销毁/重建组件用v-if;只是显示隐藏用v-show,不要依赖销毁钩子做清理。
练习
改错题:下面代码希望点击后立刻让输入框获得焦点,但无效。请指出问题并改写。
jsthis.isEdit = true this.$refs.inputRef.focus()场景题:一个商品详情组件在
mounted中启动轮询计时器。用户切换路由后计时器仍在请求。请说明应在哪些钩子中做什么处理,并写出关键代码。判断题:父组件修改了一个传给子组件的
userName,但子组件模板中并未使用它。子组件会执行beforeUpdate和updated吗?说明理由。实践题:用
v-if控制一个子组件显示/隐藏,观察子组件创建、挂载、销毁钩子的执行。请写出你会重点验证的 3 个现象,并说明v-show下结果有何不同。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 更新阶段 | 08:38 |
| 2 | 卸载阶段 | 09:06 |
| 3 | 组件钩子函数 | 15:59 |
| 4 | $nextTick | 11:35 |
| 5 | $nextTick应用 | 07:28 |
小结
- 更新阶段要区分“数据已变”和“DOM 已更新”,读 DOM 的时机选错是高频错误。
updated适合做依赖新 DOM 的收尾操作,不适合无 koşullu 改状态。- 组件销毁不等于自动清理副作用,定时器、事件、订阅必须在销毁钩子中手动释放。
- 子组件更新钩子是否触发,取决于父组件变化是否真正影响子组件视图。
$nextTick的核心价值是拿到本次数据变更后最终渲染完成的 DOM/状态。v-if与v-show对组件生命周期的影响完全不同,选错会直接影响销毁清理逻辑。- 脚手架创建项目时,Vue 版本、路由模式、CSS 预处理器等选择要在创建阶段确认,避免后续返工。