初探React,将我们的View标签化

前言

我之前喜欢玩一款游戏:全民飞机大战,而且有点痴迷其中,如果你想站在游戏的第一阶梯,便需要不断的练技术练装备,但是腾讯的游戏一般而言是有点恶心的,他会不断的出新飞机、新装备、新宠物,所以,很多时候你一个飞机以及装备还没满级,新的装备就又出来了,并且一定是更强!

于是很多人便直接抛弃当前的飞机与装备,追求更好的,这个时候如果是人民币玩家或者骨灰级大神玩家的话,基本可以很快站在世界的顶端,一者是装备好,一者是技术好,但是我不愿意投入太多钱,也不愿意投入过多精力,于是在一套极品装备满级后会积累资源,因为一代之间变化不会太大,到第二代甚至第三代才开始换飞机换装备,也基本处于了第一阶梯,一直到一次游戏大更新,直接导致我当前的飞机与装备完全二逼了,我当时一时脑热投入了所有资源去刷新的极品装备,最后闹的血本无归,于是便删除了该游戏,一年时间付诸东流!!!

再回过头来看最近两年前端的变化,单是一个前端工程化工具就变了几次,而且新出来的总是叫嚷着要替换之前的,grunt->gulp->webpack->es6

再看前端框架的一些产量:backbone、angularJS、react、canJS、vueJS......

真有点乱花渐欲迷人眼的意思,似乎前端技术也开始想要坑前端玩家,因为人家会了新技能,你就落后了,于是很多技术沉淀已经足够的大神便直接在团队使用某一技术,带领团队组员深入了解了该技术的好,并大势宣传新技术。

很多人在这种情况下就中招了!他们可能会抛弃现有技术栈,直接跟风新的技术,在现有装备都没满级的情况下又去刷新装备,如果哪天一次游戏玩法大更新,大神依旧一套极品装备在那风骚,而炮灰仓库中积累着一箩筐低等级的极品装备,却没有可用的,不可谓不悲哀!

一门技术从入门到精通,是需要时间的,在有限的时间中要学习那么多的新技术,还得落地到实际工作中,而每一次新技术的落地都是对曾经架构的否定与推翻,这个成本不可谓不高,对一些创业团队甚至是致命的。工作中也没那么多时间让你折腾新东西,所以一定是了解清楚了一门再去学习其它的,不要半途而废也不要盲目选型。

我最近回顾了这几年所学,可以说技术栈没有怎么更新,但是我对我所习的每一个技术基本上进入了深入的了解:

① 在MVVM还不太火的时候使用了MVC框架一直到最近,对为什么要使用这种模式,这种模式的好处有了比较深入的了解,并且已经碰到了更复杂的业务逻辑

② 当一个页面过于复杂时(比如1000多行代码的订单填写页),我能通过几年沉淀,将之拆分为多个业务组件模块,保持主控制器的业务清晰,代码量维护在500行之内,并且各子模块业务也清晰,根据model进行通信

③ 使用Grunt完成前端工程化,从构建项目,到打包压缩项目,到优化项目,总的来说无往不利

④ ......

就编程方法,思维习惯,解决问题的方法来说,与两年前有了很大的变化,而且感觉很难提高了。于是我认识到,就现有的装备下,可能已经玩到极限了,可能到了跟风的时候了,而时下热门的ReactJS似乎是一个很好的切入点,React一端代码多端运行的噱头也足够。

初识ReactJS

我最初接触ReactJS的时候,最火的好像是angular,React Native也没有出现,看了他的demo,对其局部刷新的实现很感兴趣。结果,翻看源码一看洋洋洒洒一万多行代码,于是马上便退却了。却不想现在火成了这般模样,身边学习的人多,用于生产的少,我想幕后必然有黑手在推动!也可以预测的是,1,2年后会有更好的框架会取代他,可能是原团队的自我推翻,也有可能是Google公司又新出了什么框架,毕竟前端最近几年才开始真正富客户端,还有很长的路要走。当然,这不是我们关心的重点,我们这里的重点是Hello world。

ReactJS的Hello World是这样写的:

 1 <!DOCTYPE html>
2 <html>
3 <head>
4 <script src="build/react.js" type="text/javascript"></script>
5 <script src="build/JSXTransformer.js" type="text/javascript"></script>
6 </head>
7 <body>
8 <div id="example">
9 </div>
10 <script type="text/jsx">
11 React.render(
12 <h1>Hello, world!</h1>,
13 document.getElementById('example')
14 );
15 </script>
16 </body>
17 </html>
<div id="example"><h1 data-reactid=".0">Hello, world!</h1></div>

React一来就搞了一个标新立异的地方:jsx(js扩展),说实话,这种做法真的有点大手笔,最初的这种声明式标签写法,在我脑中基本可以追溯到5年前的.net控件了,比如gridview与datalist组件。

在text/jsx中的代码最初不会被浏览器理会,他会被react的JSXTransformer编译为常规的JS,然后浏览器才能解析。这里与html模板会转换为js函数是一个道理,我们有一种优化方案是模板预编译,即:

在打包时候便将模板转换为js函数,免去在线解析的过程,react当然也可以这样做,这里如果要解析的话,会是这个样子:

1 React.render(
2 React.createElement("h1", null, "Hello, world!"),
3 document.getElementById('example')
4 );

因为render中的代码可以很复杂,render中的代码写法就是一种语法糖,帮助我们更好的写表现层代码:render方法中可以写html与js混杂的代码:

1 var data = [1,2,3];
2 React.render(
3 <h1>Hello, {data.toString(',')}!</h1>,
4 document.getElementById('example')
5 );
1 var data = [1,2,3];
2 React.render(
3 <h1>{
4 data.map(function(v, i) {
5 return <div>{i}-{v}</div>
6 })
7 }</h1>,
8 document.getElementById('example')
9 );

所以,react提供了很多类JS的语法,JSXTransformer相当于一个语言解释器,而解析逻辑长达10000多行代码,这个可不是一般屌丝可以碰的,react从这里便走出了不平常的路,而他这样做的意义是什么,我们还不知道。

标签化View

react提供了一个方法,将代码组装成一个组件,然后像HTML标签一样插入网页:

 1 var Pili = React.createClass({
2 render: function() {
3 return <h1>Hello World!</h1>;
4 }
5 });
6
7 React.render(
8 <Pili />,
9 document.getElementById('example')
10 );

所谓,声明试编程,便是将需要的属性写到标签上,以一个文本框为例:

<input type="text" data-type="num" data-max="100" data-min="0" data-remove=true />

我们想要输入的是数字,有数字限制,而且在移动端输入的时候,右边会有一个X按钮清除文本,这个便是我们期望的声明式标签。

react中,标签需要和原始的类发生通信,比如属性的读取是这样的:

 1 var Pili = React.createClass({
2 render: function() {
3 return <h1>Hello {this.props.name}!</h1>;
4 }
5 });
6
7 React.render(
8 <Pili name='霹雳布袋戏'/>,
9 document.getElementById('example')
10 );
11
12 //Hello 霹雳布袋戏!

上文中Pili便是一个组件,标签使用法便是一个实例,声明式写法最终也会被编译成一般的js方法,这个不是我们现在关注的重点。

由于class与for为关键字,需要使用className与htmlFor替换

通过this.props对象可以获取组件的属性,其中一个例外为children,他表示组件的所有节点:

 1 var Pili = React.createClass({
2 render: function() {
3 return (
4 <div>
5 {
6 this.props.children.map(function (child) {
7 return <div>{child}</div>
8 })
9 }
10 </div>
11 );
12 }
13 });
14
15 React.render(
16 <Pili name='霹雳布袋戏'>
17 <span>素还真</span>
18 <span>叶小钗</span>
19 </Pili>
20 ,
21 document.getElementById('example')
22 );
1 <div id="Div1">
2 <div data-reactid=".0">
3 <div data-reactid=".0.0">
4 <span data-reactid=".0.0.0">素还真</span></div>
5 <div data-reactid=".0.1">
6 <span data-reactid=".0.1.0">叶小钗</span></div>
7 </div>
8 </div>

PS:return的语法与js语法不太一样,不能随便加分号

如果想限制某一个属性必须是某一类型的话,便需要设置PropTypes属性:

1 var Pili = React.createClass({
2 propType: {
3 //name必须有,并且必须是字符串
4 name: React.PropTypes.string.isRequired
5 },
6 render: function() {
7 return <h1>Hello {this.props.name}!</h1>;
8 }
9 });

如果想设置属性的默认值,则需要:

 1 var Pili = React.createClass({
2 propType: {
3 //name必须有,并且必须是字符串
4 name: React.PropTypes.string.isRequired
5 },
6 getDefaultProps : function () {
7 return {
8 title : '布袋戏'
9 };
10 },
11 render: function() {
12 return <h1>Hello {this.props.name}!</h1>;
13 }
14 });

我们仍然需要dom交互,我们有时也需要获取真实的dom节点,这个时候需要这么做:

 1 var MyComponent = React.createClass({
2 handleClick: function() {
3 React.findDOMNode(this.refs.myTextInput).focus();
4 },
5 render: function() {
6 return (
7 <div>
8 <input type="text" ref="myTextInput" />
9 <input type="button" value="Focus the text input" onClick={this.handleClick} />
10 </div>
11 );
12 }
13 });

事件触发的时候通过ref属性获取当前dom元素,然后可进行操作,我们这里看看返回的dom是什么:

<input type="text" data-reactid=".0.0">

看来是真实的dom结构被返回了,另外一个比较关键的事情,便是这里的dom事件支持,需要细读文档:http://facebook.github.io/react/docs/events.html#supported-events

表单元素,属于用户与组件的交互,内容不能由props获取,这个时候一般有状态机获取,所谓状态机,便是会经常变化的属性。

 1 var Input = React.createClass({
2 getInitialState: function() {
3 return {value: 'Hello!'};
4 },
5 handleChange: function(event) {
6 this.setState({value: event.target.value});
7 },
8 render: function () {
9 var value = this.state.value;
10 return (
11 <div>
12 <input type="text" value={value} onChange={this.handleChange} />
13 <p>{value}</p>
14 </div>
15 );
16 }
17 });
18
19 React.render(<Input/>, document.body);

组件有其生命周期,每个阶段会触发相关事件可被用户捕捉使用:

Mounting:已插入真实 DOM
Updating:正在被重新渲染
Unmounting:已移出真实 DOM

一般来说,我们会为一个状态发生前后绑定事件,react也是如此:

componentWillMount()
componentDidMount()
componentWillUpdate(object nextProps, object nextState)
componentDidUpdate(object prevProps, object prevState)
componentWillUnmount()
此外,React 还提供两种特殊状态的处理函数。
componentWillReceiveProps(object nextProps):已加载组件收到新的参数时调用
shouldComponentUpdate(object nextProps, object nextState):组件判断是否重新渲染时调用

根据之前的经验,会监控组件的生命周期的操作的时候,往往都是比较高阶的应用了,我们这里暂时不予关注。

好了,之前的例子多半来源于阮一峰老师的教程,我们这里来一个简单的验收,便实现上述只能输入数字的文本框:

 1 var NumText = React.createClass({
2 getInitialState: function() {
3 return {value: 50};
4 },
5 propTypes: {
6 value: React.PropTypes.number
7 },
8 handleChange: function (e) {
9 var v = parseInt(e.target.value);
10 if(v > this.props.max || v < this.props.min ) return;
11 if(isNaN(v)) v = '';
12 this.setState({value: v});
13 },
14 render: function () {
15 return (
16 <input type="text" value={this.state.value} onChange={this.handleChange} />
17 );
18 }
19 });
20
21 React.render(
22 <NumText min="0" max="100" />,
23 document.body
24 );

通过以上学习,我们对React有了一个初步认识,现在我们进入其todolist,看看其是如何实现的

此段参考:阮一峰老师的入门教程,http://www.ruanyifeng.com/blog/2015/03/react.html

TodoMVC

入口文件

TodoMVC为MVC框架经典的demo,难度适中,而又可以展示MVC的思想,我们来看看React此处的入口代码:

 1 <!doctype html>
2 <html lang="en" data-framework="react">
3 <head>
4 <meta charset="utf-8">
5 <title>React • TodoMVC</title>
6 <link rel="stylesheet" href="node_modules/todomvc-common/base.css">
7 <link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
8 </head>
9 <body>
10 <section class="todoapp">
11 </section>
12 <script src="node_modules/react/dist/react-with-addons.js"></script>
13 <script src="node_modules/react/dist/JSXTransformer.js"></script>
14 <script src="node_modules/director/build/director.js"></script>
15 <script src="js/utils.js"></script>
16 <script src="js/todoModel.js"></script>
17
18 <script type="text/jsx" src="js/todoItem.jsx"></script>
19 <script type="text/jsx" src="js/footer.jsx"></script>
20 <script type="text/jsx" src="js/app.jsx"></script>
21 </body>
22 </html>

页面很干净,除了react基本js与其模板解析文件外,还多了一个director.js,因为react本身不提供路由功能,所以路由的工作便需要插件,director便是路由插件,这个不是我们今天学习的重点,然后是两个js文件:

 1 var app = app || {};
2
3 (function () {
4 'use strict';
5
6 app.Utils = {
7 uuid: function () {
8 /*jshint bitwise:false */
9 var i, random;
10 var uuid = '';
11
12 for (i = 0; i < 32; i++) {
13 random = Math.random() * 16 | 0;
14 if (i === 8 || i === 12 || i === 16 || i === 20) {
15 uuid += '-';
16 }
17 uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random))
18 .toString(16);
19 }
20
21 return uuid;
22 },
23
24 pluralize: function (count, word) {
25 return count === 1 ? word : word + 's';
26 },
27
28 store: function (namespace, data) {
29 if (data) {
30 return localStorage.setItem(namespace, JSON.stringify(data));
31 }
32
33 var store = localStorage.getItem(namespace);
34 return (store && JSON.parse(store)) || [];
35 },
36
37 extend: function () {
38 var newObj = {};
39 for (var i = 0; i < arguments.length; i++) {
40 var obj = arguments[i];
41 for (var key in obj) {
42 if (obj.hasOwnProperty(key)) {
43 newObj[key] = obj[key];
44 }
45 }
46 }
47 return newObj;
48 }
49 };
50 })();

utils

 1 var app = app || {};
2
3 (function () {
4 'use strict';
5
6 var Utils = app.Utils;
7 // Generic "model" object. You can use whatever
8 // framework you want. For this application it
9 // may not even be worth separating this logic
10 // out, but we do this to demonstrate one way to
11 // separate out parts of your application.
12 app.TodoModel = function (key) {
13 this.key = key;
14 this.todos = Utils.store(key);
15 this.onChanges = [];
16 };
17
18 app.TodoModel.prototype.subscribe = function (onChange) {
19 this.onChanges.push(onChange);
20 };
21
22 app.TodoModel.prototype.inform = function () {
23 Utils.store(this.key, this.todos);
24 this.onChanges.forEach(function (cb) { cb(); });
25 };
26
27 app.TodoModel.prototype.addTodo = function (title) {
28 this.todos = this.todos.concat({
29 id: Utils.uuid(),
30 title: title,
31 completed: false
32 });
33
34 this.inform();
35 };
36
37 app.TodoModel.prototype.toggleAll = function (checked) {
38 // Note: it's usually better to use immutable data structures since they're
39 // easier to reason about and React works very well with them. That's why
40 // we use map() and filter() everywhere instead of mutating the array or
41 // todo items themselves.
42 this.todos = this.todos.map(function (todo) {
43 return Utils.extend({}, todo, { completed: checked });
44 });
45
46 this.inform();
47 };
48
49 app.TodoModel.prototype.toggle = function (todoToToggle) {
50 this.todos = this.todos.map(function (todo) {
51 return todo !== todoToToggle ?
52 todo :
53 Utils.extend({}, todo, { completed: !todo.completed });
54 });
55
56 this.inform();
57 };
58
59 app.TodoModel.prototype.destroy = function (todo) {
60 this.todos = this.todos.filter(function (candidate) {
61 return candidate !== todo;
62 });
63
64 this.inform();
65 };
66
67 app.TodoModel.prototype.save = function (todoToSave, text) {
68 this.todos = this.todos.map(function (todo) {
69 return todo !== todoToSave ? todo : Utils.extend({}, todo, { title: text });
70 });
71
72 this.inform();
73 };
74
75 app.TodoModel.prototype.clearCompleted = function () {
76 this.todos = this.todos.filter(function (todo) {
77 return !todo.completed;
78 });
79
80 this.inform();
81 };
82
83 })();

utils为简单的工具类,不予理睬;无论什么时候数据层一定是MVC的重点,这里稍微给予一点关注:

① model层实现了一个简单的事件订阅通知系统

② 从类实现来说,他仅有三个属性,key(存储与localstorage的命名空间),todos(真实的数据对象),changes(事件集合)

③ 与backbone的model不同,backbone的数据操作占了其实现大部分篇幅,backbone的TodoMVC会完整定义Model的增删差改依次触发的事件,所以Model定义结束,程序就有了完整的脉络,而我们看react这里有点“弱化”数据处理的感觉

④ 总的来说,整个Model的方法皆在操作todos数据,subscribe用于注册事件,每次操作皆会通知changes函数响应,并且存储到localstorage,从重构的角度来说inform其实只应该完成通知的工作,存储的事情不应该做,但是这与我们今天所学没有什么管理,不予理睬,接下来我们进入View层的代码。

组件化编程

React号称组件化编程,我们从标签化、声明式编程的角度来一起看看他第一个View TodoItem的实现:

  1 var app = app || {};
2
3 (function () {
4 'use strict';
5
6 var ESCAPE_KEY = 27;
7 var ENTER_KEY = 13;
8
9 app.TodoItem = React.createClass({
10 handleSubmit: function (event) {
11 var val = this.state.editText.trim();
12 if (val) {
13 this.props.onSave(val);
14 this.setState({editText: val});
15 } else {
16 this.props.onDestroy();
17 }
18 },
19
20 handleEdit: function () {
21 this.props.onEdit();
22 this.setState({editText: this.props.todo.title});
23 },
24
25 handleKeyDown: function (event) {
26 if (event.which === ESCAPE_KEY) {
27 this.setState({editText: this.props.todo.title});
28 this.props.onCancel(event);
29 } else if (event.which === ENTER_KEY) {
30 this.handleSubmit(event);
31 }
32 },
33
34 handleChange: function (event) {
35 this.setState({editText: event.target.value});
36 },
37
38 getInitialState: function () {
39 return {editText: this.props.todo.title};
40 },
41
42 /**
43 * This is a completely optional performance enhancement that you can
44 * implement on any React component. If you were to delete this method
45 * the app would still work correctly (and still be very performant!), we
46 * just use it as an example of how little code it takes to get an order
47 * of magnitude performance improvement.
48 */
49 shouldComponentUpdate: function (nextProps, nextState) {
50 return (
51 nextProps.todo !== this.props.todo ||
52 nextProps.editing !== this.props.editing ||
53 nextState.editText !== this.state.editText
54 );
55 },
56
57 /**
58 * Safely manipulate the DOM after updating the state when invoking
59 * `this.props.onEdit()` in the `handleEdit` method above.
60 * For more info refer to notes at https://facebook.github.io/react/docs/component-api.html#setstate
61 * and https://facebook.github.io/react/docs/component-specs.html#updating-componentdidupdate
62 */
63 componentDidUpdate: function (prevProps) {
64 if (!prevProps.editing && this.props.editing) {
65 var node = React.findDOMNode(this.refs.editField);
66 node.focus();
67 node.setSelectionRange(node.value.length, node.value.length);
68 }
69 },
70
71 render: function () {
72 return (
73 <li className={React.addons.classSet({
74 completed: this.props.todo.completed,
75 editing: this.props.editing
76 })}>
77 <div className="view">
78 <input
79 className="toggle"
80 type="checkbox"
81 checked={this.props.todo.completed}
82 onChange={this.props.onToggle}
83 />
84 <label onDoubleClick={this.handleEdit}>
85 {this.props.todo.title}
86 </label>
87 <button className="destroy" onClick={this.props.onDestroy} />
88 </div>
89 <input
90 ref="editField"
91 className="edit"
92 value={this.state.editText}
93 onBlur={this.handleSubmit}
94 onChange={this.handleChange}
95 onKeyDown={this.handleKeyDown}
96 />
97 </li>
98 );
99 }
100 });
101 })();

TodoItem

根据我们之前的知识,这里是创建了一个自定义标签,而标签返回的内容是:

render: function () {
return (
<li className={React.addons.classSet({
completed: this.props.todo.completed,
editing: this.props.editing
})}>
<div className="view">
<input
className="toggle"
type="checkbox"
checked={this.props.todo.completed}
onChange={this.props.onToggle}
/>
<label onDoubleClick={this.handleEdit}>
{this.props.todo.title}
</label>
<button className="destroy" onClick={this.props.onDestroy} />
</div>
<input
ref="editField"
className="edit"
value={this.state.editText}
onBlur={this.handleSubmit}
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
/>
</li>
);
}

要展示这个View需要依赖其属性与状态:

getInitialState: function () {
return {editText: this.props.todo.title};
},

这里没有属性的描写,而他本身也仅仅是标签组件,更多的信息我们需要去看调用方,该组件显示的是body部分,TodoMVC还有footer部分的操作工具条,这里的实现便比较简单了:

 1 var app = app || {};
2
3 (function () {
4 'use strict';
5
6 app.TodoFooter = React.createClass({
7 render: function () {
8 var activeTodoWord = app.Utils.pluralize(this.props.count, 'item');
9 var clearButton = null;
10
11 if (this.props.completedCount > 0) {
12 clearButton = (
13 <button
14 className="clear-completed"
15 onClick={this.props.onClearCompleted}>
16 Clear completed
17 </button>
18 );
19 }
20
21 // React idiom for shortcutting to `classSet` since it'll be used often
22 var cx = React.addons.classSet;
23 var nowShowing = this.props.nowShowing;
24 return (
25 <footer className="footer">
26 <span className="todo-count">
27 <strong>{this.props.count}</strong> {activeTodoWord} left
28 </span>
29 <ul className="filters">
30 <li>
31 <a
32 href="#/"
33 className={cx({selected: nowShowing === app.ALL_TODOS})}>
34 All
35 </a>
36 </li>
37 {' '}
38 <li>
39 <a
40 href="#/active"
41 className={cx({selected: nowShowing === app.ACTIVE_TODOS})}>
42 Active
43 </a>
44 </li>
45 {' '}
46 <li>
47 <a
48 href="#/completed"
49 className={cx({selected: nowShowing === app.COMPLETED_TODOS})}>
50 Completed
51 </a>
52 </li>
53 </ul>
54 {clearButton}
55 </footer>
56 );
57 }
58 });
59 })();

TodoFooter

我们现在将关注点放在其所有标签的调用方,app.jsx(TodoApp),因为我没看见这个TodoMVC的控制器在哪,也就是我没有看见控制逻辑的js文件在哪,所以控制流程的代码只能在这里了:

  1 var app = app || {};
2
3 (function () {
4 'use strict';
5
6 app.ALL_TODOS = 'all';
7 app.ACTIVE_TODOS = 'active';
8 app.COMPLETED_TODOS = 'completed';
9 var TodoFooter = app.TodoFooter;
10 var TodoItem = app.TodoItem;
11
12 var ENTER_KEY = 13;
13
14 var TodoApp = React.createClass({
15 getInitialState: function () {
16 return {
17 nowShowing: app.ALL_TODOS,
18 editing: null
19 };
20 },
21
22 componentDidMount: function () {
23 var setState = this.setState;
24 var router = Router({
25 '/': setState.bind(this, {nowShowing: app.ALL_TODOS}),
26 '/active': setState.bind(this, {nowShowing: app.ACTIVE_TODOS}),
27 '/completed': setState.bind(this, {nowShowing: app.COMPLETED_TODOS})
28 });
29 router.init('/');
30 },
31
32 handleNewTodoKeyDown: function (event) {
33 if (event.keyCode !== ENTER_KEY) {
34 return;
35 }
36
37 event.preventDefault();
38
39 var val = React.findDOMNode(this.refs.newField).value.trim();
40
41 if (val) {
42 this.props.model.addTodo(val);
43 React.findDOMNode(this.refs.newField).value = '';
44 }
45 },
46
47 toggleAll: function (event) {
48 var checked = event.target.checked;
49 this.props.model.toggleAll(checked);
50 },
51
52 toggle: function (todoToToggle) {
53 this.props.model.toggle(todoToToggle);
54 },
55
56 destroy: function (todo) {
57 this.props.model.destroy(todo);
58 },
59
60 edit: function (todo) {
61 this.setState({editing: todo.id});
62 },
63
64 save: function (todoToSave, text) {
65 this.props.model.save(todoToSave, text);
66 this.setState({editing: null});
67 },
68
69 cancel: function () {
70 this.setState({editing: null});
71 },
72
73 clearCompleted: function () {
74 this.props.model.clearCompleted();
75 },
76
77 render: function () {
78 var footer;
79 var main;
80 var todos = this.props.model.todos;
81
82 var shownTodos = todos.filter(function (todo) {
83 switch (this.state.nowShowing) {
84 case app.ACTIVE_TODOS:
85 return !todo.completed;
86 case app.COMPLETED_TODOS:
87 return todo.completed;
88 default:
89 return true;
90 }
91 }, this);
92
93 var todoItems = shownTodos.map(function (todo) {
94 return (
95 <TodoItem
96 key={todo.id}
97 todo={todo}
98 onToggle={this.toggle.bind(this, todo)}
99 onDestroy={this.destroy.bind(this, todo)}
100 onEdit={this.edit.bind(this, todo)}
101 editing={this.state.editing === todo.id}
102 onSave={this.save.bind(this, todo)}
103 onCancel={this.cancel}
104 />
105 );
106 }, this);
107
108 var activeTodoCount = todos.reduce(function (accum, todo) {
109 return todo.completed ? accum : accum + 1;
110 }, 0);
111
112 var completedCount = todos.length - activeTodoCount;
113
114 if (activeTodoCount || completedCount) {
115 footer =
116 <TodoFooter
117 count={activeTodoCount}
118 completedCount={completedCount}
119 nowShowing={this.state.nowShowing}
120 onClearCompleted={this.clearCompleted}
121 />;
122 }
123
124 if (todos.length) {
125 main = (
126 <section className="main">
127 <input
128 className="toggle-all"
129 type="checkbox"
130 onChange={this.toggleAll}
131 checked={activeTodoCount === 0}
132 />
133 <ul className="todo-list">
134 {todoItems}
135 </ul>
136 </section>
137 );
138 }
139
140 return (
141 <div>
142 <header className="header">
143 <h1>todos</h1>
144 <input
145 ref="newField"
146 className="new-todo"
147 placeholder="What needs to be done?"
148 onKeyDown={this.handleNewTodoKeyDown}
149 autoFocus={true}
150 />
151 </header>
152 {main}
153 {footer}
154 </div>
155 );
156 }
157 });
158
159 var model = new app.TodoModel('react-todos');
160
161 function render() {
162 React.render(
163 <TodoApp model={model}/>,
164 document.getElementsByClassName('todoapp')[0]
165 );
166 }
167
168 model.subscribe(render);
169 render();
170 })();

TodoAPP

这里同样是创建了一个标签,然后最后一段代码有所不同:

 1 var model = new app.TodoModel('react-todos');
2
3 function render() {
4 React.render(
5 <TodoApp model={model}/>,
6 document.getElementsByClassName('todoapp')[0]
7 );
8 }
9
10 model.subscribe(render);
11 render();

① 这里创建了一个Model的实例,我们知道创建的时候,todos便由localstorage获取了数据(如果有的话)

② 这里了定义了一个方法,以todoapp为容器,装载标签

③ 为model订阅render方法,意思是每次model有变化都将重新渲染页面,这里的代码比较关键,按照代码所示,每次数据变化都应该执行render方法,如果list数量比较多的话,每次接重新渲染岂不是浪费性能,但真实使用过程中,可以看到React竟然是局部刷新的,他这个机制非常牛逼啊!

④ 最后执行了render方法,开始了TodoApp标签的渲染,我们这里再将TodoApp的渲染逻辑贴出来

 1 render: function () {
2 var footer;
3 var main;
4 var todos = this.props.model.todos;
5
6 var shownTodos = todos.filter(function (todo) {
7 switch (this.state.nowShowing) {
8 case app.ACTIVE_TODOS:
9 return !todo.completed;
10 case app.COMPLETED_TODOS:
11 return todo.completed;
12 default:
13 return true;
14 }
15 }, this);
16
17 var todoItems = shownTodos.map(function (todo) {
18 return (
19 <TodoItem
20 key={todo.id}
21 todo={todo}
22 onToggle={this.toggle.bind(this, todo)}
23 onDestroy={this.destroy.bind(this, todo)}
24 onEdit={this.edit.bind(this, todo)}
25 editing={this.state.editing === todo.id}
26 onSave={this.save.bind(this, todo)}
27 onCancel={this.cancel}
28 />
29 );
30 }, this);
31
32 var activeTodoCount = todos.reduce(function (accum, todo) {
33 return todo.completed ? accum : accum + 1;
34 }, 0);
35
36 var completedCount = todos.length - activeTodoCount;
37
38 if (activeTodoCount || completedCount) {
39 footer =
40 <TodoFooter
41 count={activeTodoCount}
42 completedCount={completedCount}
43 nowShowing={this.state.nowShowing}
44 onClearCompleted={this.clearCompleted}
45 />;
46 }
47
48 if (todos.length) {
49 main = (
50 <section className="main">
51 <input
52 className="toggle-all"
53 type="checkbox"
54 onChange={this.toggleAll}
55 checked={activeTodoCount === 0}
56 />
57 <ul className="todo-list">
58 {todoItems}
59 </ul>
60 </section>
61 );
62 }
63
64 return (
65 <div>
66 <header className="header">
67 <h1>todos</h1>
68 <input
69 ref="newField"
70 className="new-todo"
71 placeholder="What needs to be done?"
72 onKeyDown={this.handleNewTodoKeyDown}
73 autoFocus={true}
74 />
75 </header>
76 {main}
77 {footer}
78 </div>
79 );
80 }

说句实话,这段代码不知为什么有一些令人感到难受......

① 他首先获取了注入的model实例,获取其所需的数据todos,注入点在:

<TodoApp model={model}/>

② 然后他由自身状态机,获取真实要显示的项目,其实这里如果不考虑路由的变化,完全显示即可

1 getInitialState: function () {
2 return {
3 nowShowing: app.ALL_TODOS,
4 editing: null
5 };
6 },

③ 数据获取成功后,便使用该数据组装为一个个独立的TodoItem标签:

 1 var todoItems = shownTodos.map(function (todo) {
2 return (
3 <TodoItem
4 key={todo.id}
5 todo={todo}
6 onToggle={this.toggle.bind(this, todo)}
7 onDestroy={this.destroy.bind(this, todo)}
8 onEdit={this.edit.bind(this, todo)}
9 editing={this.state.editing === todo.id}
10 onSave={this.save.bind(this, todo)}
11 onCancel={this.cancel}
12 />
13 );
14 }, this);

标签具有很多事件,这里要注意一下各个事件这里事件绑定与控制器上绑定有何不同

④ 然后其做了一些工作处理底部工具条或者头部全部选中的工作

⑤ 最后开始渲染整个标签:

 1 return (
2 <div>
3 <header className="header">
4 <h1>todos</h1>
5 <input
6 ref="newField"
7 className="new-todo"
8 placeholder="What needs to be done?"
9 onKeyDown={this.handleNewTodoKeyDown}
10 autoFocus={true}
11 />
12 </header>
13 {main}
14 {footer}
15 </div>
16 );

该标签事实上为3个模块组成的了:header部分、body部分、footer部分,模块与模块之间的通信依赖便是model数据了,因为这里最终的渲染皆在app的render处,而render处渲染所有标签全部共同依赖于一个model,就算这里依赖于多个model,只要是统一在render处做展示即可。

流程分析

我们前面理清了整个脉络,接下来我们理一理几个关键脉络:

① 新增

TodoApp为其头部input标签绑定了一个onKeyDown事件,事件代理到了handleNewTodoKeyDown:

 1 handleNewTodoKeyDown: function (event) {
2 if (event.keyCode !== ENTER_KEY) {
3 return;
4 }
5
6 event.preventDefault();
7
8 var val = React.findDOMNode(this.refs.newField).value.trim();
9
10 if (val) {
11 this.props.model.addTodo(val);
12 React.findDOMNode(this.refs.newField).value = '';
13 }
14 },

因为用户输入的数据不能由属性或者状态值获取,这里使用了dom操作的方法获取输入数据,这里的钩子是ref,事件触发了model新增一条记录,并且将文本框置为空,现在我们进入model新增的逻辑:

1 app.TodoModel.prototype.addTodo = function (title) {
2 this.todos = this.todos.concat({
3 id: Utils.uuid(),
4 title: title,
5 completed: false
6 });
7
8 this.inform();
9 };

model以最简的方式构造了一个数据对象,改变了todos的值,然后通知model发生了变化,而我们都知道informa程序干了两件事:

1 app.TodoModel.prototype.inform = function () {
2 Utils.store(this.key, this.todos);
3 this.onChanges.forEach(function (cb) { cb(); });
4 };

存储localstorage、触发订阅model变化的回调,也就是:

1 function render() {
2 React.render(
3 <TodoApp model={model}/>,
4 document.getElementsByClassName('todoapp')[0]
5 );
6 }
7
8 model.subscribe(render);

于是整个标签可耻的重新渲染了,我们再来看看编辑是怎么回事:

② 编辑

这个编辑便与TodoApp没有什么关系了:

1 <label onDoubleClick={this.handleEdit}>
2 {this.props.todo.title}
3 </label>

当双击标签项时,触发了代理的处理程序:

1 handleEdit: function () {
2 this.props.onEdit();
3 this.setState({editText: this.props.todo.title});
4 },

这里他做了两个事情:

onEdit,为父标签注入的方法,他这里执行函数作用域是指向this.props的,所以外层定义时指定了作用域:

 1 return (
2 <TodoItem
3 key={todo.id}
4 todo={todo}
5 onToggle={this.toggle.bind(this, todo)}
6 onDestroy={this.destroy.bind(this, todo)}
7 onEdit={this.edit.bind(this, todo)}
8 editing={this.state.editing === todo.id}
9 onSave={this.save.bind(this, todo)}
10 onCancel={this.cancel}
11 />
12 );

其次,他改变了自身状态机,而状态机或者属性的变化皆会引起标签重新渲染,然后当触发keydown事件后,完成的逻辑便与上面一致了

思考

经过之前的学习,我们对React有了一个大概的了解,是时候搬出React设计的初衷了:

 Just the ui
virtual dom
data flow

后面两个概念还没强烈的感触,这里仅仅对Just the ui有一些认识,似乎React仅仅提供了MVC中View的实现,但是这个View又强大到可以抛弃C了,可以看到上述代码控制器被无限的弱化了,而我觉得React其实真实想提供的可能是一种开发方式的思路,React便是如何帮你实现这种思路的方案:

模块化编程、组件化编程、标签化编程,可能是React真正想表达的思想

我们在组织负责业务逻辑时,也会分模块、分UI,但是我们一般是采用控制器调用组件的方式使用,React这里不同的一点是使用标签分模块,孰优孰劣要真实开发过生产项目的朋友才能认识,真实的应用路由的功能必不可少,应该有不少插件会主动抱大腿,但使用灵活性仍然得项目实践验证。

react本身很干净,不包括模块加载的机制,真正发布生产前需要通过webpack打包处理,但是对于复杂项目来说,按需加载是必不可少的,这块不知道如何

而我的关注点仍然落在了样式上,之前做组件或者做页面时,有一个优化方案,是将对应的样式作为一个View的依赖项加载,一个View保持最小的html&css&js量加载,而react对样式与动画一块的支持如何,也需要生产验证;复杂的项目开发,Model的设计一定是至关重要的,也许借鉴Backbone Model的实现+React的View处理,会是一个不错的选择

最后,因为现在没有生产项目能让我使用React试水,过多的话基本就是意淫了,根据我之前MVC的使用经验,感觉灵活性上估计React仍然有一段路要走,但是其模块化编程的思路倒是对我的项目有莫大的指导作用,对于这门技术的深入,经过今天的学习,我打算再观望一下,不知道angularJS怎么样,我也许该对这门MVVM的框架展开调研

参考资料:

http://www.cnblogs.com/yexiaochai/p/4853398.html

http://stackoverflow.com/questions/17585787/whats-data-reactid-attribute-in-html

【JavaScript】ReactJS基础的更多相关文章

  1. ReactJS基础(续)

    前边的ReactJS基础,我们可以了解到,对于React,可以说是万物皆组件 React的组件应该具有 可组合(Composeable)可重用(Reusable)可维护(Maintainable)的特 ...

  2. JavaScript RegExp 基础详谈

    前言: 正则对于一个码农来说是最基础的了,而且在博客园中,发表关于讲解正则表达式的技术文章,更是数不胜数,各有各的优点,但是就是这种很基础的东西,如果我们不去真正仔细研究.学习.掌握,而是抱着需要的时 ...

  3. JavaScript学习基础部分

    JavaScript学习基础 一.简介 1.JavaScript 是因特网上最流行的脚本语言,并且可在所有主要的浏览器中运行,比方说 Internet Explorer. Mozilla.Firefo ...

  4. JavaScript入门基础

    JavaScript基本语法 1.运算符 运算符就是完成操作的一系列符号,它有七类: 赋值运算符(=,+=,-=,*=,/=,%=,<<=,>>=,|=,&=).算术运 ...

  5. JavaScript 语言基础知识点总结(思维导图)

    JavaScript 数组 JavaScript 函数基础 Javascript 运算符 JavaScript 流程控制 JavaScript 正则表达式 JavaScript 字符串函数 JavaS ...

  6. JavaScript语言基础知识点图示(转)

    一位牛人归纳的JavaScript 语言基础知识点图示. 1.JavaScript 数据类型 2.JavaScript 变量 3.Javascript 运算符 4.JavaScript 数组 5.Ja ...

  7. JavaScript 语言基础知识点总结

    网上找到的一份JavaScript 语言基础知识点总结,还不错,挺全面的. (来自:http://t.cn/zjbXMmi @刘巍峰 分享 )  

  8. Javascript语法基础

    Javascript语法基础   一.基本数据类型   JavaScript中支持数字.字符串和布尔值三种基本数据类型: 1.数字 数字型是JavaScript中的基本数据类型.在JavaScript ...

  9. JavaScript 函数基础

    1. JavaScript 函数基础 1. 定义方法 2. 函数的调用方法 3. 函数方法 apply : 将函数作为数组的方法来调用 将参数以数组形式传递给该方法 call   : 将函数作为对象的 ...

  10. JavaScript编程:javaScript核心基础语法

    1.javaScript核心基础语法: javaScript技术体系包含了5个内容:          1.核心语言定义:          2.原生对象和雷子对象:          3.浏览器对象 ...

随机推荐

  1. Matlab处理数据导出Paraview可读的vtk文件(二)

    由于我在用SPH方法仿真时用的是FORTRAN语言,并且没有找到直接输出vtk文件的代码,因此偷懒通过MATLAB转换一下数据. 用到的Matlab子程序可通过一下链接找到. Matlab处理数据导出 ...

  2. .net core 2.0使用NLog写日志文件

    原文地址:传送门 之前也看了 linezero 大佬写的教程,但是总是没有成功写入日志文件.按照 曲廉卿 的已成功,以下正文: 最近研究了一下NLog的使用方式,简单的入了一下门. 实现的功能,对于不 ...

  3. servlet 的控制缓存时间和response的重定向

    //控制缓存时间 public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletE ...

  4. AngularJS自定义指令及指令配置项

    两种写法 //第一种 angular.module('MyApp',[]) .directive('zl1',zl1) .controller('con1',['$scope',func1]); fu ...

  5. 循序渐进PYTHON3(十三) --6-- COOKIE和SESSION

               1. 由于HTTP协议是无状态的协议(发送一次请求即断开),所以服务端需要记录用户的状态时,就需要用某种机制来识具体的用户,这个机制就是Session. 典型的场景比如购物车,当 ...

  6. pdf转tiff

    概述 基于Java,将pdf转成单一的tiff文件. MAVEN依赖 <groupId>com.sun.media</groupId> <artifactId>ja ...

  7. 每日一刷(2018多校水题+2016icpc水题)

    11.9 线段树 http://acm.hdu.edu.cn/showproblem.php?pid=6315 求逆序对个数 http://acm.hdu.edu.cn/showproblem.php ...

  8. A/B Problem(大数)

    描述 做了A+B Problem,A/B Problem不是什么问题了吧! 输入 每组测试样例一行,首先一个号码A,中间一个或多个空格,然后一个符号( / 或者 % ),然后又是空格,后面又是一个号码 ...

  9. FZU 2105 Digits Count(按位维护线段树)

    [题目链接] http://acm.fzu.edu.cn/problem.php?pid=2105 [题目大意] 给出一个序列,数字均小于16,为正数,每次区间操作可以使得 1. [l,r]区间and ...

  10. BZOJ 1202 [HNOI2005]狡猾的商人(并查集)

    [题目链接] http://www.lydsy.com/JudgeOnline/problem.php?id=1202 [题目大意] 给出一些区间和的数值,问是否存在矛盾 [题解] 用并查集维护前缀和 ...