# 深入浅出 Vue.js

# Vue.js 简史
# 什么是 Vue.js
- The Progressive Framewor 渐进式框架
在我看来,渐进式的含义, 框架本身主张最少。
如果有一个现成的服务端应用,也就是非单页应用,可以将 Vue.js 作为该应用的一部分嵌入其中,带来更加丰富的交互体验。甚至可以单纯只使用它的视图层当作模版引擎来使用。Vue.js 本身是一个自底向上增量开发的设计。
# Vue.js 简史
- 2013/07/28 Evan You 为 Vue.js 的 repo 发起第一个 commit,此时该项目名叫做Element,后来更名为Seed.js
- 2013/12/07 发布版本 0.6.0,正式更名为 Vue.js,并且把默认指令前缀改为
v-,Vue.js 正式问世。 - 2014/02/01 Evan You 将 Vue.js0.8 发布在国外的 Hacker News 网站,这代表它首次公开发布。
- 2015/10/26 Vue.js 发布 1.0.0 版本,代号“新世纪福音战士(Evangelion)”。
"The fate of destruction is also the joy of rebirth." 毁灭的命运,也是重生的喜悦。
- 2016/10/01 Vue.js 发布 2.0.0 版本,代号“攻壳机动队(Ghost in the Shell)”
"Your effort to remain what you are is what limits you." 保持本色的努力,也在限制你的发展。
最早的 Vue.js 只做视图层,没有路由,没有状态管理,更没有官方的构建工具 vue-cli,只有一个库,放在网页里就可以直接用。
最核心的部分是视图层渲染,然后往外是组件机制,在这个基础上再加入路由机制, 状态管理...,最外层是构建工具。

所谓分层,就是说你既可以只使用最核心的视图层渲染功能来实现快速开发,也可以使用全家桶来开发大型应用。
Vue.js2.0 版本与 1.0 版本之间内部变化非常大,整个渲染层逻辑重写了,但是 API 层面的变化却很小。2.0 版本引入虚拟 DOM 的概念,其渲染过程更快了。但是值得注意的是,并不是引入虚拟 DOM 后渲染速度就一定会变快,准确的说,应该是 80%场景下变得更快了,剩余的 20%反而变慢了。
# 变化侦测
# Object 的变化侦测
Vue.js 最独特的特性之一是看起来并不显眼的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单、直接。不过理解其工作原理同样重要,这样你可以规避一些常见的问题。 ——官方文档
从状态生成 DOM,再输出到用户界面显示的一整套流程叫做渲染,应用在运行时会不断的进行重新渲染。而响应式系统赋予框架重新渲染的能力,其重要组成部分是变化侦测。变化侦测是响应式系统的核心,没有它,也就没有重新渲染。框架在运行时,视图也就无法随着状态的变化而变化。
变化侦测的作用其实就是侦测数据的变化,当数据变化时,会通知视图进行相应的更新。也就是我们常说的“数据驱动”。
# 2.1 什么是变化侦测
Object和Array的变化侦测是采用不同的处理方式。
通常,在运行时应用内部的状态会不断发生变化,此时需要不停的重新渲染。这时如何确定状态中发送了什么变化?
变化侦测就是来解决这个问题的,它分为两种类型:一种是 push,另一种是 pull。
Angular.js 和 React.js 中的变化侦测都属于 pull,当状态发生变化时,它并不知道具体哪个状态改变了,只知道某个状态有可能变了,然后会发送一个信号告诉框架,框架内部收到信号后,会进行一个相对暴力的 diff 处理。在 Angular.js 中这是脏检查的流程,在 React.js 中使用的虚拟 DOM。
而 Vue.js 的变化侦测属于 push,当状态变化时,Vue.js 立刻知道了,而且在一定程度上知道哪些状态有变化,因此可以做更细粒度的更新。
更新的粒度更细,每个状态所绑定的依赖也就越多,依赖追踪在内存上的开销也就会越大。
因此,Vue.js 从 2.0 开始引入虚拟 DOM,将粒度调整为中等,也就是一个状态绑定的依赖不再是具体的 DOM 节点,而是一个组件。
# 2.2 如何追踪变化
目前在 JS 中有两种方法可以侦测到对象的变化:
- 使用
Object.defineProperty - 使用
Proxy
由于兼容性问题,在 2.x 中还是使用的Object.defineProperty这个 API,下面是进行一个简单的封装函数
function defineReactive(data, key, val) {
Object.defineProperty(data, key, {
enumeratble: true,
configurable: true,
get: function() {
return val;
},
set: function(newVal) {
if (val === newVal) {
return;
}
val = newVal;
},
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 2.3 如何收集依赖
进行简单的封装,其实并没有实际作用,真正有用的是收集依赖。
WARNING
在 Vue.js 中,模版使用数据等同于组件使用数据,所以当数据发生变化时,会将通知发送到组件,然后组件内部在通过虚拟 DOM 重新渲染。
在getter中收集依赖,在setter中触发依赖。
# 2.4 依赖在哪里
创建一个Dep类,专门用做管理依赖。使用这个类,我们可以进行收集依赖,删除依赖或向依赖发送通知。
function remove(arr, item) {
if (arr.length) {
const index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1);
}
}
}
export default class Dep {
constructor() {
this.subs = [];
}
addSub() {
this.subs.push(sub);
}
removeSub(sub) {
remove(this.subs, sub);
}
depend() {
// 假设依赖是一个函数,并且挂在window上
if (window.target) {
this.addSub(window.target);
}
}
notify() {
const subs = this.subs.slice();
for (let i = 0, length = subs.length; i < 1; i++) {
subs[i].update();
}
}
}
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
之后再改造一下defineReactive:
function defineReactive(data, key, val) {
const dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
dep.deppend();
},
set: function(newVal) {
if (val === newVal) {
return;
}
val = newVal;
dep.notify();
},
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 2.5 依赖是谁
在之前的代码中,我们收集的依赖是window.target。
我们要通知用到此数据的地方,有可能会很多,而且类型还不一样,模版、computed、watch 等都有可能。这时需要抽象出一个能集中处理这些情况的类Watcher。然后,我们在收集依赖的阶段只收集这个封装好的实例进来,通知也只通知它一个,接着,它再负责通知到其它地方。
# 2.6 什么是 Wathcer
Watcher是一个中介的角色,数据发生变化时通知它,然后它再通知其它地方。
举个简单的用法例子:
vm.$watch("a.b.c", (newVal, oldVal) => {
// do sth...
});
2
3
此段代码表示当data.a.b.c的值发生变化时,触发二参回调。
实现这个功能,也就是将 Watcher 实例添加到data.a.b.c属性的Dep中,然后当该属性的值发生变化时,通知Watcher,接着Watcher再执行回调。
根据上面逻辑写出如下代码:
export default class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm;
// 执行this.getter(),就可以读取data.a.b.c的值
this.getter = parsePath(expOrFn);
this.cb = cb;
this.value = this.get();
}
get() {
window.target = this;
const value = this.getter.call(this.vm, this.vm);
window.target = undefined;
return value;
}
update() {
const oldValue = this.value;
// 赋值,手动触发一次
this.value = this.get();
this.cb.call(this.vm, this.value, oldValue);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21