• 【JavaScript】ReactJS基础


    初探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

  • 相关阅读:
    This iPhone 6s is running iOS 11.3.1 (15E302), which may not be supported by this version of Xcode.
    vmware 里MAC 鼠标能移动 无法单击
    php获取微信的openid
    PHP 调试打印输出变量
    E0264 Unable to execute '"/usr/bin/codesign" ...'
    PHP 返回JSON
    小米手机安装证书
    CSS3:radial-gradient,径向渐变的使用方法
    CSS3:linear-gradient,线性渐变的使用方法
    CSS3:RGBA的使用方法
  • 原文地址:https://www.cnblogs.com/junneyang/p/5420170.html
Copyright © 2020-2023  润新知