其实我的技术栈并不是react,我只是一个从3个月java经验的后台转到前台的新手,差不多一年时间我也看了很多,但是我的方向比较杂,什么都会一点,现在主要研究的是angular2,但是我是去年就接触过react的,所以今天想写点什么。。
在编写react组件时,我们需要处理web事件响应,我们会采用以下几种方式:
1.使用箭头函数
class MyComponent extends React.Component {
render() {
return (
<button onClick={(event)=>{console.log(event.target.nodeName + ' clicked');}}>
Click
</button>
);
}
}
对于这种响应事件比较简单的可以写在标签内,但是逻辑本身比较复杂,就会导致render函数显得比较臃肿,看不出组件内容结构。
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {number: 0};
}
handleClick() {
this.setState({
number: ++this.state.number
});
}
render() {
return (
<div>
<div>{this.state.number}</div>
<button onClick={()=>{this.handleClick();}}>
Click
</button>
</div>
);
}
}
这种方式最大的问题是,当组件的层级越低时,性能开销就越大,因为任何一个上层组件的变化都可能会触发这个组件的render方法。这种方式也有一个好处,就是不需要考虑this的指向问题,因为这种写法保证箭头函数中的this指向的总是当前组件。
2.使用组件方法
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {number: 0,count:0};
// 想想不要这段代码会怎样
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
let count = this.state.count
+new Date()%2==0 && count++
//这里的this并不是只想当前这个类的对象
this.setState({
number: this.state.number += count
});
}
render() {
return (
<div>
<div>{this.state.number}</div>
<button onClick={this.handleClick}>
Click
</button>
</div>
);
}
}
有没有发现这回我们在onclick里面直接写的就是函数了,因为ES6 语法的缘故,ES6 的 Class 构造出来的对象上的方法默认不绑定到 this 上,需要我们手动绑定。
有没有能解决上面麻烦写法的方式呢?
3.属性初始化语法(property initializer syntax)
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {number: 0,count:0,
style:{background:yellow}
};
}
//此时再也不用bind来做任何函数绑定了
handleClick = () => {
let count = this.state.count
let background;
+new Date()%2==0 ? count++ && background = 'green' : background = 'yellow'
//这里的this并不是只想当前这个类的对象
this.setState({
number: this.state.number += count
});
}
render() {
return (
<div>
<div style={this.state.style}>{this.state.number}</div>
<button onClick={this.handleClick}>
Click
</button>
</div>
);
}
}
请原谅我上面代码装逼了,我在div的style里面绑定了state里面对应的style,每次点击根据当前时间戳作出反应,改变背景颜色。需要注意的是如果你是使用官方脚手架Create React App 创建的应用,那么这个特性是默认支持的.
所以在使用回调函数传参时,我们可以这样写
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
list: [1,2,3,4],
current: 1
};
}
handleClick = (item) => {
this.setState({
current: item
});
}
render() {
return (
<ul>
{this.state.list.map(
(item)=>(
<li className={this.state.current === item ? 'current':''}
onClick={this.handleClick(item)}>{item}
</li>
)
)}
</ul>
);
}
}
下面我们来探讨以下react的高阶组件如何书写,对于有一定函数式编程经验的人来说好理解些。高阶组件的定义是类比于高阶函数的定义。高阶函数接收函数作为参数,并且返回值也是一个函数。类似的,高阶组件接收React组件作为参数,并且返回一个新的React组件。
假设我有2个组件需要从localstorage里面取出数据出来渲染。
import React, { Component } from 'react'
class MyComponent1 extends Component {
componentWillMount() {
let data = localStorage.getItem('data');
this.setState({data});
}
render() {
return (
<ul>
this.state.data.map( (item) => (<li> item </li>))
</ul>
)
}
}
class MyComponent2 extends Component {
getInitialState(){
return {'data':localStorage.getItem('data');}
}
render() {
return (
<div>
this.state.data //这里的props.data和上面是一样的,从localstorage里取出来的
</div>
)
}
}
设想一下,如果很多组件都需要从localstorage连去除这样的数据,那我们不得写死。。。所以高阶组件就在这个时候派上用场。
import React, { Component } from 'react'
function withPersistentData(WrappedComponent) {
return class extends Component {
getDefaultprops(){
return {
'name':'sumail'
}
}
componentWillMount() {
let data = localStorage.getItem('data');
this.setState({data});
}
render() {
// 通过{...this.props} 把传递给当前组件的属性继续传递给被包装的组件WrappedComponent
return <WrappedComponent data={this.state.data} {...this.props} />
}
}
}
class MyComponent2 extends Component {
render() {
return <div>{this.props.data} i am {this.props.name}</div>
}
}
const MyComponentWithPersistentData = withPersistentData(MyComponent2)
export default MyComponentWithPersistentData
withPersisitentData函数接受一个react组件作为参数,返回另外一个组件,这个返回的组件相当于时参数组件的父组件。所以任何需要从localstorage中取出data属性的组件都可以通过这个方法加工一下。
考虑如下代码,这样写会有什么问题。
import React, { Component } from 'react'
class person extends Component{
constructor(props){
super(props)
this.setState({'title':'person detail'})
}
render(){
let create = (items) => {
return class extends Component{
componentWillMount() {
let data = items.map( item => item.age > 18 ? Object.assign(item,{'type':'adult'}):
Object.assign(item,{'type':'child'}))
this.setState({data});
}
render(){
return (
<ul>
{data.map( item =>
(<li>I am {item.name} ,a {item.type} </li>)
)
}
</ul>
)
}
}
}
//create方法结尾
return <div>
<h1> here is list of {this.state.title}</h1>
{create(this.props.details)}
<div>
}
}
我们上面同样写了一个高阶组件,不过不同的是这回我把它写在了render函数里面这样会有什么问题呢?
注意事项
不要在组件的render方法中使用高阶组件,尽量也不要在组件的其他生命周期方法中使用高阶组件。因为高阶组件每次都会返回一个新的组件,在render中使用会导致每次渲染出来的组件都不相等(===),于是每次render,组件都会卸载(unmount),然后重新挂载(mount),既影响了效率,又丢失了组件及其子组件的状态。高阶组件最适合使用的地方是在组件定义的外部,这样就不会受到组件生命周期的影响了。
下面呈上一个高阶函数的经典例子
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const fn1 = s => s.toLowerCase();
const fn2 = s => s.split('').reverse().join('');
const fn3 = s => s + '!'
const newFunc = pipe(fn1, fn2, fn3);
const result = newFunc('Time'); // emit!
其实高阶组件还支持装饰器的写法,下面我们看个例子。
import React, { Component } from 'react';
const simpleHoc = WrappedComponent => {
console.log('simpleHoc');
return class extends Component {
render() {
return <WrappedComponent {...this.props}/>
}
}
}
export default simpleHoc;
如何使用装饰器呢?
import React, { Component } from 'react';
import simpleHoc from './simple-hoc';
@simpleHoc
export default class Usual extends Component {
render() {
return (
<div>
Usual
</div>
)
}
}
学过python有没有觉得很赞。。。
refs获取组件实例
当我们包装Usual的时候,想获取到它的实例怎么办,可以通过引用(ref),在Usual组件挂载的时候,会执行ref的回调函数,在hoc中取到组件的实例。通过打印,可以看到它的props, state,都是可以取到的。
import React, { Component } from 'react';
const refHoc = WrappedComponent => class extends Component {
componentDidMount() {
console.log(this.instanceComponent, 'instanceComponent');
}
render() {
return (<WrappedComponent
{...this.props}
ref={instanceComponent => this.instanceComponent = instanceComponent}
/>);
}
};
如果WrappedComponent是一个input输入框,我们可以调用this.instanceComponent.focus()使他聚焦
关于高阶组件我们以一个例子作为结束。
// 普通组件Login
import React, { Component } from 'react';
import formCreate from './form-create';
//如果用的比较熟,可以使用这种方式来写
@formCreate
export default class Login extends Component {
render() {
return (
<div>
<div>
<label id="username">
账户
</label>
<input name="username" {...this.props.getField('username')}/>
</div>
<div>
<label id="password">
密码
</label>
<input name="password" {...this.props.getField('password')}/>
</div>
<div onClick={this.props.handleSubmit}>提交</div>
<div>other content</div>
</div>
)
}
}
看看formCreate里面是怎么写的
const formCreate = WrappedComponent => class extends Component {
constructor() {
super();
this.state = {
fields: {},
}
}
onChange = key => e => {
const { fields } = this.state;
fields[key] = e.target.value;
this.setState({
fields,
})
}
handleSubmit = () => {
console.log(this.state.fields);
}
getField = fieldName => {
return {
onChange: this.onChange(fieldName),
}
}
render() {
const props = {
...this.props,
handleSubmit: this.handleSubmit,
getField: this.getField,
}
return (<WrappedComponent
{...props}
/>);
}
};
export default formCreate;
我们注意{...this.props.getField('username')}这种写法其实是在标签上加了onChange = function (){}
函数内容如下:
e => {
const { fields } = this.state;
fields[key] = e.target.value;
this.setState({
fields,
})
在表单元素每次作出change时都会有事件响应,都会改变state的值,在提交时,会打印出fields的值。
最后一个环节我们来讲一下:
状态提升
function BoilingVerdict(props) {
if (props.celsius >= 100) {
return <p>The water would boil.</p>;
}
return <p>The water would not boil.</p>;
}
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {temperature: ''};
}
handleChange(e) {
this.setState({temperature: e.target.value});
}
render() {
const temperature = this.state.temperature;
return (
<fieldset>
<legend>Enter temperature in Celsius:</legend>
<input
value={temperature}
onChange={this.handleChange} />
<BoilingVerdict
celsius={parseFloat(temperature)} />
</fieldset>
);
}
}
上面是一个对温度作出检测的组件,判断水是否沸腾。
现在有了一个新需求。除了摄氏度的input外,还需要提供一个华氏度的input,且要求两者保持一致。
const scaleNames = {
c: 'Celsius',
f: 'Fahrenheit'
};
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {temperature: ''};
}
handleChange(e) {
this.setState({temperature: e.target.value});
}
render() {
const temperature = this.state.temperature;
const scale = this.props.scale;
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input value={temperature}
onChange={this.handleChange} />
</fieldset>
);
}
}
//现在可以将Calculator修改为渲染两个不同的温度输入框:
class Calculator extends React.Component {
render() {
return (
<div>
<TemperatureInput scale="c" />
<TemperatureInput scale="f" />
</div>
);
}
}
现在我们有两个输入框,但当向其中一个输入框输入数据时,另一个并不会随之改变。而我们的需求是两个输入框保持同步。
另外,在Calculator中也无法显示BoilingVerdict。这是因为当前温度的状态已经被隐藏至了TemperatureInput中,Calculator无法知道当前温度。
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
this.props.onTemperatureChange(e.target.value);
}
render() {
const temperature = this.props.temperature;
const scale = this.props.scale;
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input value={temperature}
onChange={this.handleChange} />
</fieldset>
);
}
}
我们需要一个转换函数用于摄氏度和华氏度之间的转化。
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
function tryConvert(temperature, convert) {
const input = parseFloat(temperature);
if (Number.isNaN(input)) {
return '';
}
const output = convert(input);
const rounded = Math.round(output * 1000) / 1000;
return rounded.toString();
}
//这个函数接收2个参数,一个是温度,一个是条件(摄氏度,华氏度)
lass Calculator extends React.Component {
constructor(props) {
super(props);
this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
this.state = {temperature: '', scale: 'c'};
}
handleCelsiusChange(temperature) {
this.setState({scale: 'c', temperature});
}
handleFahrenheitChange(temperature) {
this.setState({scale: 'f', temperature});
}
render() {
const scale = this.state.scale;
const temperature = this.state.temperature;
const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
return (
<div>
<TemperatureInput
scale="c"
temperature={celsius}
onTemperatureChange={this.handleCelsiusChange} />
<TemperatureInput
scale="f"
temperature={fahrenheit}
onTemperatureChange={this.handleFahrenheitChange} />
<BoilingVerdict
celsius={parseFloat(celsius)} />
</div>
);
}
}
这个状态改变的关键所在是作出了类型判断,2个温度组件个又一个对应的onchange处理程序,从而来改变父组件的state。
小结
React应用中任何数据的改变都应遵从 单一数据源 原则。
通常情况下,状态应被优先加入到渲染时依赖该值的组件中。但当有其他组件也依赖于该状态时,我们可以将其提升至组件们的最近公共祖先组件中,而不是尝试同步不同组件间的状态。我们应该遵守数据流的从上至下的原则。
网友评论