转载

初探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文件:

初探React,将我们的View标签化
 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的实现:

初探React,将我们的View标签化
  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部分的操作工具条,这里的实现便比较简单了:

初探React,将我们的View标签化
 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文件在哪,所以控制流程的代码只能在这里了:

初探React,将我们的View标签化
  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的框架展开调研

正文到此结束
Loading...