按照惯例,先给ReactJS背书
React是一个Facebook开发的UI库,于2013年5月开源,并迅速的从最早的UI引擎演变成一整套前后端通吃的 Web App 解决方案。衍生的 React Native 项目,目标更是宏伟,希望用写 Web App 的方式去写 Native App。
使用这个库可以很方便的开发交互式的、具有表达力的和可重用的UI组件。它本身并不是一个MVC框架,就是一个视图层,并且是一个以组件为基础的高效视图层。
React 的核心思想是:封装组件,各个组件维护自己的状态和 UI,当状态变更,自动重新渲染整个组件。基于这种方式的一个直观感受就是我们不再需要不厌其烦地来回查找某个 DOM 元素,然后操作 DOM 去更改 UI。
推荐一个脚手架程序
git clone https://github.com/hellobeifeng/reactDemo.git
gulp serve
localhost:5000
说说我理解的一些概念
- JSX
- 真要使用JSX?
- JSX的使用
- 虚拟DOM
- 单向事件流
- 组件
- 什么是组件?
- 组件的状态与属性
- 组件的生命周期
JSX
真要使用JSX?
JSX 并不是一门全新的语言,仅仅是一个语法糖,允许开发者在JavasSript中编写XML语言。
作为React的核心部分,JSX使用XML标记的方式直接声明页面。在JavaScript代码里直接写XML的语法,每一个XML标签都会被JSX转换工具转换成纯JavaScript代码。(学习React的第一个坑)
注意:使用JSX写的代码,需要编译输出成JS代码才能使用。将 JSX 语法转为 JavaScript 语法,这一步很消耗时间。现在前端项目,都会使用前端工程化,不会直接在html页面中直接写js代码,写好的js代码都会使用工具进行编译压缩等。这样的话,我们的JSX也会通过编译直接转化成js语法,让浏览器直接使用。
好消息是你不用使用这个JSX也可以直接创建组件,但是坏消息是,不用JSX你必须使用原声JavaScript通过大段的API来创建。
例如,不使用JSX创建一个标题的函数大概是这样
React.createElement('h1',{className:'question'},'Questions')
如果使用JSX,上述调用就变成了下面这种更熟悉且简练的标签
<h1 className="question"/>Questions</>
而且,一旦嵌套元素就可能出现如下的样子
var child = React.createElement('li', null, 'Text Content');
var root = React.createElement('ul', { className: 'my-list' }, child);
React.render(root, document.body);
所以,当你放下心里负担之后会发现,使用JSX的好处就是,抽象了React Element的创建过程,允许使用熟悉的语法来定义HTML元素树,提供更加语义化且易懂的标签,程序结构更容易被直观化,所以下定决心使用JSX吧~
如此使用JSX
JSX定义属性&&样式使用
在HTML中,可以通过标签上的属性来改变当前元素的样式,当然,这在JSX中也是可以的,只不过JSX在使用行内样式的时候是有缺陷的。使用{{}}而不是引号的方式。
React.render(
<div className="text-c1" style={{color:'red',marginTop:'20px'}}>
xxxxx
</div>,
document.body
);
直接在标签上使用style属性时,要写成style={{}}是两个大括号,外层大括号是告知JSX这里是js语法,和真实DOM不同的是,属性值不能是字符串而必须为对象,需要注意的是属性名同样需要驼峰命名法。即margin-top要写成marginTop,属性之间以逗号间隔。
使用变量:
JSX将两个花括号中的内容{...}渲染成动态值,花括号指明了一个JavaScript上下文环境————你在花括号中放置的任何内容都会被求值。
-
使用简单变量
var msgs = 'Hi all~'; <h2>{msgs}</h2>
-
使用函数
function dateToString(date){ return [ date.getFullYear(), date.getMonth()+1, date.getDate() ].join('-') }; <h2>{dateToString(new Date())}</h2>
JSX中的延展属性
在JSX中我们可以使用ES6中的最新语法...
来遍历对象
var HelloMessage = React.createClass({
render: function() {
return <h1>Hello {this.props.name} get {this.props.votes} votes</h1>;
}
});
var Lucy = {
name: "feng",
votes: "23"
}
ReactDOM.render(
<HelloMessage {...Lucy} />,
document.getElementById('example')
);
JSX中的事件绑定
JSX支持事件的绑定,语法类似于HTML中事件的绑定,不同的是这里事件的名称必须按照驼峰式,例如change变成onChange,click变成onClick等。
实例1:点击计数
var Counter = React.createClass({
incrementCount: function(){
this.setState({
count: this.state.count + 1
});
},
getInitialState: function(){
return {
count: 0
}
},
render: function(){
return (
<div class="my-component">
<h1>Count: {this.state.count}</h1>
<button type="button" onClick={this.incrementCount}>Increment</button>
</div>
);
}
});
React.render(<Counter />,document.getElementById('example'))
实例2:事件传递参数
var HelloComponent = React.createClass({
testClick: function (str) {
alert('hello ' + str)
},
render: function() {
return (
<p
onClick={this.testClick.bind(this, 'feng')}
style={{color:'#ff6600',width:'200px', height:'100p'}}
>
Click me
</p>
)
}
})
ReactDOM.render(
<HelloComponent />,
document.getElementById('example')
);
条件判断
JSX中,不能使用 if/else语句,替代方案:三目运算符
var MessageBox = React.createClass({
getInitialState:function(){
return {
isComplete:true
}
},
getIsComplete:function(){
return this.state.isComplete ?'is-complete':''
},
render: function(){
return (
<div className={this.getIsComplete()}>
hi all
</div>
)
}
});
注意,这里说的是在HTML中不能使用if/else语句,在函数中依然可以使用。
使用子节点
React将开始标签和结束标签中的所有子节点保存在一个叫this.props.children
的特殊组件属性内。
var SubMessageComp = React.createClass({
render: function(){
return (
<div>
{this.props.children}
</div>
)
}
});
var MessageBox = React.createClass({
render: function(){
return (
<SubMessageComp>
<li>a</li>
<li>b</li>
</SubMessageComp>
)
}
});
注释:
JSX本质上就是JavaScript,因此可以再标签内添加原声的JavaScript注释
render: function(){
//普通javascript注释
return (
//普通javascript注释
<div /*内联注释*/ className={this.getIsComplete()}>
{/*这里节点内注释*/}
hi all
</div>
)
}
//注释
不能放置在return 结构内
/*内联注释*/
内联注释不要放置到节点内(会自动被当成一个span标签)
{/*节点内注释*/}
节点内注释不要和内联注释替换位置
虚拟DOM
Virtual DOM 用于优化视图的渲染和刷新。以前我们更新视图时,需要先清空DOM容器中的内容,然后将最新的DOM和数据追加到容器中(重绘与重排),现在React将这一操作放到了内存中。
React 会在内存中维护一个虚拟 DOM 树,当我们对这个树进行读或写的时候,实际上是对虚拟 DOM 进行的。当数据变化时,然后 React 会自动更新虚拟 DOM,然后拿新的虚拟 DOM 和旧的虚拟 DOM 执行Diff算法,找到有变更的部分,得出一个Patch,然后将这个 Patch 放到一个队列里,最终批量更新这些 Patch 到 DOM 中。
这样的机制可以保证即便是根节点数据的变化,最终表现在 DOM 上的修改也只是受这个数据影响的部分,可以保证非常高效的渲染。
单向事件流
在jquery时代,我们都是基于事件驱动,对于简单的交互需求而言,这确实足够了,而且开发起来非常迅速。但业务一旦复杂,这种基于事件驱动的东西就会变得很乱:某个事件,更新的DOM逐渐变多,不好管理,就容易出错。
React中有单向数据流的概:更新 DOM 的数据总是从顶层流下来,用户事件不直接操作 DOM,而是操作顶层数据。这些数据从顶层流下来同时更新了DOM。你的代码就很少会直接处理DOM,而是只处理数据的变更。这样会很大程度上简化代码和逻辑。
举个例子:我点击一个button,然后页面上一个span里数字+1,原有的思考逻辑是“点击发生,然后数据变化,然后UI跟着变化+1”。而现在的思考逻辑是我的数据变化了,那么我的UI会自动更新,那么我只用考虑“点击发生,数据变化”。甚至可以把UI和数据变化分层然后处理。
具体地说:
在一个多组件结构里,一个父组件需要负责管理状态,并把数据通过props向下发放。
组件的状态通过setState方法更新。数据通过设置子组件的属性来传递给子组件。
子组件通过this.props来获取这些数据,通过数据和自身状态绑定事件UI,触发UI的改变。
例子: 实现一个动态查找框
var FilteredList = React.createClass({
filteredList: function (event) {
var updateList = this.state.initialItems;
updateList = updateList.filter(function (item) {
return item.toLowerCase().search(event.target.value.toLowerCase()) !== -1;
});
this.setState({items: updateList});
},
getInitialState: function () {
return {
initialItems: ["javascript", "java", "c", "c++", "python", "php", "nodejs","react"],
items: []
}
},
componentWillMount: function () {
this.setState({items: this.state.initialItems});
},
render: function () {
return (
<div className="filter-class">
<input type="text" placeholder="Search" onChange={this.filteredList} />
<List items={this.state.items} />
</div>
)
}
})
var List = React.createClass({
render: function () {
//List组件中通过this.props来获取到FilteredList传递进来的值
return (
<ul>
{
this.props.items.map(function (item) {
return <li key={item}>{item}</li>
})
}
</ul>
)
}
});
React.render(<FilteredList/>, document.getElementById("example"));
组件
创建组件
对于React应用而言,你需要分割你的页面,使其成为一个个的组件。也就是说你的应用是由一个个组件构成的。
使用React.createClass方法创建组件。
var HelloMessage = React.createClass({
render: function() {
return <div>Hello {this.props.name}</div>;
}
});
// 加载组件到 DOM 元素 mountNode
React.render(<HelloMessage name="feng" />, mountNode);
组件首字母必须大写,用来区分是组件标签还是HTML标签
props
props是一个对象,是组件用来接收外面传来数据的容器。组件内部是不允许修改自己的props属性的,只能够通过父组件来修改。具体的使用方法如下
-
props的使用
var HelloWorld = React.createClass({ render:function () { console.log(this.props) return <p>Hello,{this.props.name ? this.props.name : " World"}</p>; }, }); var HelloUniverse = React.createClass({ getInitialState:function () { return { name:'' }; }, handleChange: function (event) { this.setState({name: event.target.value}); }, render: function () { return (<div> <HelloWorld name={this.state.name}></HelloWorld> <br/> <input type="text" onChange={this.handleChange} /> </div> ); } }); ReactDOM.render(<HelloUniverse />,example);
-
propTypes
组件的属性可以接受任意值,字符串、对象、函数等等都可以。有时,我们需要一种机制,验证别人使用组件时,提供的参数是否符合要求。(一般用于开发阶段使用)
var MyTitle = React.createClass({ propTypes: { title: React.PropTypes.string.isRequired, }, render: function() { return <h1> {this.props.title} </h1>; } });
上面的Mytitle组件有一个title属性。PropTypes 告诉 React,这个 title 属性是必须的,而且它的值必须是字符串。现在,我们设置 title 属性的值是一个数值。
var data = 123; ReactDOM.render( <MyTitle title={data} />,example );
这样一来,title属性就通不过验证了。控制台会显示一行warning信息。
-
getDefaultProps
用来设置组件属性的默认值
var MyTitle = React.createClass({ getDefaultProps : function () { return { title : 'Hello World' }; }, render: function() { return <h1> {this.props.title} </h1>; } }); ReactDOM.render(<MyTitle />,document.body);
-
this.props.children
this.props 对象的属性与组件的属性一一对应,但是有一个例外,就是 this.props.children 属性。它表示组件的所有子节点
var NotesList = React.createClass({ render: function() { return ( <ol> { React.Children.map(this.props.children, function (child) { return <li>{child}</li>; }) } </ol> ); } }); ReactDOM.render( <NotesList> <span>hello</span> <span>world</span> </NotesList>,example );
上面代码的 NoteList 组件有两个 span 子节点,它们都可以通过 this.props.children 读取。这里需要注意, this.props.children 的值有三种可能:
- 如果当前组件没有子节点,它就是 undefined;
- 如果有一个子节点,数据类型是 object;
- 如果有多个子节点,数据类型就是 array
React 提供一个工具方法 React.Children 来处理 this.props.children 。我们可以用 React.Children.map 来遍历子节点,而不用担心 this.props.children 的数据类型是 undefined 还是 object。
state属性的用法
React把组件当成状态机,状态(state)是子组件内部维护的数据,一旦用户交互导致状态发生变化,触发不同的钩子函数(详见下文中生命周期),组件也会进行更新,重新渲染UI。
-
工作原理
常用的通知 React 数据变化的方法是调用 setState(data, callback)。这个方法会合并data 到 this.state,并重新渲染组件。渲染完成后,调用可选的 callback 回调。大部分情况下不需要提供 callback,因为 React 会负责把界面更新到最新状态。
-
getInitialState
object getInitialState()
getInitialState 方法用于定义初始状态,也就是一个对象,这个对象可以通过 this.state 属性读取。在组件挂载之前调用一次。返回值将会作为 this.state 的初始值。
-
setState
setState(object nextState[, function callback])
this.setState 方法用于修改状态值,每次修改以后,自动调用 this.render 方法,再次渲染组件。
注意:
- 绝对不要直接改变 this.state,因为在之后调用 setState() 可能会替换掉你做的更改。把 this.state 当做不可变的。
- setState()不会立刻改变 this.state,而是创建一个即将处理的state转变。在调用该方法之后获取 this.state 的值可能会得到现有的值,而不是最新设置的值。但是,可以通过异步(setTimeout(func, 0))的形式来获取this.state的值。。
- setState () 将总是触发一次重绘,除非在 shouldComponentUpdate()中实现了条件渲染逻辑,仅在新 state 和之前的 state 存在差异的时候调用 setState()可以避免不必要的重新渲染。
常用的模式是创建多个只负责渲染数据的无状态(stateless)组件,在它们的上层创建一个有状态(stateful)组件并把它的状态通过 props 传给子级。这个有状态的组件封装了所有用户的交互逻辑,而这些无状态组件则负责声明式地渲染数据。
生命周期
生命周期各阶段
在整个ReactJS的声明周期中,主要会经历这四个阶段:创建阶段、实例化阶段、更新阶段、销毁阶段
var OneComponent = React.createClass({
clickHandle: function(){
this.setState({
count: this.state.count + 1
})
},
//1.创建阶段
getDefaultProps: function() {
//在创建类的时候被调用
console.log('getDefaultProps');
return {
count: 0
}
},
//2.实例化阶段
getInitialState: function() {
//获取this.state的默认值
console.log('getInitialState');
return {};
},
componentWillMount: function() {
//在render之前调用此方法
//业务逻辑的处理都应该放在这里,比如对state的操作等
console.log('componentWillMount')
},
render: function() {
//渲染并返回一个虚拟DOM
console.log('render');
return (
<div> hello
<b> {this.props.name}
</b>
<button onClick={this.clickHandle}>Click me</button>
</div>
)
},
componentDidMount: function() {
//该方法发生在render方法之后
//在该方法中,ReactJS会使用render方法返回的虚拟DOM对象来创建真实DOM结构
console.log('componentDidMount');
console.log('###end###')
},
//3.个更新阶段
componentWillReceieveProps: function() {
//该方法发生在this.props被修改或父组件调用setProps()方法之后
console.log('componentWillRecieveProps');
},
shouldComponentUpdate: function() {
//是否需要更新
console.log('shouldComponentUpdate');
return true;
},
componentWillUpadate: function() {
//将要更新
console.log('componentWillUpadate');
},
componentDidUpdate: function() {
//更新完毕
console.log('componentDidUpdate');
},
//4.销毁阶段
componentWillUnmout: function() {
//销毁时被调用
console.log('componentWillUnmout');
}
})
ReactDOM.render(
<OneComponent name="World "/>,
document.getElementById('example')
);
创建阶段:
该阶段主要发生在创建组件类的时候,即在调用React.createClass的时候。这个阶段只会触发一个getDefaultProps方法,该方法会返回一个对象,并缓存下来。然后与父组件制定的props对象合并,最后赋值给this.props作为该组件的默认属性。
实例化阶段
该阶段主要发生在实例化组件类的时候,也就是组件类被调用的时候。
ReactDOM.render(
<OneComponent name="World "/>,
document.getElementById('example')
);
该组件被调用的时候,这个阶段会触发一系列的流程,具体的执行顺序如下所示。
-
getInitialState。
初始化组件的state的值,其返回值会赋值给组件的this.state属性。
-
componentWillMount 在渲染(挂载)之前调用一次。
此时this.refs对象为空。如果在此时调用this.setState()则会更新this.state对象
-
render
根据state的值,生成页面需要的虚拟DOM结构,并返回该结构
-
componentDidMount
在渲染之后调用一次。根据虚拟DOM结构而生成的真实DOM进行相应的处理,组件内部可以通过this.getDOMNode()来获取当前组件的节点,然后就可以像在web开发中那样操作里面的DOM元素了。
componentDidMount () { const textbox = React.findDOMNode(this.refs.text) if (this.props.autoFocus) textbox.focus() }
更新阶段
主要发生在用户操作或者组件有更新的时候,此时会根据用户的操作行为进行相应的页面结构的调整。该阶段也会触发一系列的流程,具体的执行顺序如下所示。
-
componentWillReceiveProps
在将要接收props时调用。在该函数中,通常可以调用this.setState方法来完成对state的修改。
props是父组件传递给资组建的。父组件发生render的时候子组件就会调用componentWillReceiveProps(不管props有没有更新,也不管父子组件之间有没有数据交换)。
componentWillReceiveProps (nextProps) { if (this.props.disabled !== nextProps.disabled) { // disabled这个属性改变了 } }
-
shouldComponentUpdate
该方法用来拦截新的props或state,然后根据事先设定好的判断逻辑,返回值决定组件是否需要update。
组件挂载之后,每次调用setState后都会调用shouldComponentUpdate判断是否需要重新渲染组件。默认返回true,需要重新render。在比较复杂的应用里,有一些数据的改变并不影响界面展示,可以在这里做判断,优化渲染效率。
shouldComponentUpdate (nextProps, nextState) { // 比较props或者states,返回true则更新照常,返回false则取消更新,且不会调用下面的两个生命周期函数 }
-
componentWillUpdate :
组件更新之前调用一次。当上一部中shouldComponentUpdate方法返回true时候,就可以在该方法中做一些更新之前的操作。
-
render
根据一系列的diff算法,生成页面需要的虚拟DOM结构,并返回该结构。实际表明,在render中,最好只做数据和模板的结合,而不进行state等逻辑的修改,这样组件结构更清晰。
-
componentDidUpdate 组件的更新已经同步到DOM中后出发。
除了首次render之后调用componentDidMount,其它render结束之后都是调用componentDidUpdate。
销毁阶段
-
componentWillUnmount
会在组件即将从挂载点移去时调用,用来取出去除即将被销毁的DOM节点的引用,或者是清除计时器,取消监听的时间等等。
componentWillMount、componentDidMount和componentWillUpdate、componentDidUpdate可以对应起来。区别在于,前者只有在挂载的时候会被调用;而后者在以后的每次更新渲染之后都会被调用。
组件生命周期流程图
组件之间的通信
先创建一个父类组件Parent,它内部调用一个叫做Child的子组件,并将接收到的外部参数name传递给子组件Child。
var Parent = React.createClass({
handleClick: function() {
this.refs.myTextInput.focus();
},
render: function() {
return (
<div onClick={this.handleClick}>
<input type="text" ref="myTextInput" />
Parent is:
<Child name={this.props.name}></Child>
</div>
)
}
});
在创建一个子类组件Child
var Child = React.createClass({
render: function() {
return <span>{this.props.name}</span>
}
});
自后通过React.render方法将组件渲染到页面上
ReactDOM.render(
<Parent name="React" />,document.getElementById('example')
);
运行结果
- 子组件调用父组件的方法。从上面的例子可以看出,子组件要拿到父组件的属性,需要通过this.props方法。所以,如果子组件想要调用父组件的方法,只需要父组件把要被调用的方法以属性的方式放在子组件上,子组件内部便可通过"this.props.被调用的方法"这样的方式拿到name属性的。然后,每次父组件修改了传入的name属性,子组件便会得到通知,然后自动获取新的name属性
- 父组件调用子组件的方法。子组件调用父组件是通过prop属性,而反过来,父组件调用子组件通过的就是 ref 属性。子组件被设置了ref属性之后,父组件便可以通过this.ref.xxx来获取到子组件了,其中xx为子组件的ref值。