深入学习 vue
---
highlight: a11y-dark
theme: smartblue
---
# vue 的生命周期
由于 vue2 和 vue3 的生命周期区别并没有很大,更多只是名字上的区分和使用上的微小差异,因此下文中,以 vue2 钩子函数 为例子,讲解 vue 的生命周期。
## 生命周期钩子
- **beforeCreate**:在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。
- 在当前阶段,data、methods、computed 以及 watch 上的数据和方法都不能被访问。
- **created**:实例已经创建完成之后被调用。
- 在这一步,实例已经完成以下配置:数据观测(data observer),属性和方法的运算,event/watcher 事件回调。
- 这里没有 $el,如果需要与 Dom 进行交互,可以通过 vm.$nextTick 来访问 Dom
- **beforeMount**:在挂载开始之前被调用。
- render 函数首次被调用。
- **mounted**:在挂载完成后发生。
- 在当前阶段,真实的 Dom 挂载完毕,数据完成双向绑定,可以访问到 Dom 节点
- **beforeUpdate**:数据更新时调用,发生在虚拟 Dom 重新渲染和打补丁(patch)之前。
- 可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。
- **updated**:发生在更新完成之后,当前阶段组件 Dom 已完成更新。
- 避免在此期间更改数据,因为可能会导致无限循环的更新。
- 该钩子在服务器端渲染期间不再调用。
- **beforeDestroy**:实例销毁之前调用。
- 在这一步,实例仍然完全可用。可以在这时进行善后收尾工作,比如清除计时器。
- **destroyed**:实例销毁后调用。
- 这个阶段,实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。
- 该钩子在服务器端渲染期间不再调用。
- **activated**:组件被激活时调用,***是 keep-alive 专属***。
- **deactivated**:组件被销毁时调用,***是 keep-alive 专属***。
## 异步请求发起阶段
可以在钩子函数 **created、beforeMount、mounted** 中进行异步请求,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。
如果异步请求不需要依赖 Dom,可以在 **created** 钩子函数中调用异步请求,因为在 **created** 钩子函数中调用异步请求有以下优点:
- 能更快获取到服务端数据,减少页面 loading 时间
- ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性
## 源码分析
### --- 初始化流程
从 `new Vue(options)` 开始作为入口,`Vue` 只是一个简单的构造函数,内部是这样的:
```js
function Vue(options) {
this._init(options)
}
Vue.prototype._init = function(options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor), options || {}, vm)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
```
进入了 `_init` 函数之后,先初始化了一些属性。
1. `initLifecycle`:初始化一些属性如`$parent,$children`。根实例没有 `$parent,$children` 开始是空数组,直到它的 子组件 实例进入到 `initLifecycle` 时,才会往父组件的 `$children` 里把自身放进去。所以 `$children` 里的一定是组件的实例。
2. `initEvents`:初始化事件相关的属性,如 `_events` 等。
3. `initRender`:初始化渲染相关如 `$createElement`,并且定义了 `$attrs` 和 `$listeners` 为浅层响应式属性。具体可以查看细节章节。并且还定义了`$slots、$scopedSlots`,其中 `$slots` 是立刻赋值的,但是 `$scopedSlots` 初始化的时候是一个 `emptyObject`,直到组件的 `vm._render` 过程中才会通过 `normalizeScopedSlots` 去把真正的 `$scopedSlots` 整合后挂到 `vm` 上。
然后开始第一个生命周期:
```js
callHook(vm, 'beforeCreate')
```
#### 一、beforeCreate 之后:
1. 初始化 `inject`
2. 初始化 `state`
- 初始化 `props`
- 初始化 `methods`
- 初始化 `data`
- 初始化 `computed`
- 初始化 `watch`
3. 初始化 `provide`
所以在 `data` 中可以使用 `props` 上的值,反过来则不行。
然后进入 `created` 阶段:
```js
callHook(vm, 'created')
```
#### 二、created 被调用完成
调用 `$mount` 方法,开始挂载组件到 `Dom` 上。
如果使用了 `runtime-with-compile` 版本,则会把你传入的 `template` 选项,或者 `html` 文本,通过一系列的编译生成 `render` 函数。
- 编译这个 template,生成 ast 抽象语法树。
- 优化这个 ast,标记静态节点。(渲染过程中不会变的那些节点,优化性能)。
- 根据 ast,生成 render 函数。
对应具体的代码就是:
```js
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options)
}
const code = generate(ast, options)
```
#### 三、beforeMount 被调用完成
把 渲染组件的函数 定义好,具体代码是:
```js
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
```
`vm._render` 其实就是调用上一步拿到的 `render` 函数生成一个 `vnode`,而 `vm._update` 方法则会对这个 `vnode` 进行 `patch` 操作, 把 `vnode` 通过 `createElm` 函数创建新节点并且渲染到 `dom节点` 中。
接下来是由 `响应式原理` 的一个核心类 `Watcher` 负责执行这个函数。
> 为什么要 Watcher 来代理执行呢?
>
> 因为我们需要在这段过程中去 观察 这个函数读取了哪些响应式数据,将来这些响应式数据更新的时候,我们需要重新执行 `updateComponent` 函数。
>
如果是更新后调用 `updateComponent` 函数的话,`updateComponent` 内部的 `patch` 就不再是初始化时候的创建节点,而是对新旧 `vnode` 进行 `diff`,最小化的更新到 `dom节点` 上去,这个过程使用的是 diff 算法。
这一切交给 Watcher 完成:
```js
new Watcher(
vm,
updateComponent,
noop,
{
before() {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
},
},
true /* isRenderWatcher */
)
```
这里在 `before` 属性上定义了 `beforeUpdate` 函数,也即,在 `Watcher` 被响应式属性的更新触发之后,重新渲染新视图之前,会先调用 `beforeUpdate` 生命周期。
> 注意,在 `render` 的过程中,如果遇到了 子组件,则会调用 `createComponent` 函数。
`createComponent` 函数内部,会为子组件生成一个属于自己的构造函数,可以理解为子组件自己的 `Vue` 函数:
```js
Ctor = baseCtor.extend(Ctor)
```
在普通的场景下,其实这就是 `Vue.extend` 生成的构造函数,它继承自 `Vue` 函数,拥有它的很多全局属性。
> 除了组件有自己的生命周期外,其实 `vnode` 也有自己的 `生命周期`。
那么子组件的 `vnode` 会有自己的 `init` 周期,这个周期内部会做这样的事情:
```js
// 创建子组件
const child = createComponentInstanceForVnode(vnode)
// 挂载到 dom 上
child.$mount(vnode.elm)
```
深入察看,`createComponentInstanceForVnode` 内部会调用 `子组件` 的构造函数。
```js
new vnode.componentOptions.Ctor(options)
```
继续深入,构造函数的内部:
```js
const Sub = function VueComponent(options) {
this._init(options)
}
```
这个 `_init` 其实就是本文开头的那个函数。
如果遇到 子组件,那么就会优先开始子组件的构建过程,也就是说,从 `beforeCreated` 重新开始。这是一个递归的构建过程。
也即,如果我们有 父 -> 子 -> 孙 这三个组件,那么它们的初始化生命周期顺序是这样的:
```js
父 beforeCreate
父 created
父 beforeMount
子 beforeCreate
子 created
子 beforeMount
孙 beforeCreate
孙 created
孙 beforeMount
孙 mounted
子 mounted
父 mounted
```
然后,`mounted` 生命周期被触发。
#### 四、mounted 被调用完成
至此,组件的挂载就完成了,初始化的生命周期结束。
### --- 更新流程
当一个响应式属性被更新后,触发了 `Watcher` 的回调函数,也就是 `vm._update(vm._render())`,在更新之前,会先调用刚才在 `before` 属性上定义的函数,也即:
```js
callHook(vm, 'beforeUpdate')
```
> 注意,由于 `Vue` 的异步更新机制,`beforeUpdate` 的调用已经是在 `nextTick` 中了。具体代码如下:
```js
nextTick(flushSchedulerQueue)
function flushSchedulerQueue {
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
// callHook(vm, 'beforeUpdate')
watcher.before()
}
}
}
```
#### 五、beforeUpdate 被调用完成
然后经历了一系列的 `patch、diff` 流程后,组件重新渲染完毕,调用 `updated` 钩子。
> 注意,这里是对 `watcher` 倒序 `updated` 调用的。
```js
function callUpdatedHooks(queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted) {
callHook(vm, 'updated')
}
}
}
```
也即,假如同一个属性通过 `props` 分别流向 `父 -> 子 -> 孙` 这个路径,那么收集到依赖的先后也是这个顺序,但是触发 `updated` 钩子确是 `孙 -> 子 -> 父` 这个顺序去触发的。
#### 六、updated 被调用完成
至此,渲染更新流程完毕。
### --- 销毁流程
在上文提及的更新后的 `patch` 过程中,如果发现有组件在下一轮渲染中消失了,比如 `v-for` 对应的数组中少了一个数据。那么就会调用 `removeVnodes` 进入组件的销毁流程。
`removeVnodes` 会调用 `vnode` 的 `destroy` 生命周期,而 `destroy` 内部则会调用我们相对比较熟悉的 `vm.$destroy()`。(`keep-alive` 包裹的子组件除外) 这时,就会调用 `callHook(vm, 'beforeDestroy')`。
#### 七、beforeDestroy 被调用完成
beforeDestroy 调用完成,之后就会经历一系列的清理:比如清除父子关系、watcher 关闭等逻辑。但是注意,$destroy 并不会把组件从视图上移除,如果想要手动销毁一个组件,则需要我们自己去完成这个逻辑。
然后,调用最后的 `callHook(vm, 'destroyed')`
#### destroyed 被调用完成
至此,销毁流程完毕。
# 组件的通信
vue 组件间通信主要有以下七种方式:
## (一)props / `$emit`
常用的**父子**间通信方式。父组件向子组件**传值**,通过绑定属性来向子组件传入数据,子组件通过 **Props 属性**获取对应数据。
- 适用场景:父组件传递数据给子组件
- 子组件设置 props 属性,定义接收父组件传递过来的参数
- 父组件在使用子组件标签中通过字面量来传递值
- 适用场景:子组件传递数据给父组件
- 子组件通过 `$emit` 触发自定义事件,`$emit` 第二个参数为传递的数值
- 父组件绑定监听器获取到子组件传递过来的参数
## (二)`$emit` / `$on`
**eventBus** 中央事件总线
这个方法是通过创建了一个空的 **vue 实例**,当做 $emit 事件的处理中心(事件总线),通过他来触发以及监听事件,方便的实现了**任意组件**间的通信,包含父子,兄弟,隔代组件。
- 使用场景:兄弟组件传值
- 创建一个中央时间总线 EventBus
- 兄弟组件通过 `$emit` 触发自定义事件,`$emit` 第二个参数为传递的数值
- 另一个兄弟组件通过 `$on` 监听自定义事件
## (三)`$attrs` / `$listeners`
适用于创建高级别的组件或者封装第三方组件。
- `$attrs`:包含了父作用域中不作为 Prop 被识别 (且获取) 的特性绑定(Class 和 Style 除外)。当一个组件没有声明任何 Prop 时,这里会包含所有父作用域的绑定 (Class 和 Style 除外),并且可以通过 `v-bind="$attrs"` 传入内部组件。
- `$listeners`:包含了父作用域中的 (不含 `.native`修饰器的) `v-on` 事件监听器。它可以通过 `v-on="$listeners"` 传入内部组件。
- 适用场景:**祖先**传递数据给**子孙**
- 设置批量向下传属性 `$attrs` 和 `$listeners`
- 包含了父级作用域中不作为 prop 被识别 (且获取) 的特性绑定 ( class 和 style 除外)。
- 可以通过 `v-bind="$attrs"` 传⼊内部组
## (四)Provider / Inject
这对选项需要一起使用,以允许一个**祖先**组件向其**所有子孙后代**注入一个**依赖**,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。
简单来说,就是父组件通过 Provider 传入变量,**任意**子孙组件通过 Inject 来拿到变量。
Provide 和 Inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的**属性**还是**可响应**的。
Provider / Inject 在项目中需要有较多公共传参时使用还是颇为方便的。传输数据父级一次注入,子孙组件一起共享的方式。
- 在**祖先**组件定义 provide 属性,返回传递的值
- 在**后代**组件通过 inject 接收组件传递过来的值
## (五)`$parent` / `$children`
指定已创建的实例之**父实例**,在两者之间建立父子关系。子实例可以用 `this.$parent` 访问父实例,子实例被推入父实例的 `$children` 数组中。**不能跨级以及兄弟间通信**。
## (六)`$refs`
一个对象,持有注册过 ref 的所有 DOM 元素和组件实例。
ref 被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 `$refs` 对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件。**不能跨级以及兄弟间通信**。
- 父组件在使用子组件的时候设置 ref
- 父组件通过设置**子组件 ref** 来获取数据
## (七)Vuex
vue 状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
Vuex 实现了一个**单项数据流**,通过创建一个全局的 State 数据,组件想要修改 State 数据只能通过 Mutation 来进行,例如页面上的操作想要修改 State 数据时,需要通过 Dispatch (触发 Action ),而 Action 也不能直接操作数据,还需要通过 Mutation 来修改 State 中数据,最后根据 State 中数据的变化,来渲染页面。
1. Mutation: 是修改 State 数据的唯一推荐方法,且只能进行**同步**操作。
2. Getter: Vuex 允许在 Store 中定义 “ Getter ”(类似于 Store 的计算属性)。Getter 的返回值会根据他的依赖进行缓存,只有依赖值发生了变化,才会重新计算。统一的维护了一份共同的 State 数据,方便组件间共同调用。
- state 用来存放共享变量的地方
- getter 可以增加一个 getter 派生状态,(相当于store中的计算属性),用来获得共享变量的值
- mutations 用来存放修改 state 的方法。
- actions 也是用来存放修改 state 的方法,不过 action 是在 mutations 的基础上进行。常用来做一些**异步**操作。
## Vue 组件通信总结
- 父子通信:`Props/$emit`,`$emit/$on`,`Vuex`,`$attrs/$listeners`,`provide/inject`,`$parent/$children`,`$refs`
- 兄弟通信:`$emit/$on`,`Vuex`
- 隔代(跨级)通信:`$emit/$on`,`Vuex`,`provide/inject`,`$attrs/$listeners`
# Vue 路由原理
## hash 模式
vue-router 默认是 **hash** 模式,即使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。hash(#)虽然在 url 中,但不会被包括在http 请求中,对后端完全没有影响,因此**改变 hash 不会重新加载页面**。
## history 模式
路由的 **history** 模式充分利用 `history.pushState` API 来完成 URL 跳转而无须重新加载页面。若想配置这种模式,还需要**后台配置支持**。因为当前 vue 应用是个单页客户端应用(SPA),如果后台没有正确的配置,当用户在浏览器直接访问 `http://oursite.com/user/id` 就会返回 404。
为了避免这种情况,开发者应该在 Vue 应用里面覆盖所有的路由情况,然后再配置一个 404 页面。或者,使用 Node.js 服务器,通过使用服务端路由匹配到来的 URL,并在没有匹配到路由的时候返回 404,以实现回退。
history 模式,利用了 `html5 History Interface` 中新增的 pushState() 和 replaceState() 方法。这两个方法应用于浏览器的**历史记录栈**,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 url,但浏览器不会立即向后端发送请求。
hash 模式和 history 模式都属于浏览器自身的特性,vue-router 只是利用了这两个特性(通过浏览器提供的接口)来实现前端路由。pushState 方法不会触发页面刷新,只是导致了 history 对象发生变化,地址栏会有反应。如果 pushState 的 url 参数,设置了一个新的**锚点值**(即hash),并不会触发 hashChange 事件,如果设置了一个跨域网址,则会报错。
每当同一个文档的浏览历史(即history)出现变化时,就会触发 popState 事件。需要注意:仅仅调用 pushState 方法或 replaceState 方法,并不会触发该事件,只有用户点击浏览器后退和前进按钮时,或者使用 js 调用 back、forward、go 方法时才会触发。
另外该事件只针对**同一个文档**,如果浏览历史的切换,导致加载不同的文档,该事件不会被触发。使用的时候,可以为 popState 事件指定回调函数。
> 注意:页面第一次加载的时候,浏览器不会触发popState事件。