最近学习了下 React,这里记录下从 Vue 快速切换到 React 的一些笔记
Quick Start
可以使用 create-react-app 快速创建 react 项目,里面已经封装好了常用的 webpack 的配置。这个工具其实就和 Vue 里面的 vue-cli 一样,都是用来快速创建脚手架的
npm install -g create-react-app
初始化一个项目
create-react-app demo
cd demo
npm run start
如果你需要定制 webpack 的配置可以执行run run eject
,字面意思就是弹射出 webpack 配置,将 webpack 的配置暴露出来,注意这个操作是不可逆的。
不过使用@craco/craco
可以在不执行run run eject
的情况下修改 webpack 配置,一般也都是这么做的。
JSX 语法
这个就不多说了,在 Vue 里面也用过 JXS 语法,两者用法是一致的,使用三元表达式或者使用与运算的懒惰特性来动态显示隐藏节点,使用 map 函数来循环输出节点
对于在元素上声明的属性可以在组件的 props 中拿到;对于 style 属性,只支持对象,无法写字符串;对于 class 属性使用 className 属性来定义,只支持字符串,不支持['c1', 'c2']
和{c1: true, c2: false}
这种写法,不过可以写成['c1', 'c2'].join(' ')
和Object.keys(obj).filter(k=>obj[k]).join(' ')
注意: 字符串,数字直接渲染。数组会被渲染成列表,里面可以是数字/字符串/JSX,而不是调用它的 toString 方法,对象无法渲染,会报错
对于 Vue 中的 slot,在 React 是把它当作一个 props 来处理的,在 props 中有个 children 存放着组件的一级子节点。这种定义方式似乎很灵活,缺点就是不够语义化
<div>
{this.props.children[0]}
{this.props.children[1]}
</div>
另外对于具名插槽,实现起来也是很方便。由于 props 也可以传递 JSX,你甚至不许需要写插槽也能实现
<Dialog>
<div name="head">title</div>
<div name="body">body</div>
</Dialog>
<div class="dialog">
{props.children.find(v => v.props.name === 'head')}
{props.children.find(v => v.props.name === 'body')}
</div>
let head = <div>title</div>
let body = <div>body</div>
<Dialog head={head} body={body} />
<div class="dialog">
{props.head ? props.head : null}
{props.body ? props.body : null}
</div>
本质上,JSX 是 React.createElement 的语法糖,最终都会编译为 React.createElement。
更多 React 中使用 JSX 的语法可以参考深入 JSX
组件
分为函数组件和 Class 组件。函数组件没有生命周期函数,没有组件实例(this),不支持创建 state。相比较之下 Class 组件就显得功能齐全了,不过 Hook 出现了之后,Class 组件能实现的,函数组件也能实现。
function App(props) {
return (
<div className="App">
<h1>{props.title}</h1>
</div>
)
}
class App extends React.Component {
constructor(props, context) {
super()
console.log(this)
console.log(...arguments)
}
render(h) {
return (
<div className="App">
<h1>{this.props.title}</h1>
</div>
)
}
}
在 class 组件事件绑定中的 this 有个坑需要注意下,因为原生事件的 this 是绑定到 DOM 上的,而事件经过 React 合成后 this 的指向就有问题了,所以默认就干脆让它指向了undefined
,下面是几种修改事件 this 指向的写法
class App extends React.Component {
click1() {
// undefined
console.log(this)
}
click2 = () => {
console.log(this)
}
click3() {
console.log(this)
}
click4() {
console.log(this)
}
click5() {
console.log(this)
}
constructor(){
super()
this.click5 = this.click5.bind(this)
}
render() {
return (
<div className="App">
<div onClick={this.click1}>click1</div>
<div onClick={this.click2}>click2</div>
<div onClick={this.click3.bind(this)}>click3</div>
<div onClick={() => {this.click4()}}>click4</div>
<div onClick={this.click5}>click5</div>
</div>
)
}
}
组件数据
组件的数据有两种 props 和 state。props 是外部传进来的,肯定是不能去变它的,参考 Vue 的单向数据流。组件自身的数据是通过 state 来定义的
Vue 中实现数据绑定靠的是数据劫持(Object.defineProperty())和发布订阅模式,一切都是自动的。在 React 中,需要显式地去调用 setState 去改变 state 中的数据
出于性能考虑,setState 的过程是异步的,React 可能会把多个 setState() 调用合并成一个调用,比如下面的代码可能会得不到预期的结果
// num 5
this.setState({ num: 0 })
// 在这一步,this.state.num的值可能是5
this.setState({ num: this.state.num + 1 })
好在 setState 的第二个参数可以传入一个回调函数,在更新成功后会触发。
this.setState((state, props) => {
return {
num: state.num + 1
}
})
实际上在组件生命周期或 React 合成事件中,setState 是异步;在 setTimeout 或者原生 dom 事件中,setState 是同步。
还有一个就是 state 初始化,一般都是在 constructor 构造函数中完成,但是如果 state 中的数据依赖 props 中的数据,后续 props 改变时 state 是不会变化的。
若期望跟着变化,可以实现生命周期中的 getDerivedStateFromProps。注意这里的方法是静态的拿不到 this
class App2 extends React.Component {
constructor(props) {
super(props)
this.state = { num: props.num }
}
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.num !== prevState.num) {
return {
num: nextProps.num
}
}
return null
}
render() {
return <small>{this.state.num}</small>
}
}
setState 与纯函数
state 要修改的项尽量不去直接改变,而是返回一个新的数据
修改 object 中某项
this.setState({
object: { ...object, key: value }
})
数组操作
temp = array.slice(0)
temp.splice(index, 1)
temp.push(99)
this.setState({
array: temp
})
复杂类型修改,不建议使用
this.setState(prevState => return newState)
如果上面的都不好使,可以使用 forceUpdate
组件性能优化
每次 setState 都会触发子组件的重新渲染。通过shouldComponentUpdate
生命周期函数,我们可以拿到 state 和 props 变化后还未更新上去的值和当前 state 进行手动比较来判断是否需要渲染。默认情况下,该函数始终返回 true,也就是只要 state 发生改变,就会调用 render 方放法。
为了避免所有的组件类都需要重写shouldComponentUpdate
函数,只需要继承 PureComponent 就行了,他对 props 和 state 进行浅层比较,对于函数式组件同样提供了memo
高阶组件进行包裹。
但是也要注意,由于 PureComponent 是浅层比较,下面的第一种写法是不会触发更新的。
class App extends React.PureComponent {
constructor() {
super()
this.state = {
arr1: [1, 2, 3],
arr2: [1, 2, 3]
}
}
update1() {
this.state.arr1.push(1)
this.setState({ arr1: this.state.arr1 })
}
update2() {
let arr = this.state.arr2.slice(0)
arr.push(1)
this.setState({ arr2: arr })
}
render() {
return (
<div>
<span>{this.state.arr1}</span>
<span>{this.state.arr2}</span>
<button onClick={this.update1.bind(this)}>update1</button>
<button onClick={this.update2.bind(this)}>update2</button>
</div>
)
}
}
props 校验/默认值
通过 propTypes 中声明的校验器对 propName 进行校验,建议每个组件都设置 propTypes,别人通过看 propTypes 就能知道该组件可以传入哪些 props
class App2 extends React.Component {
render() {
return <div>{this.props.age}</div>
}
}
// 感觉有点分散,可以写成Class的静态属性
App2.propTypes = {
age: function (props, propName, componentName) {
if (typeof props[propName] != 'number') {
return new Error(`${componentName} props[${propName}] must be number`)
}
}
}
对于一些常用的类型判断,可以导入 prop-types 来判断
import PropTypes from 'prop-types'
App2.propTypes = {
age: PropTypes.number
}
对于默认值,除了在使用的时候判断还可以使用 defaultProps
App2.defaultProps = {
age: 18
}
另外上述在 Class 外声明的属性都是静态属性,目前 ES6 明确规定,Class 内部只有静态方法,没有静态属性,但是有 Babel 去处理,如下写法也可以
class App2 extends React.Component {
static defaultProps = {
age: 20
}
render() {
return <div>{this.props.age}</div>
}
}
表单双向绑定
这里的双向绑定和 Vue 中的 v-model 差的太多了,React 实现双向绑定的方式是在 change 后手动去 setState
这里有个坑,传入的 event 是 React 包装过的,当函数执行完毕,里面的东西就没了,所以在控制台打印的 event 里面的 target 是 null,解决这个问题手动调用下event.persist()
就好了
为了处理拼音候选的问题,你可能会按照下面的方式去封装,其实只需要监听onChange
事件就行了,React 应该对他做了封装,因为原生的onchange
是在失去焦点的时候触发,但是 React 的onChange
只要发生改变就会触发同时也替我们解决了拼音候选的问题
function MyInput(props) {
let composition = false
function onInput() {
if (!composition && props.onInput) {
props.onInput(...arguments)
}
}
function onCompositionStart() {
composition = true
}
function onCompositionEnd() {
composition = false
onInput(...arguments)
}
return (
<input
type="text"
{...props}
onInput={onInput}
onCompositionStart={onCompositionStart}
onCompositionEnd={onCompositionEnd}
/>
)
}
export default class extends React.Component {
constructor(props) {
super()
this.state = {
value: ''
}
}
handleChange(e) {
this.setState({
value: e.target.value
})
}
render(h) {
return (
<React.Fragment>
<div>{this.state.value}</div>
<input type="text" value={this.state.value} onChange={this.handleChange.bind(this)} />
</React.Fragment>
)
}
}
还有一类表单是文件,它的值是只读的,无法通过 value 去改变他,在 React 中称为非受控组件。处理的方式和 Vue 一样,就是使用 ref 拿到 DOM 节点
constructor(props) {
super(props)
this.file = React.createRef()
}
render() {
return (
<div className="App">
<input type="file" ref={this.file} />
</div>
)
}
组件数据共享
方案一,层层传递 props
在非父子组件时,无法向下流动 props 了,官方的做法是状态提升,因为这两个组件虽然无法形成父子组件,但是他们一定有公共的父组件,所以就把数据定义在公共的父组件里,公共父组件定义修改 state 的方法,然后将此方法调用 prop 传递到子组件
这种通信方式和 Vue 的 emit 的方式一样
class App2 extends React.Component {
handleChange = event => {
this.props.emitChange(event.target.value)
}
render() {
return <input type="text" value={this.props.text} onChange={this.handleChange} />
}
}
class App3 extends React.Component {
render() {
return <div>{this.props.text}</div>
}
}
class App extends React.Component {
constructor(props) {
super(props)
this.state = { text: 'text' }
}
emitChange = v => {
this.setState({ text: v })
}
render() {
return (
<div className="App">
<App2 text={this.state.text} emitChange={this.emitChange}></App2>
<App3 text={this.state.text}></App3>
</div>
)
}
}
方案二,使用 Context
要通信的两个组件和他们的公共父组件相距太远时,需要为中间每一个中间组件传递 props 是很麻烦的。使用 context, 我们可以避免通过中间元素传递 props
声明一个 Context,React.createContext()
接收一个参数,当使用 Consumer 时,会找到离他最近的 Provider,未找到 Provider。未找到时该默认值,一旦在 JSX 中写了 Provider 则必须要带上 value 属性。
const ThemeContext = React.createContext()
使用 Provider 包裹后,里面的组件若使用 Consumer,则可以拿到 Provider 中的 value
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
theme: {
now: 'light',
light: {},
dark: {},
toggle: this.toggleTheme
}
}
}
toggleTheme = () => {
let now = this.state.theme.now
let next = now === 'dark' ? 'light' : 'dark'
this.setState({ theme: { ...this.state.theme, now: next } })
}
render() {
return (
<div className="App">
<ThemeContext.Provider value={this.state.theme}>
<App2></App2>
<App3></App3>
</ThemeContext.Provider>
</div>
)
}
}
组件使用 Consumer 来订阅 Context 的变动
class App3 extends React.Component {
render() {
return (
<div>
<ThemeContext.Consumer>
{({ now, toggle }) => (
<div>
<p>当前主题:{now}</p>
<button onClick={toggle}>切换主题</button>
</div>
)}
</ThemeContext.Consumer>
</div>
)
}
}
使用了 contextType 后,上面的写法可以简化为如下写法,同时在各个生命周期函数中都可以通过 this 拿到 Context。同时 contextType 也可以写成一个 map,从而支持消费多个 context。
class App3 extends React.Component {
render() {
let ctx = this.context
return (
<div>
<p>当前主题:{ctx.now}</p>
<button onClick={ctx.toggle}>切换主题</button>
</div>
)
}
}
App3.contextType = ThemeContext
注意上面写法在 context 更新时会导致所有子组件都更新,参考避免 React Context 导致的重复渲染。应该按照下面的写法去写
class ThemeProvider extends React.Component {
constructor() {
super()
this.state = {
now: 'light',
toggle: this.toggleTheme.bind(this)
}
}
toggleTheme() {
let now = this.state.now
let next = now === 'dark' ? 'light' : 'dark'
this.setState({ now: next })
}
render() {
return <ThemeContext.Provider value={this.state}>{this.props.children}</ThemeContext.Provider>
}
}
class App extends React.Component {
render() {
return (
<div className="App">
<ThemeProvider>
<App2></App2>
<App3></App3>
</ThemeProvider>
</div>
)
}
}
方案三,使用 Redux
略…
组件相互调用
组件的相互调用本质上就是一个组件能够拿到另一个组件的实例,然后通过实例去调用组件的方法。Class 组件是有实例的,但是函数组件是无法拿到实例的。
通过 ref 属性可以拿到 Class 组件和 DOM 的实例。
<div>
<ClassComponent ref="ref1"></ClassComponent>
<h1 ref="ref2">123</h1>
</div>
函数式组件没有实例,所以下面的写法会报错的
<FunctionComponent ref="ref3"></FunctionComponent>
函数式组件虽说没有实例,但是函数式组件内的组件是有实例的,可以传递给父组件。通过下面不太正规的写法,可以通过 props 把组件内的 ref 绑定到父组件的属性上
function FunctionComponent(props) {
return <div ref={props.ref3}>FunctionComponent</div>;
}
<FunctionComponent ref3={this.ref3}></FunctionComponent>
为了规范上面的做法,官方提供了高阶组件来包装函数式组件来实现上面的写法。通过 forwardRef 可以将 ref 转发到子组件,子组件拿到父组件中创建的 ref,绑定到自己的某一个元素中。从能够使用 ref 属性来看,应该是返回了一个新的 class 组件
const FunctionComponent = React.forwardRef(function (props, ref) {
return <div ref={ref}>FunctionComponent</div>;
});
<FunctionComponent ref="ref3"></FunctionComponent>
对于非父子父组件之间的相互调用,可以通过发布-订阅的模式来实现,比如 Vue 中的 EventBus。React 中虽说没有 EventBus。但是可以引入第三方库。虽说通过这种方式能实现我们的需求,但是最好还是通过共享数据实现组件之间的通信,毕竟这样更符合组件的思想。
yarn add events
两种特殊的渲染
- Fragment
类似于 Vue 中 template,不过更强大, Vue 的 template 虽说也是渲染一个不占位置的节点,但是是不能作为 Root 的,也就是一个组件渲染出来必须只能有一个 Root 节点,但是 Fragment 却可以直接作为 Root 节点
<React.Fragment>
<span>Hello</span>
{/* 简写方式 不支持props */}
<>
<span>World</span>
</>
</React.Fragment>
- Portal
虽然写在了某个组件下面,可以控制实际渲染到的位置。和ReactDOM.render
差不多,不过 ReactDOM.createPortal
返回的是一个组件
组件生命周期
当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:
- constructor()
- static getDerivedStateFromProps()
- render()
- componentDidMount()
当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:
- static getDerivedStateFromProps()
- shouldComponentUpdate()
- render()
- getSnapshotBeforeUpdate()
- componentDidUpdate()
当组件从 DOM 中移除时会调用如下方法
- componentWillUnmount()
DOM 操作
ReactDOM 的三个常用 API
render()
unmountComponentAtNode()
findDOMNode()
Toast 组件实现
toast 组件是一种特殊的组件,因为他不是写在别的组件内的,而是通过调用函数去触发,完成之后再自动销毁。
在 Vue 中实现这个功能一般是通过 Plugin 功能把调用方式挂载到全局,调用方法后,通过 Vue 把组件的 DOM 渲染出来,通过原生的 语法把 DOM 插入到网页的指定位置,在销毁时清除事件移除 DOM。React 中也是类似的操作
下面是一个简单的事件,要实现动画的话推荐使用react-transition-group
function toast(text, timeout) {
let container = document.querySelector('body>.toast-container')
if (!container) {
container = document.createElement('div')
container.className = 'toast-container'
document.body.appendChild(container)
}
let dom = document.createElement('div')
container.appendChild(dom)
let toast = <div onClick={() => console.log('test')}>{text}</div>
ReactDOM.render(toast, dom)
setTimeout(() => {
ReactDOM.unmountComponentAtNode(dom) // 移除时间,清空DOM
container.removeChild(dom)
dom = null
if (container.childElementCount === 0) {
container.parentNode.removeChild(container)
container = null
}
}, timeout || 3000)
}
CSS 样式
一般是通过 import 进来的 css 是全局的
import './index.css'
为了防止 css 全局污染,除了使用 BEM 命名规则尽可能的规范命名外,根本的解决方式就是模块化使用 CSS
一种写法是声明行内样式。缺点也很明显:大量的样式, 代码混乱,一些子选择器,伪类和一些动画没法用这种方式实现
const style1 = {
fontSize: '15px',
color: 'pink'
}
<div>
<p style={style1}>{this.props.age}</p>
<p style={{ ...style1, color: 'blue' }}>{this.state.value}</p>
</div>
另一种写法是使用 CSS Modules
create-react-app 脚手架都配置好了,当导入.module.css/.module.less/.module.scss
结尾的文件时,它会自动重命名 class 的类名,默认情况下重命名的规则是组件名_类名__hash
。
app.module.css
/*等价于:local(.s1)*/
.s1 {
color: pink;
}
.s1 span {
color: blue;
}
.s3 {
composes: s1; /*继承s1, 也可以引入其他css文件选择去继承某个class*/
font-size: 15px;
}
/*不会被css-loader重命名*/
:global(.s2) {
color: red;
}
使用
import style from './app.module.css'
render() {
return (
<div>
<p className={[style.s1]}>{this.props.age}</p>
<p className={[style.s3]}>{this.state.value}</p>
</div>
)
}
还有一种方案是 CSS in JS,如 styled-components,通过使用方式可以看到它其实是一个高阶组件,由于没有和 webpack 交互,无法在打包时单独抽取为 css 文件,由于是代码生成的,所以肯定是能实现 Vue 的 scoped style 的效果。
npm i styled-components -S
// 没想到吧,函数还可以这样调用!!!
const BoxStyle = styled.div`
button {
width: ${props => props.width};
border: none;
outline: none;
}
`
<BoxStyle width="100px">
<button onClick={this.toggle}>点击</button>
</BoxStyle>
Hook
Hook 是函数组件的拓展,在函数组件中使用 Hook 可以达到和 class 组件相同的功能,同时写法上也更加简单。
-
只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。避免产生 bug
-
只能在函数组件和自定义的 Hook 中调用 Hook,不要在其他 JavaScript 函数中调用
-
所有的 hook 都已
use
开头,包括自定义 hook
-
useState
const [state, setState] = useState(initialState);
返回一个数组,第一个是初始状态,第二个是状态更新函数。状态改变时候,函数式组件会被重新调用重新渲染,注意更新的过程可能不是同步的,而且多次更新可能会推迟到一起去执行,如果新的 state 需要通过使用先前的 state 计算得出,可以传一个函数,函数的第一个参数是最新的 state 当 setState 传入对象时和 class 组件中的 setState 方法不同,并不会合并内容,因此要传入一个完整的 state。
-
useEffect
用来实现 componentDidMount、componentDidUpdate、componentWillUnmount 这几个生命周期。第二个参数(shouldComponentUpdate)和回调函数的返回值具有重要的意义,使用的时候需要注意。多个 useEffect 的执行按照他们的定义顺序执行
-
useContext
虽说在函数式组件中也可以使用 Context,但是使用 useContext 可以不用被 Consumer 包裹了,等价于 class 组件的 contextType
-
useReducer
对于简单的状态,可以使用多个 useState,但是状态多且更新逻辑复杂时可以使用 useReducer。和 redux 的思想很像。
-
useMemo
可以理解为 Vue 中的计算属性,假设有 var1,var2,var3=va1*va1,当 var2 更新时,函数组件重新渲染,var3 有被重新计算,但实际上 va3 并没有依赖 var2,这时就可以使用 useMemo 了,避免重新计算。
-
useCallback
和 useMemo 一样,只不过返回的是函数,如果依赖没有改变,则不会去创建新的函数。可以使用 set 去验证效果
-
useRef
能够拿到子组件/DOM 的实例
-
useImperativeHandle
对 forwardRef 返回的 ref 做限制,只暴露出固定的操作
-
useLayoutEffect
会在渲染的内容更新到 DOM 上之前执行,会阻塞 DOM 的更新。因为 useEffect 是异步的,有时候在 useEffect 中去改变 state 会导致循环渲染,可以通过 useLayoutEffect 来解决这个问题
import React, { useState, useEffect, useReducer } from 'react'
function countReducer(state, action) {
switch (action.type) {
case 'inc':
return { ...state, count: state.count + 1 }
case 'dec':
return { ...state, count: state.count - 1 }
default:
return state
}
}
function Example(props) {
const [state, dispatch] = useReducer(countReducer, { count: 0 })
// 数组的解构赋值的语法, useState 返回一个数组
const [count, setCount] = useState(0)
const [value, setValue] = useState(0)
// componentDidMount,适合初始化一些依赖DOM的插件
useEffect(() => {
console.log('componentDidMount')
// 若返回函数会在componentWillUnmount时候执行,适合清除插件事件
return () => {
console.log('componentWillUnmount')
}
}, [])
// componentDidMount + componentDidUpdate
useEffect(() => {
console.log('componentDidUpdate')
})
// componentDidMount + shouldComponentUpdate
useEffect(() => {
console.log('shouldComponentUpdate on value change')
}, [value])
return (
<div>
<div>
<span>count: {count}</span>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
<div>
<span>value: {value}</span>
<button onClick={() => setValue(count + 1)}>Click me</button>
</div>
<div>
<span>count: {state.count}</span>
<button onClick={() => dispatch({ type: 'inc' })}>inc</button>
<button onClick={() => dispatch({ type: 'dec' })}>dec</button>
</div>
</div>
)
}
有时候为了复用逻辑,可以自定义 hook。但是注意名字一定要以 use 开头
比如下面实现了一个在函数组件生命周期中打印日志的 hook,在对应的组件内直接调用useLogger()
就可以了
function useLogger() {
useEffect(() => {
console.log('组件 mounted')
return () => {
console.log('组件 unmounted')
}
}, [])
}
最佳实践
React 的生态圈可谓是五花八门,不像 Vue 那样都有官方的库,导致无法愉快地使用 React 编码
项目结构可以参考 hy-react-web-music这个项目
public
src/assets/css
src/assets/img
src/components/album-cover/index.js
src/components/album-cover/style.js # styled-components
pages/discover/index.js # router 对应的组件,按照url路径划分
pages/discover/style.js
pages/router/index.js # router 配置文件
services/album.js # api 接口
store/index.js # 全局状态
store/reducer.js
App.js
index.js
写样式体验下来目前就就两个用起来不错:CSS Modules
,styled-components
简单的脚手架可以使用create-react-app
推荐搭配craco
使用来添加更多的 webpack 插件
另外大部分项目都是使用UmiJS作为脚手架
组件库AntDesign的知名度比较高
想要实现 Vue 那样的组件过渡动画可以使用react-transition-group
状态管理的话必定是redux
了
总结
大致了解下 React 后发现还是 Vue 香
感觉 React 的一些概念很难搞懂,比如 Render Props、Hook。相比较而言还是 Vue 的一些概念简单易懂,也可能是 Vue 的中文文档写的比较好吧。
其次就是性能,慢脑子的想我这样会不会导致组件/子组件全部重新渲染,而 Vue 就不会有这种心智负担.
还有就是脚手架,官方的 create-react-app 可以说没留给用户配置的地方,除非你把 webpack 的配置给弹射出来去手动配置,相比较之下 Vue-cli 真的是非常人性化了。不过还好可以使用 UmiJS 这类的第三方脚手架