https://react.docschina.org/docs/introducing-jsx.html
简述
React起源于 FaceBook 的内部项目,于2013年在github上开源。React用于构建用户界面的 JavaScript 库,具有声明式、组件化等优点。
视图渲染
构建视图一直是 React 的重点,从 createElement 到 JSX;
createElement
React.createElement(
type,
[props],
[...children]
)
// 示例
let h1 = React.createElement("h1", null, "Hello React");
let p = React.createElement("p", null, "欢迎大家学习 React");
let header = React.createElement("header", null, [h1,p]);
ReactDOM.render(
header,
document.querySelector("#root");
);
// 最终效果如下:
<header>
<h1>Hello React</h1>
<p>欢迎大家学习 React</p>
</header>
使用 JSX 编写的代码将会被转换成使用 React.createElement() 的形式。如果使用了 JSX 方式,那么一般来说就不需要直接调用 React.createElement()。
JSX语法
JSX 是 JS 的语法扩展,但是浏览器并不识别这些扩展,所以需要借助 babel.js 来对 JSX 进行编译,使其成为浏览器识别的语法。
const element = <h1>Hello, world!</h1>;
这个有趣的标签语法既不是字符串也不是 HTML。
Component(组件)
在 React 中提倡组件化开发,使用 React 编写项目时,会把视图抽象为一个组件,最终使用这些组件组装成开发者想要的视图。React中编写组件的方式,一种是类组件,另一种是函数式组件。
注意:
- 为了区分标签和组件,标签一点要全小写,组件首字母全大写。
创建方式
类组件
React 类组件必须继承 React.Component,并且必须有 render 方法,在 render 方法中的 return 中定义要渲染的视图。
class App extends React.Components {
render(){
return <div>创建类组件</div>
}
}
函数式组件
function Todo(props) {
return <li>Hello, 图雀</li>;
}
props
React 为组件提供了 Props,使得在使用组件时,可以给组件传入属性进行个性化渲染。
类组件中使用 Props
类组件中基本和函数式组件中的 Props 保持一致,除了是通过 this.props 来获取父组件传递下来的属性内容:
class Todo extends React.Component {
render() {
return <li>Hello, {this.props.content}</li>;
}
}
<Todo content="图雀" />
函数式组件中使用 Props
函数式组件默认接收 props 参数,它是一个对象,用于保存父组件传递下来的内容:
function Todo(props) {
return (
<li>Hello, {props.content}</li>
)
}
<Todo content="图雀" />
子组件->父组件
利用 props callback 通信,父组件传递一个 callback 到子组件,当事件触发时将参数放置到 callback 带回给父组件.
// 父组件
callback = (value) => {
// 此处的value便是子组件带回
this.setState({
info: value,
})
}
<div>
<Son callback={this.callback} />
</div>
// 子组件(Son)
handleChange = (e) => {
// 在此处将参数带回父组件
this.props.callback(e.target.value)
}
<input type='text' onChange={this.handleChange} />
PropTypes 进行类型检查
注意:
- 自 React v15.5 起,React.PropTypes 已移入另一个包中。请使用 prop-types库 代替。
- 我们提供了一个 codemod 脚本来做自动转换。
https://zh-hans.reactjs.org/docs/typechecking-with-proptypes.html
props.children(与Vue插槽类似)
每个组件都可以获取到 props.children。它包含组件的开始标签和结束标签之间的内容。例如:
// 插槽
<Welcome>
<div>Hello world!</div>
<div>666</div>
</Welcome>
function Welcome(props) {
return <div>{props.children}</div>;
}
// 具名插槽
<Welcome>
{
{
left: <span>左</span>,
right: <span>右</span>
}
}
</Welcome>
function Welcome(props) {
return (
<div>
<div>{props.children.left}</div>
<div>中间内容</div>
<div>{props.children.right}</div>
</div>
)
}
context(跨组件通信)
组件层级嵌套比较深的情况下,传递数据将会特别麻烦;
const ThemeContext = React.createContext(null)
const ThemeProvider = ThemeContext.Provider //提供者
const ThemeConsumer = ThemeContext.Consumer // 订阅消费者
// 爷组件
<ThemeContext.Provider value={{info: "我是info"}}>
<div>
<Son />
</div>
</ThemeContext.Provider>
// 孙组件(Son)
<ThemeContext.Consumer>
{(value) => (
// 通过Consumer直接获取父组件的值
<div>
<p>爷组件的值:{value.info}</p>
</div>
)}
</ThemeContext.Consumer>
很多优秀的 React 组件的核心功能都通过 Context 来实现的,比如 react-redux 和 react-router 等,所以掌握 Context 是必须的。
State
注意:
- 只需要传入要修改的状态,不需要传入所有状态,setState会自动进行合并。
- setState 是一个异步方法。
- 多个 setState 会被合并,但只会引起一次视图渲染(render)。
类组件中使用 State
定义 、使用 、更新
constructor(props) {
super(props);
this.state = {
todoList: ["图雀", "图雀写作工具", "图雀社区", "图雀文档"]
};
}
/*
这里 constructor 方法接收的 props 属性就是我们在上一节中讲到的那个 props;
并且 React 约定每个继承自 React.Component 的组件在定义 constructor 方法
时,要在方法内首行加入 super(props) 。
*/
constructor(props) {
super(props);
this.state = {
todoList: ["图雀", "图雀写作工具", "图雀社区", "图雀文档"]
};
}
render() {
return (
<div>{this.state.todoList[0]}</div>
);
}
// 通过 this.state 的方式来使用 state;
this.setState({ todoList: [1,2,3] });
// 通过 this.setState 方法来更新 state,它会把新的 state 和旧的 state 进行合并;
函数式组件中使用 State
Hooks 中的 useState(),下面的 Hooks 章节中会进行介绍。
Refs
开发项目时,会遇到一些特殊的需求,需要使用原生 DOM 节点。比如让文本框获得焦点。
注意:
- ref的命名虽然可以自定义,但也要注意 JS 的命名规范,另外要遵循驼峰命名法;
- 单个组件内,ref不能重名;
- 获取 ref 时要在 componentDidMount 和 componentDidUpdate 中进行,否则 ref 是还没有赋值或还没更新的。
- 你不能在函数组件上使用 ref 属性,因为他们没有实例。
string ref
如果你之前使用过 React,你可能了解过之前的 API 中的 string 类型的 ref 属性,例如 “textInput”。你可以通过 this.refs.textInput 来访问 DOM 节点。我们不建议使用它,因为 string 类型的 refs 存在 一些问题。它已过时并可能会在未来的版本被移除。
createRef
class Child extends Component {
render(){
return <p>子组件内容</p>
}
}
class App extends Component {
perent = createRef();
child = createRef();
componentDidMount(){
console.log(this.parent.current); // 打印真实的 DOM 节点
console.log(this.child.current); // 打印 Child 的实例化对象
}
render(){
<p ref={this.perent}>父组件内容</p>
<Child ref={this.child} />
}
}
其它
dangerouslySetInnerHTML
- dangerouslySetInnerHTML 是 React 为浏览器 DOM 提供 innerHTML 的替换方案;
- 使用dangerouslySetInnerHTML属性的虚拟dom元素之间不能有内容,否则会报错。 ```jsx function createMarkup() { return {__html: “createMarkup“}; }
function MyComponent() { return
; }
---
<a name="uHgU4"></a>
# 高阶组件(HOC)
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。**高阶组件是参数为组件,返回值为新组件的函数。**
<a name="NCthq"></a>
## 两种不同的高阶组件
常用的高阶组件有**属性代理**和**反向继承**两种,两者之间有一些共性和区别。接下来分别介绍一下两种模式下的高阶组件。
<a name="XmkXU"></a>
### 属性代理
属性代理,就是用组件包裹一层代理组件,在代理组件上,可以做一些,对源组件的强化操作。这里注意属性代理返回的是一个新组件,被包裹的原始组件,将在新的组件里被挂载。
```javascript
function HOC(WrapComponent){
return class Advance extends React.Component{
state={
name:'alien'
}
render(){
return <WrapComponent { ...this.props } { ...this.state } />
}
}
}
优点:
- ① 属性代理可以和业务组件低耦合,零耦合,对于条件渲染和 props 属性增强,只负责控制子组件渲染和传递额外的 props 就可以了,所以无须知道,业务组件做了些什么。所以正向属性代理,更适合做一些开源项目的 HOC ,目前开源的 HOC 基本都是通过这个模式实现的。
- ② 同样适用于类组件和函数组件。
- ③ 可以完全隔离业务组件的渲染,因为属性代理说白了是一个新的组件,相比反向继承,可以完全控制业务组件是否渲染。
- ④ 可以嵌套使用,多个 HOC 是可以嵌套使用的,而且一般不会限制包装 HOC 的先后顺序。
缺点:
- ① 一般无法直接获取原始组件的状态,如果想要获取,需要 ref 获取组件实例。
- ② 无法直接继承静态属性。如果需要继承需要手动处理,或者引入第三方库。
③ 因为本质上是产生了一个新组件,所以需要配合 forwardRef 来转发 ref。
反向继承
反向继承和属性代理有一定的区别,在于包装后的组件继承了原始组件本身,所以此时无须再去挂载业务组件。
class Index extends React.Component{
render(){
return <div> hello,world </div>
}
}
function HOC(Component){
return class wrapComponent extends Component{ /* 直接继承需要包装的组件 */
}
}
export default HOC(Index)
优点:
① 方便获取组件内部状态,比如 state ,props ,生命周期,绑定的事件函数等。
- ② es6继承可以良好继承静态属性。所以无须对静态属性和方法进行额外的处理。
缺点:
- ① 函数组件无法使用。
- ② 和被包装的组件耦合度高,需要知道被包装的原始组件的内部状态,具体做了些什么?
③ 如果多个反向继承 HOC 嵌套在一起,当前状态会覆盖上一个状态。这样带来的隐患是非常大的,比如说有多个 componentDidMount ,当前 componentDidMount 会覆盖上一个 componentDidMount 。这样副作用串联起来,影响很大。
高阶组件功能说明
强化props、控制渲染(渲染劫持、动态加载)、组件赋能(ref获取实例、事件监听)
高价组件注意事项
谨慎修改原型链
- 不要在函数组件内部或类组件render函数中使用HOC
- ref的处理
- 注意多个HOC嵌套顺序问题
- 继承静态属性
进阶实践-权限拦截
第一步,在根部注入权限。
```jsx export const Permission = React.createContext([])
export default function Index(){
const [ rootPermission , setRootPermission ] = React.useState([])
React.useEffect(()=>{
/ 获取权限列表 /
getRootPermission().then(res=>{
const { code , data } = res as any
code === 200 && setRootPermission(data) // [ ‘docList’ , ‘tagList’ ]
})
},[])
return
<a name="zdboa"></a>
### 第二步:重点编写HOC
```jsx
/* 没有权限 */
function NoPermission (){
return <div>您暂时没有权限,请联系管理员开通权限!</div>
}
/* 编写HOC */
export function PermissionHoc(authorization){
return function(Component){
return function Home (props){
const matchPermission =(value,list)=> list.indexOf(value) /* 匹配权限 */
return <Permission.Consumer>
{
(permissionList) => matchPermission(authorization,permissionList) >= 0 ? <Component {...props} /> : <NoPermission />
}
</Permission.Consumer>
}
}
}
第三部:绑定权限
@PermissionHoc('writeDoc') // 绑定文档录入页面
export default class Index extends React.Component{}
export default PermissionHoc('writeTag')(index) //绑定标签录入页面
export default PermissionHoc('tagList')(index) //绑定标签列表页面
export default PermissionHoc('docList')(Index) // 绑定文档列表页面
生命周期
https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
class 中生命周期函数经常包含不相关的逻辑,使用 Hook 就可以解决这一个问题。
Hooks
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数(Hook 就是 JavaScript 函数)。Hook 不能在 class 组件中使用 —— 这使得你不使用 class 也能使用 React。
注意:
- 只能在函数最外层调用 Hook。不要在循环、条件判断、其他 JavaScript 函数中、子函数中调用。
- 只能在 React 的函数组件 中调用 Hook。
- 在自定义 Hook 中调用其他 Hook。
基础 Hook
useState(状态)
注意:
- 不会把新的 state 和旧的 state 进行合并;
- 返回一对值:当前状态和一个让你更新它的函数。
function Example(){
let [count, setCount] = useState(0);
return (
<div>
<Box>
<div>count:{count}</div>
<button onClick={()=>setCount(count+1)}>count++</button>
</Box>
</div>
)
}
useEffect(副作用)
它跟 class 组件中的生命周期, componentDidMount(挂载时)、componentDidUpdate(更新时) 和 componentWillUnmount(卸载时) 具有相同的用途,只不过被合并成了一个 API。
注意:
- 跟 useState 一样,你可以在组件中多次使用 useEffect ;
函数(第一个参数)
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
需要清除的 effect
如果你的 effect 返回 一个 函数, React 会在组件卸载的时候调用它。
useEffect(() => {
// ......
return function cleanup() {
// 在组件卸载的时候调用它
};
});
数组(第二个参数)
可以传入 有数据的数组 或 空数组。当传入 有数据的数组 时,此数据将会被监听,一旦监听的数据发生改变, Effect 将会被执行;传入 空数组 时,因为没有数据被监听, Effect 只会执行一次。
// 有数据的数组
let [count, setCount] = useState();
useEffect(() => {
// ......
}, [count]);
// 空数组
useEffect(() => {
// ......
}, []);
useContext
useContext 让你不使用组件嵌套就可以订阅 React 的 Context。接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
额外的 Hook API
以下介绍的 Hook,有些是上一节中基础 Hook 的变体,有些则仅在特殊情况下会用到。不用特意预先学习它们。
useReducer - 独立状态管理
是React提供的一个高级Hook,它不像useEffect、useState、useRef等必须hook一样,没有它我们也可以正常完成需求的开发,但 useReducer 可以使我们的代码具有更好的可读性、可维护性、可预测性。
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
// useState版login
function LoginPage() {
const [name, setName] = useState(''); // 用户名
const [pwd, setPwd] = useState(''); // 密码
const [isLoading, setIsLoading] = useState(false); // 是否展示loading,发送请求中
const [error, setError] = useState(''); // 错误信息
const [isLoggedIn, setIsLoggedIn] = useState(false); // 是否登录
const login = (event) => {
event.preventDefault();
setError('');
setIsLoading(true);
login({ name, pwd })
.then(() => {
setIsLoggedIn(true);
setIsLoading(false);
})
.catch((error) => {
// 登录失败: 显示错误信息、清空输入框用户名、密码、清除loading标识
setError(error.message);
setName('');
setPwd('');
setIsLoading(false);
});
}
return (
// 返回页面JSX Element
)
}
// useReducer版login
const initState = {
name: '',
pwd: '',
isLoading: false,
error: '',
isLoggedIn: false,
}
function loginReducer(state, action) {
switch(action.type) {
case 'login':
return {
...state,
isLoading: true,
error: '',
}
case 'success':
return {
...state,
isLoggedIn: true,
isLoading: false,
}
case 'error':
return {
...state,
error: action.payload.error,
name: '',
pwd: '',
isLoading: false,
}
default:
return state;
}
}
function LoginPage() {
const [state, dispatch] = useReducer(loginReducer, initState);
const { name, pwd, isLoading, error, isLoggedIn } = state;
const login = (event) => {
event.preventDefault();
dispatch({ type: 'login' });
login({ name, pwd })
.then(() => {
dispatch({ type: 'success' });
})
.catch((error) => {
dispatch({
type: 'error'
payload: { error: error.message }
});
});
}
return (
// 返回页面JSX Element
)
}
// 定义初始化值
const initState = {
name: '',
pwd: '',
isLoading: false,
error: '',
isLoggedIn: false,
}
// 定义state[业务]处理逻辑 reducer函数
function loginReducer(state, action) {
switch(action.type) {
case 'login':
return {
...state,
isLoading: true,
error: '',
}
case 'success':
return {
...state,
isLoggedIn: true,
isLoading: false,
}
case 'error':
return {
...state,
error: action.payload.error,
name: '',
pwd: '',
isLoading: false,
}
default:
return state;
}
}
// 定义 context函数
const LoginContext = React.createContext();
function LoginPage() {
const [state, dispatch] = useReducer(loginReducer, initState);
const { name, pwd, isLoading, error, isLoggedIn } = state;
const login = (event) => {
event.preventDefault();
dispatch({ type: 'login' });
login({ name, pwd })
.then(() => {
dispatch({ type: 'success' });
})
.catch((error) => {
dispatch({
type: 'error'
payload: { error: error.message }
});
});
}
// 利用 context 共享dispatch
return (
<LoginContext.Provider value={dispatch}>
<...>
<LoginButton />
</LoginContext.Provider>
)
}
function LoginButton() {
// 子组件中直接通过context拿到dispatch,出发reducer操作state
const dispatch = useContext(LoginContext);
const click = () => {
if (error) {
// 子组件可以直接 dispatch action
dispatch({
type: 'error'
payload: { error: error.message }
});
}
}
}
总结:
- 如果你的页面state很简单,可以直接使用useState
- 如果你的页面state比较复杂(state是一个对象或者state非常多散落在各处)请使用userReducer
- 如果你的页面组件层级比较深,并且需要子组件触发state的变化,可以考虑useReducer + useContext
useCallback - 缓存方法
useMemo - 存储计算结果
useRef - 函数存储容器
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
useImperativeHandle -
useLayoutEffect -
useDebugValue -
自定义Hook
有时候我们会想要在组件之间重用一些状态逻辑。目前为止,有两种主流方案来解决这个问题:高阶组件和 render props。自定义 Hook 可以让你在不增加组件的情况下达到同样的目的。
注意:
- 关注分离,提取共同逻辑;
- 三条规则:use开头、不共享、独立state;
- useYourImagination();
React-Router
https://reactrouter.com/docs/en/v6
Redux(状态管理)
https://redux.js.org/introduction/getting-started
Mobx
DvaJS
UmiJS
Taro
https://taro-docs.jd.com/taro/docs/