本文最后更新于 2024年10月3日 晚上
React 与状态 React.js
自从Facebook一推出,就受到Web前端工程师的强烈推崇。虽说曾经火过一时的Angular.js
颠覆了前端的工程,但是React
更多颠覆的,是下一代UI编程的思维。
传统UI编程,基本很多地方都需要将数据来源,绑定到对应的UI对象,比如用户点击了一个操作,更改了名称,那么你需要更新执行一个回调函数来处理点击操作,并且把新的数据更新原有的UI对象的属性,比如大概就是这样的东西
1 2 3 4 5 func onClick(sender) { var data = getData(sender ); self.button.title = data .name; self.button.color = data .color; }
这样虽然说直观,但是有很大的问题。试想,假如有很多种的回调函数,每个回调函数监听不同的操作,比如onMounseDown
,onMouseUp
,onKeyDown
,onScroll
……甚至根据不同的sender,我们会有不同的操作,我们就必须得手写很多机械的
1 2 self .xx = data .xxself .xy = data .xy
这类的代码,这就给日后维护和扩展带来了灾难,假如我要换一个UI组件,又得一个个检查是否赋值成功;假如我要把这个UI组件在新的UIView里面重用,我又得改动所有的赋值代码,对于大型项目这种UI对象成百上千,UI属性上万,这样是非常可怕的。
而React
,就将所有的赋值,数据绑定,抽象成为一个个状态,不同的事件监听,就是不同的状态而已,而这些状态之间相互独立,不会受到某些全局变量更改而造成UI混乱的情况,更好的是,开发者不需要考虑到底这个属性什么时候赋值,是在数据更新之前还是之后,需不需要定时刷新这种无意义的苦力活上。
React-Native 示例
完整代码:Tutorial
看看React-Native
的sample,需求就是实现一个电影列表显示的View。类似这样:
这段代码中,只有View和ViewModel(Model就是临时的JSON),React用状态把UI属性和数据绑定起来,从而避免了事件监听手动判断时机来赋值
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 var AwesomeProject = React .createClass ({ getInitialState : function ( ) { return { dataSource : new ListView .DataSource ({ rowHasChanged : (row1, row2 ) => row1 !== row2, }), loaded : false , }; }, fetchData : function ( ) { fetch (REQUEST_URL ) .then ((response ) => response.json ()) .then ((responseData ) => { this .setState ({ dataSource : this .state .dataSource .cloneWithRows (responseData.movies ), loaded : true , }); }) .done (); }, render : function ( ) { return ( <ListView //这里是JSX语法 ,在JS里面返回标签 dataSource ={this.state.dataSource} renderRow ={this.renderMovie} style ={styles.listView} /> ); } }
完整代码:iOS Demo
相比来说,原生Cocoa Touch的实现,就要丑陋的多了,尤其是渲染部分绑定UI对象的属性和数据来源,假如你有多处数据来源,多种UI属性,你就得写很多判断来保证你的UI对象的属性符合预期的赋值顺序。
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 override func viewDidLoad ( ) { super .viewDidLoad () fetchData () } func fetchData ( ) { indicator.startAnimating () request (.GET , API_URL , parameters : ["apikey" : API_KEY , "page_limit" : PAGE_SIZE ]) .responseJSON { _, _, data, _ in self.render (JSON (data!)) } } func render (result:JSON ) { indicator.stopAnimating () movieJSON = result self.tableView .reloadData () } func tableView (tableView : UITableView , numberOfRowsInSection section : Int ) -> Int { var movieNum = movieJSON["movies" ].arrayValue .count return movieNum } func tableView (tableView : UITableView , cellForRowAtIndexPath indexPath : NSIndexPath ) -> UITableViewCell { let cell = self.tableView .dequeueReusableCellWithIdentifier ("movieIdentifier" , forIndexPath : indexPath) as ! MovieTableViewCell let row = indexPath.row cell.movieTitle .text = movieJSON["movies" ][row]["title" ].stringValue cell.movieYear .text = movieJSON["movies" ][row]["year" ].stringValue var movieImageUrl = movieJSON["movies" ][row]["posters" ]["thumbnail" ].stringValue if cell.movieImage .image == nil { request (.GET , movieImageUrl).response { _, _, data, _ in let movieImage = UIImage (data : data as ! NSData ) cell.movieImage .image = movieImage } } return cell }
React 与函数式 React
更多的,就是一种类似函数式的想法,把UI对象属性,数据来源,当作一个Monad包裹起来,传统意义上的不同数据来源进行UI属性赋值,相当于这个Monad经过不同的函数作用,达到状态的切换,好处就是大大减少了开发者手动维护UI属性的工作,而且可以达到更高的开发效率。而且再也不怕扩展了,因为这时候可以把多个组件分配给不同的人,每个人完全不需要管别人内部的变量名是什么,UI属性是什么,只要把自己的状态管理好,Model层接口统一,剩下的合并即可。
React 与效率 既然提到了状态,因为React
采取VirtualDOM
来diff需要进行状态更新的UI对象,每次确保了只更新属性发生改变的部分。实现也很高效,使用了一个普通的二叉树vtree
。每个结点vnode
就是对应的Tag,比如之类,结点还存储了一个struct用来保存这些Tag的属性(比如Image的size)
其中的diff算法可以一看……代码地址在:GitHub-vtree
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var aChildren = a.children var orderedSet = reorder (aChildren, b.children )var bChildren = orderedSet.children var len = aLen > bLen ? aLen : bLenfor (var i = 0 ; i < len; i++) { var leftNode = aChildren[i] var rightNode = bChildren[i] index += 1 if (!leftNode) { if (rightNode) { apply = appendPatch (apply, new VPatch (VPatch .INSERT , null , rightNode)) } } else { walk (leftNode, rightNode, patch, index) } }
这是核心reorder代码,目标找到是同一结点不同属性的diff,多出来的不需要管
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 for (var i = 0 ; i < aChildren.length ; i++) { var aItem = aChildren[i] var itemIndex if (aItem.key ) { if (bKeys.hasOwnProperty (aItem.key )) { itemIndex = bKeys[aItem.key ] newChildren.push (bChildren[itemIndex]) } else { itemIndex = i - deletedItems++ newChildren.push (null ) } } }for (var j = 0 ; j < bChildren.length ; j++) { var newItem = bChildren[j] if (newItem.key ) { if (!aKeys.hasOwnProperty (newItem.key )) { newChildren.push (newItem) } }var simulate = newChildren.slice ()for (var k = 0 ; k < bChildren.length ;) {var wantedItem = bChildren[k] simulateItem = simulate[simulateIndex]while (simulateIndex < simulate.length ) { simulateItem = simulate[simulateIndex] removes.push (remove (simulate, simulateIndex, simulateItem && simulateItem.key )) }
整体的时间复杂度,达到了 $$ O(M+N) $$ 已经是理论下界了。比起手动管理状态来说,效率可以说是直接持平,甚至对部分滥用事件监听的写法效率会更高。
总结 虽然我并不喜欢UI编程,但是自从图形化出现之后,UI已经成为了继数据结构、算法外,面向终端用户的应用又一个大工程。
从最早的指令式跳转赋值UI,函数指针响应处理,到中期的面向对象,消息发送回调事件,手动管理属性,在到如今的React以状态和VirualDOM来绑定数据和组件。
UI编程其实也是在不断进化的,也许今后会有更好的开发方式让我们这群不会写UI的人也能够轻松写起来UI。