1. 参考文献

强烈推荐:双向数据绑定的 3 种实现方式
vue 源码学习:Object.defineProperty 对数组监听
深度剖析:如何实现一个 Virtual DOM 算法
构建一个使用 Virtual-DOM 的前端模版引擎
vue 底层实现分析
请说一下 vuex 工作原理

2.操作 Dom 更新时机

React:框架不主动追什么时候数据变了,需要手动通知框架状态变化
Vue:setter 监听变化

2.1 setter 监听变化

2.1.1 问题

问题:
1、给对象添新属性(data.newKey = ‘value’)时,setter 监听不到
2、delete 删掉现有属性,setter 监听不到
3、数组变化监听不到

解决方法:
1、通过 API 手动添加新属性 vm.$set('b', 2)
2、无关紧要,需要 delete 的场景很少,赋值 undefined 再添一点额外判断就能避免 delete 了
3、Vue 通过篡改原生 Array 方法-篡改原生 Array 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var arrayPush = {};

(function (method) {
var original = Array.prototype[method];
arrayPush[method] = function () {
// this 指向可通过下面的测试看出
console.log(this); // 这里可以增加一些触发data变化的函数方法,达到触发的效果
return original.apply(this, arguments);
};
})('push');

var testPush = [];
testPush.__proto__ = arrayPush;
// 通过输出,可以看出上面所述 this 指向的是 testPush
// []
testPush.push(1);
// [1]
testPush.push(2);

2.1.2 原理

1、observe:主要是通过遍历 Object.defineProperty 为每个属性添加 get 和 set 方法

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
function observe(obj) {
// 判断类型
if (!obj || typeof obj !== 'object') {
return;
}
// 重写数组
// if(Array.isArray(target)){
// target.__proto__ = arrProto
// }
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
}
export function defineReactive(obj, key, val) {
var dep = new Dep();
var childOb = observe(val);

Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: () => {
// 说明这是watch 引起的
if (Dep.target) {
dep.addSub(Dep.target);
}
return val;
},
set: (newVal) => {
var value = val;
if (newVal === value) {
return;
}
val = newVal;
childOb = observe(newVal); // 最好再遍历一下新设置的值
dep.notify(); // 更新DOM ☆☆☆☆☆
},
});
}

2.监听队列:

1
2
3
4
5
6
7
8
9
10
11
export default class Dep {
constructor() {
this.subs = []; //这个就是监听队列的数组
}
addSub(sub) {
this.subs.push(sub); //将要监听的属性放进去
}
notify() {
this.subs.forEach((sub) => sub.update()); //在属性的set方法里会触发notify方法,进行遍历这些被监听或变化了的属性,进行dom更新
}
}

3、 订阅者–分辨是主动触发还是自动触发
单线程,当调用 watch 的时候会触发调用 get,这时候先标记一个全局变量,然后在普通属性里会判断是否含有这个变量,有的话就是自动触发,没有的话就是手动调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export default class Watcher {
constructor(obj, key, cb) {
// 将 Dep.target 指向自己
// 然后触发属性的 getter 添加监听
// 最后将 Dep.target 置空
Dep.target = this;
this.cb = cb;
this.obj = obj;
this.key = key;
this.value = obj[key]; // 主要是这里 会触发getter 把this添加到dp中通过dp.addSub(Dep.target)
Dep.target = null;
}
update() {
// 获得新值
this.value = this.obj[this.key];
// 调用 update 方法更新 Dom
this.cb(this.value);
}
}

4、下边是一个简单的例子,但是不是通过上边这种观察者模式实现的,所以仅供参考

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
92
93
94
95
96
97
<body>
<input type="text" id="input" data-bind="value" />
<div id="output" data-bind="value"></div>
<button id="btn">update value</button>

<script>
var $ = document.querySelector.bind(document);
var $input = $('#input');
var $output = $('#output');

var data = {
value: 'default text',
};

// 根据data生成cache
var cacheArr = [];
var cacheData = function (data) {
// check cache arr
for (var i = 0; i < cacheArr.length; i++) {
if (cacheArr[i]._cacheSource === data) {
return cacheArr[i];
}
}

// create
var cache = {
_cacheSource: data,
};
for (var k in data) {
if (data.hasOwnProperty(k)) {
cache[k] = data[k];
}
}
cacheArr.push(cache);
return cache;
};

var updateView = function (cache, newValue) {
var nodes = cache._cachedNodes;
var VALUE_NODES = ['INPUT', 'TEXTAREA'];

nodes.forEach(function (node) {
if (VALUE_NODES.indexOf(node.tagName) !== -1) {
if (node.value !== newValue) {
node.value = newValue;
}
} else {
node.innerText = newValue;
}
});
};

var bind = function (node, data) {
var key = node.getAttribute('data-bind');
if (!key) return console.error('no data-bind key');

// 1.将数据都遍历绑定到cache上
var cache = cacheData(data);

// 2.检查是否缓存了_cachedNodes(DOM节点数据)
if (cache._cachedNodes) {
cache._cachedNodes.push(node);
} else {
// 3.未缓存的情况,将其缓存
cache._cachedNodes = [node];
// 4.并且给data数据增加了一个key,这个key就是data-bind的value 其实和data的属性重合了,data的属性也有value,所以当点击按钮才会更新
Object.defineProperty(data, key, {
enumerable: true,
set: function (newValue) {
cache[key] = newValue;
// update view
updateView(cache, newValue);
return cache[key];
},
get: function () {
return cache[key];
},
});
}

// init view
updateView(cache, cache[key]);
};
bind($input, data);
bind($output, data);

// event: view to data
$input.addEventListener('input', function () {
data[$input.getAttribute('data-bind')] = this.value;
});

// 手动改变量值
$('#btn').onclick = function () {
data.value = 'updated value ' + Date.now();
};
</script>
</body>

2.1.3 依赖收集来源

data
template 中 解析为 render 函数的时候 会触发 get
computed 中 执行一个会触发 get

2.1.4 异步更新

set 触发 update,推入到执行队列
通过 nextTick 来进行一次性执行
微任务队列清空后执行 UI Render 渲染界面
兼容性尝试
Promise → MutationObserver → setImmediate → setTimeout

优势:修改数据,一次性更新 DOM

2.1.5 宏任务 + 微任务

宏任务:
setTimeout、setInterval、setImmediate、requestAnimationFrame、I/O、UI 交互事件、MessageChannel
微任务:
process.nextTick、MutationObserver、Promise

2.1.6 缓存 keep-alive

keep-alive 可以理解为一个函数式组件(render 组件),每次都会执行
1、取第一个子组件
2、获取 name 和其他参数,以及 include,exclude、max
3、命中缓存直接从缓存中取,并重置到最后一位。
4、如果没有缓存,将其缓存,并判断 max,超过则删除第一个
5、不会执行 created 和 mounted
6、在渲染后即 mounted 后,判断是否已经执行完了,才会调用 activated 并递归其子组件都执行这个钩子
7、在 destroyed 钩子后执行和 deactivated 钩子函数并递归执行子组件的这个钩子

2.1.7 computed 缓存 原理

1、initComputed 创建 watchers 的观察者对象

1
var watchers = (vm._computedWatchers = Object.create(null));

2、将 computed 的 key 都创建 Watcher 实例,并赋值给 watchers。watchers[key] = new Watcher({getter: computed[key].get})

1
2
3
4
watchers = Object.assign(watchers, {
key1: new Watcher({ getter: computed[key1].get }),
key2: new Watcher({ getter: computed[key2].get }),
});

3、将 computed 的 key 都挂载到 data 上,并且修改 getter 为自定义的 getter

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
var data= {
key1:createComputedGetter(key1)
key2:createComputedGetter(key2)
}

defineComputed(vm, key);
function defineComputed(target, key){
// 修改getter
Object.defineProperty(target, key, createComputedGetter(key));
}
// 重定义的getter函数
function createComputedGetter(key) {
return function computedGetter() {
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) { // 为false走缓存
// true,懒执行
watcher.evaluate(); // 执行watcher方法后设置dirty为false
// set触发notify 后会再重置为true
}
if (Dep.target) {
watcher.depend();
}
return watcher.value; //返回观察者的value值
}
};
}

4、当修改了依赖的 data 属性时,执行 update,使 dirty = true

1
2
3
4
5
6
7
8
9
10
Watcher.prototype.update = function update() {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};

5、页面更新,读取到 data 上的 computed 值,触发 watcher.evaluate,返回修改后的值

1
2
3
4
5
6
7
function evaluate() {
if (this.dirty) {
this.value = this.get(); // get直接求值
this.dirty = false;
}
return this.value;
}

解析 Vue.js 中的 computed 工作原理-手写
Vue computed 源码解析
Vue computed 实现原理

2.1.8 Diff 原理

1、判断节点是否相同
2、如果老节点存在,新节点也存在
3、判断是否完全相同,是否是静态节点,文本节点,如果都不是则
4、while 循环中间靠拢比较

diff 算法:
还是和上面一样,依然先获取到最新的 listData
然后新的 data 进行_render 操作,得到新的 vnode
对比前后 vnode,也就是 patch 过程
对于同一层级的节点,会进行 updateChildren 操作(diff),进行最小的变动(while 循环)

不用 key 做索引的原因:
如果插入一个元素,造成 index 全部变化,就失去了虚拟 dom 的意义了

2.1.9 如何监听数组变化

利用 AOP 面向切面这种方式来重写了数组的 push 等方法,在调用 push 的时候执行了切面函数来触发视图更新
Vue2.0 响应式原理以及重写数组方法

3.DOM 更新效率

两种方式:
1、不做 diff 处理,当接受到更新指令时(setState)更新所有数据
React 中,setState()不关注数据是不是之前的,变了还是没变,只要传入了状态,就丢弃上一个状态。
也就是说,把原数据对象原封不动的再 setState()一遍,也必须向下检查整棵子树,才能得到数据没变,不需要更新视图的结论。
因为状态丢弃机制,不追踪数据变化细节,即便传入同一份数据,也没办法立即确定状态没变

解决:
React 的话,有简单的优化方案,可以跳过没变的数据检查:用不可变的数据结构,比如Immutable.js
用不可变的数据结构能够快速排除砍掉没变的,得到发生变化的部分,提升 diff 效率 2.通过 diff 处理,只更新变动部分的数据
不知道这次的状态更新对子树的影响。
但维护数据状态、追踪变化方案的明显优势就是知道每份数据所关联的视图,也就是说与指定数据相关的真实节点列表是已知

3.1 虚拟 DOM

脑图总结

深度剖析:如何实现一个 Virtual DOM 算法
构建一个使用 Virtual-DOM 的前端模版引擎

4.Vuex

vuex 中的 store 本质就是没有 template 的隐藏着的 vue 组件

Vuex 和单纯的全局对象有以下两点不同:
1、支持 mvvm
2、修改属性需要通过 commit

流程:
dispatch(action) → commit(mutations) → state

源码:

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
class Store {
constructor(options) {
/*-------------------------------state原理-------------------------------------------*/
//给每个组件的$store上挂一个state,让每个组件都可以用 this.$store.state
this.state = options.state;

//在state上面传入一个name:'Fan'打印一下
// console.log(this.state); //打印结果 {name: "Fan"}
/*------------------------------------------------------------------------------*/
}
}
//混入
Vue.mixin({
beforeCreate() {
//保证每一个组件都能得到仓库
//判断如果是main.js的话,就把$store挂到上面
if (this.$options && this.$options.store) {
this.$store = this.$options.store;
} else {
//如果不是根组件的话,也把$store挂到上面,因为是树状组件,所以用这种方式
this.$store = this.$parent && this.$parent.$store;

//在App.vue上的mounted({console.log(this.$store)})钩子中测试,可以得到store ---> Store {}
}
},
});

5.Redux

6.Fiber

解决两个问题:
a. 保证任务在浏览器空闲的时候执行;
b. 将任务进行碎片化;

1)、requestIdleCallback
requestIdleCallback(callback)是实验性 API,可以传入一个回调函数,回调函数能够收到一个 deadline 对象,通过该对象的 timeRemaining()方法可以获取到当前浏览器的空闲时间,如果有空闲时间,那么就执行一小段任务,如果时间不足了,则继续 requestIdleCallback,等到浏览器又有空闲时间的时候再接着执行。

2)、链表结构
目前的虚拟 DOM 是树结构,当任务被打断后,树结构无法恢复之前的任务继续执行,所以需要一种新的数据结构,即链表,链表可以包含多个指针,可以轻易找到下一个节点,继而恢复任务的执行。

Fiber 采用的链表中包含三个指针,parent 指向其父 Fiber 节点,child 指向其子 Fiber 节点,sibling 指向其兄弟 Fiber 节点。一个 Fiber 节点对应一个任务节点。

Fiber 架构的简单理解与实现

7.Nuxt

webpack:
入口:
server entry + client entry
打包后:
Server 部分:
Server Bundle + BundleRenderer
↓ Render

Html

↑ Hydrate
ClientBundle
以上为 Browser 部分

最佳实践:
lru 缓存
Redis

页面/接口/组件 三个维度

KeepAlive 开启避免资源浪费

lazy-hydration:只加载页面 交互后增加

docker
压测
优化、缓存
页面结构设计
容灾降级
性能监控
进程监控 monitor
错误监控 sentry
内网域名

参考文献

← Prev Next →