# Vue 响应式
这里主要记录课程中 Vue 响应式和插件部分原理和简单实现。
# 响应式
我们在实际编程中可能对响应式( reactivity )存在一些误解和混淆。对于某些人来说,可能仅仅意味着 rx。实际上响应式编程就是类似流之类的东西,但在我们的上下文(即 Vue)中,这实际意味着当你改变状态时,状态是如何使得整个系统做出相应的变化以及更新的。
在这里,这意味着状态的改变是如何影响 DOM 的,以及 Vue 是如何跟踪这边变化的。首先让我们假设一个变量 a=3
。然后我们拥有另外一个变量它等于 10 倍的 a 。因此这就是一个需求,变量 b 应该一直等于 10 倍的 a 。你可以像以下代码一样实现它。
let a = 3;
let b = a * 10;
2
上面是典型的命令式编程,问题是当 a 发生变化时,b 并不会发生变化,因为这是过程式的,两个变量之间的关系并不同步。
console.log(b);// 30
a = 4;
console.log(b); // 30
b = a * 10;
console.log(b); // 40
2
3
4
5
所以我们如何使其保持他们之间同步变化,除了类似上面的每次都进行手动更新,实际上我们希望他们的关系是声明式的。类似一个 Excel 表中,我们可以使用函数来表明两个单元格的联系,单元格 B 永远是单元格 A 的十倍,但是在项目中我们应该怎么办呢?
如果我们有一个 onAChanged
函数,它接收一个内部函数,并且每当 a 变化的时候,函数都会执行一次,这样我们的问题就解决了。所以这个函数应该如何实现呢?
onAchanged(()=>{
b = a * 10;
})
2
3
但在深入研究这个问题之前,让我们先把这个问题转换成更接近web开发的东西。这里我们有一个 span cell,通过 DOM API 来描述其与 cell a 的关系。但是如果我们将其包裹在 onStateChanged
函数中,它们就变成声明式的了。
<span class="cell b1"></span>
document
.querySelector('.cell b1')
.textContent = state.a * 10;
onStateChanged(() => {
document
.querySelector('.cell b1')
.textContent = state.a * 10;
})
2
3
4
5
6
7
8
9
10
11
如果我们进一步抽象,把这个命令式的 DOM 抽象成模板语言,我们实际上创建了一个 Vue 库。
<span class="cell b1">
{{ state.a * 10 }}
</span>
onStateChanged(() => {
view = render(state);
})
2
3
4
5
6
7
这里的内部函数view = render(state)
实际上是所有 Vue 渲染系统如何工作的高度抽象。这里面包括了 DOM、DOM 实现、virtual DOM 实现的所有细节。
这里我们主要关注外层函数 onStateChanged
具体是怎么实现的,下面的代码可能展示其大概是如何实现的。
let update;
const onStateChanged = _update => {
update = _update;
};
const setState = newState => {
state = newState;
update();
};
2
3
4
5
6
7
8
它只是将更新函数保存在某个地方,我们不允许用户任意操纵状态,而是要求他们总是通过调用函数 setState
来操纵状态。setState
只是简单的接收 newState 并将旧的 state 进行替换,然后调用 update
函数。如果你使用 React,你会发现两者非常相似。因为 React 迫使你触发 setState 的状态进行更改:
onStateChanged(() => {
view = render(state);
})
setState({ a: 5 });
2
3
4
这就是 React 内部工作机制的简单概括,但是在 Angular 和 Vue 中我们不通过调用 setState 函数来操作 state。
所以它是如何实现的呢?Angular 和 Vue 的实现方法有所不同,因为 angular 使用的是 dirty checking
(脏数据检查机制),它在某种程度上会拦截你的事件,例如点击执行摘要循环。 然后它会检查所有事物,是否已更改。而 vue 则是做的更加细致,它将对象的状态变成响应式的。
vue 通过 ES5 的Object.defineProperty
API,将所有的属性变成了 getter
和 setter
。因此在上面的 state.a ,我们实际上将 a 转换成了 getter 和 setter。在其他情景中,我们将会做一些有趣的事情,例如我们将 onStateChanged
函数重名为 autorun
函数,这本上是一种基本的依赖追踪形式。
我们会实现一个小型数据观察者,它类似于在 vue 中的工作方式。
# Getters and Setters
实现一个 convert
函数:
- 接收一个对象作为参数
- 使用 Object.defineProperty 函数将对象的属性转换为 getter 和 setter
- 转换后的度系i昂需要保持初始行为,同时输出所有 get/set 操作
测试案例:
const obj = { foo: 123 }
convert(obj)
obj.foo // should log: 'getting key "foo": 123'
obj.foo = 234 // should log: 'setting key "foo" to: 234'
obj.foo // should log: 'getting key "foo": 234'
2
3
4
5
6
7
代码实现:
function convert(obj) {
Object.keys(obj).forEach(function (val) {
let initialValue = obj[val];
Object.defineProperty(obj, val, {
get() {
console.log(`getting key "${val}": ${initialValue}`);
return initialValue;
},
set(newVal) {
initialValue = newVal;
console.log(`setting key "${val}" to: ${newVal}`);
}
})
});
return obj;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Dependency Tracking
目标:
创建一个
Dep
类,包含depend
和notify
函数创建一个
autorun
函数,它接收一个 updater 函数在 updater 函数内部,可以通过调用
dep.depend()
函数来显式的依赖于Dep
的实例最后,你可以调用
dep.notify()
函数来触发 updater 函数执行// 测试案例如下 const dep = new Dep() autorun(() => { dep.depend() console.log('updated') }) // should log: "updated" dep.notify() // should log: "updated"
1
2
3
4
5
6
7
8
9
10实现代码:
window.Dep = class Dep { constructor() { this.subscirber = new Set(); } depend() { if (activeUpdate) { this.subscirber.add(activeUpdate); } } notify() { this.subscirber.forEach(sub => sub()); } } let activeUpdate = null; function autorun(update) { function wrappedFunction() { activeUpdate = wrappedFunction; update(); activeUpdate = null; } wrappedFunction(); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Mini Observer
结合前面的两个函数,将 convert()
重命名为 observe()
,并保留 autorun()
。
observe()
函数接收一个对象,并将对象的属性变成响应式的。对于每一个转换的属性,它被分配一个Dep
实例,该实例跟踪订阅更新函数的列表,并在调用其setter
时触发它们重新运行。autorun()
函数接受一个 update 函数,并在 update 函数订阅的属性发生变化时重新运行它。如果 update 函数在计算期间依赖于某个属性,则 update 函数被该属性 subscribing。
// 测试案例
const state = {
count: 0
}
observe(state)
autorun(() => {
console.log(state.count)
})
// should immediately log "count is: 0"
state.count++
// should log "count is: 1"
2
3
4
5
6
7
8
9
10
11
12
13
14
代码实现如下:
//这是依赖性跟踪的一个非常简化的版本
// system used in Vue, Knockout, MobX and Meteor Tracker (each with different
// implementation details, of course).
// 它不能覆盖所有可能的边界情况,也不处理数组;
class Dep {
constructor() {
this.subscribers = new Set();
}
depend() {
if (activeUpdate) {
this.subscribers.add(activeUpdate);
}
}
notify() {
this.subscribers.forEach(sub => sub());
}
}
function observe(obj) {
//遍历对象上的所有属性将它们转换成getter/setter
// Object.defineProperty ()
Object.keys(obj).forEach(function (val) {
let initialValue = obj[val];
//每个属性都有一个依赖实例
const dep = new Dep();
Object.defineProperty(obj, val, {
// getter负责注册订阅者
get() {
dep.depend();
return initialValue;
},
// set 负责通知更改
set(newVal) {
const changed = newVal !== initialValue;
initialValue = newVal;
// 触发re-computation
if (changed) {
dep.notify();
}
}
})
});
return obj;
}
let activeUpdate = null;
function autorun(update) {
//将原始更新函数包装成一个“job”函数,该函数注册并
//在调用时将自身注销为当前活动job
const wrappedUpdate = () => {
activeUpdate = wrappedUpdate;
update();
activeUpdate = null;
};
wrappedUpdate();
}
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
# 插件
# Vue.use( plugin ) (opens new window)
plugin 其实就是一个函数,它接收两个参数,第一个参数是 Vue,第二个参数是可选参数,如下所示:
function (Vue, options) {
// ... plugin code
}
2
3
然后你在该函数内部编写一些 Vue 代码。当我们编写插件时,通常会用到 Vue.mixin API 。mixin 本质上是部分可重用代码片段或者部分 Vue 组件选项,它们可以被混合到现有组件中。因此我们可以在每个组件的基础上使用它们,但是 Vue.mixin 是一个全局 API,它将影响每一个之后创建的 Vue 实例。所以使用时需要格外的小心!
# Vue.mixin( options ) (opens new window)
如果想在 Vue APP 中使用一些全局功能,那么我们将会使用全局混入。推荐将其作为 plugin 的一个接口来使用,因为 Vue.use
会自动阻止多次注册相同插件,届时即使多次调用也只会注册一次该插件。
vue.mixin
不会自动删除,从而通过插件将其发布,可以避免应用重复混入。另一个比较常用的是实例属性 $options
。
$options
存在于每个组件中,它包含了当前 Vue 实例的初始化选项,因为我们知道 Vue 组件可以来自各个地方,可能来自全局的 mixins ,可能来自组件自己的定义,肯呢个来自创建实例时你所传递的 options ,同时也有可能来自通过 vue.extend
创建的关系链中的组件。而这些最终混合进了当前实例的 $options
中。同时它还包含了我们给组件添加的自定义选项。
# Simple Plugin
创建一个插件,使 Vue 组件可以处理自定义 "rules" 选项。“rules” 选项接收一个对象,可以完成对组件中的数据进行自定义规则验证。
- plugin 应该包含一个全局混入
- 全局混入包含
created
钩子 - 在钩子函数中,检查
this.$options.rules
测试案例:
const vm = new Vue({
data: { foo: 10 },
rules: {
foo: {
validate: value => value > 1,
message: 'foo must be greater than one'
}
}
})
vm.foo = 0 // should log: "foo must be greater than one"
2
3
4
5
6
7
8
9
10
11
代码实现:
const RulesPlugin = {
// Implement this!
install(Vue) {
Vue.mixin({
created() {
let rules = this.$options.rules
if (rules) {
Object.keys(rules).forEach(key => {
const {
validate,
message
} = rules[key];
this.$watch(key, newVal => {
const valid = validate(newVal);
if (!valid) {
console.log(message);
}
})
})
}
}
})
}
}
Vue.use(RulesPlugin)
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
← Introduction 渲染函数 →