-
-
Notifications
You must be signed in to change notification settings - Fork 62
/
Copy pathgetting-started.html
executable file
·389 lines (368 loc) · 27.3 KB
/
getting-started.html
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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<meta name="description" content="十分钟入门 MobX & React">
<link rel="stylesheet" href="assets/getting-started-assets/style.css" />
<link href='https://fonts.googleapis.com/css?family=PT+Serif' rel='stylesheet' type='text/css'>
<link rel="shortcut icon" type="image/png" href="assets/getting-started-assets/images/favicon.png" />
<title>十分钟入门 MobX & React</title>
</head>
<body>
<div class="github-fork-ribbon-wrapper right-bottom fixed">
<div class="github-fork-ribbon">
<a href="https://github.com/mobxjs/mobx">Fork me on GitHub</a>
</div>
</div>
<table class="root">
<tr>
<td class="left">
<div class="left-content-wrapper">
<div class="left-content">
<header>
<a href="index.html" style="float:left">
<img style="width: 120px; padding-right: 20px;" src="assets/getting-started-assets/images/mobservable.png" id="logo" /></a>
<h1 id="project_title">MobX</h1>
<h2 id="project_tagline" style="font-size: 18pt">十分钟入门 MobX & React</h2>
<hr />
</header>
<section id="main_content">
<p>
<a href="https://github.com/mobxjs/mobx"><code>MobX</code></a> 是一种简单、可扩展并且身经百战的状态管理解决方案。本教程会在十分钟之内教会你 MobX 中所有重要的概念。MobX 是一个独立的库,但大多数人都把它和 React 放在一起使用,本教程就着重介绍这个组合。
</p>
<h3>核心理念</h3>
<p>
状态是每个应用的核心。如果你想打造出漏洞百出又难以管理的应用程序,那么搞出不一致的状态或与徘徊着的局部变量不同步的状态就是最快的方法。因此,许多状态管理解决方案会——比如通过使状态不可变——试图限制你修改状态的方式。但这样就会引出新的问题:数据需要被归一化、引用的完整性无法得到保证,而且万一你喜欢像类这样功能强大的概念,那你几乎没办法再用到它们。
</p>
<p>
MobX 通过解决根本问题让状态管理又一次变得简单起来:它让不一致的状态不再可能出现。要做到这一点,策略很简单: <em>确保从应用状态中派生出的一切都将被自动派生出来</em>。
</p>
<p>
从概念上讲, MobX 会把你的应用程序视为一份电子表格。
<p>
<img src="assets/getting-started-assets/overview.png" width="100%" />
<ol>
<li>
首先是 <em>应用状态</em>。就是那些呈图状结构分布、组成你应用状态模型的对象、数组、原始值和引用。这些值是你应用中的“单元格”。
</li>
<li>
其次是 <em>derivations</em>。也就是任何可以从你的应用状态中被自动计算出来的值。这些 derivations ——或者计算值——可以是简单的值,比如未完成待办的个数;也可以是复杂的值,比如你待办清单的 HTML 视觉表示。
</li>
<li>
<em>Reactions</em> 与 derivations 非常相似。但主要的区别在于这些函数不会返回值,而会自动执行一些任务。通常这些任务与 I/O 有关。它们会确保 DOM 的更新或者在对的时间自动发出网络请求。
</li>
<li>
最后是 <em>actions</em>。actions 就是所有会修改状态的代码。MobX 会让所有由于你的 actions 引发的应用状态的改变都被全部 derivations 和 reactions 同步而顺畅地自动处理掉。
</li>
</ol>
<h3>一个简单的待办 store……</h3>
<p>
理论已经够多了,与其仔细阅读上面的那些字,看看实际的操作可能会带来更好的理解。为了原创性,我们就从一个简单的待办 store 开始。请注意,以下所有代码块都是可以编辑的,也可以用 <em>运行代码</em> 按钮运行。下面是一个非常简单直白的待办 store <code>TodoStore</code>,里面装着一个待办集合。暂时还没有 MobX 什么事儿。
</p>
<textarea spellcheck="false" class="prettyprint" id="code1" rows="25">
class TodoStore {
todos = [];
get completedTodosCount() {
return this.todos.filter(
todo => todo.completed === true
).length;
}
report() {
if (this.todos.length === 0)
return "<无>";
const nextTodo = this.todos.find(todo => todo.completed === false);
return `下一个待办:"${nextTodo ? nextTodo.task : "<无>"}"。 ` +
`进度:${this.completedTodosCount}/${this.todos.length}`;
}
addTodo(task) {
this.todos.push({
task: task,
completed: false,
assignee: null
});
}
}
const todoStore = new TodoStore();
</textarea>
<p>
我们刚刚创建了一个带有待办 <code>todos</code> 集合的 <code>todoStore</code> 实例。是时候往 <code>TodoStore</code> 里装些对象了。为了看到更改的效果,我们在每次更改后都调用 <code>todoStore.report</code> 把它打印出来。请注意,这个 report 会故意总是只打印第一个任务。这会让这个例子看起来有点不自然,但我们稍后会看到这样可以很好地展示出 MobX 依赖追踪的动态性。
</p>
<textarea spellcheck="false" class="prettyprint" id="code2" rows="15">
todoStore.addTodo("阅读 MobX 教程");
console.log(todoStore.report());
todoStore.addTodo("试用 MobX");
console.log(todoStore.report());
todoStore.todos[0].completed = true;
console.log(todoStore.report());
todoStore.todos[1].task = "在自己的项目中试用 MobX";
console.log(todoStore.report());
todoStore.todos[0].task = "理解 MobX 教程";
console.log(todoStore.report());
</textarea>
<button onClick="runCode(['#code1', '#code2'])" class="btn-run">运行代码</button>
<h3>变成响应式</h3>
<p>
到目前为止,这段代码并没有什么特别之处。但如果我们不是非得刻意调用 <code>report</code>,而是可以规定它必须在每次 <em>相关</em> 状态改变时都被调用呢?那样我们就不需要从代码中所有 <em>有可能</em> 对 report 产生影响的地方手动调用 <code>report</code> 了。我们确实想让最新的 report 被打印出来,但并不想因为要手动去组织它而费功夫。
</p>
<p>
幸运的是,这正是 MobX 能为我们做到的事情——自动执行只依赖于状态的代码。这样我们的 <code>report</code> 函数就可以自动更新,就像电子表格里的一个图表一样。为了做到这一点,<code>TodoStore</code> 就必须变成 observable,这样 MobX 就可以对所有正在进行的更改进行追踪。为了实现这一点,我们来改一下这个类。
</p>
<p>
还有,<code>completedTodosCount</code> 属性可以从待办清单中被自动派生出来。通过使用 <code>observable</code> 和 <code>computed</code> 注解,我们可以为一个对象引入 observable 属性。在下面的例子中,我们为了刻意地展示出注解而使用了 <code>makeObservable</code>,但我们也可以改用 <code>makeAutoObservable(this)</code> 来简化这个过程。
</p>
<textarea spellcheck="false" class="prettyprint" id="code3" rows="8">
class ObservableTodoStore {
todos = [];
pendingRequests = 0;
constructor() {
makeObservable(this, {
todos: observable,
pendingRequests: observable,
completedTodosCount: computed,
report: computed,
addTodo: action,
});
autorun(() => console.log(this.report));
}
get completedTodosCount() {
return this.todos.filter(
todo => todo.completed === true
).length;
}
get report() {
if (this.todos.length === 0)
return "<无>";
const nextTodo = this.todos.find(todo => todo.completed === false);
return `下一个待办:"${nextTodo ? nextTodo.task : "<无>"}"。 ` +
`进度:${this.completedTodosCount}/${this.todos.length}`;
}
addTodo(task) {
this.todos.push({
task: task,
completed: false,
assignee: null
});
}
}
const observableTodoStore = new ObservableTodoStore();
</textarea>
<p>
就是这样!我们把有些属性标记为 <code>observable</code>,以便告诉 Mobx 这些值会随时间的推移而改变。有些值中的 <code>computed</code> 用来标明这些值能够从状态中被派生出来,而且只要底层状态没有发生改变,那么它们还可以从缓存中被派生出来。
</p>
<p>
我们目前还没有用到 <code>pendingRequests</code> 和 <code>assignee</code> 属性,但是会在这个教程后面的部分用到的。
</p>
<p>
在构造函数中,我们创建了一个打印 <code>report</code> 的小函数并把它用 <code>autorun</code> 包装了起来。而 autorun 会创建一个 <em>action</em>,这个 action 会先自动运行一次,之后每当小函数用到的任意一个 observable 数据发生了改变,它也会自动重新运行。<code>report</code> 会因为使用了 observable 属性 <code>todos</code> 而适时打印出 report。这一点会在下列代码中展示出来。只需要你按下 <em>运行代码</em> 按钮:
</p>
<textarea spellcheck="false" class="prettyprint" id="code4" rows="6">
observableTodoStore.addTodo("阅读 MobX 教程");
observableTodoStore.addTodo("试用 MobX");
observableTodoStore.todos[0].completed = true;
observableTodoStore.todos[1].task = "在自己的项目中试用 MobX";
observableTodoStore.todos[0].task = "理解 MobX 教程";
</textarea>
<button onClick="runCode(['#code1', '#code3', '#code4'])" class="btn-run">运行代码</button>
<p>
很有意思,对吧?<code>report</code> 的确自动同步而顺畅地打印出来了。如果你仔细查看打印出来的日志,你会看到第五行代码最后并没有再触发一行日志。这是因为 report <em>实际上</em> 并没有因为第五行代码的重命名而发生改变——尽管它背后的数据变了。另一方面,更改第一个待办的名称确实更新了 report,因为 report 正使用着那个新名字。这很好地证明了 <code>autorun</code> 不仅监视观察着 <code>todos</code> 数组,还监视着待办条目中的各个属性。
</p>
<h3 id="reactive-reactjs-components">把 React 变成响应式</h3>
<p>
好的,目前为止我们把一个傻傻的 report 变成了响应式。是时候围绕着这同一个 store 构建一个响应式的用户界面了。React 组件本身并不是响应式的(他们的名字除外)。<code>mobx-react-lite</code> 包中的 <code>observer</code> 高阶组件包装器通过(简而言之)把 React 组件用 <code>autorun</code> 包装起来解决了这个问题。这样可以自动让组件和状态相同步。这跟我们刚才对于 <code>report</code> 的做法在概念上并没有什么不同。
</p>
<p>
接下来这段代码定义了几个 React 组件。唯一 MobX 专用的代码是 <code>observer</code> 包装器。这就足够让每个组件在相关数据发生改变时单独重新渲染了。我们不用再调用状态的 <code>useState</code> setter 方法了,也不用去弄清楚怎样通过使用选择器或者需要手动配置的高阶组件来追踪应用状态中正确的那一部分。简而言之,所有组件都变得智能了。但他们是以一种傻瓜式、声明式的方式被定义出来的。
</p>
<p>
按下 <em>运行代码</em> 按钮,查看以下代码的实际运行状况。代码是可编辑的所以尽管动手玩起来。比如说,试试移除所有 <code>observer</code> 调用,或者只移除那个装饰在 <code>TodoView</code> 上的。右边预览中的数字突出显示了每个组件的渲染频率。
</p>
<textarea spellcheck="false" class="" id="react1" rows="44">
const TodoList = observer(({store}) => {
const onNewTodo = () => {
store.addTodo(prompt('输入新的待办:','请来杯咖啡'));
}
return (
<div>
{ store.report }
<ul>
{ store.todos.map(
(todo, idx) => <TodoView todo={ todo } key={ idx } />
) }
</ul>
{ store.pendingRequests > 0 ? <marquee>正在加载……</marquee> : null }
<button onClick={ onNewTodo }>新待办</button>
<small>(双击待办进行编辑)</small>
<RenderCounter />
</div>
);
})
const TodoView = observer(({todo}) => {
const onToggleCompleted = () => {
todo.completed = !todo.completed;
}
const onRename = () => {
todo.task = prompt('任务名称', todo.task) || todo.task;
}
return (
<li onDoubleClick={ onRename }>
<input
type='checkbox'
checked={ todo.completed }
onChange={ onToggleCompleted }
/>
{ todo.task }
{ todo.assignee
? <small>{ todo.assignee.name }</small>
: null
}
<RenderCounter />
</li>
);
})
ReactDOM.render(
<TodoList store={ observableTodoStore } />,
document.getElementById('reactjs-app')
);
</textarea>
<button onClick="runCode(['#code1', '#code3', '#code4', '#react1'])" class="btn-run">运行代码</button>
<p>
接下来的代码很好地证明了我们只需要更改数据,而不需要做更多的数据记录。MobX 会重新从 store 里的状态中自动派生并更新用户界面中相关的部分。
</p>
<textarea spellcheck="false" class="" id="play1" rows="8">
const store = observableTodoStore;
store.todos[0].completed = !store.todos[0].completed;
store.todos[1].task = "随机待办 " + Math.random();
store.todos.push({ task: "找到一块好奶酪", completed: true });
// 诸如此类……在这里添加你自己的语句
</textarea>
<button onClick="if (typeof observableTodoStore === 'undefined') { runCode(['#code1', '#code3', '#code4', '#react1']) } runCode(['#play1'])" class="btn-run">运行代码</button>
<button id="runline-btn" onClick="runCodePerLine()" class="btn-run">逐行运行</button>
<p> </p>
<h3>引用的使用</h3>
<p>
到目前为止我们创建了 observable 对象(原型对象和普通对象)、数组和原始值。你可能会好奇,MobX 是怎么处理引用的呢?我的状态可以组成图结构吗?在之前的代码中你可能已经注意到了每个待办对象中都有一个 <code>assignee</code> 属性。我们通过引入另一个装着若干人员的 “store” (好吧,只是一个被授予了非凡荣耀的数组)来给它们一些值,并把任务分配给它们。
</p>
<textarea spellcheck="false" class="" id="store2" rows="8">
const peopleStore = observable([
{ name: "Michel" },
{ name: "我" }
]);
observableTodoStore.todos[0].assignee = peopleStore[0];
observableTodoStore.todos[1].assignee = peopleStore[1];
peopleStore[0].name = "Michel Weststrate";
</textarea>
<button onClick="runCode(['#code1', '#code3', '#code4', '#react1', '#store2'])" class="btn-run">运行代码</button>
<p>
我们现在有了两个独立的 store。一个装着人员,一个装着待办。刚才,为了把人员 store 中的人赋给 <code>assignee</code>,我们使用了一个引用。这些改变会被 <code>TodoView</code> 自动识别出来。有了 MobX,你就不需要先对数据进行归一化处理和手写选择器来保证组件的更新。其实,数据存放在哪儿并不重要。只要对象被转化成了 <em>observable</em>,MobX 就能对它们进行追踪。用真正的 JavaScript 引用就行。只要它们跟一个 derivation 有关,MobX 就会自动对它们进行追踪。如果想进行测试,就试着在下面的输入框内更改你的名字(要事先确定你之前已经按过了上面的 <em>运行代码</em> 按钮!)
</p>
<hr />
<p style="text-align:center">你的名字:
<input onkeyup="peopleStore[1].name = event.target.value" />
</p>
<hr />
<p>
顺便说一句,上面输入框的 HTML 也就是
<pre><input onkeyup="peopleStore[1].name = event.target.value" /></pre> 这么简单而已。
</p>
<h3>异步 actions</h3>
<p>既然我们小小待办应用中的一切都是从状态中派生出来的,那么状态在 <em>什么时候</em> 发生改变其实并不重要。这一点会让创建异步 actions 变得很简单。想模拟异步加载新待办的过程就按下下面的按钮吧(多按几次)。
</p>
<hr />
<p style="text-align:center">
<button onclick="observableTodoStore.pendingRequests++; setTimeout(function() { observableTodoStore.addTodo('随机待办 ' + Math.random()); observableTodoStore.pendingRequests--; }, 2000);">加载待办</button>
</p>
<hr />
<p>
这背后的代码真的很简单。我们首先更新 store 中的 <code>pendingRequests</code> 属性好让 UI 反映出当前正在加载的状态。一旦加载完成,我们就更新 store 中的待办事项并减小 <code>pendingRequests</code> 计数器的数值。请比较一下这段代码和之前 <code>TodoList</code> 的定义,看看 <code>pendingRequests</code> 属性是如何被使用的。
</p>
<p>
请注意,延时函数被包装在了 <code>action</code> 里。我们并不是非得这样做,但这样可以用一个 transaction 把两个变动都处理掉,让所有 observer 都只有在两个更新都完成后才收到通知。
<pre>observableTodoStore.pendingRequests++;
setTimeout(action(() => {
observableTodoStore.addTodo('随机待办 ' + Math.random());
observableTodoStore.pendingRequests--;
}), 2000);</pre>
</p>
<h3>总结</h3>
<p>
就这些了! 没有样板代码。只是用了一些组成我们完整的 UI 的简单声明式组件。而且它们全部都是从我们的状态中被响应式地派生出来的。现在你已经准备好在你自己的应用程序中使用 <code>mobx</code> 和 <code>mobx-react-lite</code> 包了。对你目前所学到的东西做一个小结:
</p>
<ol>
<li>
使用 <code>observable</code> 装饰器或 <code>observable(对象或数组)</code> 函数让对象能够被 MobX 追踪到。
</li>
<li>
<code>computed</code>装饰器可以用来创建能够从状态中自动派生值并缓存它们的函数。
</li>
<li>
使用 <code>autorun</code> 来自动运行依赖于某些 observable 状态的函数。这对于记录日志、进行网络请求等都很有用。
</li>
<li>
使用 <code>mobx-react-lite</code> 包中的 <code>observer</code> 包装器让你的 React 组件做到真正的响应式。它们将会自动而高效地进行更新——即使是在数据量大的大型复杂应用中使用。
</li>
</ol>
<p>
你可以随意用上面的可编辑代码块多玩一会儿,以便对于 MobX 对你所有更改的反应方式有个大致的感觉。比如,你可以在 <code>report</code> 函数中添加一个日志语句,看看它会在什么时候被调用。或者直接不展示 <code>report</code>,看看那样会对 <code>TodoList</code> 渲染造成怎样的影响。又或者只在某些特定情况下才把它展示出来……
</p>
<h3>MobX 并不支配架构</h3>
<p>
请注意,以上的例子都是刻意为之;推荐还是采用正确的工程实践,比如将逻辑封装到方法里、在 stores 里对它们进行组织,或采用 MVC(模型-视图-控制器)架构等等。你可以采用很多不同的架构模式,对于其中的某些模式,官方文档中有更详细的讨论。以上以及官方文档中的例子展示的都是我们 <em>可以</em> 如何使用 MobX,而不是 <em>必须</em> 如何使用。或者,如同 HackerNews 上的某个人所说的:
<blockquote><em>
“MobX,我在别的地方已经提过了,但我还是忍不住要为它唱赞歌。用 MobX 写代码意味着使用 controllers、dispatchers、actions、supervisors 或另一种管理数据流的形式回归成了一种架构上的考虑——你可以根据自己应用的需要来采用架构模式——而不是编写待办应用之外所有的东西都默认必须要做的事情。”
</em></blockquote>
</p>
<div style="text-align:center;">
<a class="github-button" href="https://github.com/mobxjs/mobx" data-icon="octicon-star" data-style="small" data-count-href="/mobxjs/mobx/stargazers" data-count-api="/repos/mobxjs/mobx#stargazers_count" data-count-aria-label="# stargazers on GitHub" aria-label="Star mobxjs/mobx on GitHub">Star</a>
<a href="https://twitter.com/share" class="twitter-share-button" data-via="mweststrate" data-hashtags="mobx">发推</a>
</div>
</section>
<footer>
<p class="copyright">MobX 由 <a href="https://twitter.com/mweststrate">mweststrate</a> 维护</p>
</footer>
</div>
</div>
</td>
<td class="right">
<div class="right-content">
<h3>React 预览</h3>
<div id="reactjs-app">
<p style="text-align: center">读下去并按下你遇到的 <em>运行</em> 按钮!</p>
</div>
<hr />
<h3>控制台打印
<button onclick="clearConsole();" id="clear-btn">清除</button>
</h3>
<div id="consoleout"></div>
</div>
</td>
</tr>
</table>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-fork-ribbon-css/0.1.1/gh-fork-ribbon.min.css" />
<script src="assets/getting-started-assets/javascripts/jquery-2.1.4.min.js"></script>
<script src="assets/getting-started-assets/javascripts/codemirror/lib/codemirror.js"></script>
<link rel="stylesheet" href="assets/getting-started-assets/javascripts/codemirror/lib/codemirror.css">
<link rel="stylesheet" href="assets/getting-started-assets/javascripts/codemirror/theme/xq-light.css">
<script src="assets/getting-started-assets/javascripts/codemirror/javascript/javascript.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<script src="assets/getting-started-assets/babel.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/mobx.umd.development.js"></script>
<script src="https://unpkg.com/[email protected]/dist/mobxreactlite.umd.development.js"></script>
<script src="assets/getting-started-assets/script.js"></script>
<script type="text/javascript">
var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
</script>
<script type="text/javascript">
try {
var pageTracker = _gat._getTracker("UA-197655897-1");
pageTracker._trackPageview();
} catch (err) { }
</script>
<script async defer id="github-bjs" src="https://buttons.github.io/buttons.js"></script>
<script>
!function (d, s, id) { var js, fjs = d.getElementsByTagName(s)[0], p = /^http:/.test(d.location) ? 'http' : 'https'; if (!d.getElementById(id)) { js = d.createElement(s); js.id = id; js.src = p + '://platform.twitter.com/widgets.js'; fjs.parentNode.insertBefore(js, fjs); } }(document, 'script', 'twitter-wjs');
</script>
</body>
</html>