Vue是什么

用于构建用户界面的渐进式JavaScript框架。易用、灵活、高效的单页应用驱动。一款轻量级(未完全遵循)MVVM模型框架。
渐进式:通俗点讲就是,你想用啥你就用啥,咱也不强求你。你想用component就用,不用也行,你想用vuex就用,不用也可以

优点

  • 优点:渐进式,组件化,轻量级,虚拟dom,响应式,单页面路由,数据与视图分开
  • 缺点:单页面不利于seo,不支持IE8以下,首屏加载时间长
  1. Vue、React区别
  • 相同点
    • 虚拟dom
    • 组件化开发
    • 单向数据流(父子组件之间,不建议子修改父传下来的数据)
      -支持服务端渲染
  • 不同点
    • React的JSX,Vue的template
    • 数据变化,React手动(setState),Vue自动(初始化已响应式处理,Object.defineProperty)
    • React单向绑定,Vue双向绑定
    • React的Redux,Vue的Vuex
    • React依靠Facebook,社区完善。Vue依靠由于尤雨溪和社区开发者,或内活跃度高
  1. Vue和JQuery
  • jQuery是直接操作DOM,Vue不直接操作DOM,Vue的数据与视图是分开的,Vue只需要操作数据即可
  • 在操作DOM频繁的场景里,jQuery的操作DOM行为是频繁的,而Vue利用虚拟DOM的技术,大大提高了更新DOM时的性能
  • Vue中不倡导直接操作DOM,开发者只需要把大部分精力放在数据层面上
  • Vue集成的一些库,大大提高开发效率,比如Vuex,Router等
  1. Angular
  • 采用TypeScript
  • 数据脏检查,Watcher越多越慢
  • 依靠Google,社区完善

模型(MVVM)

MVVM是Model View ViewModel缩写,也就是把MVC中的Controller演变成ViewModel。Model层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。
MVC 全名是 Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范

  • Model 数据模型层(处理数据)
  • View 用户操作界面层(当ViewModel对Model进行更新的时候,会通过数据绑定更新到View)
  • ViewModel 业务逻辑层(View需要什么数据,ViewModel要提供这个数据,View有某些操作,ViewModel就要响应这些操作)
  • Controller 控制器(是应用程序中处理用户交互的部分,读取Model的数据赋值给View)

那么问题来了为什么官方要说Vue没有完全遵循MVVM思想呢?
严格的MVVM要求View不能和Model直接通信,而Vue提供了$refs这个属性,让Model可以直接操作View,违反了这一规定,所以说Vue没有完全遵循MVVM。

总结:MVVM模式简化了界面与业务的依赖,解决了数据频繁更新。MVVM在使用当中,利用双向绑定技术,使得Model变化时,ViewModel会自动更新,而ViewModel 变化时,View也会自动变化。

生命周期

Vue实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载等一系列过程,我们称这是Vue的生命周期

生命周期描述
beforeCreate组件实例被创建之初,组件的属性生效之前
created组件实例已经完全创建,属性也绑定,但真实dom还没有生成,$el还不可用
beforeMount在挂载开始之前被调用:相关的 render 函数首次被调用
mountedel 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子
beforeUpdate组件数据更新之前调用,发生在虚拟 DOM 打补丁之前
update组件数据更新之后
activitedkeep-alive专属,组件被激活时调用
deadctivatedkeep-alive专属,组件被销毁时调用
beforeDestory组件销毁前调用
destoryed组件销毁后调用

可以根据以上信息,Vue生命周期总共有8个阶段(段创建前/后,载入前/后,更新前/后,销毁前/后)

父子组件生命周期钩子函数执行顺序

  • 加载渲染过程 (父 beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted)
  • 子组件更新过程 (beforeUpdate->子 beforeUpdate->子 updated->父 updated)
  • 父组件更新过程 (beforeUpdate->父 updated)
  • 销毁过程 (beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed)

知识点

  • 第一次页面加载会触发beforeCreatecreatedbeforeMountmounted
  • DOM渲染在 mounted 中就已经完成
  • 异步请求可以在createdbeforeMountmounted钩子函数里进行。一般推荐在created钩子函数中调用异步请求,应为有以下优点:
    • 能更快获取到服务端数据,减少页面 loading 时间
    • SSR暂不支持 beforeMountmounted 钩子函数,有利于一致性

Data函数

组件中的 data 写成一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的 data,类似于给每个组件实例创建一个私有数据空间,让各个组件实例维护各自的数据。而单纯的写成对象形式,就使得所有组件实例共用了一份 data,就会造成数据污染(一个数据改变,影响全部数)

组件

简介

组件是可复用的 Vue 实例,且带有一个名字

  • 特点
    • 组件化开发能大幅提高应用开发效率、测试性、复用性
    • 常用的组件化技术:属性、自定义事件、插槽
    • 降低更新范围,值重新渲染变化的组件
    • 高内聚、低耦合、单向数据流
  • 写name好处
    • 增加name属性,会在components属性中增加组件本身,实现组件的递归调用
    • 可以表示组件的具体名称,方便调试和查找对应的组件

通讯

组件之间数据通信主要分为三类:父传子、子传父、兄弟互传

  1. props/emit(父传子通过props传输/[子]接受,子传父通过[子]emit事件触发)
  2. parent、children 获取当前组件的父组件和当前组件的子组件
  3. attrs、listeners (A->B->C)
  4. provide(提供变量)/inject(注入变量)官网不推荐,实际开发组件库可经常应用到
  5. $refs(获取组件实例)
- 获取`dom`元素`this.$refs.box`
- 获取子组件中的`data` `this.$refs.box.msg`
- 用子组件中的方法 `this.$refs.box.open()`
  1. eventBus(事件总线 - 兄弟组件数据传递)
  2. vuex/pinia(状态管理)

项目开发中我经常使用到props/$emit$refsvuex/pinia

内置指令

  • v-text 更新元素的textContent(文本内容不解析Html标签,可使用[插值])
  • v-html 更新元素的 innerHTML(会视为普通Html元素,注意XSS攻击)
  • v-show 不管值为true还是false,html元素都会存在(CSS:display[none/block])
  • v-if(v-else、v-else-if) 可以配置template使用(render函数解析为三元函数)、不会生成Html
  • v-for 循环指令(次渲染元素或模板块)
    • for…of 适用遍历数/数组对象/字符串/map/set等拥有迭代器对象的集合
    • for in 适用遍历对象{},遍历对象的键名
    • key 建议使用(id, uuid等唯一值),不建议使用index。key 是为 Vue 中 vnode 的唯一标记。我们的 diff 操作可以更准确、更快速
      • 更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确
      • 更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      // 判断两个vnode的标签和key是否相同 如果相同 就可以认为是同一节点就地复用
      function isSameVnode(oldVnode, newVnode) {
      return oldVnode.tag === newVnode.tag && oldVnode.key === newVnode.key;
      }

      // 根据key来创建老的儿子的index映射表 类似 {'a':0,'b':1} 代表key为'a'的节点在第一个位置 key为'b'的节点在第二个位置
      function makeIndexByKey(children) {
      let map = {};
      children.forEach((item, index) => {
      map[item.key] = index;
      });
      return map;
      }
      // 生成的映射表
      let map = makeIndexByKey(oldCh);

    • 比v-if优先级高(Vue3则反之),所以建议不要写在同一标签,或者使用计算属性
  • v-on 事件监听器(DOM) v-on:可以简写@
    在监听原生 DOM 事件时,方法以事件为唯一的参数。如果使用内联语句,语句可以访问一个 $event property:v-on:click="handle('ok', $event)"
    1
    2
    3
    4
    5
    6
    7
    8
    <!-- 动态事件缩写 (2.6.0+) -->
    <button @[event]="doThis"></button>
    <!-- 停止冒泡 -->
    <button @click.stop="doThis"></button>
    <!-- 阻止默认行为 -->
    <button @click.prevent="doThis"></button>
    <!-- 阻止默认行为,没有表达式 -->
    <form @submit.prevent></form>
    • 修饰符
      • .stop - 调用 event.stopPropagation()
      • .prevent - 调用 event.preventDefault()
      • .capture - 添加事件侦听器时使用 capture 模式
      • .self - 只当事件是从侦听器绑定的元素本身触发时才触发回调
      • .{keyCode | keyAlias} - 只当事件是从特定键触发时才触发回调
      • .native - 监听组件根元素的原生事件
      • .once - 只触发一次回调
      • .left - (2.2.0) 只当点击鼠标左键时触发
      • .right - (2.2.0) 只当点击鼠标右键时触发
      • .middle - (2.2.0) 只当点击鼠标中键时触发
      • .passive - (2.3.0) 以 { passive: true } 模式添加侦听器
  • v-bind 动态地绑定一个或多个 attribute,或一个组件 prop 到表达式 v-bind:缩写为:
    1
    2
    3
    4
    <!-- 绑定一个 attribute -->
    <img :src="imageSrc">
    <!-- class 绑定 -->
    <div :class="[classA, { classB: isB, classC: isC }]">
  • v-model 在表单控件或者组件上创建双向绑定 <input>、<select>、<textarea>、components

在普通标签上变成(在组件上面处理)为value和input的语法糖

  • 修饰符
    • .lazy - 取代 input 监听 change 事件
    • .number - 输入字符串转为有效的数字
    • .trim - 输入首尾空格过滤
  • v-slot 提供具名插槽或需要接收 prop 的插槽,可以缩写为#
  • v-pre 跳过这个元素和它的子元素的编译过(会加快编译)
  • v-cloak 保持在元素上直到关联实例结束编译
  • v-once 元素或者组件(所有节点)只渲染一次,选然后不会因为数据变化中心渲染(可视为静态内容)

扩展问题:v-if 、 v-show 和 display:none、visibility:hidden 和 opacity:0的区别

  • v-if 在编译过程中被转化成三元表达式,条件不成立不渲染节点,适用于在运行时很少改变条件,不需要频繁切换条件的场景
  • v-show 会被编译成指令,条件不成立控制CSS样式将对应节点隐藏 (display:none) 适用于需要非常频繁切换条件的场景

  • display: none 不占位,子元素不会继承,也不会显示,无法触发(自己的)绑定事件,transition无效果
  • visibility:hidden 占位,会被子元素继承,通过visibility:visible显示子元素,无法触发(自己的)绑定事件,transition无效果
  • opacity:0 占位,会被子元素继承,不能显示,可以触发(自己的)绑定事件,transition有效果

单向数据流

数据总是从父组件传到子组件,子组件没有权利修改父组件传过来的数据,只能请求父组件对原始数据进行修改。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

注意:在子组件直接用 v-model 绑定父组件传过来的 prop 这样是不规范的写法 开发环境会报警告(不建议使用此方法)
如果实在要改变父组件的 prop 值 建议可以再 data 里面定义一个变量 并用 prop 的值初始化它 之后用$emit 通知父组件去修改

组件作用域内的 CSS

  • `style scoped(免了组件间样式污染)

样式作用于当下的模块,很好的实现了样式私有化的目的。但是要非常谨慎使用(通常当作公共组件的时候是无法调整的)
- 使用 /deep/
- 使用两个style标签

侦听属性

其实computed(计算属性)和watch(侦听属性)全都是借助 Watcher 进行实现,那么computed 和 watch 的区别和运用的场景

  • computed(计算属性) 依赖其他属性计算值,并且值有缓存,只有当计算值变化才会返回内容,一般用在模板渲染中
  • watch(侦听属性) 监听到值的变化就会执行回调,在回调中可以(观测某个值的变化)进行一些逻辑操作

响应式数据

数据劫持+发布者-订阅者模式

通过 Object.defineProperty() 来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应监听回调。当把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项时,Vue 将遍历它的属性,用 Object.defineProperty() 将它们转为 getter/setter。用户看不到 getter/setter,但是在内部它们让 Vue追踪依赖,在属性被访问和修改时通知变化。
vue的数据双向绑定 将MVVM作为数据绑定的入口,整合Observer,Compile和Watcher三者,通过Observer来监听自己的model的数据变化,通过Compile来解析编译模板指令(vue中是用来解析 {{}}),最终利用watcher搭起observer和Compile之间的通信桥梁,达到数据变化 —>视图更新;视图交互变化(input)—>数据model变更双向绑定效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Observer {
// 观测值
constructor(value) {
this.walk(value);
}
walk(data) {
// 对象上的所有属性依次进行观测
let keys = Object.keys(data);
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
let value = data[key];
defineReactive(data, key, value);
}
}
}
// Object.defineProperty数据劫持核心 兼容性在ie9以及以上
function defineReactive(data, key, value) {
observe(value); // 递归关键
// --如果value还是一个对象会继续走一遍odefineReactive 层层遍历一直到value不是对象才停止
// 思考?如果Vue数据嵌套层级过深 >>性能会受影响
Object.defineProperty(data, key, {
get() {
console.log("获取值");

//需要做依赖收集过程 这里代码没写出来
return value;
},
set(newValue) {
if (newValue === value) return;
console.log("设置值");
//需要做派发更新过程 这里代码没写出来
value = newValue;
},
});
}
export function observe(value) {
// 如果传过来的是对象或者数组 进行属性劫持
if (
Object.prototype.toString.call(value) === "[object Object]" ||
Array.isArray(value)
) {
return new Observer(value);
}
}

Vue3.x 改用 Proxy 替代 Object.defineProperty。因为 Proxy 可以直接监听对象和数组的变化,并且有多达 13 种拦截方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import { mutableHandlers } from "./baseHandlers"; // 代理相关逻辑
import { isObject } from "./util"; // 工具方法

export function reactive(target) {
// 根据不同参数创建不同响应式对象
return createReactiveObject(target, mutableHandlers);
}
function createReactiveObject(target, baseHandler) {
if (!isObject(target)) {
return target;
}
const observed = new Proxy(target, baseHandler);
return observed;
}

const get = createGetter();
const set = createSetter();

function createGetter() {
return function get(target, key, receiver) {
// 对获取的值进行放射
const res = Reflect.get(target, key, receiver);
console.log("属性获取", key);
if (isObject(res)) {
// 如果获取的值是对象类型,则返回当前对象的代理对象
return reactive(res);
}
return res;
};
}
function createSetter() {
return function set(target, key, value, receiver) {
const oldValue = target[key];
const hadKey = hasOwn(target, key);
const result = Reflect.set(target, key, value, receiver);
if (!hadKey) {
console.log("属性新增", key, value);
} else if (hasChanged(value, oldValue)) {
console.log("属性值被修改", key, value);
}
return result;
};
}
export const mutableHandlers = {
get, // 当获取属性时调用此方法
set, // 当修改属性时调用此方法
};

检测数组

数组考虑性能原因没有用 defineProperty 对数组的每一项进行拦截,而是选择对 7 种数组(push,shift,pop,splice,unshift,sort,reverse)方法进行重写(AOP 切片思想)
所以在 Vue 中修改数组的索引和长度是无法监控到的。需要通过以上 7 种变异方法修改数组才会触发数组对应的 watcher 进行更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// src/obserber/array.js
// 先保留数组原型
const arrayProto = Array.prototype;
// 然后将arrayMethods继承自数组原型
// 这里是面向切片编程思想(AOP)--不破坏封装的前提下,动态的扩展功能
export const arrayMethods = Object.create(arrayProto);
let methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"reverse",
"sort",
];
methodsToPatch.forEach((method) => {
arrayMethods[method] = function (...args) {
// 这里保留原型方法的执行结果
const result = arrayProto[method].apply(this, args);
// 这句话是关键
// this代表的就是数据本身 比如数据是{a:[1,2,3]} 那么我们使用a.push(4) this就是a ob就是a.__ob__ 这个属性就是上段代码增加的 代表的是该数据已经被响应式观察过了指向Observer实例
const ob = this.__ob__;

// 这里的标志就是代表数组有新增操作
let inserted;
switch (method) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2);
default:
break;
}
// 如果有新增的元素 inserted是一个数组 调用Observer实例的observeArray对数组每一项进行观测
if (inserted) ob.observeArray(inserted);
// 之后咱们还可以在这里检测到数组改变了之后从而触发视图更新的操作--后续源码会揭晓
return result;
};
});

虚拟 DOM

通常在浏览器中操作 DOM 是很代价昂贵(性能、交互)的。频繁的操作 DOM,会产生一定的性能和交互问题。这就是虚拟 Dom 的产生原因,本质就是用一个原生的 JS 对象去描述一个 DOM 节点,是对真实 DOM 的一层抽象。

优点

  • 保证性能下限 (适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的,但是比起粗暴的 DOM 操作性能要好很多,依然可以提供还不错的性能)
  • 无需手动操作 DOM (不需要手动去操作 DOM,只需要写好 View-Model 的逻辑,框架帮我们以可预期的方式更新视图,极大提高我们的开发效率)
  • 跨平台 (本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作。服务器渲染、weex 开发)

缺点

  • 无法极致优化 (在一些要求极高的应用中虚拟 DOM 无法进行针对性的极致优化)
  • 首次渲染(加载)较慢 (首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,会比 innerHTML 插入慢)

v-model

v-model 只是语法糖,在内部为不同的输入元素使用不同的 property 并抛出不同的事件

  • text 和 textarea 元素使用 value property 和 input 事件
  • checkbox 和 radio 使用 checked property 和 change 事件
  • select 字段将 value 作为 prop 并将 change 作为事件

在普通标签上

1
2
3
<!-- 两者等同 -->
<input v-model="inputText" />
<input v-bind:value="inputText" v-on:input="inputText = $event.target.value" />
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<currency-input v-model="price"></currentcy-input>
<!--上行代码是下行的语法糖
<currency-input :value="price" @input="price = arguments[0]"></currency-input>
-->

<!-- 子组件定义 -->
Vue.component('currency-input', {
template: `
<span>
<input
ref="input"
:value="value"
@input="$emit('input', $event.target.value)"
>
</span>
`,
props: ['value'],
})

插槽

在 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令,可以缩写#)。它取代了 slot 和 slot-scope 这两个目前已被废弃但未被移除且仍在文档中的 attribute。新语法的由来可查阅这份 RFC。
通俗的理解就是“占坑”,在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填坑(替换组件模板中slot位置)

  • 内容插槽 有名字的插槽,子组件未定义的名字的插槽,父级将会把 未指定插槽的填充的内容填充到默认插槽中
  • 后备内容(默认内容)插槽 组件没有给你内容的时候,那么默认的内容就会被渲染
  • 具名插槽 插槽取个名字。一个子组件可以放多个插槽,而且可以放在不同的地方,而父组件填充内容时,可以根据这个名字把内容填充到对应插槽中
  • 作用域插槽 带数据的插槽,仅限于插槽中使用。父组件可根据子组件传过来的插槽数据来进行不同的方式展现和填充插槽内容

注意:

  • 父级的填充内容如果指定到子组件的没有对应名字插槽,那么该内容不会被填充到默认插槽中
  • 如果子组件没有默认插槽,而父级的填充内容指定到默认插槽中,那么该内容就“不会”填充到子组件的任何一个插槽中
  • 如果子组件有多个默认插槽,而父组件所有指定到默认插槽的填充内容,将“” “全都”填充到子组件的每个默认插槽中

事件绑定

原生事件绑定是通过 addEventListener 绑定给真实元素的,组件事件绑定是通过 Vue 自定义的on 实现的。如果要在组件上使用原生事件,需要加.native 修饰符,这样就相当于在父组件中把子组件当做普通 html 标签,然后加上原生事件 `on$emit` 是基于发布订阅模式的,维护一个事件中心,on 的时候将事件按名称存在事件中心里,称之为订阅者,然后 emit 将对应的事件进行发布,去执行事件中心里的对应的监听器。

路由 Vue-router

路由模式

  • hash 模式 使用createWebHashHistory()创建。特点 兼容性好但是不美观,
    1. location.hash 的值实际就是 URL 中#后面的东西 它的特点在于:hash 虽然出现 URL 中,但不会被包含在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面
    2. 可以为 hash 的改变添加监听事件
    1
    window.addEventListener("hashchange", funcRef, false);
    每一次改变 hash(window.location.hash),都会在浏览器的访问历史中增加一个记录利用 hash 的以上特点,就可以来实现前端路由“更新视图但不重新请求页面”的功能了
  • history(HTML5) 模式 使用createWebHistory()创建 虽然美观,但是刷新会出现 404 需要在服务端(Nginx/Apache)配置。官方推荐
    利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。
    这两个方法应用于浏览器的历史记录站,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前 URL 改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。

路由钩子的执行流程, 钩子函数种类有:全局守卫路由守卫组件守卫

  • 导航解析流程:

    1. 导航被触发
    2. 在失活的组件里调用 beforeRouteLeave 守卫
    3. 调用全局的 beforeEach 守卫
    4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)
    5. 在路由配置里调用 beforeEnter
    6. 解析异步路由组件
    7. 在被激活的组件里调用 beforeRouteEnter
    8. 调用全局的 beforeResolve 守卫 (2.5+)
    9. 导航被确认
    10. 调用全局的 afterEach 钩子
    11. 触发 DOM 更新
    12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入
  • 全局守卫 (router.beforeEach)

    • to: Route: 即将要进入的目标(路由对象)
    • from: Route: 当前导航正要离开的路由
    • next: Function: 一定要调用该方法来 resolve 这个钩子。(一定要用这个函数才能去到下一个路由,如果不用就拦截)
      执行效果依赖 next 方法的调用参数。
    • next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
    • next(false):取消进入路由,url地址重置为from路由地址(也就是将要离开的路由地址)
  • 动态路由 (我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件)
    例如,我们有一个 User 组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。那么,我们可以在 vue-router 的路由路径中使用“动态路径参数”(dynamic segment) 来达到这个效果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const User = {
    template: "<div>User</div>",
    };

    const router = new VueRouter({
    routes: [
    // 动态路径参数 以冒号开头
    { path: "/user/:id", component: User },
    ],
    });

    问题:vue-router 组件复用导致路由参数失效怎么办?

    1. 通过 watch 监听路由参数再发请求
    1
    2
    3
    4
    5
    watch: { //通过watch来监听路由变化
    "$route": function(){
    this.getData(this.$route.params.xxx);
    }
    }
    1. :key 来阻止“复用”
    1
    <router-view :key="$route.fullPath" />

    参数

    • 使用query方法传入的参数使用this.$route.query接收
    • 使用params方式传入的参数使用this.$route.params接收

    router和route的区别

    • route为当前router跳转对象里面可以获取name、path、query、params等
    • router为VueRouter实例,想要导航到不同URL,则使用router.push方法

    跳转

    • 声明式(标签跳转)
      1
      <router-link :to="index">
    • 编程式( js跳转)
      1
      router.push('index')

状态管理 Vuex

vuex 是专门为 vue 提供的全局状态管理系统,用于多个组件中数据共享、数据缓存等。(无法持久化、内部核心原理是通过创造一个全局实例 new Vue)

  • 主要包括以下几个模块:
    • State:定义了应用状态的数据结构,可以在这里设置默认的初始状态
    • Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。
    • Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
    • Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。
    • Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。
  • 页面刷新数据丢失怎么解决
    需要做 vuex 数据持久化 一般使用本地存储的方案来保存数据 可以自己设计存储方案 也可以使用第三方插件
  • 模块与命名空间
    • 模块:由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块。
    • 命名空间:默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。

SSR

SSR 也就是服务端渲染,也就是将 Vue 在客户端把标签渲染成 HTML 的工作放在服务端完成,然后再把 html 直接返回给客户端。

优点

  • 更好的 SEO
  • 首屏加载速度更快

缺点

  • 开发条件会受到限制,服务器端渲染只支持 beforeCreate 和 created 两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于 Node.js 的运行环境
  • 服务器会有更大的负载需求

设计模式

  • 工厂模式 (传入参数即可创建实例。虚拟节点的创建 Vnode)
  • 单例模式 (整个程序有且仅有一个实例。vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉)
  • 发布/订阅模式 eventBus
  • 代理模式:_data属性、proxy、防抖、节流
  • 观察者模式 watch和dep
  • 装饰模式 (@装饰器的用法)
  • 策略模式 (策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案-比如选项的合并策略)

混入 Vue.mixin

在日常的开发中,我们经常会遇到在不同的组件中经常会需要用到一些相同或者相似的代码,这些代码的功能相对独立,可以通过 Vue 的 mixin 功能抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化时会调用 mergeOptions 方法进行合并,采用策略模式针对不同的属性进行合并。当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
export default function initMixin(Vue){
Vue.mixin = function (mixin) {
// 合并对象
this.options = mergeOptions(this.options,mixin)
};
}
};

// src/util/index.js
// 定义生命周期
export const LIFECYCLE_HOOKS = [
"beforeCreate",
"created",
"beforeMount",
"mounted",
"beforeUpdate",
"updated",
"beforeDestroy",
"destroyed",
];

// 合并策略
const strats = {};
// mixin核心方法
export function mergeOptions(parent, child) {
const options = {};
// 遍历父亲
for (let k in parent) {
mergeFiled(k);
}
// 父亲没有 儿子有
for (let k in child) {
if (!parent.hasOwnProperty(k)) {
mergeFiled(k);
}
}

//真正合并字段方法
function mergeFiled(k) {
if (strats[k]) {
options[k] = strats[k](parent[k], child[k]);
} else {
// 默认策略
options[k] = child[k] ? child[k] : parent[k];
}
}
return options;
}

nextTick

nextTick 中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。主要思路就是采用微任务优先的方式调用异步方法去执行 nextTick 包装的方法

例如,在Vue生命周期的created()钩子函数进行的DOM操作一定要放在this(Vue).nextTick()的回调函数中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
let callbacks = [];
let pending = false;
function flushCallbacks() {
pending = false; //把标志还原为false
// 依次执行回调
for (let i = 0; i < callbacks.length; i++) {
callbacks[i]();
}
}
let timerFunc; //定义异步方法 采用优雅降级
if (typeof Promise !== "undefined") {
// 如果支持promise
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks);
};
} else if (typeof MutationObserver !== "undefined") {
// MutationObserver 主要是监听dom变化 也是一个异步方法
let counter = 1;
const observer = new MutationObserver(flushCallbacks);
const textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true,
});
timerFunc = () => {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
} else if (typeof setImmediate !== "undefined") {
// 如果前面都不支持 判断setImmediate
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
// 最后降级采用setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}

export function nextTick(cb) {
// 除了渲染watcher 还有用户自己手动调用的nextTick 一起被收集到数组
callbacks.push(cb);
if (!pending) {
// 如果多次调用nextTick 只会执行一次异步 等异步队列清空之后再把标志变为false
pending = true;
timerFunc();
}
}

keep-alive

使用keep-alive包裹动态组件时,会对组件进行缓存,避免组件重新创建,使用有两个场景,一个是动态组件,一个是router-view
keep-alive 是 Vue 内置的一个组件,可以实现组件缓存,当组件切换时不会对当前组件进行卸载。

  • 常用的两个属性 include/exclude,允许组件有条件的进行缓存
  • 两个生命周期 activated/deactivated,用来得知当前组件是否处于活跃状态
  • keep-alive 的中还运用了 LRU(最近最少使用) 算法,选择最近最久未使用的组件予以淘汰

Vue.set

了解 Vue 响应式原理的同学都知道在两种情况下修改数据 Vue 是不会触发视图更新的

  • 在实例创建之后添加新的属性到实例上(给响应式对象新增属性)
  • 直接更改数组下标来修改数组的值
    因为响应式数据 我们给对象和数组本身都增加了__ob__属性,代表的是 Observer 实例。当给对象新增不存在的属性 首先会把新的属性进行响应式跟踪 然后会触发对象__ob__的 dep 收集到的 watcher 去更新,当修改数组索引时我们调用数组本身的 splice 方法去更新数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export function set(target: Array | Object, key: any, val: any): any {
// 如果是数组 调用我们重写的splice方法 (这样可以更新视图)
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val;
}
// 如果是对象本身的属性,则直接添加即可
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
const ob = (target: any).__ob__;

// 如果不是响应式的也不需要将其定义成响应式属性
if (!ob) {
target[key] = val;
return val;
}
// 将属性定义成响应式的
defineReactive(ob.value, key, val);
// 通知视图更新
ob.dep.notify();
return val;
}

Vue.extend

官方:Vue.extend 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。
通俗讲就是一个子类构造器 是 Vue 组件的核心 api 实现思路就是使用原型继承的方法返回了 Vue 的子类 并且利用 mergeOptions 把传入组件的 options 和父类的 options 进行了合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default function initExtend(Vue) {
let cid = 0; //组件的唯一标识
// 创建子类继承Vue父类 便于属性扩展
Vue.extend = function (extendOptions) {
// 创建子类的构造函数 并且调用初始化方法
const Sub = function VueComponent(options) {
this._init(options); //调用Vue初始化方法
};
Sub.cid = cid++;
Sub.prototype = Object.create(this.prototype); // 子类原型指向父类
Sub.prototype.constructor = Sub; //constructor指向自己
Sub.options = mergeOptions(this.options, extendOptions); //合并自己的options和父类的options
return Sub;
};
}

Vue.use

Vue.use是用来使用插件的。我们可以在插件中扩展全局组件、指令、原型方法等。 会调用install方法将Vue的构建函数默认传入,在插件中可以使用vue,无需依赖vue库

自定义指令

指令本质上是装饰器,是 vue 对 HTML 元素的扩展,给 HTML 元素增加自定义功能。vue 编译 DOM 时,会找到指令对象,执行指令的相关方法。
自定义指令有五个生命周期(也叫钩子函数),分别是 bind、inserted、update、componentUpdated、unbind

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置
  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)
  • update:被绑定于元素所在的模板更新时调用,而无论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新
  • componentUpdated:被绑定元素所在模板完成一次更新周期时调用
  • unbind:只调用一次,指令与元素解绑时调用

模板编译

Vue 的编译过程就是将 template 转化为 render 函数的过程

  1. 将模板字符串 转换成 element ASTs(解析器)
  2. 对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器)
  3. 使用 element ASTs 生成 render 函数代码字符串(代码生成器)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export function compileToFunctions(template) {
// 我们需要把html字符串变成render函数
// 1.把html代码转成ast语法树 ast用来描述代码本身形成树结构 不仅可以描述html 也能描述css以及js语法
// 很多库都运用到了ast 比如 webpack babel eslint等等
let ast = parse(template);
// 2.优化静态节点
// 这个有兴趣的可以去看源码 不影响核心功能就不实现了
// if (options.optimize !== false) {
// optimize(ast, options);
// }

// 3.通过ast 重新生成代码
// 我们最后生成的代码需要和render函数一样
// 类似_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))
// _c代表创建元素 _v代表创建文本 _s代表文Json.stringify--把对象解析成文本
let code = generate(ast);
// 使用with语法改变作用域为this 之后调用render函数可以使用call改变this 方便code里面的变量取值
let renderFn = new Function(`with(this){return ${code}}`);
return renderFn;
}

生命周期钩子

Vue 的生命周期钩子核心实现是利用发布订阅模式先把用户传入的的生命周期钩子订阅好(内部采用数组的方式存储)然后在创建组件实例的过程中会一次执行对应的钩子方法(发布)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export function callHook(vm, hook) {
// 依次执行生命周期对应的方法
const handlers = vm.$options[hook];
if (handlers) {
for (let i = 0; i < handlers.length; i++) {
handlers[i].call(vm); //生命周期里面的this指向当前实例
}
}
}

// 调用的时候
Vue.prototype._init = function (options) {
const vm = this;
vm.$options = mergeOptions(vm.constructor.options, options);
callHook(vm, "beforeCreate"); //初始化数据之前
// 初始化状态
initState(vm);
callHook(vm, "created"); //初始化数据之后
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};

函数式组件

函数式组件与普通组件的区别

  1. 函数式组件需要在声明组件是指定 functional:true
  2. 不需要实例化,所以没有this,this通过render函数的第二个参数context来代替
  3. 没有生命周期钩子函数,不能使用计算属性,watch
  4. 不能通过$emit 对外暴露事件,调用事件只能通过context.listeners.click的方式调用外部传入的事件
  5. 因为函数式组件是没有实例化的,所以在外部通过ref去引用组件时,实际引用的是HTMLElement
  6. 函数式组件的props可以不用显示声明,所以没有在props里面声明的属性都会被自动隐式解析为prop,而普通组件所有未声明的属性都解析到$attrs里面,并自动挂载到组件根元素上面(可以通过inheritAttrs属性禁止)

优点

  • 由于函数式组件不需要实例化,无状态,没有生命周期,所以渲染性能要好于普通组件
  • 函数式组件结构比较简单,代码结构更清晰

diff算法

Vue的diff算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式+双指针方式比较

  • 先比较两个节点是不是相同节点
  • 相同节点比较属性,复用老节点
  • 先比较儿子节点,考虑老节点和新节点儿子的情况
  • 优化比较:头头、尾尾、头尾、尾头
  • 比对查找,进行复用

兼容问题

  • 兼容IE浏览器 使用babel-polyfill插件

解决问题

  • 快速定位组件出现性能问题 用 timeline 工具。 大意是通过 timeline 来查看每个函数的调用时常,定位出哪个函数的问题,从而能判断哪个组件出了问题

性能优化

开发阶段

  • 不要在模板里面写过多表达式
  • 对象层级不要过深,否则性能就会差
  • 不需要响应式的数据不要放到 data 中(可以用 Object.freeze() 冻结数据)
  • v-for 遍历必须加 key,key 最好是 id 值,且避免同时使用 v-if
  • 频繁切换的使用v-show,不频繁切换的使用v-if
  • computed 和 watch 区分使用场景
  • 大数据列表和表格性能优化-虚拟列表/虚拟表格
  • 防止内部泄漏,组件销毁后把全局变量和事件销毁
  • 图片懒加载
  • 路由懒加载
  • 按需引入(第三方插件/组件)
  • 适当采用 keep-alive 缓存组件
  • 防抖、节流运用
  • 服务端渲染 SSR or 预渲染
  • 静态资源本地缓存
  • 增加用户体验(骨架屏/PWA)

打包编译

  • 对图片进行压缩
  • Webpack 使用 CommonsChunkPlugin 插件提取公共代码

运维阶段

  • 开启Gzip压缩
  • 部署CDN

vue-cli

  1. 构建vue-cli工程应用到那些技术,他们的作用是什么?
    • vue.js vue-cli工程的核心,主要特点是 双向数据绑定 和 组件系统
    • vue-router 官方推荐路由框架
    • vuex 官方推荐状态管理器,用于组件通信、数据共享
    • axios 基于Promise的http异步请求库
    • webpack 模块加载和vue-cli工程打包器
  2. vue-cli 工程常用的 npm 命令
    • 下载 node_modules 资源包的命令
      1
      npm install
    • 启动 vue-cli 开发环境的 npm 命令
      1
      npm run dev
    • vue-cli 生成 生产环境部署资源的 npm 命令
      1
      npm run build
    • 用于查看 vue-cli 生产环境部署资源文件大小的 npm 命令
      1
      npm run build --report

      在浏览器上自动弹出一个 展示 vue-cli 工程打包后 app.js、manifest.js、vendor.js 文件里面所包含代码的页面。可以具此优化 vue-cli 生产环境部署的静态资源,提升 页面 的加载速度