深入JSX
date:20170412
笔记原文
其实JSX是React.createElement(component, props, ...children)
的语法糖。
JSX代码:
<MyButton color="blue" shadowSize={2}>
Click Me
</MyButton>
将会被编译为
React.createElement(
MyButton,
{color: 'blue', shadowSize: 2},
'Click Me'
)
如果没有包含子标签,你也可以用自闭标签:
<div className="sidebar" />
将被编译为
React.createElement(
'div',
{className: 'sidebar'},
null
)
在线Bable编译器可以测试你的JSX代码将会被编译成的JS代码。
指定React 元素类型
JSX标签的第一部分决定了React元素的类型。类型首字母大写说明引用的是React组件,而不是html标签。在当前文件中,应该引入使用的组件。
React 必须要在上下文中
例如:
import React from 'react';
import CustomButton from './CustomButton';
function WarningButton() {
// return React.createElement(CustomButton, {color: 'red'}, null);
return <CustomButton color="red" />;
}
导入React和CustomButton都是必须的。
在JSX类型中,使用.表达式
在JSX中,也可以用点表达式来引用React组件。如果你在一个类型中声明了很多React组件,这会方便引用。
import React from 'react';
const MyComponents = {
DatePicker: function DatePicker(props) {
return <div>Imagine a {props.color} datepicker here.</div>;
}
}
function BlueDatePicker() {
return <MyComponents.DatePicker color="blue" />;
}
自定义组件名首字母必须大写
如果类型首字母是小写时,说明它是一个内建的组件就像div
和span
。类型首字母大写的时候,将会编译为React.createElement(Foo)
,并且对应于在JS中定义的组件。
我们建议将自定义组件的首字母大写。如果组件名是小写的时候,那必须在使用之前,赋值给大写变量。
这是一个错误的案例。
import React from 'react';
// Wrong! This is a component and should have been capitalized:
function hello(props) {
// Correct! This use of <div> is legitimate because div is a valid HTML tag:
return <div>Hello {props.toWhat}</div>;
}
function HelloWorld() {
// Wrong! React thinks <hello /> is an HTML tag because it's not capitalized:
return <hello toWhat="World" />;
}
为了修改这个bug,我们要将小写组件名变为大写;
import React from 'react';
// Correct! This is a component and should be capitalized:
function Hello(props) {
// Correct! This use of <div> is legitimate because div is a valid HTML tag:
return <div>Hello {props.toWhat}</div>;
}
function HelloWorld() {
// Correct! React knows <Hello /> is a component because it's capitalized.
return <Hello toWhat="World" />;
}
在运行的时候选择类型
要实现这样的功能,下面的代码是不可以的:
import React from 'react';
import { PhotoStory, VideoStory } from './stories';
const components = {
photo: PhotoStory,
video: VideoStory
};
function Story(props) {
// Wrong! JSX type can't be an expression.
return <components[props.storyType] story={props.story} />;
}
要想通过prop来决定渲染哪个组件,必须先要把它赋值给一个大写变量:
import React from 'react';
import { PhotoStory, VideoStory } from './stories';
const components = {
photo: PhotoStory,
video: VideoStory
};
function Story(props) {
// Correct! JSX type can be a capitalized variable.
const SpecificStory = components[props.storyType];
return <SpecificStory story={props.story} />;
}
JSX中的Props
在JSX中指定props有几种方法
JavaScript 表达式
可以在JSX中插入任何的JS表达式,例如:
<MyComponent foo={1 + 2 + 3 + 4} />
这里MyComponent
组件的foo
属性九是10,因为表达式已经计算好了。
if
和for
语句不是表达式。所以不能直接使用。但是我们可以用变量来存储代码。
function NumberDescriber(props) {
let description;
if (props.number % 2 == 0) {
description = <strong>even</strong>;
} else {
description = <i>odd</i>;
}
return <div>{props.number} is an {description} number</div>;
}
字符串字面量
我们可以直接将字符串赋值给props。以下两种方法是一样的:
<MyComponent message="hello world" />
<MyComponent message={'hello world'} />
如果直接传递字符串,HTML的标签要转义。
<MyComponent message="<3" />
<MyComponent message={'<3'} />
Props默认为 True
如果不在props中传值,默认是true。以下两个表达式是一致的:
<MyTextBox autocomplete />
<MyTextBox autocomplete={true} />
通常,我们不建议用这种方法,因为按照ES6对象简写中的语法,{foo}
表示的是{foo: foo}
而不是{foo:true}
。这种逻辑之所以存在,是因为符合HTML的逻辑。
属性展开
如果你有一个props
对象,并且想要将它传递给JSX,你可以使用...
来展开对象。以下代码效果是一致的:
function App1() {
return <Greeting firstName="Ben" lastName="Hector" />;
}
function App2() {
const props = {firstName: 'Ben', lastName: 'Hector'};
return <Greeting {...props} />;
}
这个方法,在构建自定义的组件的时候很有用。但是也会使代码变乱,因为这样做也会把不需要的变量也传递进去。所以这个特性也得谨慎的使用。
JSX中的子类
在JSX表达式中,也会有开放的标签和封闭的标签。但是在封闭标签之中的内容会通过props.children
传递给组件。以下是传递子类的几种方法。
字符串字面量
直接将字符串用标签包裹起来,那么props.children
就是该字符串。这个写法很常见。
<MyComponent>Hello world!</MyComponent>
MyComponent
组件里的props.children
就是字符串Hello world!
。HTML需要转义。
<div>This is valid HTML & JSX at the same time.</div>
JSX会将行首和行尾的空白字符删掉,也会把空白行删除。和标签毗邻的空行会被删除;在字符串之间的空行也会被删除。以下的代码渲染结果都是一样的。
<div>Hello World</div>
<div>
Hello World
</div>
<div>
Hello
World
</div>
<div>
Hello World
</div>
JSX后代
可以直接将JSX元素作为子代传入。在嵌套渲染的时候很有用:
<MyContainer>
<MyFirstComponent />
<MySecondComponent />
</MyContainer>
两种类型混合也是可以的。
<div>
Here is a list:
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
</div>
一个React组件不能返回多个React元素,但是一个简单的JSX表达式可以拥有很多个后代。所以如果想要渲染多个视图,可以把这些视图用<div>
包裹起来。
javascript表达式
可以嵌入任何JavaScript表达式,但是要用{}
包裹起来。以下两种写法是一致的:
<MyComponent>foo</MyComponent>
<MyComponent>{'foo'}</MyComponent>
这个写法对于渲染一个任意长度的列表视图是很有用的。例如:
function Item(props) {
return <li>{props.message}</li>;
}
function TodoList() {
const todos = ['finish doc', 'submit pr', 'nag dan to review'];
return (
<ul>
{todos.map((message) => <Item key={message} message={message} />)}
</ul>
);
}
js表达式可以和其他类型的混合起来。通常在用变量替代字符串。
function Hello(props) {
return <div>Hello {props.addressee}!</div>;
}
传递函数作为后代
通常JSX中的js表达式会当作字符串,React元素或者一个数组。然而,props.children
就像其他的属性一样,可以传递任何数据。例如,我们可以在自定义的组件中,传递一个回调函数:
// Calls the children callback numTimes to produce a repeated component
function Repeat(props) {
let items = [];
for (let i = 0; i < props.numTimes; i++) {
items.push(props.children(i));
}
return <div>{items}</div>;
}
function ListOfTenThings() {
return (
<Repeat numTimes={10}>
{(index) => <div key={index}>This is item {index} in the list</div>}
</Repeat>
);
}
这种用法是不太常见。
Boolean Null 和Undifined会被忽略
false
,null
,undefined
和true
都是合理的后代。但是通常他们不会被渲染。以下代码渲染的结果都是一样的:
<div />
<div></div>
<div>{false}</div>
<div>{null}</div>
<div>{undefined}</div>
<div>{true}</div>
在条件渲染的时候是很有用的。例如:
<div>
{showHeader && <Header />}
<Content />
</div>
只有在showHeader
为true的时候,才会渲染<Header>
。
需要注意的是,0
不是false
。例如:
<div>
{props.messages.length &&
<MessageList messages={props.messages} />
}
</div>
要修改这个bug,可以这么改:
<div>
{props.messages.length > 0 &&
<MessageList messages={props.messages} />
}
</div>
相反的,如果想要渲染false
,true
,null
或者undefined
的时候,需要先转换为String。
<div>
My JavaScript variable is {String(myVariable)}.
</div>
DOM和Refs
date:20170413
原文链接
按照典型的React数据流,[props][react-props]是父控件影响子控件的唯一方式。如果要修改子控件,那就必须用新的prop重新渲染。但有时候我们需要另外的方法才能满足需求。
什么时候使用Refs
refs的使用场景:
- 焦点管理,文本选择或者媒体回放
- 触发动画
- 集成第三方库
我们不应该滥用refs。
例如,在控制对话框的显示上,我们不应该暴露open()
或者close()
接口,用isOpen
的props属性就好了。
在DOM元素中添加Ref
React 通过ref
属性来传递一个回调函数,这个回调函数在组件的加载和卸载的时候马上执行。当在HTML元素中使用ref
的时候,当前的HTML元素回作为参数传递给它。例如:
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
this.focus = this.focus.bind(this);
}
focus() {
// Explicitly focus the text input using the raw DOM API
this.textInput.focus();
}
render() {
// Use the `ref` callback to store a reference to the text input DOM
// element in an instance field (for example, this.textInput).
return (
<div>
<input
type="text"
ref={(input) => { this.textInput = input; }} />
<input
type="button"
value="Focus the text input"
onClick={this.focus}
/>
</div>
);
}
}
以上代码中,React会在转载的时候马上回调ref
回调函数,对textInput
进行初始化,然后在卸载的时候将textInput
设置为null
。
使用ref
回调是操作DOM的一种固定模式。以上的有关refs的代码也可以简写为ref={input => this.textInput = input}
。
在类组件中定义Ref
当ref
属性用在通过类定义的组件里的时候,ref
获取到的是加载的类组件。例如:
class AutoFocusTextInput extends React.Component {
componentDidMount() {
this.textInput.focus();
}
render() {
return (
<CustomTextInput
ref={(input) => { this.textInput = input; }} />
);
}
}
这里获取到的就是CustomTextInput
组件。
需要注意的是,这只有在CustomTextInput
声明为类的时候才有效果。
Refs和函数组件
在函数定义的组件中是不可以使用ref
的,因为它不具有实例。
这个例子就不会有效:
function MyFunctionalComponent() {
return <input />;
}
class Parent extends React.Component {
render() {
// 这里就会**出错**
return (
<MyFunctionalComponent
ref={(input) => { this.textInput = input; }} />
);
}
}
你必须将它转换为类函数,就像需要生命周期和state
一样。这些都只能出现在类组件的特性。
但是你可以在函数组件中的DOM元素或者类组件中使用。
function CustomTextInput(props) {
// textInput must be declared here so the ref callback can refer to it
let textInput = null;
function handleClick() {
textInput.focus();
}
return (
<div>
<input
type="text"
ref={(input) => { textInput = input; }} />
<input
type="button"
value="Focus the text input"
onClick={handleClick}
/>
</div>
);
}
不能滥用Refs
如果需要在app中使用ref
,那就有必要花点时间想想state应该在组件关系中的那个层级。通常是在较高的层级(父级)。参见提升state
遗留的API:字符串Refs
以前的版本中可以通过this.refs.textInput
来引用,但是这会引起问题。这个api将来会在某个版本移除,所以应该用回调的模式使用ref
。
要注意的陷阱
如果ref
通过单行函数定义的时候,在更新的时候会执行两次。第一次是null
,第二次正常。这是因为react更新的时候会重新生成一个组件实例,所以之前的实例销毁,设置为null
,当创建了新的实例之后,又传递进来。你可以通过定义方法(a bound method,非单行函数)来避免这个问题,但是通常情况下并不影响。
非控制组件
date:20170414
笔记原文
通常情况下,我们推荐使用控制组件来实现表单。在控制组件中,React组件来处理表单数据,但是在非控制组件中,DOM自己来处理组件。
在实现非控制组件的时候,我们不用事件监听来获取state的变化,我们使用ref从DOM中获取数据。举个例子,通过非控制组件来获取姓名:
class NameForm extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(event) {
alert('A name was submitted: ' + this.input.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" ref={(input) => this.input = input} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
因为非控制组件直接保存的是DOM元素,所以有时候会很方便的将React和非React组件结合起来。
如果你对什么时候使用那种类型的组件还不太清楚的时候,可以参看控制vs非控制组件文章。
默认值
在React的生命周期里,表单元素中的value
值会覆盖DOM的值。在非控制组件中,常常需要指定初始值,这种情况下,可以直接指定defaultValue
属性。
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input
defaultValue="Bob"
type="text"
ref={(input) => this.input = input} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
另外,<input type="checkbox">
和<input type="radio">
支持defaultChecked
属性,<select>
和<textarea>
支持defaultValue
属性。
性能优化
date:20170418
React使用了一些技术技巧来减少DOM操作的开销。对于大多数应用来说,不需要优化就已经能够快速的呈现用户界面。尽管如此,这里也有几种方法来加速React应用。
使用发布版本
如果你在测试app性能,那么确认使用最小化过的发布版本,我一开始选用了Create-React-App的构建工具,所以这里我只记录这个工具的使用。原文中还有其他工具的使用方法。
- 在Create React App 中,我们使用
npm run build
命令。
因为版本有很多调试信息会使得app变慢,所以很有必要使用发布版本来做新能分析和产品发布。
组件在chrome中的表现
在开发版本模式,我们使用性能工具能够以图表的方式查看加载组件,更新组件和卸载组件的性能。
- 在url后面添加
?react_perf
- 打开Chrome DevTool
TimeLine
选项卡,并点击Record
。 - 开始操作app,事件不要超过20s,不然会导致Chrome挂起。
- 停止记录。
- 追踪到的记录会在
User Timing
选项卡中显示。
需要注意到的是这些数值都是相对的,在发布版本中可能会渲染得更快。但是你可以发现一些错误,比如渲染了无关的模块。还可以发现模块更新的频繁程度。
目前这个功能只能在Chrome,Edge和IE傻姑娘支持,不过可以使用User Timing API来支持更多的浏览器。
避免视图刷新
React使用了自己的一套渲染UI的方法。React不需要创建DOM结点,也不需要操作它们,因为操作dom比操作js对象慢。这里用的方法就是虚拟DOM。
当组件的props或者state变化的时候,react会对比返回的元素和之前的元素,如果不同,就会更新视图。
有时候,你也可以将这个过程加速:复写生命周期方法shouldComponentUpdate
,这个回调会在渲染过程开始之前回调。默认的实现如下,始终返回true
,来刷新视图:
shouldComponentUpdate(nextProps, nextState) {
return true;
}
如果有些情况下,你确定不需要刷新的时候,那就直接返回false
,来避免刷新。
使用 shouldComponentUpdate 生命周期方法
原文在这里用一个例子来说明界面更新的逻辑。这里就简单总结下:如果shouldComponentUpdate
返回true
,就去对比状态是否改变,如果改变了,就刷新,没有改变就不刷新。shouldComponentUpdate
返回false
,那就说没有必要刷新了,也不会有比较状态这个步骤了。
举例
如果你的组件,只有在props.color
或state.count
变化的情况下才需要刷新界面,你必须要在shouldComponentUpdate
中检查:
class CounterButton extends React.Component {
constructor(props) {
super(props);
this.state = {count: 1};
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props.color !== nextProps.color) {
return true;
}
if (this.state.count !== nextState.count) {
return true;
}
return false;
}
render() {
return (
<button
color={this.props.color}
onClick={() => this.setState(state => ({count: state.count + 1}))}>
Count: {this.state.count}
</button>
);
}
}
这种模式很简单,反正就检查下关心的变量。React也提供了实现这个模式的辅助类,React.PureComponent
。以下的代码效果与之前一样。
class CounterButton extends React.PureComponent {
constructor(props) {
super(props);
this.state = {count: 1};
}
render() {
return (
<button
color={this.props.color}
onClick={() => this.setState(state => ({count: state.count + 1}))}>
Count: {this.state.count}
</button>
);
}
}
大多数情况下,就使用React.PureComponent
就可以了,不用自己实现shouldComponentUpdate
。
这里原文提到了一个浅对比(shallow comparsion)
,对比文章后面的内容,我理解为:对比变量的引用,而不是数据。例如数组,并不对比数组中的每个值,而是这个数组引用。
但是,对于复杂的数据结构就会有问题了。例如有个ListOfWords
的控件用于渲染逗号隔开的一组词汇,并放置在WordAdder
的控件中。WordAdder
的功能就是通过点击按钮,给ListOfWords
添加单词。以下的代码存在一个bug:
class ListOfWords extends React.PureComponent {
render() {
return <div>{this.props.words.join(',')}</div>;
}
}
class WordAdder extends React.Component {
constructor(props) {
super(props);
this.state = {
words: ['marklar']
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// This section is bad style and causes a bug
const words = this.state.words;
words.push('marklar');
this.setState({words: words});
}
render() {
return (
<div>
<button onClick={this.handleClick} />
<ListOfWords words={this.state.words} />
</div>
);
}
}
这个问题是由于PrueComponent
只是简单的比较了this.props.words
,由于数组的引用不变,得出的比较是相等的,尽管数组的内容已经变了。
不要直接改变数据
要避免这个问题,最简单的方法就是,避免直接改变props和state的值。例如,上面的handleClick
方法可以重写成这样:
handleClick() {
this.setState(prevState => ({
words: prevState.words.concat(['marklar'])
}));
}
对于操作数组,ES6有个展开语法,使用起来会比较简单。在Create React App 脚手架里,这个功能是默认可用的,其他脚手架就不知道了。
handleClick() {
this.setState(prevState => ({
words: [...prevState.words, 'marklar'],
}));
};
对于对象,我们也可以使用类似的方法来避免突变。例如,以下的方法:
function updateColorMap(colormap) {
colormap.right = 'blue';
}
需要修改为:
function updateColorMap(colormap) {
return Object.assign({}, colormap, {right: 'blue'});
}
现在updateColorMap直接返回一个新的对象,而不是在原来的对象上修改。
有个JS提议,希望添加对象属性展开语法,那么代码就可以修改为下面的样子了:
function updateColorMap(colormap) {
return {...colormap, right: 'blue'};
}
现在是不可以直接这么写的。
使用不可变的数据结构
Immutable.js是另外一种解决问题的方法。它通过结构分享,提供了永久的,不可变的集合。
- 不可变:一旦集合创建了,就不会再改变了
- 永久:新的集合可以从以前的集合创建出来。但是原来的集合也是可以使用的。
- 结构共享:新的集合会使用原来的数据结构,以避免拷贝数据影响细性能。
这种不可变的特性使得追踪数据变化变得很简单。因为数据的变化总是会导致对象的变化,所以我们就只需要对比引用有没有变化。例如:
const x = { foo: 'bar' };
const y = x;
y.foo = 'baz';
x === y; // true
这里,尽管y
已经改变了,但是和x
的引用一样,所以,对比表达式返回的还是true
。用immutablejs
重写的代码如下:
const SomeRecord = Immutable.Record({ foo: null });
const x = new SomeRecord({ foo: 'bar' });
const y = x.set('foo', 'baz');
x === y; // false
另外两个类似的库是seamless-immutable和immutability-helper.
不可变这一机理可以让我们很方便的跟踪数据变化,同时也提升性能有很大的帮助。
不使用ES6时的React
date:20170420
很多时候,我们会通过类的形式定义组件:
class Greeting extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
如果你还没有开始使用ES6,那么你可以使用Create-react-class
模块:
var createReactClass = require('create-react-class');
var Greeting = createReactClass({
render: function() {
return <h1>Hello, {this.props.name}</h1>;
}
});
ES6的API和createReactClass()
是一样的效果。
定义默认Props
在类组件中,我们可以将默认的属性像组件的属性一样定义:
class Greeting extends React.Component {
// ...
}
Greeting.defaultProps = {
name: 'Mary'
};
如果通过creatReactClass()
方法,你需要定义getDefaultProps()
函数:
var Greeting = createReactClass({
getDefaultProps: function() {
return {
name: 'Mary'
};
},
// ...
});
设置初始状态
在ES6中,你可以在构造函数中对this.state
赋值,来定义初始状态。
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {count: props.initialCount};
}
// ...
}
如果通过creatReactClass()
方法,你需要定义getInitialState()
函数:
var Counter = createReactClass({
getInitialState: function() {
return {count: this.props.initialCount};
},
// ...
});
自动绑定
在ES6定义的组件中,方法与ES6的类具有相同的语义。这说明,我们不会给实例自动绑定this
,我们需要在构造函数中手动的调用.bind(this)
来绑定。
class SayHello extends React.Component {
constructor(props) {
super(props);
this.state = {message: 'Hello!'};
// This line is important!
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
alert(this.state.message);
}
render() {
// Because `this.handleClick` is bound, we can use it as an event handler.
return (
<button onClick={this.handleClick}>
Say hello
</button>
);
}
}
在createReactClass()
中,我们就不需要手动绑定了,那都是自动完成的:
var SayHello = createReactClass({
getInitialState: function() {
return {message: 'Hello!'};
},
handleClick: function() {
alert(this.state.message);
},
render: function() {
return (
<button onClick={this.handleClick}>
Say hello
</button>
);
}
});
这意味着ES6类会在定义事件处理函数上都写些代码,但是这样做的好处是能够提升大型应用的性能。如果这烦人的代码对你没有吸引力,你可以在Babel中开启实验的类属性语法提议。
class SayHello extends React.Component {
constructor(props) {
super(props);
this.state = {message: 'Hello!'};
}
// WARNING: this syntax is experimental!
// Using an arrow here binds the method:
handleClick = () => {
alert(this.state.message);
}
render() {
return (
<button onClick={this.handleClick}>
Say hello
</button>
);
}
}
需要注意的是,这个功能还在实验阶段,语法可能会改变,或者提议会被放弃。
如果你想要安全些,你有几个注意的地方:
- 在构造函数中绑定
- 使用箭头函数,
onClick=>{(e) => this.handleClick(e)}
. - 使用
createReactClass
Mixins
注意:
ES6并不支持Mixins,所以在React的ES6类中,不支持任何的Mixins.
我们发现在使用Mixins之后会有很多问题,所以我们不推荐使用。
这个章节只是为了提及这一点。
有时候,个别组件需要公用一些共同的函数。这涉及到了交叉剪切?(cross-cutting)的概念。createReactClass
了一使用遗留的mixins
机制来实现。
一个很常见的用法是,组件需要通过interval来更新自己。使用setInterval()
很简单,但是要在不需要定时的时候取消定时,来减少内存的消耗。React提供了生命周期方法让你知道组件的创建和销毁。举个例子:
var SetIntervalMixin = {
componentWillMount: function() {
this.intervals = [];
},
setInterval: function() {
this.intervals.push(setInterval.apply(null, arguments));
},
componentWillUnmount: function() {
this.intervals.forEach(clearInterval);
}
};
var createReactClass = require('create-react-class');
var TickTock = createReactClass({
mixins: [SetIntervalMixin], // Use the mixin
getInitialState: function() {
return {seconds: 0};
},
componentDidMount: function() {
this.setInterval(this.tick, 1000); // Call a method on the mixin
},
tick: function() {
this.setState({seconds: this.state.seconds + 1});
},
render: function() {
return (
<p>
React has been running for {this.state.seconds} seconds.
</p>
);
}
});
ReactDOM.render(
<TickTock />,
document.getElementById('example')
);
如果在组件中的几个生命周期方法中定义了多个mixins方法,所有的生命周期方法都会按顺序被执行。
好了,这个Mixins是被废弃了,也不需要做过多的了解。
不使用JSX时的React
date:20170421
原文链接
在使用React的时候,JSX也不是必须。当你不想配置jsx的编译环境的时候,不使用JSX会尤其方便些。JSX是React.createElement(component,props,...children)
的语法糖,所以不使用JSX的时候,可以直接使用这个底层的方法。
例如,用JSX的代码如下:
class Hello extends React.Component {
render() {
return <div>Hello {this.props.toWhat}</div>;
}
}
ReactDOM.render(
<Hello toWhat="World" />,
document.getElementById('root')
);
编译为非JSX版本的代码如下:
class Hello extends React.Component {
render() {
return React.createElement('div', null, `Hello ${this.props.toWhat}`);
}
}
ReactDOM.render(
React.createElement(Hello, {toWhat: 'World'}, null),
document.getElementById('root')
);
如果你对JSX如何编译为JavaScript很好奇,那么你可以看看在线Babel编译器.组件可以是字符串,也可以是React.Component
的子类,也是可以一个不包含state的函数组件。
如果你觉得React.createElement
输入太多,一个简单的方法是将它赋值给简短的变量:
const e = React.createElement;
ReactDOM.render(
e('div', null, 'Hello World'),
document.getElementById('root')
);
看吧,这也跟使用JSX一样方便了,对吧。
视图刷新
date:20170421
原文链接
React提供了一组API使得我们可以不需要完全了解每次界面更新。这使得编码简单化了,但是我们不知道里边的原理。这篇文章就来介绍下为什么我们会选择这么奇特的逻辑。
动机
使用React的时候,在某个简单的点上,调用render()
会生成一个树状的React元素。当props或者state更新的时候,render()
返回新的React元素。这时候React就需要快速有效的找到需要跟新的元素。
对于这个逻辑问题,有许多普通的解决方法:找到变换UI的最小的操作次数。但是这个逻辑的复杂度是O(n3),n是渲染树中的元素。
如果我们在React中使用了1000个元素,那么将要进行10亿次比较。开销太大。React基于两个假设,使用的是O(n)的逻辑:
1. 两个不同的元素渲染不同的界面。
2. 开发者通过key
属性,提示是否要刷新。
在实际运用中,这些假设几乎在所有的情况下都可行。
对比差异的逻辑
获取到两个树之后,React首先会比较两个根元素。之后的对比会根据根元素的不同有些差异。
不同类型的元素
如果根目录的元素不一样的时候,React会重新渲染整个树。从<a>
变为<img>
,<Article>
变为<Comment>
,<Button>
变为<div>
。所有的这些都会导致重新构建。
当出去了原来的渲染树的时候,所有的元素会被销毁,回调componentWillUnmount()
。新的DOM会插入。新的组件回调componentWIllMount()
,然后componentDidMount()
。所有以前的元素,将会消失。
所有根元素下的组件会卸载,状态会销毁。例如:
<div>
<Counter />
</div>
<span>
<Counter />
</span>
旧的Counter会销毁,新的Counter会挂载上去。
相同类型的DOM元素
对比相同类型的元素的时候,React会查看元素的所有属性,保留相同的,更新变化的。例如:
<div className="before" title="stuff" />
<div className="after" title="stuff" />
对比上面的两个组件,React只会更新组件的类名。
当更新样式的时候,React也只会更新变化的部分:
<div style={{color: 'red', fontWeight: 'bold'}} />
<div style={{color: 'green', fontWeight: 'bold'}} />
这里,React只会更新color
属性,不会更新fontWeight
属性。
在父节点更改之后,会遍历子节点。
相同类型的组件元素
当组件更新的时候,并不重新创建新的元素。所以组件的状态都会保持。React直接更新组件的Props,而不会去比较它。组件回调componentWillReceiveProps()
和componentWillUpdate()
方法。
接着,回调render()
函数,然后再对比组件中的差异。
子组件对比逻辑
默认情况下,React会同时遍历新旧组件的子组件,当对比出差异的时候,直接更改。
例如,我们在列表底部添加子元素的时候:
<ul>
<li>first</li>
<li>second</li>
</ul>
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
我们会对比两个<li>first</li>
,对比<li>second</li>
,然后在插入<li>third</li>
。如果你直接在第一项中添加了一项,那么性能就会很糟糕:
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
React并不会意识到其中的两项不必更新,所以这个列表的更新就会比较低效。
keys
React通过key参数,来解决上述列表更新的问题:通过比较key来决定是否更新列表。
<ul>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
<ul>
<li key="2014">Connecticut</li>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
这样,React就可以发现,只有key为2014
的元素是新的,另外两个只要移动下位置就可以了。
实际上,给key赋值也是很简单的。很多时候数据本身带有唯一的id。
<li key={item.id}>{item.name}</li>
如果没有这个key,你可以添加一个属性,或是进行hash运算,计算出一个唯一的值。键值在列表中必须是唯一的,但是在全局中可以不同。你也可以用数据项的索引来充当键值。如果不重新排序,这个做法没有什么问题,但是要重新排序就会有问题了。
总结(tradeoffs)
熟悉React的更新逻辑是很重要的。在触发每个动作之后,React会重新渲染整个app,尽管有些时候UI并没有变化。
在当前的实现中,你可以解释子树在同级间的移动,但是不知道移动到哪里了。这套算法会重新渲染整个子树。
由于React的这个实现逻辑是发散式的,如果这个实现逻辑背后的假设并不成立的话,性能就会很差。
- 这个算法并不会比较不同类型组件的子组件。如果你的两个子组件很相似,并且经常切换,那么需要将它们合二为一。在实际应用中,我们也没有发现什么问题。
- key必须要稳定的,可以预测的以及唯一的。不稳定的key(通过
Math.random()
生成的key)将会导致很多组件实例和DOM结点没有必要的重建,影响性能,同时状态也不会保留下来。
上下文
date:20170422
笔记原文
注意:由于在React v15.5中
React.PropTypes
已经废弃了,所以我们推荐使用prop-types
库来定义contextTypes
.
React可以很容易地跟踪数据流。当你查看一个组件的时候,你可以查看传递了哪个prop,这可以很容易找到问题。
有时候,你不需要通过传递props来传递数据。你可以通过上下文传递数据。
为什么不使用上下文
很多app并不需要使用上下文。
- 如果你想让app更加稳定,就不要使用上下文。这个一个试验功能,可能在以后的版本中废除。
- 如果你不熟悉状态管理库
Redux
或者Mobx
,就不要使用context。推荐使用Redux
,因为很多组件都基于这些三方库。 - 如果你不是经验丰富的React开发者,就不要用context。
- 如果你一定要使用context,那么将代码分离,限定在小范围内。这有利于升级API。
如何使用Context
假设你有如下的结构:
class Button extends React.Component {
render() {
return (
<button style={{background: this.props.color}}>
{this.props.children}
</button>
);
}
}
class Message extends React.Component {
render() {
return (
<div>
{this.props.text} <Button color={this.props.color}>Delete</Button>
</div>
);
}
}
class MessageList extends React.Component {
render() {
const color = "purple";
const children = this.props.messages.map((message) =>
<Message text={message.text} color={color} />
);
return <div>{children}</div>;
}
}
在这个例子中,我们通过props传递color参数。如果使用context,那么代码如下:
const PropTypes = require('prop-types');
class Button extends React.Component {
render() {
return (
<button style={{background: this.context.color}}>
{this.props.children}
</button>
);
}
}
Button.contextTypes = {
color: PropTypes.string
};
class Message extends React.Component {
render() {
return (
<div>
{this.props.text} <Button>Delete</Button>
</div>
);
}
}
class MessageList extends React.Component {
getChildContext() {
return {color: "purple"};
}
render() {
const children = this.props.messages.map((message) =>
<Message text={message.text} />
);
return <div>{children}</div>;
}
}
MessageList.childContextTypes = {
color: PropTypes.string
};
通过在MessageList
中添加childContextTypes
和getChildContext
。React自动传递了参数,任何子组件可以通过contextTypes
获取参数。
如果contextTypes
没有定义,那么context
就会为空。
父组件与子组件间通讯
Context可以实现父子组件间的通讯。例如,React Router V4就是按照这个方法来的。
import { BrowserRouter as Router, Route, Link } from 'react-router-dom';
const BasicExample = () => (
<Router>
<div>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About</Link></li>
<li><Link to="/topics">Topics</Link></li>
</ul>
<hr />
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/topics" component={Topics} />
</div>
</Router>
);
通过Router
组件向下传递了一些信息,每个Link
和Route
都可以向Router传递信息。
当你这样的方式实现逻辑的时候,考虑下是否有个更加清楚的替代方法。例如你可以通过props传递React参数。
在生命周期函数中引用Context
如果在组件中定义了contextTypes
,以下的生命周期方法,将会获取到context
对象。
constructor(props,context)
componentWillReceiveProps(nextProps,nextContext)
shouldComponentUpdate(nextProps,nextState,nextContext)
componentWillUpdate(nextProps,nextState,nextContext)
componentDidUpdate(preProps,prevState,prevContext)
在不包含状态的函数组件中引用Context
如果定义了函数组件的contextTypes
参数,也可以使用context
。例如下面的Button
组件:
const PropTypes = require('prop-types');
const Button = ({children}, context) =>
<button style={{background: context.color}}>
{children}
</button>;
Button.contextTypes = {color: PropTypes.string};
更新Context
千万别更新Context。React提供了API来更新上下文。
当state或者props更新的时候,getChildContext
函数会被回调,来更新context中的数据。state的更新使用this.setState
方法。这会触发生成新的context,自组件也会获取到新的数据。
const PropTypes = require('prop-types');
class MediaQuery extends React.Component {
constructor(props) {
super(props);
this.state = {type:'desktop'};
}
getChildContext() {
return {type: this.state.type};
}
componentDidMount() {
const checkMediaQuery = () => {
const type = window.matchMedia("(min-width: 1025px)").matches ? 'desktop' : 'mobile';
if (type !== this.state.type) {
this.setState({type});
}
};
window.addEventListener('resize', checkMediaQuery);
checkMediaQuery();
}
render() {
return this.props.children;
}
}
MediaQuery.childContextTypes = {
type: PropTypes.string
};
这有个问题是,如果context跟新的时候,子组件如果在shouldComponentUpdate
中返回了false
就不会自动跟新了。这就会使得自组件使用context失去控制。所以没有基础的方法来更新context
。这篇博客详细阐述了这个问题的缘由以及如何去绕过这个问题。
web组件
date: 20170422
React和web组件用于解决不同的问题。web组件可以提供高度封装好的,可以重用的组件。然而React提供了声明好的库。它们相辅相成。开发者可以自由的在web组件中使用React,或者在React中使用web组件。
很多人会使用React,但是不使用web组件。但是你可能使用了用web组件实现的第三方库。
在React中使用web组件
class HelloMessage extends React.Component {
render() {
return <div>Hello <x-search>{this.props.name}</x-search>!</div>;
}
}
注意:
web组件可能会提供一组接口。例如video
web组件需要暴露play()
和pause()
方法。如果要使用这些API,那么你要做下封装,然后可以直接控制DOM结点。如果你使用第三方的web组件,最好的方法是用React来封装web组件。
web组件触发的事件可能不会被React捕捉到。你需要自己实现事件监听。
有个容易出错的地方就是在web组件中使用class
而不是className
.
function BrickFlipbox() {
return (
<brick-flipbox class="demo">
<div>front</div>
<div>back</div>
</brick-flipbox>
);
}
在web组件中使用React
const proto = Object.create(HTMLElement.prototype, {
attachedCallback: {
value: function() {
const mountPoint = document.createElement('span');
this.createShadowRoot().appendChild(mountPoint);
const name = this.getAttribute('name');
const url = 'https://www.google.com/search?q=' + encodeURIComponent(name);
ReactDOM.render(<a href={url}>{name}</a>, mountPoint);
}
}
});
document.registerElement('x-search', {prototype: proto});
高阶组件
date:20170426
高阶组件(HOC)是React重用组件的高级技术。HOC不是React API的一部分,而是React封装的一种模式。
其实,HOC是一个能够将一个组件变为另一个组件的函数。
const EnhancedComponent = higherOrderComponent(WrappedComponent);
一个组件,将属性展示为UI;而一个高阶组件,将一个组件变为另一个组件。HOC在三方库中非常常见。例如Redux的connect
和Relay的createContainer
.
在这个文档中,我们将解释为什么HOC会很有用处,并且如何去实现自己的HOC。
HOC的横切关注点
注意:
我们之前会推荐使用mixins来处理横切关注点。我们后来意识到mixins会导致很多bug。详情参见这篇博客,阐述废弃mixins的原因以及如何更改原有代码。
组件是React里的最初单元。但是有时候你会发现有些模式使用传统的组件并不能满足条件。
例如,你有一个CommentList
组件,监听外部的数据来渲染列表。
class CommentList extends React.Component {
constructor() {
super();
this.handleChange = this.handleChange.bind(this);
this.state = {
// "DataSource" is some global data source
comments: DataSource.getComments()
};
}
componentDidMount() {
// Subscribe to changes
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
// Clean up listener
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
// Update component state whenever the data source changes
this.setState({
comments: DataSource.getComments()
});
}
render() {
return (
<div>
{this.state.comments.map((comment) => (
<Comment comment={comment} key={comment.id} />
))}
</div>
);
}
}
后来,你又需要监听博客的提交数据,使用的是相同的模式:
class BlogPost extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
blogPost: DataSource.getBlogPost(props.id)
};
}
componentDidMount() {
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
blogPost: DataSource.getBlogPost(this.props.id)
});
}
render() {
return <TextBlock text={this.state.blogPost} />;
}
}
CommentList
和BlogPost
是不同的组件,它们调用各自的方法,监听各自的不同的数据,渲染不同的输出。但是它们的模式是一样的:
- 在加载组件的时候,对数据源添加监听函数。
- 在监听函数中,当数据变化的时候调用
setState
- 在卸载的时候,移除监听。
可以想象,在大型应用中,这种监听数据源,设置更新数据的模式经常会用到。所以我们需要把这部分功能抽象出来,并且在很多个组件中实现公用。这就是高阶组件做的事情。
我们现在先来实现一个函数,用来生成类似CommentList
或者BlogPost
的组件。这个函数有两个参数,一个参数是需要监听数据源的组件,另一个参数是数据源。函数名称定义为withSubscription
:
const CommentListWithSubscription = withSubscription(
CommentList,
(DataSource) => DataSource.getComments()
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
});
第一个参数就是被包裹的组件,第二个参数就是提过我们感兴趣的数据的函数。
当CommentListWithSubscription
和BlogPostWithSubscription
渲染的时候,CommentList
和BlogPost
将会被传入一个data
属性:
// This function takes a component...
function withSubscription(WrappedComponent, selectData) {
// ...and returns another component...
return class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props)
};
}
componentDidMount() {
// ... that takes care of the subscription...
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}
render() {
// ... and renders the wrapped component with the fresh data!
// Notice that we pass through any additional props
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}
这里我们看到,HOC并不会改变输入的组件,也不复制组件。而是在容器中将原来的组件包裹起来。HOC是一个不影响任何一方的“纯净”组件。它获取到关于组件的所有参数,并赋值给新的变量data
,用来渲染界面。HOC不关心数据的来源,也不关心数据的用法。
因为withSubscription
是一个很平常的函数,你可以添加任意多的参数,来实现你想要的功能。
withSubscription
和被包裹的组件之间是基于props的,所以在需要的时候替换HOC是很简单的。
不要改变原来的组件,选择包裹
不要在HOC中改变传递进来的组件:
function logProps(InputComponent) {
InputComponent.prototype.componentWillReceiveProps(nextProps) {
console.log('Current props: ', this.props);
console.log('Next props: ', nextProps);
}
// The fact that we're returning the original input is a hint that it has
// been mutated.
return InputComponent;
}
// EnhancedComponent will log whenever props are received
const EnhancedComponent = logProps(InputComponent);
这样做会导致一些问题。首先就是输入组件脱离了这个强化组件就不能重用了。更重要的是,如果你传入另一个组件,也会改变componentWillReceiveProps
函数,之前HOC的功能就被复写了。而且这个HOC也不支持函数组件,因为函数组件并没有生命周期。
这样的HOC并不是一个好的实现,使用者必须知道HOC是怎么实现的,才能避免与其他组件产生冲突。
所以,我们不能改变原来组件,而是重新生成一个组件,将输入组件包裹起来:
function logProps(WrappedComponent) {
return class extends React.Component {
componentWillReceiveProps(nextProps) {
console.log('Current props: ', this.props);
console.log('Next props: ', nextProps);
}
render() {
// Wraps the input component in a container, without mutating it. Good!
return <WrappedComponent {...this.props} />;
}
}
}
这个HOC和之前的直接修改的实现的功能是一样的,但是这个可以避免冲突,并且可以支持函数组件。由于它是纯组件,所以它和其他的HOC是兼容的。
你会注意到HOC和容器组件很相似。容器组件是为了分离不同级别之间的功能的一种策略。容器管理订阅和状态,传递props给组件。容器组件是HOC的一部分。HOC可以理解为参数化的容器组件。
公约:给包裹的组件只传递相关的props
HOC只是添加功能,不能对原有的组件做很大的变化。我们期望输入和输出只有温和的变化。HOC传递的参数,只是需要的参数,不相关的参数就不要传递进去:
render() {
// Filter out extra props that are specific to this HOC and shouldn't be
// passed through
const { extraProp, ...passThroughProps } = this.props;
// Inject props into the wrapped component. These are usually state values or
// instance methods.
const injectedProp = someStateOrInstanceMethod;
// Pass props to wrapped component
return (
<WrappedComponent
injectedProp={injectedProp}
{...passThroughProps}
/>
);
}
这个约定有利于HOC更加灵活和更容易实现重用。
公约:兼容性最大化
并不是所有的HOC都是一样的形式,有的只有一个参数,需要被包裹的组件:
const NavbarWithRouter = withRouter(Navbar);
通常,HOC需要额外的参数。举一个Relay的例子,为了说明数据的依赖,需要传递一个配置对象:
const CommentWithRelay = Relay.createContainer(Comment, config);
HOC常见的形式如下:
// React Redux's `connect`
const ConnectedComment = connect(commentSelector, commentActions)(Comment);
这个形式有点看不清楚,换个写法,你就知道了:
// connect is a function that returns another function
const enhance = connect(commentListSelector, commentListActions);
// The returned function is an HOC, which returns a component that is connected
// to the Redux store
const ConnectedComment = enhance(CommentList);
connect
是一个高阶组件返回一个高阶组件。这种形式可能会比较迷惑,或者不必要。但是确实是很有用的一个特性。像connect
返回的单参数HOC一样,具有Component=>Component
这样的特征。这就很容易兼容:
// Instead of doing this...
const EnhancedComponent = connect(commentSelector)(withRouter(WrappedComponent))
// ... you can use a function composition utility
// compose(f, g, h) is the same as (...args) => f(g(h(...args)))
const enhance = compose(
// These are both single-argument HOCs
connect(commentSelector),
withRouter
)
const EnhancedComponent = enhance(WrappedComponent)
有很多第三方库提供这样的compose
功能。例如lodash.flowRight,Redux和Ramda
公约:起一个方便调试的名称
这些HOC生成的容器组件能够在React调试工具显示出来。为了方便调试,我们要给它们取一个方便的名字,以便调试。
通用的做法就是对原来的组件名称包裹下。例如,高阶组件的名称叫做withSubscription
,被包裹的组件叫做CommentList
的时候,使用名称WithSubscription(CommentList)
:
function withSubscription(WrappedComponent) {
class WithSubscription extends React.Component {/* ... */}
WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
return WithSubscription;
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
陷阱
如果你是新手,那么就得注意这些问题了:
不能在渲染函数中使用HOCs
React 差异算法通过比较组件是否相同,来决定是否更新它还是直接销毁它。如果在render函数中返回的组件是和之前的组件是相同的,那么就和新的比较下,更新差异;如果不同就直接卸载。
通常情况下不需要想太多,但是如果这里遇到了HOC的话,就要小心了:
render() {
// A new version of EnhancedComponent is created on every render
// EnhancedComponent1 !== EnhancedComponent2
const EnhancedComponent = enhance(MyComponent);
// That causes the entire subtree to unmount/remount each time!
return <EnhancedComponent />;
}
这里不是因为性能的问题,而是重新渲染组件,导致组件状态丢失的问题。
所以,应该在组件外使用HOC来确保组件只是生成一次。
若果有时候你需要运用HOC,你也可以在生命周期方法或者构造函数方法中使用。
静态函数必须要手动拷贝过来
有时候在React组件中定义静态方法是很有用的。例如,Relay容器就暴露了一个方法,getFragmnet
来加快生成GraphQL fragments。
如果一个组件中使用了HOC,但是源组件又是一个容器组件。这意味着新的组件并没有元组件中的任何静态方法。
// Define a static method
WrappedComponent.staticMethod = function() {/*...*/}
// Now apply an HOC
const EnhancedComponent = enhance(WrappedComponent);
// The enhanced component has no static method
typeof EnhancedComponent.staticMethod === 'undefined' // true
为了解决这个问题,你必须将容器里的静态方法手动拷贝出来:
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
// Must know exactly which method(s) to copy :(
Enhance.staticMethod = WrappedComponent.staticMethod;
return Enhance;
}
所以你又必须知道你具体要拷贝什么函数。你可以使用hoist-non-react-statics来自动拷贝非react的静态代码:
import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
hoistNonReactStatic(Enhance, WrappedComponent);
return Enhance;
}
另一个解决方法是在容器中分离出静态的方法:
// Instead of...
MyComponent.someFunction = someFunction;
export default MyComponent;
// ...export the method separately...
export { someFunction };
// ...and in the consuming module, import both
import MyComponent, { someFunction } from './MyComponent.js';
refs不会被传递
虽然组件的所有属性都会传递给新的包裹组件。但是不会传递refs。这是因为ref
并不是prop,比如key
,这些个都会被React单独处理。
如果你面临这样的问题,最好的办法是如何才能不使用ref
。可以尝试用props替代。
当时有时候又必须使用refs。例如让input获得焦点。一个解决方法是将refs回调方法通过props传递进去,但是要不同的命名:
function Field({ inputRef, ...rest }) {
return <input ref={inputRef} {...rest} />;
}
// Wrap Field in a higher-order component
const EnhancedField = enhance(Field);
// Inside a class component's render method...
<EnhancedField
inputRef={(inputEl) => {
// This callback gets passed through as a regular prop
this.inputEl = inputEl
}}
/>
// Now you can call imperative methods
this.inputEl.focus();
这并不是一个完美的解决方法。我们希望能够有个库来处理ref,而不是手动处理。我们还在寻找优雅的方法。。。
差不多一个月的时间,把两篇guide都学习完了。感觉我是在翻译文章。按道理来说,笔记应该是结合自己的相法。在复习的时候可以重拾自己的理解。我觉得我并没有完全理解React。入门?如果没有项目,我也觉得心虚。所以keep coding~