前言
我之前喜歡玩一款遊戲:全民飛機大戰,而且有點痴迷其中,如果你想站在遊戲的第一階梯,便需要不斷的練技術練裝備,但是騰訊的遊戲一般而言是有點噁心的,他會不斷的出新飛機、新裝備、新寵物,所以,很多時候你一個飛機以及裝備還沒滿級,新的裝備就又出來了,並且一定是更強!
於是很多人便直接拋棄當前的飛機與裝備,追求更好的,這個時候如果是人民幣玩家或者骨灰級大神玩家的話,基本可以很快站在世界的頂端,一者是裝備好,一者是技術好,但是我不願意投入太多錢,也不願意投入過多精力,於是在一套極品裝備滿級後會積累資源,因為一代之間變化不會太大,到第二代甚至第三代才開始換飛機換裝備,也基本處於了第一階梯,一直到一次遊戲大更新,直接導致我當前的飛機與裝備完全二逼了,我當時一時腦熱投入了所有資源去重新整理的極品裝備,最後鬧的血本無歸,於是便刪除了該遊戲,一年時間付諸東流!!!
再回過頭來看最近兩年前端的變化,單是一個前端工程化工具就變了幾次,而且新出來的總是叫嚷著要替換之前的,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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<!DOCTYPE html> <html> <head> <script src="build/react.js" type="text/javascript"></script> <script src="build/JSXTransformer.js" type="text/javascript"></script> </head> <body> <div id="example"> </div> <script type="text/jsx"> React.render( <h1>Hello, world!</h1>, document.getElementById('example') ); </script> </body> </html> |
1 |
<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 2 3 4 |
React.render( React.createElement("h1", null, "Hello, world!"), document.getElementById('example') ); |
因為render中的程式碼可以很複雜,render中的程式碼寫法就是一種語法糖,幫助我們更好的寫表現層程式碼:render方法中可以寫html與js混雜的程式碼:
1 2 3 4 5 |
var data = [1,2,3]; React.render( <h1>Hello, {data.toString(',')}!</h1>, document.getElementById('example') ); |
1 2 3 4 5 6 7 8 9 |
var data = [1,2,3]; React.render( <h1>{ data.map(function(v, i) { return <div>{i}-{v}</div> }) }</h1>, document.getElementById('example') ); |
所以,react提供了很多類JS的語法,JSXTransformer相當於一個語言直譯器,而解析邏輯長達10000多行程式碼,這個可不是一般屌絲可以碰的,react從這裡便走出了不平常的路,而他這樣做的意義是什麼,我們還不知道。
標籤化View
react提供了一個方法,將程式碼組裝成一個元件,然後像HTML標籤一樣插入網頁:
1 2 3 4 5 6 7 8 9 10 |
var Pili = React.createClass({ render: function() { return <h1>Hello World!</h1>; } }); React.render( <Pili />, document.getElementById('example') ); |
所謂,宣告試程式設計,便是將需要的屬性寫到標籤上,以一個文字框為例:
1 |
<input type="text" data-type="num" data-max="100" data-min="0" data-remove=true /> |
我們想要輸入的是數字,有數字限制,而且在移動端輸入的時候,右邊會有一個X按鈕清除文字,這個便是我們期望的宣告式標籤。
react中,標籤需要和原始的類發生通訊,比如屬性的讀取是這樣的:
1 2 3 4 5 6 7 8 9 10 11 12 |
var Pili = React.createClass({ render: function() { return <h1>Hello {this.props.name}!</h1>; } }); React.render( <Pili name='霹靂布袋戲'/>, document.getElementById('example') ); //Hello 霹靂布袋戲! |
上文中Pili便是一個元件,標籤使用法便是一個例項,宣告式寫法最終也會被編譯成一般的js方法,這個不是我們現在關注的重點。
1 |
由於class與for為關鍵字,需要使用className與htmlFor替換 |
通過this.props物件可以獲取元件的屬性,其中一個例外為children,他表示元件的所有節點:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
var Pili = React.createClass({ render: function() { return ( <div> { this.props.children.map(function (child) { return <div>{child}</div> }) } </div> ); } }); React.render( <Pili name='霹靂布袋戲'> <span>素還真</span> <span>葉小釵</span> </Pili> , document.getElementById('example') ); |
1 2 3 4 5 6 7 8 |
<div id="Div1"> <div data-reactid=".0"> <div data-reactid=".0.0"> <span data-reactid=".0.0.0">素還真</span></div> <div data-reactid=".0.1"> <span data-reactid=".0.1.0">葉小釵</span></div> </div> </div> |
PS:return的語法與js語法不太一樣,不能隨便加分號
如果想限制某一個屬性必須是某一型別的話,便需要設定PropTypes屬性:
1 2 3 4 5 6 7 8 9 |
var Pili = React.createClass({ propType: { //name必須有,並且必須是字串 name: React.PropTypes.string.isRequired }, render: function() { return <h1>Hello {this.props.name}!</h1>; } }); |
如果想設定屬性的預設值,則需要:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var Pili = React.createClass({ propType: { //name必須有,並且必須是字串 name: React.PropTypes.string.isRequired }, getDefaultProps : function () { return { title : '布袋戲' }; }, render: function() { return <h1>Hello {this.props.name}!</h1>; } }); |
我們仍然需要dom互動,我們有時也需要獲取真實的dom節點,這個時候需要這麼做:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var MyComponent = React.createClass({ handleClick: function() { React.findDOMNode(this.refs.myTextInput).focus(); }, render: function() { return ( <div> <input type="text" ref="myTextInput" /> <input type="button" value="Focus the text input" onClick={this.handleClick} /> </div> ); } }); |
事件觸發的時候通過ref屬性獲取當前dom元素,然後可進行操作,我們這裡看看返回的dom是什麼:
1 |
<input type="text" data-reactid=".0.0"> |
看來是真實的dom結構被返回了,另外一個比較關鍵的事情,便是這裡的dom事件支援,需要細讀文件:http://facebook.github.io/react/docs/events.html#supported-events
表單元素,屬於使用者與元件的互動,內容不能由props獲取,這個時候一般有狀態機獲取,所謂狀態機,便是會經常變化的屬性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var Input = React.createClass({ getInitialState: function() { return {value: 'Hello!'}; }, handleChange: function(event) { this.setState({value: event.target.value}); }, render: function () { var value = this.state.value; return ( <div> <input type="text" value={value} onChange={this.handleChange} /> <p>{value}</p> </div> ); } }); React.render(<Input/>, document.body); |
元件有其生命週期,每個階段會觸發相關事件可被使用者捕捉使用:
1 2 3 |
Mounting:已插入真實 DOM Updating:正在被重新渲染 Unmounting:已移出真實 DOM |
一般來說,我們會為一個狀態發生前後繫結事件,react也是如此:
1 2 3 4 5 6 7 8 |
componentWillMount() componentDidMount() componentWillUpdate(object nextProps, object nextState) componentDidUpdate(object prevProps, object prevState) componentWillUnmount() 此外,React 還提供兩種特殊狀態的處理函式。 componentWillReceiveProps(object nextProps):已載入元件收到新的引數時呼叫 shouldComponentUpdate(object nextProps, object nextState):元件判斷是否重新渲染時呼叫 |
根據之前的經驗,會監控元件的生命週期的操作的時候,往往都是比較高階的應用了,我們這裡暫時不予關注。
好了,之前的例子多半來源於阮一峰老師的教程,我們這裡來一個簡單的驗收,便實現上述只能輸入數字的文字框:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
var NumText = React.createClass({ getInitialState: function() { return {value: 50}; }, propTypes: { value: React.PropTypes.number }, handleChange: function (e) { var v = parseInt(e.target.value); if(v > this.props.max || v < this.props.min ) return; if(isNaN(v)) v = ''; this.setState({value: v}); }, render: function () { return ( <input type="text" value={this.state.value} onChange={this.handleChange} /> ); } }); React.render( <NumText min="0" max="100" />, document.body ); |
通過以上學習,我們對React有了一個初步認識,現在我們進入其todolist,看看其是如何實現的
此段參考:阮一峰老師的入門教程,http://www.ruanyifeng.com/blog/2015/03/react.html
TodoMVC
入口檔案
TodoMVC為MVC框架經典的demo,難度適中,而又可以展示MVC的思想,我們來看看React此處的入口程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<!doctype html> <html lang="en" data-framework="react"> <head> <meta charset="utf-8"> <title>React • TodoMVC</title> <link rel="stylesheet" href="node_modules/todomvc-common/base.css"> <link rel="stylesheet" href="node_modules/todomvc-app-css/index.css"> </head> <body> <section class="todoapp"> </section> <script src="node_modules/react/dist/react-with-addons.js"></script> <script src="node_modules/react/dist/JSXTransformer.js"></script> <script src="node_modules/director/build/director.js"></script> <script src="js/utils.js"></script> <script src="js/todoModel.js"></script> <script type="text/jsx" src="js/todoItem.jsx"></script> <script type="text/jsx" src="js/footer.jsx"></script> <script type="text/jsx" src="js/app.jsx"></script> </body> </html> |
頁面很乾淨,除了react基本js與其模板解析檔案外,還多了一個director.js,因為react本身不提供路由功能,所以路由的工作便需要外掛,director便是路由外掛,這個不是我們今天學習的重點,然後是兩個js檔案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
var app = app || {}; (function () { 'use strict'; app.Utils = { uuid: function () { /*jshint bitwise:false */ var i, random; var uuid = ''; for (i = 0; i < 32; i++) { random = Math.random() * 16 | 0; if (i === 8 || i === 12 || i === 16 || i === 20) { uuid += '-'; } uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random)) .toString(16); } return uuid; }, pluralize: function (count, word) { return count === 1 ? word : word + 's'; }, store: function (namespace, data) { if (data) { return localStorage.setItem(namespace, JSON.stringify(data)); } var store = localStorage.getItem(namespace); return (store && JSON.parse(store)) || []; }, extend: function () { var newObj = {}; for (var i = 0; i < arguments.length; i++) { var obj = arguments[i]; for (var key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = obj[key]; } } } return newObj; } }; })(); utils |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
var app = app || {}; (function () { 'use strict'; var Utils = app.Utils; // Generic "model" object. You can use whatever // framework you want. For this application it // may not even be worth separating this logic // out, but we do this to demonstrate one way to // separate out parts of your application. app.TodoModel = function (key) { this.key = key; this.todos = Utils.store(key); this.onChanges = []; }; app.TodoModel.prototype.subscribe = function (onChange) { this.onChanges.push(onChange); }; app.TodoModel.prototype.inform = function () { Utils.store(this.key, this.todos); this.onChanges.forEach(function (cb) { cb(); }); }; app.TodoModel.prototype.addTodo = function (title) { this.todos = this.todos.concat({ id: Utils.uuid(), title: title, completed: false }); this.inform(); }; app.TodoModel.prototype.toggleAll = function (checked) { // Note: it's usually better to use immutable data structures since they're // easier to reason about and React works very well with them. That's why // we use map() and filter() everywhere instead of mutating the array or // todo items themselves. this.todos = this.todos.map(function (todo) { return Utils.extend({}, todo, { completed: checked }); }); this.inform(); }; app.TodoModel.prototype.toggle = function (todoToToggle) { this.todos = this.todos.map(function (todo) { return todo !== todoToToggle ? todo : Utils.extend({}, todo, { completed: !todo.completed }); }); this.inform(); }; app.TodoModel.prototype.destroy = function (todo) { this.todos = this.todos.filter(function (candidate) { return candidate !== todo; }); this.inform(); }; app.TodoModel.prototype.save = function (todoToSave, text) { this.todos = this.todos.map(function (todo) { return todo !== todoToSave ? todo : Utils.extend({}, todo, { title: text }); }); this.inform(); }; app.TodoModel.prototype.clearCompleted = function () { this.todos = this.todos.filter(function (todo) { return !todo.completed; }); this.inform(); }; })(); |
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
var app = app || {}; (function () { 'use strict'; var ESCAPE_KEY = 27; var ENTER_KEY = 13; app.TodoItem = React.createClass({ handleSubmit: function (event) { var val = this.state.editText.trim(); if (val) { this.props.onSave(val); this.setState({editText: val}); } else { this.props.onDestroy(); } }, handleEdit: function () { this.props.onEdit(); this.setState({editText: this.props.todo.title}); }, handleKeyDown: function (event) { if (event.which === ESCAPE_KEY) { this.setState({editText: this.props.todo.title}); this.props.onCancel(event); } else if (event.which === ENTER_KEY) { this.handleSubmit(event); } }, handleChange: function (event) { this.setState({editText: event.target.value}); }, getInitialState: function () { return {editText: this.props.todo.title}; }, /** * This is a completely optional performance enhancement that you can * implement on any React component. If you were to delete this method * the app would still work correctly (and still be very performant!), we * just use it as an example of how little code it takes to get an order * of magnitude performance improvement. */ shouldComponentUpdate: function (nextProps, nextState) { return ( nextProps.todo !== this.props.todo || nextProps.editing !== this.props.editing || nextState.editText !== this.state.editText ); }, /** * Safely manipulate the DOM after updating the state when invoking * `this.props.onEdit()` in the `handleEdit` method above. * For more info refer to notes at https://facebook.github.io/react/docs/component-api.html#setstate * and https://facebook.github.io/react/docs/component-specs.html#updating-componentdidupdate */ componentDidUpdate: function (prevProps) { if (!prevProps.editing && this.props.editing) { var node = React.findDOMNode(this.refs.editField); node.focus(); node.setSelectionRange(node.value.length, node.value.length); } }, 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> ); } }); })(); TodoItem |
根據我們之前的知識,這裡是建立了一個自定義標籤,而標籤返回的內容是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
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需要依賴其屬性與狀態:
1 2 3 |
getInitialState: function () { return {editText: this.props.todo.title}; }, |
這裡沒有屬性的描寫,而他本身也僅僅是標籤元件,更多的資訊我們需要去看呼叫方,該元件顯示的是body部分,TodoMVC還有footer部分的操作工具條,這裡的實現便比較簡單了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
var app = app || {}; (function () { 'use strict'; app.TodoFooter = React.createClass({ render: function () { var activeTodoWord = app.Utils.pluralize(this.props.count, 'item'); var clearButton = null; if (this.props.completedCount > 0) { clearButton = ( <button className="clear-completed" onClick={this.props.onClearCompleted}> Clear completed </button> ); } // React idiom for shortcutting to `classSet` since it'll be used often var cx = React.addons.classSet; var nowShowing = this.props.nowShowing; return ( <footer className="footer"> <span className="todo-count"> <strong>{this.props.count}</strong> {activeTodoWord} left </span> <ul className="filters"> <li> <a href="#/" className={cx({selected: nowShowing === app.ALL_TODOS})}> All </a> </li> {' '} <li> <a href="#/active" className={cx({selected: nowShowing === app.ACTIVE_TODOS})}> Active </a> </li> {' '} <li> <a href="#/completed" className={cx({selected: nowShowing === app.COMPLETED_TODOS})}> Completed </a> </li> </ul> {clearButton} </footer> ); } }); })(); TodoFooter |
我們現在將關注點放在其所有標籤的呼叫方,app.jsx(TodoApp),因為我沒看見這個TodoMVC的控制器在哪,也就是我沒有看見控制邏輯的js檔案在哪,所以控制流程的程式碼只能在這裡了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
var app = app || {}; (function () { 'use strict'; app.ALL_TODOS = 'all'; app.ACTIVE_TODOS = 'active'; app.COMPLETED_TODOS = 'completed'; var TodoFooter = app.TodoFooter; var TodoItem = app.TodoItem; var ENTER_KEY = 13; var TodoApp = React.createClass({ getInitialState: function () { return { nowShowing: app.ALL_TODOS, editing: null }; }, componentDidMount: function () { var setState = this.setState; var router = Router({ '/': setState.bind(this, {nowShowing: app.ALL_TODOS}), '/active': setState.bind(this, {nowShowing: app.ACTIVE_TODOS}), '/completed': setState.bind(this, {nowShowing: app.COMPLETED_TODOS}) }); router.init('/'); }, handleNewTodoKeyDown: function (event) { if (event.keyCode !== ENTER_KEY) { return; } event.preventDefault(); var val = React.findDOMNode(this.refs.newField).value.trim(); if (val) { this.props.model.addTodo(val); React.findDOMNode(this.refs.newField).value = ''; } }, toggleAll: function (event) { var checked = event.target.checked; this.props.model.toggleAll(checked); }, toggle: function (todoToToggle) { this.props.model.toggle(todoToToggle); }, destroy: function (todo) { this.props.model.destroy(todo); }, edit: function (todo) { this.setState({editing: todo.id}); }, save: function (todoToSave, text) { this.props.model.save(todoToSave, text); this.setState({editing: null}); }, cancel: function () { this.setState({editing: null}); }, clearCompleted: function () { this.props.model.clearCompleted(); }, render: function () { var footer; var main; var todos = this.props.model.todos; var shownTodos = todos.filter(function (todo) { switch (this.state.nowShowing) { case app.ACTIVE_TODOS: return !todo.completed; case app.COMPLETED_TODOS: return todo.completed; default: return true; } }, this); var todoItems = shownTodos.map(function (todo) { return ( <TodoItem key={todo.id} todo={todo} onToggle={this.toggle.bind(this, todo)} onDestroy={this.destroy.bind(this, todo)} onEdit={this.edit.bind(this, todo)} editing={this.state.editing === todo.id} onSave={this.save.bind(this, todo)} onCancel={this.cancel} /> ); }, this); var activeTodoCount = todos.reduce(function (accum, todo) { return todo.completed ? accum : accum + 1; }, 0); var completedCount = todos.length - activeTodoCount; if (activeTodoCount || completedCount) { footer = <TodoFooter count={activeTodoCount} completedCount={completedCount} nowShowing={this.state.nowShowing} onClearCompleted={this.clearCompleted} />; } if (todos.length) { main = ( <section className="main"> <input className="toggle-all" type="checkbox" onChange={this.toggleAll} checked={activeTodoCount === 0} /> <ul className="todo-list"> {todoItems} </ul> </section> ); } return ( <div> <header className="header"> <h1>todos</h1> <input ref="newField" className="new-todo" placeholder="What needs to be done?" onKeyDown={this.handleNewTodoKeyDown} autoFocus={true} /> </header> {main} {footer} </div> ); } }); var model = new app.TodoModel('react-todos'); function render() { React.render( <TodoApp model={model}/>, document.getElementsByClassName('todoapp')[0] ); } model.subscribe(render); render(); })(); TodoAPP |
這裡同樣是建立了一個標籤,然後最後一段程式碼有所不同:
1 2 3 4 5 6 7 8 9 10 11 |
var model = new app.TodoModel('react-todos'); function render() { React.render( <TodoApp model={model}/>, document.getElementsByClassName('todoapp')[0] ); } model.subscribe(render); render(); |
① 這裡建立了一個Model的例項,我們知道建立的時候,todos便由localstorage獲取了資料(如果有的話)
② 這裡了定義了一個方法,以todoapp為容器,裝載標籤
③ 為model訂閱render方法,意思是每次model有變化都將重新渲染頁面,這裡的程式碼比較關鍵,按照程式碼所示,每次資料變化都應該執行render方法,如果list數量比較多的話,每次接重新渲染豈不是浪費效能,但真實使用過程中,可以看到React竟然是區域性重新整理的,他這個機制非常牛逼啊!
④ 最後執行了render方法,開始了TodoApp標籤的渲染,我們這裡再將TodoApp的渲染邏輯貼出來
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
render: function () { var footer; var main; var todos = this.props.model.todos; var shownTodos = todos.filter(function (todo) { switch (this.state.nowShowing) { case app.ACTIVE_TODOS: return !todo.completed; case app.COMPLETED_TODOS: return todo.completed; default: return true; } }, this); var todoItems = shownTodos.map(function (todo) { return ( <TodoItem key={todo.id} todo={todo} onToggle={this.toggle.bind(this, todo)} onDestroy={this.destroy.bind(this, todo)} onEdit={this.edit.bind(this, todo)} editing={this.state.editing === todo.id} onSave={this.save.bind(this, todo)} onCancel={this.cancel} /> ); }, this); var activeTodoCount = todos.reduce(function (accum, todo) { return todo.completed ? accum : accum + 1; }, 0); var completedCount = todos.length - activeTodoCount; if (activeTodoCount || completedCount) { footer = <TodoFooter count={activeTodoCount} completedCount={completedCount} nowShowing={this.state.nowShowing} onClearCompleted={this.clearCompleted} />; } if (todos.length) { main = ( <section className="main"> <input className="toggle-all" type="checkbox" onChange={this.toggleAll} checked={activeTodoCount === 0} /> <ul className="todo-list"> {todoItems} </ul> </section> ); } return ( <div> <header className="header"> <h1>todos</h1> <input ref="newField" className="new-todo" placeholder="What needs to be done?" onKeyDown={this.handleNewTodoKeyDown} autoFocus={true} /> </header> {main} {footer} </div> ); } |
說句實話,這段程式碼不知為什麼有一些令人感到難受……
① 他首先獲取了注入的model例項,獲取其所需的資料todos,注入點在:
1 |
<TodoApp model={model}/> |
② 然後他由自身狀態機,獲取真實要顯示的專案,其實這裡如果不考慮路由的變化,完全顯示即可
1 2 3 4 5 6 |
getInitialState: function () { return { nowShowing: app.ALL_TODOS, editing: null }; }, |
③ 資料獲取成功後,便使用該資料組裝為一個個獨立的TodoItem標籤:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var todoItems = shownTodos.map(function (todo) { return ( <TodoItem key={todo.id} todo={todo} onToggle={this.toggle.bind(this, todo)} onDestroy={this.destroy.bind(this, todo)} onEdit={this.edit.bind(this, todo)} editing={this.state.editing === todo.id} onSave={this.save.bind(this, todo)} onCancel={this.cancel} /> ); }, this); |
標籤具有很多事件,這裡要注意一下各個事件這裡事件繫結與控制器上繫結有何不同
④ 然後其做了一些工作處理底部工具條或者頭部全部選中的工作
⑤ 最後開始渲染整個標籤:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
return ( <div> <header className="header"> <h1>todos</h1> <input ref="newField" className="new-todo" placeholder="What needs to be done?" onKeyDown={this.handleNewTodoKeyDown} autoFocus={true} /> </header> {main} {footer} </div> ); |
該標籤事實上為3個模組組成的了:header部分、body部分、footer部分,模組與模組之間的通訊依賴便是model資料了,因為這裡最終的渲染皆在app的render處,而render處渲染所有標籤全部共同依賴於一個model,就算這裡依賴於多個model,只要是統一在render處做展示即可。
流程分析
我們前面理清了整個脈絡,接下來我們理一理幾個關鍵脈絡:
① 新增
TodoApp為其頭部input標籤繫結了一個onKeyDown事件,事件代理到了handleNewTodoKeyDown:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
handleNewTodoKeyDown: function (event) { if (event.keyCode !== ENTER_KEY) { return; } event.preventDefault(); var val = React.findDOMNode(this.refs.newField).value.trim(); if (val) { this.props.model.addTodo(val); React.findDOMNode(this.refs.newField).value = ''; } }, |
因為使用者輸入的資料不能由屬性或者狀態值獲取,這裡使用了dom操作的方法獲取輸入資料,這裡的鉤子是ref,事件觸發了model新增一條記錄,並且將文字框置為空,現在我們進入model新增的邏輯:
1 2 3 4 5 6 7 8 9 |
app.TodoModel.prototype.addTodo = function (title) { this.todos = this.todos.concat({ id: Utils.uuid(), title: title, completed: false }); this.inform(); }; |
model以最簡的方式構造了一個資料物件,改變了todos的值,然後通知model發生了變化,而我們都知道informa程式幹了兩件事:
1 2 3 4 |
app.TodoModel.prototype.inform = function () { Utils.store(this.key, this.todos); this.onChanges.forEach(function (cb) { cb(); }); }; |
儲存localstorage、觸發訂閱model變化的回撥,也就是:
1 2 3 4 5 6 7 8 |
function render() { React.render( <TodoApp model={model}/>, document.getElementsByClassName('todoapp')[0] ); } model.subscribe(render); |
於是整個標籤可恥的重新渲染了,我們再來看看編輯是怎麼回事:
② 編輯
這個編輯便與TodoApp沒有什麼關係了:
1 2 3 |
<label onDoubleClick={this.handleEdit}> {this.props.todo.title} </label> |
當雙擊標籤項時,觸發了代理的處理程式:
1 2 3 4 |
handleEdit: function () { this.props.onEdit(); this.setState({editText: this.props.todo.title}); }, |
這裡他做了兩個事情:
onEdit,為父標籤注入的方法,他這裡執行函式作用域是指向this.props的,所以外層定義時指定了作用域:
1 2 3 4 5 6 7 8 9 10 11 12 |
return ( <TodoItem key={todo.id} todo={todo} onToggle={this.toggle.bind(this, todo)} onDestroy={this.destroy.bind(this, todo)} onEdit={this.edit.bind(this, todo)} editing={this.state.editing === todo.id} onSave={this.save.bind(this, todo)} onCancel={this.cancel} /> ); |
其次,他改變了自身狀態機,而狀態機或者屬性的變化皆會引起標籤重新渲染,然後當觸發keydown事件後,完成的邏輯便與上面一致了
思考
經過之前的學習,我們對React有了一個大概的瞭解,是時候搬出React設計的初衷了:
1 2 3 |
Just the ui virtual dom data flow |
後面兩個概念還沒強烈的感觸,這裡僅僅對Just the ui有一些認識,似乎React僅僅提供了MVC中View的實現,但是這個View又強大到可以拋棄C了,可以看到上述程式碼控制器被無限的弱化了,而我覺得React其實真實想提供的可能是一種開發方式的思路,React便是如何幫你實現這種思路的方案:
1 |
模組化程式設計、元件化程式設計、標籤化程式設計,可能是React真正想表達的思想 |
我們在組織負責業務邏輯時,也會分模組、分UI,但是我們一般是採用控制器呼叫元件的方式使用,React這裡不同的一點是使用標籤分模組,孰優孰劣要真實開發過生產專案的朋友才能認識,真實的應用路由的功能必不可少,應該有不少外掛會主動抱大腿,但使用靈活性仍然得專案實踐驗證。
react本身很乾淨,不包括模組載入的機制,真正釋出生產前需要通過webpack打包處理,但是對於複雜專案來說,按需載入是必不可少的,這塊不知道如何
而我的關注點仍然落在了樣式上,之前做元件或者做頁面時,有一個優化方案,是將對應的樣式作為一個View的依賴項載入,一個View保持最小的html&css&js量載入,而react對樣式與動畫一塊的支援如何,也需要生產驗證;複雜的專案開發,Model的設計一定是至關重要的,也許借鑑Backbone Model的實現+React的View處理,會是一個不錯的選擇
最後,因為現在沒有生產專案能讓我使用React試水,過多的話基本就是意淫了,根據我之前MVC的使用經驗,感覺靈活性上估計React仍然有一段路要走,但是其模組化程式設計的思路倒是對我的專案有莫大的指導作用,對於這門技術的深入,經過今天的學習,我打算再觀望一下,不知道angularJS怎麼樣,我也許該對這門MVVM的框架展開調研