Vue.js

渐进式 MVVM 单向数据流 双向绑定
Vue 官网
Vue.js 技术揭秘

Nuxt 2
Nuxt 3

VueUse

组件通信

  1. 父子组件通信
    • props / $emit + v-on
    • v-model
    • $attrs / $listeners(inheritAttrs: false)
  2. 兄弟组件通信
    • $parent.$children, 在 $children 中通过组件 name 查询到需要的组件实例
    • Vue3: $refs
  3. 跨层级组件通信
    • provide inject
  4. 任意组件通信
    • Vuex
    • EventBus

自定义 v-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
<template>
<input :value="modelValue" @input="inputHandler">
</template>

<script>
export default {
name: 'VModel',
props: {
modelValue: {
type: String,
default: ''
}
},
setup(props, context) {
function inputHandler(e) {
context.emit('update:modelValue', e.target.value)
}

return {
inputHandler
}
}
}
</script>

computed

1
2
3
4
5
6
7
8
9
10
11
12
13
new Vue({
data: { a: 1 },
computed: {
aPlus: {
get: function () {
return this.a + 1
},
set: function (v) {
this.a = v - 1
}
}
}
})

watch

1
2
3
4
5
6
7
8
vm.$watch('obj', {
// 深度遍历
deep: true,
// 立即触发
immediate: true,
// 执行函数
handler: function(val, oldVal) {}
})

生命周期 Vue2

beforeCreate

初始化一个空的 Vue 实例,data methods 等尚未被初始化,无法调用

created

Vue 实例初始化完成,data methods 都已初始化完成,但还没开始渲染模板

beforeMount

编译模板,调用 render 函数生成 vdom ,还没有开始渲染 DOM

mounted

渲染 DOM 完成,组件创建完成,开始进入运行阶段

beforeUpdate

在数据发生改变后,DOM 被更新之前被调用
在 DOM 更新之前移除手动添加的事件监听器

updated

在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用
尽量不要在 updated 中继续修改数据,否则可能会死循环

onActivated

keep-alive 缓存的组件激活时调用

onDeactivated

keep-alive 缓存的组件卸载时调用

beforeUnmount

销毁组件实例前调用,在这个阶段,实例仍然是完全正常的
清空解绑全局变量、定时器、注册的全局事件、自定义事件

unmounted

销毁组件实例后调用,所有子组件实例也被销毁

mountedupdated 都不会保证所有子组件都挂载完成,$nextTick 等待所有视图渲染完成

Vue3 Composition API

  • setup 代替了 beforeCreatecreated
  • 生命周期换成了函数的形式,如 mounted -> onMounted
1
2
3
4
5
6
7
8
9
10
11
12
import { onUpdated, onMounted } from 'vue'

export default {
setup() {
onMounted(() => {
console.log('mounted')
})
onUpdated(() => {
console.log('updated')
})
}
}

编译过程

  • parse模板解析为 AST 树
  • optimize优化 AST 树
  • codegenAST 树转换成 Render 函数

通过执行 Render 函数生成 Virtual DOM 最终映射为真实 DOM

React 的 JSX,Vue 的 template 其实都是语法糖,它们本质上都是一个函数render 函数

1
2
3
4
// JSX: <p id="p1">hello world</p>
function render(): VNode {
return createElement('p', { id: 'p1' }, ['hello world'])
}

每次数据更新(如 React setState)render 函数都会生成 newVnode,然后前后对比 diff(vnode, newVnode),计算出需要修改的 DOM 节点,再做更新

Virtual DOM

实现数据视图分离数据驱动视图的根本,和 JQuery 有了本质的区别

  • DOM 操作是昂贵的, JS 操作对象数据很快
    • DOM 属于渲染引擎, JS 属于JS引擎, 两个线程之间通信上的性能损耗
    • 操作 DOM 会带来回流重绘的情况
  • 通过 JS 模拟 DOM 并且渲染对应的 DOM
  • 判断新旧两个 JS 对象的最小差异并且实现局部更新DOM

Virtual DOM 一定就比原生 DOM 快吗?
不一定。如果可以人肉地精准去局部更新 DOM,那么 Virtual DOM 必然没有直接操作 DOM 快生成 vdom,进行 diff 算法,再修改 DOM

  • 初始渲染 Virtual DOM > 脏检查 >= 依赖收集
  • 小量数据更新 依赖收集 >> Virtual DOM + 优化 > 脏检查(无法优化) > Virtual DOM 无优化
  • 大量数据更新 脏检查 + 优化 >= 依赖收集 + 优化 > Virtual DOM(无法/无需优化) >> MVVM 无优化
  • 函数式的 UI 编程方式
  • 将 Virtual DOM 作为一个兼容层,实现跨端

Svelte

与使用虚拟(virtual)DOM 差异对比不同。Svelte 编写的代码在应用程序的状态更改时就能像做外科手术一样更新 DOM。

Diff 算法

Diff 算法是一个非常普遍常用的方法,例如提交 github pr 或者(gitlab mr)时,会对比当前提交代码的改动,这就是 Diff

vdom 是一个多叉树的结构,完整对比两颗树的差异,传统的 tree diff 时间复杂度是O(n^3),算法不可用

优化算法实现时间复杂度O(n),只同层对比节点,而不跨层对比

  • 从上至下,从左往右遍历对象,也就是树的深度遍历
    • tag不同则直接删掉重建
  • 然后再判断子节点
    • 旧的列表中是否有节点被移除
    • 新的列表中是否有节点的加入
    • 节点是否有移动, key属性复用节点
  • 同时还需判断节点的属性是否有变化

Vue React diff 算法有什么区别?

  • React 仅向右移动
    比较子节点时,仅向右移动,不向左移动
  • Vue2 双端比较
    定义四个指针,分别比较
    1. oldStartNode 和 newStartNode
    2. oldStartNode 和 newEndNode
    3. oldEndNode 和 newStartNode
    4. oldEndNode 和 newEndNode
      然后指针向中间移动,直到指针汇合
  • Vue3 最长递增子序列
    尽量减少 DOM 的移动,达到最少的 DOM 操作

nextTick

响应式原理

  • Vue2中一个组件只有一个 Watcher,组件内部虚拟dom Diff
  • new vue的过程: init $mount compile render vnode patch DOM
  1. 数据监听器 Observer
    • 响应式 利用 Object.defineProperty 给数据添加 getter依赖收集 和 setter派发更新
    • Dep 是整个 getter 依赖收集的核心,是对 Watcher 的一种管理
  2. 指令解析器 Compile
    • 解析模板字符串生成 AST const ast = parse(template, options)
    • 优化语法树 optimize(ast, options) 标记静态节点、标记静态根
    • 生成代码 const code = generate(ast, options)
  3. Watcher,作为连接 Observer 和 Compile 的桥梁
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
function Observer(data) {
this.data = data;
this.walk(data);
}

Observer.prototype = {
constructor: Observer,
walk: function (data) {
var me = this;
Object.keys(data).forEach(function (key) {
me.convert(key, data[key]);
});
},
convert: function (key, val) {
this.defineReactive(this.data, key, val);
},

defineReactive: function (data, key, val) {
var dep = new Dep();
var childObj = observe(val);

Object.defineProperty(data, key, {
configurable: false,
enumerable: true,
get: function () {
if (Dep.target) {
dep.depend();
}
return val;
},
set: function (newVal) {
if (newVal === val) {
return;
}
val = newVal;
childObj = observe(newVal);
dep.notify();
}
});
}
};

function observe(value, vm) {
if (!value || typeof value !== 'object') {
return;
}

return new Observer(value);
};

/*
new vue
observe: 每个值有一个 dep
compile
new Watcher 读值时触发 get
一个 wacther 可以被多个 dep 收集
每个 dep 里不会有相同的 watcher
*/

var uid = 0;

function Dep() {
this.id = uid++;
this.subs = [];
}

Dep.prototype = {
addSub: function (sub) {
this.subs.push(sub);
},

depend: function () {
Dep.target.addDep(this);
},

removeSub: function (sub) {
var index = this.subs.indexOf(sub);
if (~index) {
this.subs.splice(index, 1);
}
},

notify: function () {
this.subs.forEach(function (sub) {
sub.update();
});
}
};

Dep.target = null;

Vue-router

hash

使用 url Hash 修改路由地址onhashchange

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
location.protocol // 'http:'
location.hostname // '127.0.0.1'
location.host // '127.0.0.1:8888'
location.port // '8888'
location.pathname // '/hash.html'
location.search // '?a=100&b=20'
location.hash // '#/aaa/bbb'
location.origin // http://127.0.0.1:8888
location.href // http://127.0.0.1:8888/hash.html?a=100&b=20#/aaa/bbb

/**
* hash 变化,包括:
* 1. JS 修改 url
* 2. 手动修改 url 的 hash
* 3. 浏览器前进、后退
*/
window.onhashchange = (event) => {
console.log('old url', event.oldURL)
console.log('new url', event.newURL)

console.log('hash', location.hash)
}

history

使用 H5 history API 修改路由地址

  • history.pushState
  • window.onpopstate

页面刷新时,服务端要做处理

abstract

不改变 url,路由地址在内存中,页面刷新会重新回到首页

Vue-router v4 升级之后,mode: 'xxx' 替换为 API 的形式,但功能是一样的

  • mode: 'hash' 替换为 createWebHashHistory()
  • mode: 'history' 替换为 createWebHistory()
  • mode: 'abstract' 替换为 createMemoryHistory()

性能优化

v-if 和 v-show

  • v-if 组件销毁与重建
  • v-show 组件显示和隐藏(切换 CSS display

v-for 使用 key

key 可以优化 diff 算法,遍历数组时 key 不要使用 index

computed 缓存

computed 可以缓存计算结果,依赖的数据不变则缓存不失效

keep-alive

<keep-alive> 可以缓存子组件,只创建一次。通过 activateddeactivated 生命周期监听是否处于激活状态

异步组件按需加载

  • Vue3 defineAsyncComponent

路由懒加载

SSR 服务端渲染

  • Nuxt.js

错误监控

errorCaptured 组件错误

1
2
3
errorCaptured(error, instance, info) {
console.log(error, instance, info)
}

会监听所有下级组件的错误。可以返回 false 阻止向上传播,因为可能会有多个上级节点都监听错误

errorHandler 全局错误

1
2
3
4
const app = createApp(App)
app.config.errorHandler = (error, instance, info) => {
console.log(error, instance, info)
}
  • Vue 全局错误,所有组件的报错都会汇总到这里来
  • 如果 errorCaptured 返回 false不会到这里
  • errorHandler 会阻止错误走向 window.onerror

window.onerror try…catch 其它错误、异步

1
2
3
4
window.onerror = function(msg, source, line, column, error) {
console.log(msg, source, line, column, error)
console.log(`https://stackoverflow.com/search?q=[js]+${msg}`)
}