React 入门
环境搭建
前提:node 环境
脚手架:create-react-app
先使用官方脚手架来完成我们第一个 react 项目
安装及使用:
npm install create-react-app -g //全局安装脚手架
create-react-app react-demo //初始化一个名为react-demo的项目
cd react-demo //进入项目
npm start //启动项目
脚手架默认的是 3000 端口,启动完成便可以访问了
工程目录
index.js-项目入口文件
JSX 语法
使用 JSX 语法必须引入 react,JSX 语法不需要加引号。
//App.js
import React, { component } from "react";
class App extends Component {
render() {
//JSX语法
return <div>111</div>;
}
}
export default App;
JSX 语法最外层必须包裹一个元素,可用占位符<Fragment></Fragment>
事件和 vue 相似,小驼峰 eg: onChange/onClick
绑定的值需要被{}包裹起来,相当于{}内部是 js 语法,jsx 里面的注释也要放到{}里,与 js 单行多行注释相同
响应式设计:数据即界面,在 react 里改变数据需要用到 this.setSate(),因为 react 的 immutable 思想,后期也有优化空间
react 里会认为 class 是一个类,而非样式类名,react 的 JSX 中使用样式类时使用 className;会认为 label 标签的 for 是循环的 for,需要替换成 htmlFor
组件通信
父组件向子组件传值
属性传值:属性名自定义,在子组件中通过this.props.属性名
来获取,
如果子组件需要与父组件通信,调用父组件的方法也是如此,如下图的 delete。
子组件向父组件传值
这里并不是真正意义上的给父组件传值,而是调用父组件里的方法。在 react 中是单向数据流,允许数据从父组件到子组件,子组件可以使用但不可更改父组件的数据,所以说 react 只是一个视图层框架,不负责数据相关事情。
React 特点
声明式开发:指定你要做什么,但怎么做不做说明(what to do);与之相对的,命令式开发,会说明步骤(how to do)。
可以与其他框架并存:
组件化、单向数据流、视图层框架(不负责数据流之类)、
函数式编程:面向测试编程
React 进阶
安装 react-devtools
2020/11 官网已经有 chrome 扩展程序的压缩包了,直接下载并拖到 chrome 扩展程序中就好了,是 react 开发的网页那图标会变成黑色。
详细步骤:安装react-devtools
(题外话:假如 chrome 扩展程序下载好之后是.crx 文件无法安装的话,可更改为 rar,解压之后在去扩展程序中,加载已解压的扩展程序)
propTypes 和 defaultProps
propTypes:类似 typescript , 用于对属性类型做校验
defaultProps:默认值
虚拟 dom
props、state 与 render() 之间的关系:
当组件的 props 或者 state 发生改变的时候,render 函数会执行;
当父组件的 render 函数执行的时候,子组件的 render 函数也会执行;
React 数据驱动的底层逻辑
- 数据 state
- 模板 JSX
- 数据 + 模板 (state + template)生成虚拟 dom(虚拟 dom 就是一个 js 对象)如:
{'div',{id:'#root'},{'span',{},'hello world'}}
- 根据虚拟 dom 生成真实 dom
<div id='root'><span>hello world</span></div>
- 数据 state 发生变化
- 数据 state+模板 JSX 生成新的虚拟 dom
{'div',{id:'#root'},{'span',{},'hello'}}
- 用新的虚拟 dom 与原始虚拟 dom 进行比较,发现 span 内容不同
- 直接操作 dom,改变 span 中的值
优点:
- 极大的提升了性能
- 使得跨端应用得以实现 React Native
虚拟 dom 的 diff 算法
setState
setState 是异步的,假如在相隔较近的一段时间内多次调用 setState,那 setState 会整合之后调用一次。
diff 的同级比较
算法空间复杂度低,只比对一层 dom,某层不同则会替换从这一层开始往下的所有 dom
key 的作用
在比较过程中,会根据 key 值快速进行比较,所以 key 值需要是唯一且稳定的。因此在循环中不要使用 index 作为 key 值。
这里唯一指的是:数组元素中使用的 key 在其兄弟节点之间应该是独一无二的。
然而,它们不需要是全局唯一的。当我们生成两个不同的数组时,我们可以使用相同的 key 值
ref
尽量不要使用 ref 来操作 dom,但如果有需要的话,需要在 state 回调函数中进行操作避免一些奇怪的 bug
…
//JSX
生命周期函数
生命周期函数指在某一时刻组件会自动调用执行
mount:挂载,指的是组件第一次被放到页面上的时候
React17中将废除componentWillMount、componentWillRecieveProps、componentWIllUpdate,因为这几个生命周期可能会被误用,比如 在componentWillMount里面去请求数据,就可能会造成空渲染,渲染两次等问题,所以最好的是请求放在 componentDidMount。
新增的生命周期:getDerivedStateFromProps
组件
纯函数指的是给定固定的输入,就一定又固定的输出,而且不会有任何副作用;
所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。
无状态组件/ 有状态组件
没有state不需要维护state就是无状态组件,无状态组件只需要根据外部传入的state来进行展示。
UI 组件/容器组件
按功能来分,只负责视图,不处理数据,就是UI组件
函数组件/类组件
按组件定义方式来分,函数式定义的组件,例如下面就是一个函数组件:
const TodoListUI = (props) => {
return (
<Fragment>
<div>
{/* 注释:TodoList-demo label的作用:扩大点击热区,点击时光标会聚焦到对应input框 */}
<Input
id="input"
value={props.inputValue}
onChange={props.inputChange}
style={{width:300}}
></Input>
<Button type="primary" onClick={props.submit}>提交</Button>
</div>
<ul>
{/* { getItem() } */
props.list.map((item, index) => {
return (
<TodoItem
key={index}
content={item}
index={index}
deleteItem={props.delete.bind(this)}
/>
)
})
}
</ul>
</Fragment>
)
}
类组件,按照es6类的方式来定义的组件:
class TodoList extends Component {
constructor(props) {
super(props);
}
render() {
return (
<TodoListUI/>
);
}
componentDidMount() {}
}
函数组件一定是一个无状态组件,他没有内部的state,所以他的职责更明确;
类组件可以维护自己内部的state状态,内部有自己的生命周期,功能更强大。
如果组件简单不需要管理自身state就优先选择函数组件,代码简洁不繁琐,也符合react视图数据分离的口号。
react最近函数式编程+hooks呼声四起,接下来看看hooks吧
hook
React 性能
- 改变作用于的话,放在 constructor 里面绑定 this,只操作一次
- 虚拟 dom,diff 算法
- setState 异步
- shouldComponentUpdate
- 多使用无状态组件 直接 return 一个函数 没有组件实例,可以提升性能
Redux
Redux = Reducer + Flux
一个组件改变了 store 内的数据,别的组件会感知到,并且再次去取值。
Redux 工作流程
- 首先需要有 store, store 是唯一的,store 的数据只有 store 自己可以改变
// store/index.js
import { createStore } from "redux";
const store = createStore();
- 其次要有 reducer,管理 action,返回新的 state
// store/reducer.js
const defaultState = {};
export default (prevState = defaultState, action) => {
//action对应的一些操作
return state;
};
- store 与 reducer 建立连接
// store/index.js
import { createStore } from "redux";
import reducer from "./reducer";
const store = createStore(reducer);
-
组件中获取 store 的数据
store.getState()
并订阅 storestore.subscribe(func)
(订阅:store 里的数据有变动会触发 func)
// src/todoList.js
import React, { component } from "react";
import store from "./store";
class todoList extends Component {
constructor(props) {
super(props);
this.handle = this.handle.bind(this);
this.handleStoreChange = this.handleStoreChange.bind(this);
this.state = store.getState();
store.subscribe(this.handleStoreChange);
}
render() {
//...
}
handleStoreChange() {
this.setState(store.getState());
}
}
- 组件发起一个 action
// src/todoList.js
handle(e) {
const action = {
type: 'input_change',
value: e.target.value
}
store.dispatch(action)
}
- reducer 处理 action 并返回 newState
// store/reducer.js
const defaultState = {};
export default (prevState = defaultState, action) => {
//action对应的一些操作
if (action.type === "input_change") {
const newState = JSON.parse(JSON.stringify(prevState));
newState.inputValue = action.value;
return newState;
}
return prevState;
};
- 因为上面我们已经订阅了 store ,所以现在 store 的数据有变动组件会再去取 store 的新数据
提出 Action
提取出 action 可以避免 actionTypes 为字符串拼写错误导致的一系列问题。
ActionTypes
在 store 文件夹下新建 actionTypes.js 的文件夹,定义所需要的 actionTypes,组件和 reducer 引入 actionTypes 文件。
// store/actionTypes.js
export const CHANGE_INPUT_VALUE = "change_input_value";
export const SUBMIT_INPUT_VALUE = "submit_input_value";
export const DELETE_ITEM = "delete_item";
// src/todoList.js
import { CHANGE_INPUT_VALUE } from './store/actionTypes'
handle(e) {
const action = {
type: CHANGE_INPUT_VALUE,
value: e.target.value
}
store.dispatch(action)
}
ActionCreator
// store/actionCreator.js
import { CHANGE_INPUT_VALUE } from "./actionTypes";
//返回一个对象
export const getInputChangeAction = (value) => ({
type: CHANGE_INPUT_VALUE,
value: value,
});
// src/todoList.js
import { getInputChangeAction, submitInputAction, deleteItemAction } from './store/actionCreator'
handle(e) {
const action = getInputChangeAction(value)
store.dispatch(action)
}
中间件
中间件指的是 redux 的中间件,在 action — store 之间
redux-saga 和 redux-thunk 都是解决异步代码问题,拆分异步代码。
优点: 有助于做自动化测试、代码拆分管理
redux-thunk 使用
- 安装
npm install --save redux-thunk
- 引入 thunk 中间件
// store/index.js
// 安装引用中间件可以上官网查看
import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import reducer from "./reducer";
//使用redux-devtools和redux-thunk 两个中间件
const composeEnhancers =
typeof window === "object" && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
// Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
})
: compose;
const enhancer = composeEnhancers(
applyMiddleware(thunk)
// other store enhancers if any
);
const store = createStore(reducer, enhancer);
export default store;
- 组件中就可以使用
store.dispacth
传递函数了
// src/todolist.js
import Redux, { component } from "redux";
import { getTodoList } from "./store/actionCreator.js";
class todoList extends Component {
render() {
// ...
}
componentDidMount() {
const action = getTodoList();
store.dispatch(action);
}
}
-
将 axios 请求在 actionCreator 中定义好,如下:
getTodoList 返回一个函数,函数的参数是 dispatch。
发送请求并获取到数据后,生成一个 action,并触发 store.dispacth 告知 store 数据有变化,在进行下一步操作。
// store/actionCreator.js
import axios from "axios";
import { INIT_DATA } from "actionTypes";
const initData = (value) => ({
type: "INIT_DATA",
value: value,
});
export const getTodoList = () => {
return (dispatch) => {
axios.get("/api/todolist").then((res) => {
const data = res.data;
const action = initData(data);
dispatch(action);
});
};
};
redux-saga 使用
- 安装
npm install --save redux-saga
- 引入 redux-saga 中间件
// store/index.js
import { createStore, applyMiddleware } from "redux";
import createSagaMiddleware from "redux-saga";
import reducer from "./reducer";
import todoSaga from "./sagas";
const sagaMiddleWare = createSagaMiddleWare();
const composeEnhancers =
typeof window === "object" && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
// Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
})
: compose;
const enhancer = composeEnhancers(applyMiddleWare(sagaMiddleWare));
const store = createStore(reducer, enhancer);
sagaMiddleWare.run(todoSaga);
export default store;
- actionTypes 和 actionCreator
// store/actionTypes.js
export const INIT_TODO_LIST = "init_todo_list";
export const GET_INIT_DATA = "get_init_data";
// store/actionCreator.js
import { INIT_TODO_LIST, GET_INIT_DATA } from "actionTypes";
export const initTodoList = (value) => ({
type: INIT_TODO_LIST,
value: value,
});
export const getInitData = () => ({
type: GET_INIT_DATA,
});
- saga 文件
// saga.js
import { takeEvery, put } from "redux-saga/effects";
import axios from "axios";
import { GET_INIT_DATA } from "./actionTypes";
import { initTodoList } from "./actionCreator";
function* getTodoList() {
try {
const res = yield axios.get("api/todolist");
const action = initTodoList(res.data);
yield put(action);
} catch (e) {}
}
function* todoSaga() {
yield takeEvery(GET_INIT_DATA, getTodoList);
}
- 组件中
// src/todolist.js
import Redux, { component } from "redux";
import { getInitData } from "./store/actionCreator.js";
class todoList extends Component {
render() {
// ...
}
componentDidMount() {
const action = getInitData();
store.dispatch(action);
}
}
其他中间件
redux-log :日志
React-Redux
react-redux 是第三方的模块,能使开发者更好的在 react 中使用 redux
1.安装
npm install --save react-redux
2.入口文件引入 provider,提供者,可以把 store 的数据给到它所包含的所有组件
// index.js
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import store from "./store";
import TodoList from "./TodoList";
ReactDOM.render(
<Provider store={store}>
<TodoList />
</Provider>,
document.getElementById("root")
);
3.TodoList.js 文件,使用 connect 去和 Todolist 做连接,不在需要导出 TodoList 组件,直接导出 connect
connect 方法接受四个参数 mapStateToProps(映射到 props)、mapDispatchToProps、mergeProps、(Todolist)(组件)
函数将被调用两次,一次是映射,一次是组件与 store 进行连接,生成新的 connector,并不会改变原始组件
import React, { Component } from 'react';
import { connect } from 'react-redux'
import { getInitData, getInputChangeAction, submitInputAction, deleteItemAction} from './store/actionCreator'
import TodoListUI from './TodoListUI'
class TodoList extends Component {
render() {
return (
return (
<TodoListUI
inputValue={this.props.inputValue}
list={this.props.list}
inputChange={this.props.handleInputValue}
submit={this.props.handleSubmit}
delete={this.props.handleDelete}
/>
);
)
}
}
const mapStateToProps = (state) => {
return {
inputValue: state.inputValue,
list: state.list
}
}
const mapDispatchToProps = (dispatch) => {
return {
handleInputValue: (e) => {
const value = e.target.value
const action = getInputChangeAction(value)
dispatch(action)
},
handleSubmit: () => {
const action = submitInputAction()
dispatch(action)
},
handleDelete: (index) => {
const action = deleteItemAction(index)
dispatch(action)
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
样式
react 动画
…
styled-components
第三方样式插件 styled-components,生成样式组件,相当于给样式加了命名空间,可避免全局样式污染
一个彩蛋:你还在写 reset.css 吗,看过来
https://meyerweb.com/eric/tools/css/reset/
实战遇到的问题
1. 引入图片的两种方式:
1.1 import 引入
import imgURL from "./../images/photo.png";
<img src={imgURL} />;
1.2 require 引入
<img src={require("./../images/photo.png")} />
推荐第一种方式引入,第一种在打包过程中,webpack 会认为是资源从而根据你的配置小于多少 kb 来进行压缩或者 base64 编码;
第二种方式引入,require 内必须是字符串,打包时也只会认为是字符串,不会进行优化,也可能出现找不到图片的问题。
2. styled-components 的安装使用
npm install --save styled-components
2.1 基础用法
/* 创建了一个Wrapper样式组件,该组件渲染之后是一个div标签 */
const Wrapper = styled.div`
color: blue;
`;
/* Wrapper组件跟其余的react组件一样,只不过现在他们有了自己的样式 */
render(<Wrapper>Hello World!</Wrapper>);
2.2 选择器
const Wrapper = styled.div`
/* 应用于Wrapper组件本身和Wrapper组件里的所有html标签 */
color: black;
/* 应用于Wrapper组件里的h3标签 */
h3 {
color: red;
}
/* 应用于Wrapper组件里的className为blue的html标签 */
.blue {
color: blue;
}
`;
render(
<Wrapper>
<p>黑色 p 标签 </p>
<h3>红色 h3 标签</h3>
<p className="blue">蓝色 p 标签</p>
</Wrapper>
);
const Thing = styled.button`
color: blue;
::before {
content: "!!!";
}
:hover {
color: red;
}
`;
render(<Thing>Hello world!</Thing>);
const Thing = styled.div`
/* 应用于className为blue的Thing组件 */
&.blue {
color: blue;
}
/* 应用于className为red的Thing组件里的所有子组件或者html标签 */
.red {
color: red;
}
`;
render(
<React.Fragment>
<Thing className="blue">Thing组件</Thing>
<Thing>
<p className="red">p标签</p>
</Thing>
</React.Fragment>
);
const Thing = styled.div`
/* 应用于紧邻Thing组件的下一个Thing组件 */
& + & {
color: red;
}
`;
render(
<React.Fragment>
<Thing>第一个Thing组件</Thing>
<Thing>第二个Thing组件</Thing>
</React.Fragment>
);