基础
- jsx(原理?)
- 实现一个简单的createElement及render方法
- 组件声明方式
- props和state的区别
- 绑定事件(一些特殊的事件也可以往上冒泡,如onBlur)
- 属性校验
- setState的使用(有两种写法)
- 复合组件(多个组件进行组合,父子通信、子父通信的实现)
- 受控与非受控组件
jsx表达式的用法
- 可以放JS的执行结果
- 如果换行需要用()包裹jsx代码
- 可以把JSX元素当作函数的返回值
- <{来判断是表达式还是js
jsx属性
在JSX中分为普通属性和特殊属性,像class要写成className,for要写成htmlFor style要采用对象的方式, dangerouslyInnerHTML插入html
组件声明方式
两种,普通函数及class
属性校验
通常这个是会写在一个component组件中,提供给别人使用。
在react中props是组件对外暴露的接口,但通常组件并不会明显的申明他会暴露那些接口及类型,这不太利于组件的复用,但比较好的是React提供了PropTypes这个对象用于校验属性的类型,PropTypes包含组件属性的所有可能类型,以下我们通过一个示列来说明(对象的key是组件的属性名,value是对应属性类型)组件属性的校验
class Person extends Component {
// 传的props格式不对,不会中断页面渲染
static propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number,
gender: PropTypes.oneOf(['男', '女']),
hobby: PropTypes.array,
// 自定义类型
salary: function (props, key, com) {
if (props[key] < 1000) {
throw new Error(`${com} error ${props[key]} is too low`)
}
},
position: PropTypes.shape({
x: PropTypes.number,
y: PropTypes.number
})
}
constructor(props) {
super();
}
render() {
let { name, age, gender, hobby, salary, position } = this.props;
return (<div>
{name}{age}
{gender}{hobby}
{salary} {JSON.stringify(position)}
</div>)
}
}
受控与非受控组件
一般受控或非受控组件,指的是表单元素,如input、select、checkbox等。
受控是指表单的值,必须通过事件+状态
来改变,比如说:
<input value="123" />
在不加onChange
事件的前提下,我们是没法将123改为其他值的。
当有多个表单元素时,不同的元素onChange
事件可以通过event.target.name
来做区分(前提是每个元素需要加上name
的属性)
非受控,是指不需要通过状态来改变,上面的代码可以改为:
<input defaultValue="123" />
那么它在表单提交时,怎么来获取元素值呢?答案是通过ref
。
ref的写法
字符串
// 在某个方法中 this.refs.username // jsx<input ref="username" />stylus
函数
// 在某个方法中 this.username // jsx<input ref={ref => this.username=ref} />verilog
对象
// 在constructor里面this.username = React.createRef();// 在某个方法中this.username.current // 这个就是dom元素// jsx<input ref={this.username} />kotlin
第一种写法现在不是太推荐了,一般使用第二种或者第三种,第三种需要v16.3以上的版本。
生命周期
在网上找到一张图,还是挺直观的:
import React, { Component } from 'react';
class Counter extends React.Component {
static defaultProps = {
name: 'zpu'
};
constructor(props) {
super();
this.state = { number: 0 }
console.log('1.constructor构造函数')
}
componentWillMount() {
console.log('2.组件将要加载 componentWillMount');
}
componentDidMount() {
console.log('4.组件挂载完成 componentDidMount');
}
handleClick = () => {
this.setState({ number: this.state.number + 1 });
};
shouldComponentUpdate(nextProps, nextState) { // 代表的是下一次的属性 和 下一次的状态
console.log('5.组件是否更新 shouldComponentUpdate');
return nextState.number % 2;
}
componentWillUpdate() {
console.log('6.组件将要更新 componentWillUpdate');
}
componentDidUpdate() {
console.log('7.组件完成更新 componentDidUpdate');
}
render() {
console.log('3.render');
return (
<div>
<p>{this.state.number}</p>
{this.state.number > 3 ? null : <ChildCounter n={this.state.number} />}
<button onClick={this.handleClick}>+</button>
</div>
)
}
}
class ChildCounter extends Component {
componentWillUnmount() {
console.log('组件将要卸载componentWillUnmount')
}
componentWillMount() {
console.log('child componentWillMount')
}
render() {
console.log('child-render')
return (<div>
{this.props.n}
</div>)
}
componentDidMount() {
console.log('child componentDidMount')
}
componentWillReceiveProps(newProps) { // 第一次不会执行,之后属性更新时才会执行
console.log('child componentWillReceiveProps')
}
shouldComponentUpdate(nextProps, nextState) {
console.log('child shouldComponentUpdate');
return nextProps.n % 3; // 子组件判断接收的属性 是否满足更新条件 为true则更新
}
}
export default Counter;
// defaultProps
// constructor
// componentWillMount
// render
// componentDidMount
// 状态更新会触发的
// shouldComponentUpdate nextProps,nextState=>boolean
// componentWillUpdate
// componentDidUpdate
// 属性更新
// componentWillReceiveProps newProps
// 卸载
// componentWillUnmount
到了react16.3,生命周期去掉了以下三个:
- componentWillMount
- componentWillReceiveProps
- componentWillUpdate
This lifecycle was previously named componentWillMount. That name will continue to work until version 17. Use the rename-unsafe-lifecycles codemod to automatically update your components.
上面像componentWillMount
和componentWillUpdate
去掉一般影响不会太大,但是像componentWillReceiveProps
这个就有关系了,所以react又新增了两个生命周期:
- static getDerivedStateFromProps
- getSnapshotBeforeUpdate
getDerivedStateFromProps
getDerivedStateFromProps就是用来替代componentWillReceiveProps方法的,但是需要注意是它是静态方法,在里面无法使用this,它会返回一个对象作为新的state,返回null则说明不需要更新state。
class Example extends React.Component {
static getDerivedStateFromProps(nextProps, prevState) {
// 没错,这是一个static
}
}
另外它的触发时机是:在组件构建之后(虚拟dom之后,实际dom挂载之前) ,以及每次获取新的props之后。和之前componentWillReceiveProps有一个区别是,后者只有获取新的props之后,才会触发,第一次是不触发的。
简单例子如下:
if (nextProps.currentRow !== prevState.lastRow) {
return {
...
lastRow: nextProps.currentRow,
};
// 不更新state
return null
}
getSnapshotBeforeUpdate
文档的意思大概是使组件能够在可能更改之前从DOM中捕获一些信息(例如滚动位置)。简单地说就是在更新前记录原来的dom节点属性,然后传给componentDidUpdate。
componentDidCatch
我们都知道如果组件中有错误,那整个页面可能就会变成空白,然后控制台一堆红色报错。
在 React 16.x 版本中,引入了所谓 Error Boundary 的概念,从而保证了发生在 UI 层的错误不会连锁导致整个应用程序崩溃;未被任何异常边界捕获的异常可能会导致整个 React 组件树被卸载。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state={hasError:false};
}
componentDidCatch(err,info) {
this.setState({hasError: true});
}
render() {
if (this.state.hasError) {
return <h1>Something Went Wrong</h1>
}
return this.props.children;
}
}
class Clock extends Component {
render() {
return (
<div>hello{null.toString()}</div>
)
}
}
class Page extends Component {
render() {
return (
<ErrorBoundary>
<Clock/>
</ErrorBoundary>
)
}
}
ReactDOM.render(<Page/>,document.querySelector('#root'));
上下文(context api)
传统写法
父组件:
static childContextTypes={
color: PropTypes.string,
changeColor:PropTypes.func
}
getChildContext() {
return {
color: this.state.color,
changeColor:(color)=>{
this.setState({color})
}
}
}
简单地说,就是写两个东西:
声明context类型
声明context对象,即子组件需要的context
子组件:
static contextTypes = {
color: PropTypes.string,
changeColor: PropTypes.func
}
// 使用
this.context.color;
也是先要声明类型(这里特指需要用到的context类型,如果不需要用的话,就不需要声明),然后使用this.context来取,就OK了。。
新式写法
上面的传统写法,其实是有点问题的:如果某个组件shouldComponentUpdate返回了false后面的组件就不会更新了。
当然这个我在团队中提及,他们有些人觉得scu返回了false,应该是不让它去更新了,但我觉得是需要更新的。
来看看写法吧。
// 创建一个消费者和提供者
let { Consumer,Provider} = React.createContext();
class Parent extends Component {
render() {
// Provider通过value来传递数据
return (
<Provider value={{ a: 1, b: 2 }}>
<Son></Son>
</Provider>
);
}
}
class Son extends Component {
render() {
// Consumer的children是一个函数,函数的参数为Provider的value对象
return (
<Consumer>
{
({a, b}) => {
return (
<div>{a}, {b}</div>
)
}
}
</Consumer>
)
}
}
写法上,比传统的写法更加舒服一些。当然实际应用中,可能会有多个Provider、多个Consumer,然后嵌套,不过这样层级写多了,比较恶心
插槽(Portals)
在react16中,提供了一个方法:
ReactDOM.createPortal(child, container)
React16.0中发布了很多新特性,我们来看portal,React提供了一个顶级API—portal,用来将子节点渲染到父节点之外的dom节点
Portals 提供了一种很好的将子节点渲染到父组件以外的 DOM 节点的方式。
ReactDOM.createPortal(child, container)
第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或碎片。第二个参数(container)则是一个 DOM 元素。
From React文档
import React from "react";
import { createPortal } from "react-dom";
import classnames from "classnames";
const rootEle = document.body;
/**
* show: 这个属性通过切换类名改变样式控制组件控制弹层的出现/隐藏
* onSwitch: 通过传递函数,给予弹出层自我切换的方法
* children: react组件自带属性,获取组件的开始和结束标记之间的内容
*/
export default ({ show, onSwitch, children }) =>
createPortal(
<div
className={classnames("modal", { "modal-show": show })}
onClick={onSwitch}
>
{children}
</div>,
rootEle
);
调用,在应用中创建一个show状态来管理弹出层的切换,以及switchModal方法用来对该状态进行切换,并将这两个属性传递给弹出层组件
import React from "react";
import ReactDOM from "react-dom";
import Modal from "./Modal";
import "./styles.css";
class App extends React.PureComponent {
state = {
show: false
};
switchModal = () => this.setState({ show: !this.state.show });
render() {
const { show } = this.state;
return (
<div id="App">
<h1 onClick={this.switchModal}>点我弹出</h1>
<Modal show={show} onSwitch={this.switchModal}>
点击关闭
</Modal>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
片段(fragments)
React 中一个常见模式是为一个组件返回多个元素。 片段(fragments) 可以让你将子元素列表添加到一个分组中,并且不会在DOM中增加额外节点。
举个例子,比如说我们将一个h2元素和h3元素放到root节点去,很早之前的做法是必须要在h2和h3元素外面套一层div。但现在不一定非要这么做:
<React.Fragment>
<h2></h2>
<h3></h3>
</<React.Fragment>
这个也可以用在多个li返回上面,当然多个li的时候,也可以返回一个数组,加上不同的key即可,如:
const items = [
<li key="1">1</li>,
<li key="2">2</li>,
<li key="3">3</li>
]
ReactDOM.render((
<React.Fragment>
<div>aaa</div>
<h3>bb</h3>
{items}
</React.Fragment>
), document.getElementById('root'));
高阶组件(HOC)
HOC,全称: Higher-Order Components。简单地说,就是对原有的component再包装一层,有点类似extend。
HOC(High Order Component) 是 react
中对组件逻辑复用部分进行抽离的高级技术,但HOC并不是一个 React API
。 它只是一种设计模式,类似于装饰器模式。
具体而言,HOC就是一个函数,且该函数接受一个组件作为参数,并返回一个新组件。
从结果论来说,HOC相当于 Vue
中的 mixins(混合)
。其实 React
之前的策略也是采用 mixins
,但是后来 facebook
意识到 mixins
产生的问题要比带来的价值大,所以移除了 mixins
。
Why ? 为什么使用HOC
import React, { Component } from 'react'
class Page1 extends Component{
componentWillMount(){
let data = localStorage.getItem('data')
this.setState({ data })
}
render() {
return (
<h2>{this.state.data}</h2>
)
}
}
export default Page1
这个例子中在组件挂载前需要在 localStorage 中取出 data 的值,但当其他组件也需要从 localStorage 中取出同样的数据进行展示的话,每个组件都需要重新写一遍 componentWillMount 的代码,那就会显得非常冗余。那么在 Vue 中通常我们采用:
mixins: []
但是在 React 中我们需要采用HOC模式咯
import React, { Component } from 'react'
const withStorage = WrappedComponent => {
return class extends Component{
componentWillMount() {
let data = localStorage.getItem('data')
this.setState({ data })
}
render() {
return <WrappedComponent data={this.state.data} {...this.props} />
}
}
}
export default withStorage
当我们构建好一个HOC之后,我们使用的时候就简单多了,还看最开始的例子,我们就不需要写 componentWillMount 了。
import React, { Component } from 'react'
import withStorage from '@/utils/withStorage'
class Page1 extends Component{
render() {
return <h2>{this.props.data}</h2>
}
}
export default withStorage(Page1)
很明显,这是一个装饰器模式,那么就可以使用ES7形式
import React, { Component } from 'react'
import withStorage from '@/utils/withStorage'
@withStorage
class Page1 extends Component{
render() {
return <h2>{this.props.data}</h2>
}
}
export default Page1
欢迎大家补充!