最近学习了下 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 配置。注意这个操作书不可逆向的,如果需要回退,不仅仅是代码的回退,还需要删除 node_modules 文件夹进行重新安装
JSX 语法
这个就不多说了,在 Vue 里面也用过 JXS 语法,两者用法是一致的,使用三元表达式或者使用与运算的懒惰特性来动态显示隐藏节点,使用 map 函数来循环输出节点
对于在元素上声明的属性可以在组件的 props 中拿到,对于 class 属性,建议使用 className 属性来定义,以此来和原生定义进行区分
对于 Vue 中的 slot,在 React 是把它当作一个 props 来处理的,在 props 中有个 children 存放着组件的一级子节点。这种定义方式似乎很灵活,缺点就是不够语义化
<div>
{this.props.children[0]}
{this.props.children[1]}
</div>
你看,通过简单的定义就实现了 Vue 中的具名插槽
<div>
{this.props.children.find(v => v.props.name === '1')}
{this.props.children.find(v => v.props.name === '2')}
</div>
本质上,JSX 是 React.createElement 的语法糖,最终都会编译为 React.createElement。
更多 React 中使用 JSX 的语法可以参考深入 JSX
组件
分为函数组件和 Class 组件
function App(props) {
return (
<div className="App">
<h1>{props.title}</h1>
</div>
)
}
上面的函数组件可以改写为 Class 组件。在开发的过程中肯定是推荐使用使用 Class 组件的,因为可以在里面定义各种生命周期函数和点击事件
这里关于点击事件有一个 this 指向的坑,解决方案是使用 bind 或者箭头函数,不然的事件处理函数内拿到的 this 是 undefined
关于使用 bind 也有好几种方式,可以在 constructor 中 bind,也可以在事件发生时 bind
class App extends React.Component {
divClick = () => {
console.log(this)
}
spanClick = function () {
console.log(this)
}
render() {
return (
<div className="App">
<h1>{this.props.title}</h1>
<div onClick={this.divClick}>123</div>
<span onClick={this.spanClick.bind(this)}>456</span>
</div>
)
}
}
组件数据
组件的数据有两种 props 和 state。props 是外部传进来的,肯定是不能去变它的,参考 Vue 的单向数据流。组件自身的数据是通过 state 来定义的
Vue 中实现数据绑定靠的是数据劫持(Object.defineProperty())和发布订阅模式,一切都是自动的。在 React 中,需要显式地去调用 setState 去改变 state 中的数据
出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用,他是异步的,比如下面的代码可能会得不到预期的结果
// num 5
this.setState({ num: 0 })
// 在这一步,this.state.num的值可能是5
this.setState({ num: this.state.num + 1 })
如果你在更新 state 时依赖原有 state 的内容,为了解决这一问题,官方建议使用下面的写法
this.setState((state, props) => {
return {
num: state.num + 1
}
})
还有一个就是 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 基本使用
修改 object 中某项
this.setState({
object: { ...object, key: value }
})
数组操作
array.splice(index, 1)
array.push(99)
this.setState({
array: array
})
复杂类型修改,不建议使用
this.setState(prevState => return newState)
如果上面的都不好使,可以使用 forceUpdate
props 校验/默认值
通过 propTypes 中声明的校验器对 propName 进行校验,建议每个组件都设置 propTypes,别人通过看 propTypes 就能知道该组件可以传入哪些 props
class App2 extends React.Component {
render() {
return <div>{this.props.age}</div>
}
}
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 实现双向绑定都是监听 input 事件手动去 setState
这里有个坑,传入的 event 是 React 包装过的,当函数执行完毕,里面的东西就没了,所以在控制台打印的 event 里面的 target 是 null,解决这个问题手动调用下event.persist()
就好了
handleChange = event => {
this.setState({
num: Number(event.target.value)
})
}
render() {
return (
<div className="App">
<input type="number" value={this.state.num} onChange={this.handleChange} />
</div>
)
}
还有一类表单是文件,它的值是只读的,无法通过 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,未找到时该默认值
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。同时也说明了一个问题,一个组件可能会消费多个 context,但是最好还是消费一个 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
组件生命周期
当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:
- constructor()
- static getDerivedStateFromProps()
- render()
- componentDidMount()
当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:
- static getDerivedStateFromProps()
- shouldComponentUpdate()
- render()
- getSnapshotBeforeUpdate()
- componentDidUpdate()
当组件从 DOM 中移除时会调用如下方法
- componentWillUnmount()
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
。看起来不错,假如有个类命名带横线时.main-title
就没办法取出了
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,CSS 由 JavaScript 生成而不是在外部文件中定义。改功能并不是 React 的一部分,而是由第三方库提供,比如 styled-components
其他知识点
-
组件渲染失败的处理
在父组件/根组件使用 componentDidCatch 方法监听失败错误,用于记录日志。用 getDerivedStateFromError 方法来更新 state 的状态,一般是使一些错误提示组件取消隐藏
-
组件可以没有根元素
在 render 函数中,必须要有一个根元素,一般都是 div。在 DOM 节点如果不需要这个根节点,可以使用
<React.Fragment>
包裹,或者使用<> </>
包裹 -
使用 jQuery
这一点和 Vue 一样,都是使用 ref。需要注意的是的,在 componentWillUnmount 阶段,记得调用一些 jQuery 插件的销毁时间
-
Portals
默认都是在父组件内渲染,使用 Portals 可以指定渲染的位置
import ReactDOM from 'react-dom' class App4 extends React.Component { constructor(props) { super(props) this.slot = document.querySelector('#outer') if (!this.slot) { this.slot = document.createElement('div') this.slot.setAttribute('id', 'outer') document.body.appendChild(this.slot) } } render() { let node = <div>node</div> return ReactDOM.createPortal(node, this.slot) } }
-
Profiler
一个工具节点,可以计算子子组件的渲染消耗
Hook
Hook 就是 JavaScript 函数,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。简单来说 Hook 就是函数组件的拓展,达到和 class 组件相同的功能,同时写法上也更加简单
-
只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。避免产生 bug
-
只能在函数组件和自定义的 Hook 中调用 Hook,不要在其他 JavaScript 函数中调用
useState:返回一个数组,第一个是值,第二个更新值的函数。同时 React 会在重复渲染时保留这个 state。在一个函数组件 useState 可以多次调用,创建多个 state,另外 useState 也可以传入对象和数组
useEffect:它跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API,相当于声明了一个回调函数,默认情况下,它在第一次渲染之后和每次更新之后都会执行。useEffect 可以调用多次
useContext:不用被 Consumer 包裹,也可以在 jsx 外拿到 Context
import React, { useState } from 'react'
function Example(props) {
// 数组的解构赋值的语法, useState 返回一个数组
const [count, setCount] = useState(0)
const [value, setValue] = useState(0)
// 在useEffect内调用update时,要给定 dependences 列表,防止无限更新
useEffect(() => {
setValue(count * count)
}, [count])
return (
<div>
<p>
You clicked {count} computed: {value}
</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
)
}
总结
大致了解下 React 后发现还是 Vue 香
感觉 React 的一些概念很难搞懂,比如 Render Props、Hook。相比较而言还是 Vue 的一些概念简单易懂,也可能是 Vue 的中文文档写的比较好吧。
其次就是脚手架,官方的 create-react-app 可以说没留给用户配置的地方,除非你把 webpack 的配置给弹射出来去手动配置,相比较之下 Vue-cli 真的是非常人性化了。不过还好可以使用 UmiJS 这类的第三方脚手架