@nullcc
2016-06-16T11:57:31.000000Z
字数 6749
阅读 1468
react
flux
前端组件
最初听到React而还未深入了解它时,大多数人可能和我的想法一样:难道又是一个新的MVC/MVVM前端framework?深入了解后发现不是这么一回事,React关注的东西很单纯,就是view,并且它也确实解决了前端目前的一些问题,比如view代码的复用,封装组件。应该说React提出了一些新的东西,让前端开发人员有机会重新审视view层的开发策略。
先来快速看一个React的简单例子:
HelloMessage组件:
使用HelloMessage组件:
当然别忘了还有组件要挂载到的节点:
效果:
这个例子很好理解,在HelloMessage.react.js中,我们用React创建了一个名叫HelloMessage的UI组件,它的输出是
<div>Hello, {this.props.name}!</div>
this.props.name就是在使用HelloMessage组件时传入的name属性:
<HelloMessage name="Tom"/>
于是会显示 "Hello, Tom!" 就很好理解了,很简单吧?
要跑起这个例子我们还需要一些引入一些库和配置一些自动化构建的工作流,这部分比较枯燥,放在最后给出,想马上动手试一试的童鞋可以先跳转到[环境搭建]一节中。
简单来说就是,各个组件维护自己的状态(state)和UI,当状态变更时,自动重新渲染整个组件。这种做法的一个好处就在于我们可以得到一个个UI组件,并且它们的状态是自管理的。比如当用户在某个UI组件上触发一个事件时,我们不必编写查找DOM的代码去找到要操作的DOM元素,再施加操作;取而代之的是我们在编写组件时,就已经写好组件能响应什么事件,要如何响应事件。没有没繁琐的DOM查找,代码也变得清晰很多。
React基本上由以下部分组成:
组件是React的核心,刚才的HelloMessage就是一个组件,组件包含两个核心概念,props和state。props是组件的配置属性,是不会变化的,就像刚才我们看到的示例,在声明组件时我们传入了name="Tom",就是指定了HelloMessage这个组件的name属性值为"Tom",可以定义多个不同的组件属性。state是组件的状态,是会变化的,当state变化时组件会自动重新渲染自己。有一篇讨论React的文章中谈到组件,以下引用这篇文章的这段话:
所谓组件,就是状态机器
React将用户界面看做简单的状态机器。当组件处于某个状态时,那么就输出这个状态对应的界面。通过这种方式,就很容易去保证界面的一致性。
在React中,你简单的去更新某个组件的状态,然后输出基于新状态的整个界面。React负责以最高效的方式去比较两个界面并更新DOM树。
可能有人会发现使用React时JS代码的写法有点奇怪,直接把HTML嵌入在JS中,当然JS引擎是无法解释这种语法的,必须由我们把JSX代码编译输出后,才是JS引擎可读的代码。JSX是一种把HTML封装成组件的有效手段,它把组件的数据和UI融合在一起,使组件化成为可能。这里可能有童鞋会表示疑惑,我们在做界面开发的时候不是经常讲要"表现和逻辑分离"吗,为什么React把表现和逻辑整合在一起就没问题呢?好吧,这是个问题,我们放在后面再来讨论。
当组件state发生变化的时候,React会调用组件的render方法自动重新渲染组件UI。可以预想到的一种情况是,如果某个组件很大,其中可能还包含了很多其他组件,那一次重新渲染代价将会很大(因为要遍历这个组件的整个DOM结构进行渲染),不过React对此做了优化,React在内部实现了一个Virtual DOM,组件的DOM结构映射到这个Virtual DOM上,React在这个Virtual DOM上实现了一个diff算法,简单来说就是React会使用这个diff算法来判断某个组件是否需要更新,只更新有变化的组件。这个更新会先发生在Virtual DOM上,Virtual DOM是内存中的一个数据结构,因此更新起来很快,最后再真正地去更新真实的DOM。
React推崇一种叫做"单向数据绑定"的模式, 结合Flux这种应用架构构建客户端web应用。
传统的MVC是将模板写成模板文件独立放在某个地方,在需要时去引用这个模板,这样做确实是把表现成单独分离了,但是这又带来一些新的问题:我们要用什么姿势引用这些模板,或者说我要怎么把数据注入模板,模板存放在哪里等问题。也就是说这个模板和代码逻辑看似是分离的(物理上的分离),其实还是耦合在一起的(逻辑上是耦合的)。为了实现组件化,React引入了JSX的概念。
React实现了组件模板和组件逻辑关联,所以才有了JSX这种语法,把HTML模板直接嵌入到JS中,编译输出后可用。可以认为JSX是一种"中间层",或者说"胶水层",它把HTML模板和JS逻辑代码粘合在一起。
React会解析JSX代码来生成JS引擎可读的代码,因此最后的输出代码都是JS引擎可读的,也就是我们平常用的JS代码,因此如果有必要可以无视JSX,直接用React提供的DOM方法来构建HTML模板,比如下面这行代码是用JSX写:
<a href="http://facebook.github.io/react/">Hello!</a>
也可以使用React.createElement来构建DOM树,第一个参数是标签名,第二个参数是属性对象,第三个参数是子元素:
React.createElement('a', {href: 'http://facebook.github.io/react/'}, 'Hello!')
利用JSX来写HTML模板,可以用原生的HTML标签,也可以像使用原生标签那样引用React组件。React约定通过首字母大小写来区分这两者。原生的HTML标签和平时一样,都是小写的,React组件首字母需要大写。使用HTML标签:
var render = require('react-dom').render;
var myDivElement = <div className="foo" />;
render(myDivElement, document.body);
使用React组件:
var render = require('react-dom').render;
var MyComponent = require('./MyComponet');
var myElement = <MyComponent someProperty={true} />;
render(myElement, document.body);
可以在属性值和子组件中使用JavaScript表达式,使用{}包裹表达式。稍微改造一下HelloMessage的例子,
在属性中使用JavaScript表达式:
//HelloMessage.react.js
var React = require('react');
var HelloMessage = React.createClass({
render: function(){
return <div>Hello, {this.props.sex === 'm' ? 'Mr.' : 'Mrs.'} {this.props.fname}!</div>;
}
});
module.exports = HelloMessage;
//使用HelloMessage
var React = require('react');
var render = require('react-dom').render;
var HelloMessage = require('./components/HelloMessage.react.js');
var num = 1;
render(
<HelloMessage fname='Smith' sex={num === 0 ? 'm' : 'f'} />,
document.getElementById('helloMessage')
);
输出:
Hello, Mr. Smith!
在子组件中使用JavaScript表达式:
//HelloMessage.react.js
var React = require('react');
var HelloMessage = React.createClass({
render: function(){
return <div>Hello, {this.props.sex === 'm' ? 'Mr.' : 'Mrs.'} {this.props.fname}!</div>;
}
});
module.exports = HelloMessage;
//使用HelloMessage
var React = require('react');
var render = require('react-dom').render;
var HelloMessage = require('./components/HelloMessage.react.js');
render(
<HelloMessage fname='Smith' sex='m' />,
document.getElementById('helloMessage')
);
输出:
Hello, Mrs. Smith!
在JSX中使用注释和在JS差不多,唯一要注意的是在一个组件的子元素位置使用注释要用{}包裹起来,看下面这个可以工作的例子:
var React = require('react');
var HelloMessage = React.createClass({
render: function(){
return (
/*这里是注释1*/
<div
/*这里
是
注释2*/
>
{/*这里是注释3*/}
Hello, {this.props.sex === 'm' ? 'Mr.' : 'Mrs.'} {this.props.fname}!
</div>
);
}
});
module.exports = HelloMessage;
注意到只有注释3需要被包裹在{}内,因为存在于一个子元素的位置。
属性扩散使用了ES6起支持的扩展运算符,扩展运算符用三个点号表示,功能是把数组或类数组对象展开成一系列用逗号隔开的值,使用属性扩散可以满足我们懒惰的心理:
var React = require('react');
var render = require('react-dom').render;
var HelloMessage = require('./components/HelloMessage.react.js');
var props = {
fname: 'Smith',
sex: 'm'
};
render(
<HelloMessage {...props} />,
document.getElementById('helloMessage')
);
输出:
Hello, Mr. Smith!
还可以显式覆盖属性扩散的值:
var React = require('react');
var render = require('react-dom').render;
var HelloMessage = require('./components/HelloMessage.react.js');
var props = {
fname: 'Smith',
sex: 'm'
};
render(
<HelloMessage {...props} fname='Johnson' sex='f' />,
document.getElementById('helloMessage')
);
这里后面的fname和sex会覆盖前面的值,输出:
Hello, Mrs. Johnson!
React应用是构建在组件之上的。组件的两个核心概念:
组件通过props和state生成最终的HTML,需要注意的是,通过组件生成的HTML结构只能有一个根节点。
props就是一个组件的属性,我们可以认为属性是不变的,一旦通过属性设置传入组件,就不应该去改变props,虽然对于一个JS对象我们可以改变几乎任何东西。
state是组件的状态,之前说到组件是一个状态机,根据state改变自己的UI。组件state一旦发生变化,组件会自动调用render重新渲染UI,这个动作会通过this.setState方法触发。
应该尽可能保持组件状态的数量越少越好,状态越少组件越容易管理。那些不会变化的数据、变化不必更新UI的数据,都可以归为props;需要变化的、变化需要更新UI的则归为state。
一些很简单的组件可能并不需要state,只需要props即可渲染组件。在ES6中可以用纯函数(没有副作用,无状态)来定义:
const HelloMessage = (props) => <div> Hello, {props.name}!</div>;
render(<HelloMessage name="Smith" />, mountNode);
getInitialState:
初始化this.state的值,只在组件装载之前调用一次。
getDefaultProps
只在组件创建时调用一次并缓存返回的对象(即在 React.createClass 之后就会调用)。在组件加载之后,这个方法的返回结果会保证当访问this.props的属性时,就算在JSX中没有设置相应属性,也总是能取到一个默认的值。
render
每个组件都必须实现的方法,用来构造组件的HTML结构。可以返回null或者false,此时ReactDOM.findDOMNode(this)会返回null。
componentWillMount
只在组件装载之前调用一次,在render之前调用,可以在这里面调用setState改变状态,并且不会导致额外的一次render。
componentDidMount
只会在组件装载完成之后调用一次,在render之后调用,从这里开始可以通过ReactDOM.findDOMNode(this)获取到组件的DOM节点。
这些方法不会在首次render组件的周期调用
findDOMNode
当组件加载到页面上以后,就可以用react-dom的findDOMNode方法来获得组件对用的DOM元素了,注意,findDOMNode不能用在无状态组件上。
refs
还有一种方法是在要引用的DOM元素上设置一个ref属性,通过this.refs.name可以引用到相应的对象。如果ref是设置在原生HTML元素上,它拿到的就是DOM元素,如果设置在自定义组件上,它拿到的就是组件实例,这时候就需要通过findDOMNode来拿到组件的DOM元素。需要注意的是,刚才提到的无状态组件没有实例,它就是个函数,所以ref属性不能设置在无状态组件上。因为无状态组件没有实例方法,不需要用ref去拿实例然后调用实例方法,如果真的想获得无状态组件的DOM元素的时候,需要用一个有状态组件封装一层,然后用this.refs.name和findDOMNode去获取DOM。
可以使用ref调用组件内子组件的实例方法,比如this.refs.myInput.focus();
refs 是访问到组件内部 DOM 节点唯一可靠的方法。
refs 会自动销毁对子组件的引用(当子组件删除时)
不要在render或者render之前访问refs
不要滥用refs,比如只是用它来按照传统的方式操作界面UI:找到DOM->更新DOM
//todo
父子组件间通信
父子组件间通信可以通过props属性来传递,在父组件中给子组件设置props,子组件就可以通过props访问到父组件的属性和方法。
非父子组件间通信
使用全局事件Pub/Sub模式,在componentDidMount里面订阅事件,在componentWillUnmount里面取消订阅,当收到事件触发的时候调用setState更新UI。