1 对react的理解
优势:
- 高效灵活
- 声明式的设计,简单使用
- 组件式开发,提高代码复用率
- 单向响应的数据流会比双向绑定的更安全,速度更快
2 real dom和virtual dom
jsx:
-
JSX
实际是一种语法糖,在使用过程中会被babel
进行编译转化成JS
代码, 相当于React.createElement()
方法。 -
ReactDOM.render()
用于将创建好的虚拟DOM
节点插入到某个真实节点上,并渲染到页面上 。
区别:
- 虚拟DOM不会进行重排与重绘操作,而真实DOM会频繁重排与重绘;
- 虚拟DOM的总损耗是“虚拟DOM增删改+真实DOM差异增删改+排版与重绘”,真实DOM的总损耗是“真实DOM完全增删改+排版与重绘”。
3 生命周期
新版:
旧版:
创建阶段:
constructor:
-
实例过程中自动调用的方法,在方法内部通过
super
关键字获取来自父组件的props
-
在该方法中,通常的操作为初始化
state
状态或者在this
上挂载方法
componentWillMount:
- 仅一次,组件第一次被渲染到真实dom前调用,执行完会调用一次render
render:
-
类组件必须实现的方法,用于渲染
DOM
结构,可以访问组件state
与prop
属性 -
注意: 不要在
render
里面setState
, 否则会触发死循环导致内存崩溃
componentDidMount:
-
组件挂载到真实
DOM
节点后执行,其在render
方法之后执行 -
此方法多用于执行一些数据获取,事件监听等操作
更新阶段:
shouldComponentUpdate:
-
用于告知组件本身基于当前的
props
和state
是否需要重新渲染组件,默认情况返回true
-
执行时机:到新的props或者state时都会调用,通过返回true或者false告知组件更新与否
-
一般情况,不建议在该周期方法中进行深层比较,会影响效率
-
同时也不能调用
setState
,否则会导致无限循环调用更新
卸载阶段:
componentWillUnmount:
-
此方法用于组件卸载前,清理一些注册是监听事件,或者取消订阅的网络请求等
-
一旦一个组件实例被卸载,其不会被再次挂载,而只可能是被重新创建
4 state和props
相同点:
- 两者都是 JavaScript 对象
- 两者都是用于保存信息
- props 和 state 都能触发渲染更新
区别:
- props 是外部传递给组件的,而 state 是在组件内被组件自己管理的,一般在 constructor 中初始化
- props 在组件内部是不可修改的,但 state 在组件内部可以进行修改
- state 是多变的、可以修改
类组件中,super(props)给子组件绑定this,之后可以通过this.props访问;函数组件中,直接function Page(props)即可。
setState()异步同步执行情况
5 组件通信
(1)父组件 => 子组件
在子组件标签内传递参数,子组件通过props
属性就能接收父组件传递过来的参数
function EmailInput(props) {
return (
<label>
Email: <input value={props.email} />
</label>
);
}
const element = <EmailInput email="123124132@163.com" />;
(2)子组件 => 父组件
父组件向子组件传一个函数,然后通过这个函数的回调,拿到子组件传过来的值
父组件对应代码如下:
class Parents extends Component {
constructor() {
super();
this.state = {
price: 0
};
}
getItemPrice(e) {
this.setState({
price: e
});
}
render() {
return (
<div>
<div>price: {this.state.price}</div>
{/* 向子组件中传入一个函数 */}
<Child getPrice={this.getItemPrice.bind(this)} />
</div>
);
}
}
子组件对应代码如下:
class Child extends Component {
clickGoods(e) {
// 在此函数中传入值
this.props.getPrice(e);
}
render() {
return (
<div>
<button onClick={this.clickGoods.bind(this, 100)}>goods1</button>
<button onClick={this.clickGoods.bind(this, 1000)}>goods2</button>
</div>
);
}
}
(3)兄弟组件之间的通信
父组件作为中间层来实现数据的互通,通过使用父组件传递
class Parent extends React.Component {
constructor(props) {
super(props)
this.state = {count: 0}
}
setCount = () => {
this.setState({count: this.state.count + 1})
}
render() {
return (
<div>
<SiblingA
count={this.state.count}
/>
<SiblingB
onClick={this.setCount}
/>
</div>
);
}
}
(4)父组件向后代组件传递
父组件向后代组件传递数据是一件最普通的事情,就像全局数据一样
使用context
提供了组件之间通讯的一种方式,可以共享数据,其他数据都能读取对应的数据
通过使用React.createContext
创建一个context
const PriceContext = React.createContext('price')
context
创建成功后,其下存在Provider
组件用于创建数据源,Consumer
组件用于接收数据,使用实例如下:
Provider
组件通过value
属性用于给后代组件传递数据:
<PriceContext.Provider value={100}>
</PriceContext.Provider>
如果想要获取Provider
传递的数据,可以通过Consumer
组件或者或者使用contextType
属性接收,对应分别如下:
class MyClass extends React.Component {
static contextType = PriceContext;
render() {
let price = this.context;
/* 基于这个值进行渲染工作 */
}
}
Consumer
组件:
<PriceContext.Consumer>
{ /*这里是一个函数*/ }
{
price => <div>price:{price}</div>
}
</PriceContext.Consumer>
(5)非关系组件传递
如果组件之间关系类型比较复杂的情况,建议将数据进行一个全局资源管理,从而实现通信,例如redux
。
6 事件
事件的执行顺序(合成事件&原生事件)e.preventDefault()阻止事件冒泡
为了解决正确输出this
的问题,常见的绑定方式有如下:
- render方法中使用bind
- render方法中使用箭头函数
- constructor中bind
- 定义阶段使用箭头函数绑定(常用,建议函数组件和类组件都这样写)
7 组件
常见的组件创建方式:
- 函数式创建
- 通过 React.createClass 方法创建
- 继承 React.Component 创建
受控组件与非受控组件:
-
受控组件一般需要初始状态(比如value)和一个状态更新事件函数 (比如onChange)
-
大部分时候推荐使用受控组件来实现表单,因为在受控组件中,表单数据由
React
组件负责处理 -
如果选择非受控组件的话,控制能力较弱,表单数据就由
DOM
本身处理,但更加方便快捷,代码量少
8 react refs
React
中的 Refs
提供了一种方式,允许我们访问 DOM
节点或在 render
方法中创建的 React
元素。
本质为ReactDOM.render()
返回的组件实例,如果是渲染组件则返回的是组件实例,如果渲染dom
则返回的是具体的dom
节点。
使用
- 传入字符串
- 传入对象
- 传入函数
- 传入hook,useRef()
应用场景
-
更新组件,但一般使用props和state的方式,因为过多使用
refs
,会使组件的实例或者是DOM
结构暴露,违反组件封装的原则 。 -
某些场景下,一般使用ref:
-
对DOM 元素焦点的控制、内容选择或者媒体播放;
-
通过对DOM元素控制,触发动画特效;
-
通第三方DOM库的集成。
-
React.forwardRef()
与高阶组件
this.myRef = React.createRef(); // 创建
const node = this.myRef.current; // 访问
一些知识点
(1)为dom元素添加ref
通过 “current” 来访问 DOM 节点
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
// 创建一个 ref 来存储 textInput 的 DOM 元素
this.textInput = React.createRef();
this.focusTextInput = this.focusTextInput.bind(this);
}
focusTextInput() {
// 直接使用原生 API 使 text 输入框获得焦点
// 注意:我们通过 "current" 来访问 DOM 节点
this.textInput.current.focus();
}
render() {
// 告诉 React 我们想把 <input> ref 关联到
// 构造器里创建的 `textInput` 上
return (
<div>
<input
type="text"
ref={this.textInput} />
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
}
(2)为class组件添加ref
class AutoFocusTextInput extends React.Component {
constructor(props) {
super(props);
// 创建ref 指向 CustomTextInput 组件实例
this.textInput = React.createRef();
}
componentDidMount() {
// 调用子组件 focusTextInput方法 触发子组件内部 文本框获取焦点事件
this.textInput.current.focusTextInput();
}
render() {
return (
<CustomTextInput ref={this.textInput} />
);
}
}
当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性。
(3)函数组件与ref
默认情况下,不能在函数组件上使用 ref
属性,因为它们没有实例 ;
但可以在函数组件内部使用 ref
属性,只要它指向一个 DOM 元素或 class 组件。
function CustomTextInput(props) {
// 这里必须声明 textInput,这样 ref 才可以引用它
const textInput = useRef(null);
function handleClick() {
textInput.current.focus();
}
return (
<div>
<input
type="text"
ref={textInput} />
<input
type="button"
value="Focus the text input"
onClick={handleClick}
/>
</div>
);
}
(4)ref转发:获取子组件的dom节点
可以通过子组件的ref
访问到子组件实例的props
、state
、refs
、实例方法(非继承而来的方法)等 。
使用ref访问子组件的情况:
- 访问子组件的某个具体的dom节点完成某些逻辑,通过
this.refs.childComponentRefName.refs.someDomRefName
来完成,例如React 父组件如何获取子组件的ref值?。 - 可以访问子组件的公共实例方法完成某写逻辑。例如子组件定义了一个
reset
方法用来重置子组件表单元素值,这时父组件可以通过this.refs.childComponentRefName.reset()
来完成子组件表单元素的重置。
(5)回调refs
传递一个函数。这个函数中接受 React 组件实例或 HTML DOM 元素作为参数,以使它们能在其他地方被存储和访问。
// 使用 ref 回调函数,在实例的属性中存储对 DOM 节点的引用
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
this.textInput = null;
this.setTextInputRef = element => {
this.textInput = element;
};
this.focusTextInput = () => {
// 使用原生 DOM API 使 text 输入框获得焦点
if (this.textInput) this.textInput.focus();
};
}
componentDidMount() {
// 组件挂载后,让文本框自动获得焦点
this.focusTextInput();
}
render() {
// 使用 `ref` 的回调函数将 text 输入框 DOM 节点的引用存储到 React
// 实例上(比如 this.textInput)
return (
<div>
<input
type="text"
ref={this.setTextInputRef}
/>
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
}
React 将在组件挂载时,会调用 ref
回调函数并传入 DOM 元素,当卸载时调用它并传入 null
。在 componentDidMount
或 componentDidUpdate
触发前,React 会保证 refs 一定是最新的。
可以在组件间传递回调形式的refs。
9 context
Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。
Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。
使用
// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light'); // 关键点
class App extends React.Component {
render() {
// 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
// 无论多深,任何组件都能读取这个值。
// 在这个例子中,我们将 “dark” 作为当前的值传递下去。
return (
<ThemeContext.Provider value="dark"> // 关键点
<Toolbar />
</ThemeContext.Provider>
);
}
}
// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
class ThemedButton extends React.Component {
// 指定 contextType 读取当前的 theme context。
// React 会往上找到最近的 theme Provider,然后使用它的值。
// 在这个例子中,当前的 theme 值为 “dark”。
static contextType = ThemeContext; // 关键点
render() {
return <Button theme={this.context} />; // 使用 this.context 来消费最近 Context 上的那个值
}
}
Context 主要应用场景在于 很多不同层级的组件需要访问同样一些的数据。
API
React.createContext
:
const MyContext = React.createContext(defaultValue);
当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider
中读取到当前的 context 值。
Context.Provider
:
<MyContext.Provider value={/* 某个值 */}>
Provider 接收一个 value
属性,传递给消费组件。
当 Provider 的 value
值发生变化时,它内部的所有消费组件都会重新渲染。
Class.contextType
:
class MyClass extends React.Component {
componentDidMount() {
let value = this.context; // 关键点
/* 在组件挂载完成后,使用 MyContext 组件的值来执行一些有副作用的操作 */
}
componentDidUpdate() {
let value = this.context;
/* ... */
}
componentWillUnmount() {
let value = this.context;
/* ... */
}
render() {
let value = this.context;
/* 基于 MyContext 组件的值进行渲染 */
}
}
MyClass.contextType = MyContext;
挂载在 class 上的 contextType
属性会被重赋值为一个由 React.createContext()
创建的 Context 对象。这能让你使用 this.context
来消费最近 Context 上的那个值。
Context.Consumer
:
<MyContext.Consumer>
{value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>
Context.displayName
:
const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';
<MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中
<MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中
context 对象接受一个名为 displayName
的 property,类型为字符串。React DevTools 使用该字符串来确定 context 要显示的内容。
应用
动态context
在嵌套组件中更新context
通过 context 传递一个函数,使得 consumers 组件更新 context。
消费多个context
10 typeScript
接口的定义和使用
联合类型|
类型断言???类型推断
可选属性: 属性名字后面加一个?
索引签名: 定义对象中key(propName)和value的数据结构,后续对象中的属性,只要key和value满足索引签名的限定即可, 无论有多少个都无所谓
interface FullName{
firstName:string
lastName:string
middleName?:string // 可选属性
[propName:string]:any // 索引签名
}
?
操作符:
- 可选参数
// 这里的 ?表示这个参数 field 是一个可选参数
function getUser(user: string, field?: string) {
}
- 成员
// 这里的?表示这个name属性有可能不存在
class A {
name?: string
}
interface B {
name?: string
}
- 安全链式调用
// 这里 Error对象定义的stack是可选参数,如果这样写的话编译器会提示
// 出错 TS2532: Object is possibly 'undefined'.
return new Error().stack.split('\n');
// 我们可以添加?操作符,当stack属性存在时,调用 stack.split。
// 若stack不存在,则返回空
return new Error().stack?.split('\n');
// 以上代码等同以下代码, 感谢 @dingyanhe 的监督
const err = new Error();
return err.stack && err.stack.split('\n');
!
操作符:
- 一元运算符
// ! 就是将之后的结果取反,比如:
// 当 isNumber(input) 为 True 时返回 False;
// isNumber(input) 为 False 时返回True
const a = !isNumber(input);
- 成员
// 因为接口B里面name被定义为可空的值,但是实际情况是不为空的,
// 那么我们就可以通过在class里面使用!,重新强调了name这个不为空值
class A implemented B {
name!: string
}
interface B {
name?: string
}
这里可以参考ts 更严格的类属性检查
Typescript 2.7 引入了一个新的控制严格性的标记 –strictPropertyInitialization, 这个参数在 tsconfig.ts 中来配置
"strictNullChecks": true
"strictPropertyInitialization": true
作用
- 使用这个标记会确保类的每个实例属性都会在构造函数里或使用属性初始化器赋值。
- 它会明确地进行从变量到类的实例属性的赋值检查
举例
class C {
foo: number;
bar = "hello";
baz: boolean;
constructor() {
this.foo = 42;
}
}
上述代码,首先编辑器会报错: 属性“baz”没有初始化表达式,且未在构造函数中明确赋值。ts(2564)
。
其次在编译报错:error TS2564: Property 'baz' has no initializer and is not definitely assigned in the constructor.
两种都告诉开发者,应该给 baz 显示赋值,但是某种情况下,在初始化的时候我们并不想赋值,更期望是 undefined,而后再去赋值,此时 !: 就派上用场了。
在上述代码中 属性 baz 冒号之前加上 ! ,这样就不会报错了
class C {
foo: number;
bar = "hello";
baz!: boolean;
constructor() {
this.foo = 42;
}
}
- 强制链式调用
// 这里 Error对象定义的stack是可选参数,如果这样写的话编译器会提示
// 出错 TS2532: Object is possibly 'undefined'.
new Error().stack.split('\n');
// 我们确信这个字段100%出现,那么就可以添加!,强调这个字段一定存在
new Error().stack!.split('\n');
类型断言
11 redux
核心概念
State
state是数据集合。
action
State的变化,会导致View的变化。但是,用户接触不到 State,只能接触到View 。所以,State的变化必须是 View导致的。
action就是改变state的指令,有多少操作state的动作就会有多少action。
reducer
action发出命令后将state放入 reucer加工函数 中,返回新的state。
store
let store = createStore(reducers);
Store 就是把它们联系到一起的对象。Store 有以下职责:
维持应用的 state;
提供 getState() 方法获取 state;
提供 dispatch(action) 方法更新 state;
通过 subscribe(listener) 注册监听器;
通过 unsubscribe(listener) 返回的函数注销监听器。
我们可以通过store.getState()
来了解工厂中商品的状态, 使用store.dispatch()
发送action指令。
例子:
import { createStore } from 'redux'
// reducer加工函数,接受state和action两个参数,action自带一个type属性
const reducer = (state = {count: 0}, action) => {
switch (action.type){
case 'INCREASE': return {count: state.count + 1};
case 'DECREASE': return {count: state.count - 1};
default: return state;
}
}
const actions = {
increase: () => ({type: 'INCREASE'}),
decrease: () => ({type: 'DECREASE'})
}
// 创建store
const store = createStore(reducer);
// 监听
store.subscribe(() =>
console.log(store.getState())
);
// 发送action指令,更新state
store.dispatch(actions.increase()) // {count: 1}
store.dispatch(actions.increase()) // {count: 2}
store.dispatch(actions.increase()) // {count: 3}
react-redux
核心
< Provider store>
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
Provider 内的任何一个组件(比如这里的 Comp),如果需要使用 state 中的数据,就必须是「被 connect 过的」组件——使用 connect 方法对「你编写的组件(MyComp)」进行包装后的产物。
这个函数允许我们将 store 中的数据作为 props 绑定到组件上。
react-redux中的connect方法将store上的 getState 和 dispatch 包装成 组件的props。
例子
// ****************************** root.js ******************************
// 创建一个store全局管理state和action
const store = createStore(reducer);
// Provider在根组件<App>外面包了一层,App的所有子组件就默认都可以拿到store,通过组件的props传递
export default class Root extends Component {
render() {
return (
<Provider store={store}> // 关键点
<App/>
</Provider>
)
}
}
// ****************************** app.js ******************************
// view提供UI组件
class Counter extends Component {
render() {
// View的状态来自于props,props传递的是store中的state
const { value, onIncreaseClick } = this.props // 关键点
return (
<div>
<span>{value}</span>
<button onClick={onIncreaseClick}>Increase</button>
</div>
)
}
}
// 将UI组件和容器组件连接起来
const App = connect( // 关键点
state =>({
value: state.count //输入,将store中的state通过props输入
}),
dispatch =>({ // 输出,将action作为props绑定到View上,用户操作类型在此分发出去
onIncreaseClick: () => dispatch(increaseAction.increase())
})
)(Counter)
export default App
// ****************************** increaseAction.js ******************************
// 定义action的类型
export const INCREMENT = 'INCREMENT';
// action 创建函数只是简单的返回一个 action
export function increase(params) {
return {
type: INCREMENT,
data:data
};
}
// ****************************** counterReducer.js ******************************
// 提供一个初始的状态
initState={
count: 0
}
// 通过判断Action的类型,返回新的数据改变后的state对象,即使没有任何状态的改变,也要返回一个对象
export default function counter(state = initState, action) {
const count = state.count
switch (action.type) {
case INCREMENT:
return { count: count + 1 }
default:
return state
}
}
rematch
第一步:Init
init 用来配置你的 reducers, devtools & store。
// ****************************** index.js *****************************
import { init } from '@rematch/core'
import * as models from './models'
const store = init({
models,
//plugins: [selectPlugin(), persistPlugin, trackPlugin],
//redux: {
// devtoolOptions: {
// disabled: !__DEV__
// }
//}
})
export default store
第二步:Models
该model促使 state, reducers, async actions 和 action creators 放在同一个地方。
// ****************************** models.js *****************************
import { createModel } from '@rematch/core';
const model = {
state: 0, // initial state
reducers: {
// handle state changes with pure functions
increment(state, payload) {
return state + payload
}
},
effects: {
// handle state changes with impure functions.
// use async/await for async actions
async incrementAsync(payload, rootState) {
await new Promise(resolve => setTimeout(resolve, 1000))
this.increment(payload)
}
}
}
export default createModel(model);
第三步:Dispatch
dispatch 是我们如何在你的model中触发 reducers 和 effects。 Dispatch 标准化了你的action,而无需编写action types 或者 action creators。
import { dispatch } from '@rematch/core'
// state = { count: 0 }
// reducers
dispatch({ type: 'model/increment', payload: 1 }) // state = { count: 1 }
dispatch.model.increment(1) // state = { count: 2 }
// effects
dispatch({ type: 'model/incrementAsync', payload: 1 }) // state = { count: 3 } after delay
dispatch.model.incrementAsync(1) // state = { count: 4 } after delay
Dispatch 能被直接调用,或者用 dispatch[model][action](payload)
简写。
第四步:View
Provider和connect。connect方法将store上的 getState 和 dispatch 包装成 组件的props。
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider, connect } from 'react-redux'
import store from './index'
const Count = props => (
<div>
The count is {props.count} // 通过props.xxx访问
<button onClick={props.increment}>increment</button>
<button onClick={props.incrementAsync}>incrementAsync</button>
</div>
)
const mapState = state => ({
count: state.model
})
const mapDispatch = ({ count: { increment, incrementAsync }}) => ({
increment: () => increment(1),
incrementAsync: () => incrementAsync(1)
})
const CountContainer = connect(mapState, mapDispatch)(Count)
ReactDOM.render(
<Provider store={store}>
<CountContainer />
</Provider>,
document.getElementById('root')
)
使用 Rematch 后,我们可以通过 store.dispatch.model.xxx()
来执行一个 Action 操作,并且 store.dispatch
的类型是 TS 自动推算出来的,无需开发者手动定义。
export type Dispatch = typeof store.dispatch;
export default connect(null, (dispatch: Dispatch) => ({
showTip: dispatch.bbsMainTab.showTip,
hideTip: dispatch.bbsMainTab.hideTip
}))(MainTab);
RematchRootState
、createModel
注意ts里面的typeof用法
Xxx<typeof {name:'金毛', age:9}>
// 此处就相当于:
Xxx<typeof {name:string, age:number}>
-
[modelKey in keyof M]
循环M
对象里面所有的key值, 每次循环时命名为modelKey。 -
M[modelKey]
就是取出对应的值, 这里特指ts
里面的类型值。