你的位置:首页 > 网页设计

[网页设计]初探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.isRequired5   },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:已插入真实 DOMUpdating:正在被重新渲染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.body24 );

通过以上学习,我们对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 logic10  // out, but we do this to demonstrate one way to11  // 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: false32   });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're39   // easier to reason about and React works very well with them. That's why40   // we use map() and filter() everywhere instead of mutating the array or41   // 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           <button14             className="clear-completed"15             onClick={this.props.onClearCompleted}>16             Clear completed17           </button>18         );19       }20 21       // React idiom for shortcutting to `classSet` since it'll be used often22       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} left28           </span>29           <ul className="filters">30             <li>31               <a32                 href="#/"33                 className={cx({selected: nowShowing === app.ALL_TODOS})}>34                   All35               </a>36             </li>37             {' '}38             <li>39               <a40                 href="#/active"41                 className={cx({selected: nowShowing === app.ACTIVE_TODOS})}>42                   Active43               </a>44             </li>45             {' '}46             <li>47               <a48                 href="#/completed"49                 className={cx({selected: nowShowing === app.COMPLETED_TODOS})}>50                   Completed51               </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           <TodoFooter117             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             <input128               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             <input145               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         <TodoItem20           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         <TodoFooter41           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           <input52             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           <input69             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: null5   };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: false6  });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的框架展开调研