|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +category : JavaScript |
| 4 | +tagline: "" |
| 5 | +tags : [react] |
| 6 | +--- |
| 7 | +{% include JB/setup %} |
| 8 | + |
| 9 | +<a href="http://blog.reverberate.org/2014/02/react-demystified.html" target="_blank">原文链接</a> |
| 10 | + |
| 11 | +这篇文章不像以往blog的文章那样,讲语法分析以及低层编程语言.最近我对一些`javascript`框架比较感兴趣,包括`facebook`的<a href="http://facebook.github.io/react/index.html" target="_blank">React</a>,读了一些网上专门讲解它的文章,特别是<a href="http://swannodette.github.io/2013/12/17/the-future-of-javascript-mvcs/" target="_blank">The Future of JavaScript MVC Frameworks</a>这篇,让我对它的核心思想更加有兴趣,不过没有找到对它核心抽象思想方面的解析,就像我上一篇文章<a href="http://blog.reverberate.org/2013/07/ll-and-lr-parsing-demystified.html" target="_blank">LL and LR Parsing Demystified</a>,它讲解了核心原理,在某种程度上对我非常有意义. |
| 12 | + |
| 13 | +### The 1000-Foot View |
| 14 | + |
| 15 | +在传统的`web`应用里,操作`dom`一般是用`jquery`来实现,像下图这样: |
| 16 | + |
| 17 | +<img src="https://docs.google.com/drawings/d/1sLx_t031l82kP3Gq7547C1sGcQWgIxViPYfxCo-ZJto/pub?w=340&h=205" alt=""> |
| 18 | + |
| 19 | +我把`dom`标记为红色,因为`dom`更新通常比较昂贵.有时候,`app`内部有很多`model`类来表示一些状态,对于我来说,这些类应该实现一些内部应用细节. |
| 20 | + |
| 21 | +`react`提供了一个不同的,而又强大的方式来更新`dom`,代替直接更新`dom`的方式,这种方式就是`virtual dom`,`react`利用它来更新匹配的真实`dom`,像下图这样: |
| 22 | + |
| 23 | +<img src="https://docs.google.com/drawings/d/11ugBTwDkqn6p2n5Fkps1p3Elp8ZToIRzXzvM4LJMYaU/pub?w=543&h=229" alt=""> |
| 24 | + |
| 25 | +引入一个扩展层怎么样来保持速度的提升?如果在它们的顶部添加一个层能加快速度,岂不是意味着浏览器没有达到最理想的`dom`实现? |
| 26 | + |
| 27 | +这将意味着,除开`virtual dom`有不同的语法跟真实`dom`相比,更显著的是,更新`virtual dom`并不能保证能够马上影响真实`dom`,`react`将会等到事件循环结束,并且利用一个`diff`查找算法来尽可能少的更新真实`dom`. |
| 28 | + |
| 29 | +批量更新`dom`以及一个非常小的`diff`算法都是`react`框架自身提供的,任务应用程序像这样做的话都可以像`react`那样高效,但是手动更新`dom`太单调而且容易出问题,这些问题`react`都会帮你处理很好的. |
| 30 | + |
| 31 | +### Components |
| 32 | + |
| 33 | +我之前提到过`virtual dom`跟真实`dom`在语法有上很大差别,但是`virtual dom`在`api`方法上也有显著的差别.在真实`dom`树下的节点可以理解为一个`element`,但是在`virtual dom`下的节点它是一个完整抽象的组件. |
| 34 | + |
| 35 | +在`react`中使用组件非常重要,因为使用组件,在更新`dom`修改的时候,利用内部的`diff`算法可以避免整个`dom`树的查找,从而提高更新`dom`的效率. |
| 36 | + |
| 37 | +为了找到使用组件的好处,我们来定义一个简单的组件,下面的例子来自于官方提供的`hello world` |
| 38 | + |
| 39 | +```js |
| 40 | + |
| 41 | +/** @jsx React.DOM */ |
| 42 | +var HelloMessage = React.createClass({ |
| 43 | + render: function() { |
| 44 | + return <div>Hello {this.props.name}</div>; |
| 45 | + } |
| 46 | +}); |
| 47 | + |
| 48 | +React.renderComponent(<HelloMessage name="John" />, mountNode); |
| 49 | + |
| 50 | +``` |
| 51 | + |
| 52 | +上面的例子里并没有过多的解释组件的好处,甚至提供了一个独立的组件思想,让我来为大家一一解答这些 |
| 53 | + |
| 54 | +上面的例子里首先创建了一个`react`组件类名为`HelloMessage`,然后创建了一个`virtual dom`(`<HelloMessage>`其实是`HelloMessage`组件类的一个实例),并把这个实例添加一个真实的`mountNode`dom节点中去. |
| 55 | + |
| 56 | +首先要注意的是,`react virtual dom`是定制的,由应用程序定义的组件生成的(这里的是`HelloMessage`),而真实的`dom`都是由浏览器内置的组件创建的,像`<p>`,`<ul>`等,它没有任何特殊的应用逻辑,只是被动的接收你添加的事件监听;`react virtual dom`就不一样的,它提供了特殊的应用程序`api`以及相关的内部业务逻辑,它不仅仅是一个`dom update`库,而是一个抽象的构建视图的库. |
| 57 | + |
| 58 | +另一方面,如果你一直关注`html`方面的新特性的话,那么对<a href="http://www.html5rocks.com/en/tutorials/webcomponents/customelements/" target="_blank">HTML custom elements may be coming to browsers soon</a>这篇文章讲的肯定不陌生,这个新特性给真实`dom`提供了一个跟`react virtual dom`相似的功能:定义包含自己业务逻辑的自定义元素.但是`react`无需等待当前所有浏览器支持这些特性,它允许你现在就可以使用这些相似的功能,比如`custom elements`,`shadow dom`. |
| 59 | + |
| 60 | +好了,我们回到上面的例子上,我们已经创建了一个`<HelloMessage>`的组件并添加到一个`mountNode`节点中.我想以图形化的方式来解释`react`组件的运行机制,首先让我们想像一下`virtual dom`与真实`dom`的关系,我们先假定这个`mountNode`节点就是`body`元素,看下图: |
| 61 | + |
| 62 | +<img src="https://docs.google.com/drawings/d/1AexTHVNPYIn5jZ_ysIDnmLWM8HcLdFvioe2RYzjl2H4/pub?w=595&h=344" alt=""> |
| 63 | + |
| 64 | +上图中的箭头标示`virtual dom`添加到真实`dom`中,这个动作很快,不过让我们来看看业务描述的视图正确的样子: |
| 65 | + |
| 66 | +<img src="https://docs.google.com/drawings/d/1i0TE4RICkBC9cvwZqCQtz06fWt9r_bfqFj4v5CBmSRc/pub?w=557&h=387" alt=""> |
| 67 | + |
| 68 | +也就是说,页面上呈现的是我们自定义的组件`<HelloMessage>`,但是它的真实样子是什么样的呢? |
| 69 | + |
| 70 | +渲染自定义的组件是通过`render()`函数,`react`没有规定什么时候调用或者多长时间调用一次`render`,只在收到合适的改变通知时才执行`render`函数,不管`render`函数返回什么,最终都会呈现在真实的`dom`里. |
| 71 | + |
| 72 | +在我们的例子中,`render`函数返回一个`div`元素并包含内容,`react`调用它得到一个`<div>`元素并更新到真实的`dom`中,所以现在图看起来是这样的: |
| 73 | + |
| 74 | +<img src="https://docs.google.com/drawings/d/1T98oIuZpr6VMgCkL_ZmxGa7pRKWtW-bI3JEEVsYnZ7E/pub?w=595&h=344" alt=""> |
| 75 | + |
| 76 | +不仅仅是更新`dom`,而且它会记住这次更新了什么,好方便下次执行`diff`算法的时候快速的获取差异从而提高更新速度. |
| 77 | + |
| 78 | +我刚才忽略了一件事情,为什么`render`函数可以返回`dom`节点,这是因`jsx`语法而掩盖的,它不是简单的`javascript`,下面是编译`jsx`语法之后的原生`javascript`代码: |
| 79 | + |
| 80 | +```js |
| 81 | + |
| 82 | +/** @jsx React.DOM */ |
| 83 | +var HelloMessage = React.createClass({displayName: 'HelloMessage', |
| 84 | + render: function() { |
| 85 | + return React.DOM.div(null, "Hello ", this.props.name); |
| 86 | + } |
| 87 | +}); |
| 88 | + |
| 89 | +React.renderComponent(HelloMessage( {name:"John"} ), mountNode); |
| 90 | + |
| 91 | +``` |
| 92 | +看到没,`react`并没有返回真实的`dom`,而是用跟真实`dom`等价的`react shdow dom`(像上面的`React.DOM.div`)来代替. |
| 93 | + |
| 94 | + |
| 95 | +### Representing State and Changes |
| 96 | + |
| 97 | +到目前为止,一直都没说组件是怎样来同步更新的,如果`react`不能同步更新的话,那么就跟一些静态渲染库没什么两样了,比如`Mustache`,`HandlebarsJS`.但是`react`是一个高效的同步更新库,为了保证这些,必须允许改变. |
| 98 | + |
| 99 | +`react`里的模型是以组件中的`state`属性来表示的,看看下面的例子,主要用来描述`state`属性的运用: |
| 100 | + |
| 101 | +```js |
| 102 | + |
| 103 | +/** @jsx React.DOM */ |
| 104 | +var Timer = React.createClass({ |
| 105 | + getInitialState: function() { |
| 106 | + return {secondsElapsed: 0}; |
| 107 | + }, |
| 108 | + tick: function() { |
| 109 | + this.setState({secondsElapsed: this.state.secondsElapsed + 1}); |
| 110 | + }, |
| 111 | + componentDidMount: function() { |
| 112 | + this.interval = setInterval(this.tick, 1000); |
| 113 | + }, |
| 114 | + componentWillUnmount: function() { |
| 115 | + clearInterval(this.interval); |
| 116 | + }, |
| 117 | + render: function() { |
| 118 | + return ( |
| 119 | + <div>Seconds Elapsed: {this.state.secondsElapsed}</div> |
| 120 | + ); |
| 121 | + } |
| 122 | +}); |
| 123 | + |
| 124 | +React.renderComponent(<Timer />, mountNode); |
| 125 | + |
| 126 | +``` |
| 127 | + |
| 128 | +`getInitialState`,`componentDidMount`,`componentWillUnmount`这三个回调函数是`react`自己在适当的时机调用的,第一个是相当于构造函数,第二个是组件添加的时候运行的,第三个组件删除的时候运行的,其实看它们的命名就大概知道这些函数的用法了. |
| 129 | + |
| 130 | +所以我们可以得出下面几个猜想,在组件以及`state`改变的背后: |
| 131 | + |
| 132 | +* `render()`是组件`state`和`props`中的唯一函数 |
| 133 | +* `state`只在调用`setState`方法时才会改变 |
| 134 | +* `props`只在父级组件重新渲染不同的`props`才会改变 |
| 135 | + |
| 136 | +(之前没有提及`props`,它是父级组件在渲染的时候传递过来的`attributes`) |
| 137 | + |
| 138 | +所以之前我说`react`不会常常调用`render`函数的,因为它只在你调用`setState`或者父级组件重新渲染不同的`props`时才会重新调用`render`函数. |
| 139 | + |
| 140 | +我们可以把这些信息联系在一起来描述一个数据变化的过程,从`app`创建一个`virtual dom`改变开始(图中用的是`ajax`调用) |
| 141 | + |
| 142 | +<img src="https://docs.google.com/drawings/d/18PZGhVyiHRCX8KX2Jn5XBYIh4hBputrOazI3VRcGsSA/pub?w=616&h=397" alt=""> |
| 143 | + |
| 144 | +### Getting Data from the DOM |
| 145 | + |
| 146 | +到目前为止我们只谈到了怎么改变原始`dom`,但是在真实的应用程序里,我们也会想从原始`dom`中获取数据,因为通常我们需要了解用户输入的信息,为了了解它的机制,让我们看看`react`官网提供的第三个例子 |
| 147 | + |
| 148 | +```js |
| 149 | + |
| 150 | +/** @jsx React.DOM */ |
| 151 | +var TodoList = React.createClass({ |
| 152 | + render: function() { |
| 153 | + var createItem = function(itemText) { |
| 154 | + return <li>{itemText}</li>; |
| 155 | + }; |
| 156 | + return <ul>{this.props.items.map(createItem)}</ul>; |
| 157 | + } |
| 158 | +}); |
| 159 | +var TodoApp = React.createClass({ |
| 160 | + getInitialState: function() { |
| 161 | + return {items: [], text: ''}; |
| 162 | + }, |
| 163 | + onChange: function(e) { |
| 164 | + this.setState({text: e.target.value}); |
| 165 | + }, |
| 166 | + handleSubmit: function(e) { |
| 167 | + e.preventDefault(); |
| 168 | + var nextItems = this.state.items.concat([this.state.text]); |
| 169 | + var nextText = ''; |
| 170 | + this.setState({items: nextItems, text: nextText}); |
| 171 | + }, |
| 172 | + render: function() { |
| 173 | + return ( |
| 174 | + <div> |
| 175 | + >h3<TODO</h3> |
| 176 | + <TodoList items={this.state.items} /> |
| 177 | + <form onSubmit={this.handleSubmit}> |
| 178 | + <input onChange={this.onChange} value={this.state.text} /> |
| 179 | + <button>{'Add #' + (this.state.items.length + 1)}</button> |
| 180 | + </form> |
| 181 | + </div> |
| 182 | + ); |
| 183 | + } |
| 184 | +}); |
| 185 | +React.renderComponent(<TodoApp />, mountNode); |
| 186 | + |
| 187 | +``` |
| 188 | + |
| 189 | +一眼看上去上面的例子可能是这样运行的,给真实的`dom`添加`onChange`事件,然后触发事件,调用`setState`来改变`ui`,如果你的`app`有模型类的话,有可能在事件处理里去更新你的模型类并且也会调用`setState`方法来告诉`react`属性改变了,如果你用了一些双向绑定的库,模型改变的话会自动更新视图,反之亦然,但是这样看起来其实是一种倒退. |
| 190 | + |
| 191 | +其实这里有其它的解释,并不是看上去那样的,`react`并没有在真实的`dom`上面绑定`onChange`事件,而是在`document`上面绑定监听,让下面真实元素冒泡传递上来,然后分发给适当的`virtual dom`,这种方式能够提供性能(因为在每个真实的dom上面绑定事件是非常慢的)并且跨浏览器(浏览器中的事件本身就没有统一). |
| 192 | + |
| 193 | +所以结合这些信息,我们能够到得一张真实的运行图,当用户输入改变`dom`时,像下面这样 |
| 194 | + |
| 195 | +<img src="https://docs.google.com/drawings/d/1BopE4GQuoDr9K_aaoamX4ePial0OF2elM0lHX-5Blcg/pub?w=628&h=475" alt=""> |
| 196 | + |
| 197 | +### Conclusions 总结 |
| 198 | + |
| 199 | +写完这篇文章之后让我对`react`有了更多的理解,下面列出的是我的一些观点: |
| 200 | + |
| 201 | +* `React是一个视图库`.`react`没有利用任何你的模型类,一个`react`组件是一个视图层的概念,组件`state`也只是`ui`状态的一部分,你可以绑定任务第三方模型库到`react`中去(). |
| 202 | + |
| 203 | +* `React组件抽象化有利于推动dom的更新`.组件抽象是有原则的,并且组合的非常好,高效的dom更新来自于好的设计. |
| 204 | + |
| 205 | +* `React组件获取dom更新不是很方便`.`react`编写事件处理来获取`dom`更新要比那种视图自动更新到模型的库要低级的多 |
| 206 | + |
| 207 | +* `React过于抽像`.大部分的时候你都是在编写`virtual dom`,但是有时候你需要直接跟原始`dom`交互,这个时候你可以看看官网提供的<a href="http://facebook.github.io/react/docs/working-with-the-browser.html">Working With the Browser</a>章节. |
| 208 | + |
| 209 | +关于我对`js`方面的一些框架的看法,可以点击我的另外一篇文章<a href="http://swannodette.github.io/2013/12/17/the-future-of-javascript-mvcs/">The Future of JavaScript MVC Frameworks</a> |
| 210 | + |
| 211 | +我不是一个`react`方面的专家,所以赶紧在评论里给我指出上面文章里的错误吧:) |
| 212 | + |
| 213 | + |
| 214 | + |
| 215 | + |
| 216 | + |
| 217 | + |
| 218 | + |
| 219 | + |
| 220 | + |
| 221 | + |
| 222 | + |
0 commit comments