# Vue 基础理论

# Vue 的单向数据流

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。

这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

额外的,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。

子组件想修改时,只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改。

有两种常见的试图改变一个 prop 的情形 :

prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的 data 属性并将这个 prop 用作其初始值:

props: ['initialCounter'],
data: function () {
  return {
    counter: this.initialCounter
  }
}

prop 以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop 的值来定义一个计算属性

props: ['size'],
computed: {
  normalizedSize: function () {
    return this.size.trim().toLowerCase()
  }
}

# SPA 优缺点是什么

SPA( single-page application ) 仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。

一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。

# 优点

  • 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
  • 基于上面一点,SPA 相对对服务器压力小
  • 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;

# 缺点

  • 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;
  • 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
  • SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。

# Class 与 Style 如何动态绑定

Class 可以通过对象语法和数组语法进行动态绑定:

<div v-bind:class="{ active: isActive, 'text-danger': hasError }"></div>
<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>

data: {
  isActive: true,
  hasError: false
}

Style 也可以通过对象语法和数组语法进行动态绑定:

<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
<div v-bind:style="[styleColor, styleSize]"></div>

data: {
  activeColor: 'red',
  fontSize: 30
  styleColor: {
    color: 'red'
  },
  styleSize:{
    fontSize:'23px'
  }
}

# Vue 生命周期

Vue 实例有一个完整的生命周期:

  1. 开始创建
  2. 初始化数据
  3. 编译模版
  4. 挂载 Dom -> 渲染
  5. 更新 -> 渲染
  6. 卸载

这一系列过程,我们称这是 Vue 的生命周期。

Vue Lifecycle

# beforeCreate

new Vue()之后触发的第一个钩子,在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问。

# created

在实例创建完成后发生,当前阶段已经完成了数据观测,也就是可以使用数据,更改数据,在这里更改数据不会触发 updated 函数。可以做一些初始数据的获取,在当前阶段无法与 Dom 进行交互,如果非要想,可以通过 vm.$nextTick 来访问 Dom。

# beforeMounted

发生在挂载之前,在这之前 template 模板已导入渲染函数编译。而当前阶段虚拟 Dom 已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发 updated。

# mounted

在挂载完成后发生,在当前阶段,真实的 Dom 挂载完毕,数据完成双向绑定,可以访问到 Dom 节点,使用 $ref 属性对 Dom 进行操作。

# beforeUpdate

发生在更新之前,也就是响应式数据发生更新,虚拟 dom 重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重渲染。

# updated

发生在更新完成之后,当前阶段组件 Dom 已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。

# beforeDestroy

发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。

# destroyed

发生在实例销毁之后,这个时候只剩下了 dom 空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁。

# activated

keep-alive 组件激活时调用,该钩子在服务器端渲染期间不被调用。

# deactivated

keep-alive 组件停用时调用,该钩子在服务器端渲染期间不被调用。

# errorCaptured

当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false 以阻止该错误继续向上传播。

你可以在此钩子中修改组件的状态。因此在模板或渲染函数中设置其它内容的短路条件非常重要,它可以防止当一个错误被捕获时该组件进入一个无限的渲染循环。

# 注意点

在使用生命周期时有几点注意事项需要我们牢记。

  1. 除了 beforeCreate 和 created 钩子之外,其他钩子均在服务器端渲染期间不被调用,所以接口请求一般放在 mounted 中,但是服务端渲染需要放到 created 中。
  2. 上文曾提到过,在 updated 的时候千万不要去修改 data 里面赋值的数据,否则会导致死循环。
  3. Vue 的所有生命周期函数都是自动绑定到 this 的上下文上。所以,你这里使用箭头函数的话,就会出现 this 指向的父级作用域,就会报错。

# 学习资料

# Vue 组件生命周期调用顺序

  • 组件的调用顺序都是 先父后子,渲染完成的顺序是 先子后父
  • 组件的销毁操作是 先父后子,销毁完成的顺序是 先子后父

# 加载渲染过程

  1. 父 beforeCreate
  2. 父 created
  3. 父 beforeMount
  4. 子 beforeCreate
  5. 子 created
  6. 子 beforeMount
  7. 子 mounted
  8. 父 mounted

# 子组件更新过程

  1. 父 beforeUpdate
  2. 子 beforeUpdate
  3. 子 updated
  4. 父 updated

# 父组件更新过程

  1. 父 beforeUpdate
  2. 父 updated

# 销毁过程

  1. 父 beforeDestroy
  2. 子 beforeDestroy
  3. 子 destroyed
  4. 父 destroyed

# 在哪个生命周期内调用异步请求

可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。

但推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

  • 能更快获取到服务端数据,减少页面  loading 时间;
  • ssr  不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;

# 在什么阶段才能访问操作 DOM

钩子函数 mounted 被调用前,Vue 已经将编译好的模板挂载到页面上。

所以在 mounted 中可以访问操作 DOM。

# 父组件可以监听到子组件的生命周期吗

比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,可以通过以下写法实现:

// Parent.vue
<Child @mounted="doSomething"/>

// Child.vue
mounted() {
  this.$emit("mounted");
}

以上需要手动通过 $emit 触发父组件的事件,更简单的方式可以在父组件引用子组件时通过 @hook 来监听即可,如下所示:

//  Parent.vue
<Child @hook:mounted="doSomething" ></Child>

doSomething() {
   console.log('父组件监听到 mounted 钩子函数 ...');
},

//  Child.vue
mounted(){
   console.log('子组件触发 mounted 钩子函数 ...');
},

// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...

# Vue 组件间通信方式

# 父子通信 props/$emit

  • 父向子传递数据是通过 props,子向父是通过 events($emit)
  • 通过$parent/$children也可以通信;ref 也可以访问组件实例
  • $attrs/$listeners(信息转发)
  • provide/inject(祖先组件传递信息给后代组件)

# 兄弟通信 Bus

  • vuex
  • Bus,通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,巧妙而轻量地实现了任何组件间的通信,包括父子、兄弟、跨级

注意:记得销毁自定义事件,否则容易造成内存泄露

# 跨级通信 vuex

  • vuex(信息处理)
  • $attrs/$listeners(信息转发)
  • provide/inject(祖先组件传递信息给后代组件)

学习资料:Vue 组件间通信六种方式(完整版)

# Computed 和 Watch

Computed 本质是一个具备缓存的 watcher,依赖的属性发生变化就会更新视图。 适用于计算比较消耗性能的计算场景。当表达式过于复杂时,在模板中放入过多逻辑会让模板难以维护,可以将复杂的逻辑放入计算属性中处理。

Watch 没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开 deep:true 选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听,如果没有写到组件中,不要忘记使用 unWatch 手动注销哦。

学习资料:

# 双向数据绑定 v-model

v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:

  • text 和 textarea 元素使用 value 属性和 input 事件;
  • checkbox 和 radio 使用 checked 属性和 change 事件;
  • select 字段将 value 作为 prop 并将 change 作为事件。
<input v-model="message" />
<!-- 相当于 -->
<input :value="message" @input="message = $event.target.value" />

所以,自定义组件 v-model 默认会利用名为 value 的 prop 和名为 input 的事件:

// 父组件
<ChildA v-model="message" />
// 或者
<ChildA :value="message" @input="handle" />

// 子组件
<input type="text" @input="handle" />
// ...
props: ['value'],
methods: {
  handle(e) {
    this.$emit('input', e.target.value)
  }
}
Last Updated: 5/22/2020, 5:01:49 PM