起步
1、安装官方脚手架: npm install -g create-react-app
2、创建项目:create-react-app react-project-name
3、启动项目:npm start
4、暴露配置项:npm run eject
入口文件:index.js
ReactDOM.render(<App />, document.getElementById('root'));
babel-loader 最终会把jsx转换成js能识别的内容
JSX:是一种JavaScript的语法扩展,其格式比较像模板语言,但事实上完全是在JavaScript内部实现的
JSX实质就是 React.createElement 的调用,最终的结果是React元素(JavaScript对象)
const jsx = <div>hello react</div> ReactDOM.render(jsx, document.getElementById('root'));
组件
组件是抽象的独立功能模块,react应用程序由组件构建而成
组件的两种表现形式:class组件 function组件
class组件通常拥有状态(state)和生命周期(lifeCycle),继承于Component,实现render方法
import React, { Component } from 'react' export default class Home extends Component { render() { return ( <div> 我是home页面 </div> ) } }
function组件
函数组件通常无状态,仅关注内容展示,返回渲染结果即可。
---从React16.8开始引入了hooks,函数组件也能够拥有状态。
写一个定时器显示当前组件
import React, { Component } from 'react' export default class Home extends Component { constructor(props){ super(props) this.state = { date: new Date() } } // 组件挂载后---生命周期函数 componentDidMount(){ this.timerId = setInterval(()=>{ this.setState({ date: new Date() }) }, 1000) } // 组件卸载之前 componentWillUnmount(){ clearInterval(this.timerId) } render() { const str = '我是home页面' const {date} = this.state return ( <div> <h1>{str}</h1> <p>{date.toLocaleTimeString()}</p> </div> ) } }
setState方法
setCounter(){ // 这样写找不到this this.setState({ counter: this.state.counter + 1 }) } // 解决方法一 constructor(props){ super(props) this.state = { date: new Date(), counter: 0 } this.setCounter = this.setCounter.bind(this) } setCounter(){ this.setState({ counter: this.state.counter + 1 }) } // 解决方法二 setCounter = () => { this.setState({ counter: this.state.counter + 1 }) }
setState为异步,如何将其实现同步
setCounter = () => { this.setState({ counter: this.state.counter + 1 }) console.log(this.state.counter) // 这里打印0,页面显示1 } // 打印0时,页面显示2,并且每次只加2 setCounter = () => { this.setState({ counter: this.state.counter + 1 }) this.setState({ counter: this.state.counter + 2 }) console.log(this.state.counter) } // 实现同步 // 方式一 setCounter = () => { this.setState(nextState => { return { counter: nextState.counter + 1 } }) this.setState(nextState => { return { counter: nextState.counter + 2 } }) } // 方式二 setCounter = () => { setTimeout(() => { this.setState({ counter: this.state.counter + 1 }) this.setState({ counter: this.state.counter + 2 }) console.log(this.state.counter) }, 0) } // 方式三:原生事件上去绑定 componentDidMount(){ document.getElementsByTagName('button')[0].addEventListener('click', ()=>{ this.setState({ counter: this.state.counter + 1 }) this.setState({ counter: this.state.counter + 2 }) }) }
总结:setState只有在合成事件和钩子函数中是异步的,在原生事件和setTimeout、setInterval中都是同步的
function组件
function组件是没有状态和生命周期的
如果一定要使用state,可以进行引入 useState、useEffect
下列代码将上面的class组件转成funciton组件
import React, {useState, useEffect} from 'react' export default function User() { const [date, setDate] = useState(new Date()) useEffect(() => { const timerId = setInterval(() => { setDate(new Date()) }, 1000) return () => clearInterval(timerId) }) return ( <div> <h1>我是user页面</h1> <p>{date.toLocaleTimeString()}</p> </div> ) }
事件处理
onclick事件和onchange事件
import React, { Component } from 'react' export default class Search extends Component { constructor(props){ super(props) this.state = { name: '12' } } handler = () => { console.log('handler'); } change = (event) => { let value = event.target.value this.setState({ name: value }) console.log('change', this.state.name); } render() { const {name} = this.state console.log(this);return ( <div> <h1>我是Search页面</h1> <button onClick={this.handler}>click</button> <input onChange={this.change} value={name}/> </div> ) } }
事件回调函数注意绑定this指向,常见三种方法:
1、构造函数中绑定并覆盖:this.change = this.change.bind(this)
2、方法定义为箭头函数:change = ()=>{}
3、事件中定义为箭头函数:onChange = { () => this.change() }
--- react里遵循单项数据流,没有双向绑定,输入框要设置value和onChange,成为受控组件。
组件通信
props属性传递
// App.js中 const store = { userInfo: { userName: 'Dylan' } } function tellme(msg){ console.log('tellme:', msg); } function App() { return ( <div className="App"> <Search store={store} tellme={tellme} /> </div> ); } export default App; // Search.js中 handler = () => { console.log('handler'); const {tellme} = this.props tellme('im dylanLv') } render() { const {name} = this.state console.log(this); const {userInfo} = this.props.store return ( <div> <h1>我是Search页面,{userInfo.userName}</h1> <button onClick={this.handler}>click</button> <input onChange={this.change} value={name}/> </div> ) }
如果父组件传递的是函数,则可以把子组件信息传入父组件,这个常称为状态提示
react16的生命周期
import React, {Component} from 'react' export default class LifeCycle extends Component { constructor(props) { super(props) this.state = { counter: 0 } console.log('constructor', this.state.counter); } // 挂载之前 UNSAFE_componentWillMount() { console.log('componentWillMount', this.state.counter); } // 挂载之后 componentDidMount() { console.log('componentDidMount', this.state.counter); } // 更新之前 UNSAFE_componentWillUpdate() { console.log('componentWillUpdate', this.state.counter); } // 更新之后 componentDidUpdate() { console.log('componentDidUpdate', this.state.counter); } // 卸载之前 componentWillUnmount() { console.log('componentWillUnmount', this.state.counter); } // 是否更新--执行render--优化使用 shouldComponentUpdate(nextProps, nextState) { console.log('shouldComponentUpdate', this.state.counter, nextState.counter, nextProps); return this.state.counter != 5 } setCounter = () => { this.setState({ counter: this.state.counter + 1 }) } render() { console.log('render'); const {counter} = this.state return ( <div> <h1>我是一个lifeCycle, Hylan</h1> <p>{counter}</p> <button onClick={this.setCounter}>click</button> {!!(counter % 2) && <Foo/>} </div> ) } } class Foo extends Component { componentWillUnmount() { console.log('componentWillUnmount foo'); } render() { return <div>我是FOO组件</div> } }
V17可能会废弃的三个生命周期函数用 getDerivedStateFromProps 替代,目前使用的话加上 UNSAFE_
- componentWillMount
- componentWillReceiveProps
- conponentWillUpdate
引入两个新的生命周期
- static getDerivedStateFromProps
- getSnapshotBeforeUpdate
static getDerivedStateFromProps(props, state){ // getDerivedStateFromProps 会在调用render方法之前调用 // 并且在初始挂载以及后续更新时都会被调用 // 它应返回一个对象来更新 state ,如果返回 null 则不更新任何内容 const { counter } = state console.log('getDerivedStateFromProps', counter) return counter < 8 ? null : { counter: 0 } } // 上一次(更新前)的影像 // 返回值会作为 componentDidUpdate 的第三个参数 getSnapshotBeforeUpdate(prevProps, prevState){ const { counter } = prevState console.log('getSnapshotBeforeUpdate', counter) return null } // 更新之后 componentDidUpdate(prevProps, prevState, snapshot) { console.log('componentDidUpdate', this.state.counter); }
使用Context 上下文传值---可以跨层级传值
// App.js import React from 'react'; import './App.css'; import Home from './pages/Home'; const Context = React.createContext() const Provider = Context.Provider const Consumer = Context.Consumer const store = { home: {}, user: { name: 'Dylan' } } function App() { return ( <div className="App"> <Provider value={store}> <Consumer> {ctx => <Home {...ctx}/>} </Consumer> </Provider> </div> ); } export default App; // Home.js import React, { Component } from 'react' export default class Home extends Component { render() { // home:{ // home: {} // user: {name: "Dylan"} // } console.log("home", this.props); return ( <div> home </div> ) } }
一般使用时会将Context暴露在外部,使用者将其引用就行了
AppContext.js 文件
import React from 'react'; const Context = React.createContext() export const Provider = Context.Provider export const Consumer = Context.Consumer
App.js 文件
import React from 'react'; import './App.css'; import Home from './pages/Home'; import {Provider, Consumer} from './AppContext' const store = { home: {}, user: { name: 'Dylan' } } function App() { return ( <div className="App"> <Provider value={store}> <Consumer> {ctx => <Home {...ctx}/>} </Consumer> </Provider> </div> ); } export default App;
跨层级传值
App.js 文件
const store = { home: {}, user: { name: 'Dylan' } } function App() { return ( <div className="App"> <Provider value={store}> <Home /> </Provider> </div> ); }
Home.js文件
import React, { Component } from 'react' import {Consumer} from '../AppContext' export default class Home extends Component { render() { console.log("home", this.props); return <Consumer> {ctx => <HomeHandle {...ctx}/>} </Consumer> } } function HomeHandle(props){ console.log('HomeHandle', props); return <div> <h1>HomeHandle</h1> </div> }
组件复合---Composition
TabBar.js 文件
import React, {Component} from 'react' export default class TabBar extends Component { render() { return <div className="tabBar">TabBar</div> } }
Layout.js 文件
import React, {Component} from 'react' import TabBar from '../components/TabBar' export default class Layout extends Component { render() { const {children, showTabBar = true} = this.props return ( <div> {children} {showTabBar && <TabBar />} </div> ) } }
Home.js 文件 ,使用Layout模板
import React, {Component} from 'react' import {Consumer} from '../AppContext' import Layout from './Layout'; export default class Home extends Component { render() { return <Consumer>{ctx => <HomeHandle {...ctx}/>}</Consumer> } } function HomeHandle(props) { console.log('HomeHandle', props); return <Layout showTabBar={true}> <div> <h1>Home</h1> </div> </Layout> }
上述类似 vue 中的匿名插槽
下面代码展示具名插槽
Home.js 文件
import React, {Component} from 'react' import {Consumer} from '../AppContext' import Layout from './Layout'; export default class Home extends Component { render() { return <Consumer>{ctx => <HomeHandle {...ctx}/>}</Consumer> } } function HomeHandle(props) { console.log('HomeHandle', props); return <Layout> {{ btn: <button>按钮</button>, content: "我是内容" }} </Layout> }
Layout.js 文件
import React, {Component} from 'react' import TabBar from '../components/TabBar' export default class Layout extends Component { componentDidMount(){ const { title='商场' } = this.props document.title = title } render() { const {children, showTabBar} = this.props console.log('props:',this.props); return ( <div> {children.btn} {children.content} {showTabBar && <TabBar />} </div> ) } }
如何让layout自行判断传进来的组件是具名还是匿名
Layout.js 文件
render() { const {children, showTabBar = true} = this.props const a = [] // 如果有这个值($$typeop) , 说明是匿名的 if(children.$$typeof){ a.push(children) }else{ for(let item in children){ a.push(children[item]) } } return ( <div> { a.map((item, index) => { return <div key={`child${index}`}>{item}</div> }) } {showTabBar && <TabBar />} </div> )
高阶组件
将Home.js 进行修改
import React, {Component} from 'react' import {Consumer} from '../AppContext' import Layout from './Layout'; const consumerHandler = Cmp => props => { return <Consumer> {ctx => <Cmp {...props} {...ctx}/>} </Consumer> } function HomeHandle(props) { console.log('HomeHandle', props); return <Layout> <div> <h1>home</h1> </div> </Layout> } export default consumerHandler(HomeHandle)
可以将其放入 AppContext.js 文件(公共文件)
import React from 'react'; const Context = React.createContext() export const Provider = Context.Provider export const Consumer = Context.Consumer export const consumerHandler = Cmp => props => { return <Consumer> {ctx => <Cmp {...props} {...ctx}/>} </Consumer> }
高阶组件---HOC
为了提高组件复用性,可测试性,就要保证组件功能单一性;但是若要满足复杂需求就要扩展功能单一的组件,在React里就有了HOC(Higher-Order Components)的概念。
定义:高阶组件就是一个工厂函数,它接收一个组件并返回另一个组件。
改写 App.js,展示高阶组件的用法
function Child(props){ return <div>child</div> } const foo = Cmp => props => { return <div style={{border: '1px solid black'}}> <Cmp {...props}/> </div> } function App() { const Foo = foo(Child) return ( <div className="App"> <Child /> <Foo /> {/* <Home /> */} {/* <User /> */} </div> ); } export default App;
高阶组件实际上就是,你给我一个组件,我把这个组件封装一下,再返回给你一个封装后的组件
高阶组件可以链式调用
function Child(props){ return <div>child</div> } const foo = Cmp => props => { return <div style={{border: '1px solid black'}}> <Cmp {...props}/> </div> } const foo2 = Cmp => props => { return <div style={{border: '1px solid red', padding: '10px'}}> <Cmp {...props}/> </div> } function App() { const Foo = foo2(foo(Child)) return ( <div className="App"> <Child /> <Foo /> </div> ); } export default App;
装饰器写法
高阶组件本身是对装饰器模式的应用,自然可以利用ES7中出现的装饰器语法来更优雅的书写代码。
CRA项目中默认不支持js代码使用装饰器语法,可修改后缀名为 tsx 则可以直接支持
Hooks
Hook是React16.8的一个新增项,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
Hooks的特点:
- 是你在无需修改组件结构的情况下复用状态逻辑
- 可将组件中相互关联的部分拆分成更小的函数,复杂组件将变得更容易理解
- 更简洁、更易理解的代码
hook中使用子组件,代码如下:
import React, {useState, useEffect} from 'react' export default function HooksPage() { const [date, setDate] = useState(new Date()) const [fruits, setFruits] = useState(['apple', 'banana']) useEffect(() => { const timerId = setInterval(() => { setDate(new Date()) }, 1000) return () => clearInterval(timerId) }) return ( <div> hooks <p>{date.toLocaleTimeString()}</p> {/* 使用子组件时,需要把 fruits 和 setFruits 传入后,子组件内方可使用 */} <FruitList fruits={fruits} setFruits={setFruits}/> </div> ) } function FruitList({fruits, setFruits}) { // 点击后删除当前项 const delCur = index => { const tem = [...fruits] tem.splice(index, 1) setFruits(tem) } return <div> <ul> { fruits.map((item, index) => { return <li key={`fruit${index}`} onClick={()=>delCur(index)}>{item}</li> }) } </ul> </div> }
添加一个新增 fruits 操作
export default function HooksPage() { const [date, setDate] = useState(new Date()) const [fruits, setFruits] = useState(['apple', 'banana']) useEffect(() => { const timerId = setInterval(() => { setDate(new Date()) }, 1000) return () => clearInterval(timerId) }) return ( <div> hooks <p>{date.toLocaleTimeString()}</p> {/* 传入addFruit函数 */} <AddFruit addFruit={item=>setFruits([...fruits, item])} /> {/* 使用子组件时,需要把 fruits 和 setFruits 传入后,子组件内方可使用 */} <FruitList fruits={fruits} setFruits={setFruits}/> </div> ) } function AddFruit({addFruit}) { const [name, setName] = useState("") return <div> <input value={name} onChange={e => setName(e.target.value)} /> {/* 接收addFruit函数,点击按钮进行触发 */} <button onClick={()=>addFruit(name)}>点击增加</button> </div> }
如果在 useEffect 中加入 console.log('useEffect'),可以发现不管是 setDate执行,还是点击新增按钮后的 setFruits 执行,console.log都会执行。
但实际上当前功能只需要 date的定时器不断执行而已,可以修改代码进行优化
useEffect(() => { console.log('useEffect'); const timerId = setInterval(() => { setDate(new Date()) }, 1000) return () => clearInterval(timerId) // useEffect的第二个参数,加入后就只对 date 生效了 }, [date])
接下来 点击 新增按钮 ,就不会打印 useEffect 了
副作用钩子
useEffect给函数组件增加了执行副作用操作的能力。
副作用(Side Effect)是指一个function 做了和本身运算返回值无关的事,比如:修改了全局变量、修改了传入的参数、甚至是 console.log ,所以ajax操作,修改dom都算做副作用。
- 异步数据获取,更新HooksTest.js
useEffect(() => { setTimeout(()=>{ setFruits(['香蕉','西瓜']) }, 1000) })
测试就会发现副作用操作会被频繁调用
- 设置依赖
// 设置空数组意为没有依赖,则副作用操作仅执行一次 useEffect(()=>{...}, [])
- 清除工作:有一些副作用是需要清除的,清除工作非常重要,可以防止引起内存泄漏
useEffect(() => { const timerId = setInterval(() => { console.log('mgs'); }, 1000) return () => clearInterval(timerId) }, [])
组件卸载后回执行返回的清理函数
useReducer
useReducer是useState的可选项,常用于组件有复杂状态逻辑时
将上面的代码改成使用 useReducer
Fruit.js 文件
import React, {useState, useEffect} from 'react' export function AddFruit({addFruit}) { const [name, setName] = useState("") return <div> <input value={name} onChange={e => setName(e.target.value)}/> <button onClick={() => addFruit(name)}>点击增加</button> </div> } // FruitList 组件需要传入 fruits 和 setFruits export function FruitList({fruits, setFruits}) { // 点击后删除当前项 const delCur = index => { const tem = [...fruits] tem.splice(index, 1) setFruits(tem) } return <div> <ul> { fruits.map((item, index) => { return <li key={`fruit${index}`} onClick={() => delCur(index)}>{item}</li> }) } </ul> </div> }
HookReducer.js 文件
import React, { useEffect, useReducer } from 'react' import { FruitList, AddFruit } from '../components/Fruit' function fruitsReducer(state, action){ switch(action.type){ case 'init': case 'replace': return action.paylod default: return state } } export default function HooksReducer() { const [fruits, dispatch] = useReducer(fruitsReducer, []) useEffect(()=>{ setTimeout(()=>{ dispatch({type: 'init', paylod: ['apple', 'banana']}) }, 1000) return ()=>{ // clearup } }, []) return ( <div> HooksReducer <FruitList fruits={fruits} setFruits={(newFruitList)=>dispatch({type: 'replace', paylod: newFruitList})}/> </div> ) }
useContext
useContext 用于 快速在函数组件中导入上下文。---之前是用Consumer做的
import React, { useContext } from 'react' const Context = React.createContext() const Provider = Context.Provider export default function HooksContext() { const store = { user: { name: 'Dylan' } } return ( <div> <Provider value={store}> <ContextChild></ContextChild> </Provider> </div> ) } function ContextChild(props){ console.log(useContext(Context)); const {user} = useContext(Context) return <div> {user.name} </div> }