基础语法
基础语法
jsx/tsx【这个很重要】
全称: javascript and xml(html), 作用就是将js与html结合再一个文件中。
描述: 对js进行了扩展,让js文件可以直接定义节点类型,而且使用节点给变量赋值!理解为一个新的模板语法(可以向模板中插入值),类比成vue中template
只有一个根节点
<React.Fragment>
<div className='A'>
</div>
<div className='B'>1123</div>
</React.Fragment>
<>
<div className='A'>
</div>
<div className='B'>1123</div>
</>
占位符({})在节点中间
/**
* {} 出现在节点中
* - 变量和值
* - 字符串 数字可以渲染 对于布尔、null、undefined页面不会渲染
* - 对象:一般的对象直接报错, 有特殊情况(虚拟dom)
* - 数组: react首先会对数组的每一项进行校验, 校验通过,把每一项拿出来,渲染到页面上
* - 表达式: if for while 不是表达式是流程控制语句
* - 数学运算
* - 三目 (条件判断) 类比vue的v-if
* - 数组的操作方法 (列表循环) 类比与vue中的v-for
* - 函数执行的结果
* - 要注意函数执行的结果返回的值要满足上诉要求
*/
占位符在节点上
/**
* {} 出现在节点上(动态属性,类似于vue中的v-bind)
* - 字符串
* - 条件判断
* - 函数(绑定事件·),函数执行结果
* - 注意:给节点设置类名要用:className 给节点设置行内样式要用:style(style一定是一个对象!)
*/
render() {
const model = [1,2,3,4,'我是字符串', <span>124</span>, <p>我是p标签</p>]
const format = () => 'format'
// flag为true的时候 给一个红色的类名:red flag为false的时候给一个绿色的类名:green
// style行内样式来控制
const flag = false
const sayHi = () => {
alert('hello')
}
return (
<div>
{/* <h1 className={ flag ? 'red':'green' } id={flag ? 'active' : ''}>{ format() }</h1> */}
{/* */}
<h1 style={{ color: flag ? 'red' : 'green' }} id={flag ? 'active' : ''}>{ format() }</h1>
<button onClick={ sayHi }>点击我</button>
</div>
)
}
jsx属性与html的不同
不同点 | jsx | html |
---|---|---|
类名的设置 | className | class |
行内样式 | style跟上对象 | style跟上字符串 |
绑定事件 | onClick=函数 | onclick="函数名字" |
classnames的使用
classnames帮你快速生成节点的类名,类似于vue中的class(class可以是个字符串,也可以是个表达式,页可以是个对象)用法。
// 有一个p标签,它有两个固定的类名: title box
// 然后有一个类名: red 是根据变量isRed的变量来判断
<p class="title box" :class="{red: isRed}">reddd</p>
以上是vue的实现方法,把静态的类名与动态的类名分开了
react本身不能做到上诉功能,但是可以借助第三方包:classnames(就是一个函数)来完成
安装:yarn add classnames
import classnames from 'classnames' <p className={classnames('title', 'box', {red: isRed})}>reddd</p>
实现了将静态类名与动态类名分开的效果
指令的实现
react本身是没有像vue的指令(v-if v-for v-show),但是可以自己实现。
列表循环(v-for)
render() {
const arr = ['xfd', 're3r4', 'zssdf', '2323']
return (
arr.map(item => <p key={item}>{item}</p>)
)
}
条件判断(v-if, v-show)
<h1 style={{ display: isShow ? '' : 'none' }}>v-show</h1>
{
isIF ? <h1>v-if</h1> : null
}
jsx渲染流程
/**
* 第一次渲染流程
* 1. jsx表达式变成一个render函数(babel:js兼容ie,代码转换(es6--》 es5))
* 2. render函数会执行,执行完了之后返回一个虚拟dom对象(普通的js对象,回来描述真实dom节点)
* 3. 拿到这个虚拟的dom节点 ----》 变成真实的dom节点,然后挂载到响应的dom节点上
*/
/**
* 更新流程
* 1.组件的状态发生变化, 重新生成虚拟dom对象 (新的虚拟dom对象)
* 2.新的虚拟dom对象 与 上一次(老的)虚拟dom对象进行对比(dom-diff), 得到一个补丁包对象
* 3.拿到补丁包之后,局部的去更新
*/`
class语法回顾
// class是一个es6出来的语法糖,用来快速的定义一个类
// Person就是一个函数(对象)
class Person {
/**
* 给人类定义属性
* - 直接定义
* - 有构造函数(constructor)
*/
// 直接定义属性(name)
name = '张三'
// 构造函数的方式, new的时候会执行次函数
constructor(name) {
// this是执行我们创造的那个实例
this.name = name
}
// eat方法在原型上,所有实例共享 (普通的函数)
// eat是一个普通函数:this非常灵活
eat() {
console.log(this)
console.log(`${this.name}在吃饭!`)
}
// run方法,定义为箭头函数
// run写成箭头函数,不会放在原型上,每一个实例都有一个run方法
// 好处就是:this指向实例(待会我们遇到了this问题)
run = () => {
console.log(`${this.name}在跑步!`)
}
// 静态方法或者属性, 访问, 给Person这个函数对象添加了一个count属性: Person.count
static count = 10
}
// 继承
class Man extends Person{
sex = '男'
// 子类继承了父类,如果在子类中写了constructor,一定要写执行super
// 子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。
constructor(name) {
// 调用过一次父类的构造函数 Person.constructor.call(this, name):call继承(用来继承属性的!)
super(name)
}
}
const m = new Man('男人1')
console.log(m)
类类型组件底层渲染逻辑
定义类组件
- 如果类组件写了constructor
- constructor必须要有props参数(props接收实例化的时候传递的值!)
- 必须要在constructor里面执行super
- 默认constructor可以不写!
export default class App extends Component<any, any> {
constructor(props) {
super(props)
console.log('App 被实例化了!!!')
}
render() {
console.log(this)
return (
<div className='jsx-wrapper'>
<h1>111</h1>
<h1>222</h1>
{React.createElement("h1", null, "7787878")}
</div>
)
}
}
实例化类组件
- 初始化props
// { title: 'title', name: 'zzz', age: 24, children: 虚拟dom对象或者虚拟dom对象数组 }
<App title=“title” name='zzz' age={24}>
// 把span(jsx) --》 变成虚拟dom对象
<span>我在app下</span>
</App>
- new App(props) --> 此处的props就是第一步拼装好的对象
- 实例化之后得到一个实例,调用实例上的方法:render。 进行页面渲染
- render调用之后就是jsx渲染流程(jsx--》 渲染的代码块--》虚拟dom--》真实dom)
组件的状态
如果你要做到数据变化,页面更新。请把这类数据放在组件的state下,修改请求使用setState进行修改
// 定义
state = {num1: 'xx', age: 24, email: 'xxxx'}
agree = () => {
// 修改
this.setState({
num1: this.state.num1 + 1
})
}
<div>赞成:{ num1 }<button onClick={ this.agree }>赞成+1</button></div>
注意:初始使用setState可以改变数据更新页面,我们页可以强制的更新页面(forceUpdate),这玩意一定要慎用
事件绑定与传参【这个重要得很哟】
箭头函数+bind传参【主要解决方式】
// 定义成箭头函数,你就不用管this指向问题,它会永远指向实例
agree = (num) => {
this.setState({
num1: this.state.num1 + num
})
}
render() {
return (
<div>
<button onClick={this.agree.bind(this, 4)}>点击我</button>
</div>
)
}
记住上诉方式,类组件里面绑定事件与传参就ok了
闭包的方式也可以传参【了解一哈,毕竟闭包闭多了脑壳疼】
当然很多人习惯于vue的那种传参方式:
- onClick就是要一个函数,agree执行之后,返回一个函数
// 定义成箭头函数,你就不用管this指向问题,它会永远指向实例
agree = (num) => {
// 外层函数就是一个保留num参数的功能
return () => {
// 内层函数执行真正的逻辑
this.setState({
num1: this.state.num1 + num
})
}
}
render() {
return (
<div>
<button onClick={this.agree(4)}>点击我</button>
</div>
)
}
通过自定义属性+事件对象传参【了解】
unAgree = (e) => {
// 去button上的一个自定义属性 data-num
console.log(e.target.dataset.num)
}
<div>反对:{ num2 }<button data-num={5} onClick={this.unAgree}>反对+1</button></div>
父子组件通信
父传子(通过props)
- 在父组件里实例化子组件的时候,拼装props
子传父
- 父组件的函数给子组件执行
- 要传递的数据,做为此函数的实参
import React, { Component } from 'react'
import Child from './Child'
export default class App extends Component {
state = {
name: '小张三'
}
// 父组件的函数
getMoney = (money) => {
debugger
console.log(`我是爸爸,我得到了儿子的:${money}`)
}
render() {
return (
<div>
<h1>我的儿子组件:</h1>
<Child getMoney={this.getMoney} name={this.state.name}/>
</div>
)
}
}
父组件
import React, { Component } from 'react'
export default class Child extends Component<any, any> {
constructor(props) {
super(props)
console.log(props)
}
sendMoney = () => {
debugger
let data = '1w'
// this.$emit('自定义事件名', data)
//传data给父组件
this.props.getMoney(data)
}
render() {
return (
<div>
我是儿子,我叫:{this.props.name}
<button onClick={this.sendMoney}>孝敬爸爸</button>
</div>
)
}
}
子组件
react来实现具名插槽
import React, { Component } from 'react'
import './Layout.css'
export default class Layout extends Component<any, any> {
constructor(props) {
super(props)
console.log(props)
}
render() {
const {children} = this.props
return (
<div className='layout-wrapper'>
<div className='left'>
{/* <slot name="left"></slot> */}
{ children.find(item => item.props.slot === 'left') }
</div>
<div className='right'>
<div className='right-top'>
{/* <slot name="righttop"></slot> */}
{ children.find(item => item.props.slot === 'righttop') }
</div>
<div className='right-bottom'>
{/* <slot name="rightbottom"></slot> */}
{ children.find(item => item.props.slot === 'rightbottom') }
</div>
</div>
</div>
)
}
}
<Layout>
{/* h1 最终会变为一个虚拟dom对象, slot属性肯定共有一个地方放着 */}
<h1 slot="righttop">右上</h1>
<h1 slot="rightbottom">右下</h1>
<h1 slot="left">左边</h1>
<h1 slot="left">左边1</h1>
<h1 slot="left">左边2</h1>
</Layout>
类类型组件props进行校验
jsx版本
官方例子:https://zh-hans.reactjs.org/docs/typechecking-with-proptypes.html
- 安装prop-types
- 引入prop-types
- 类.propTypes进行校验
import PropTypes from 'prop-types';
export default class Test extends Component {
render() {
return (
<div>Test</div>
)
}
}
Test.propTypes = {
// props下有一个name属性,这个属性是string类型,而且这个属性必须有值
name: PropTypes.string.isRequired,
// sex是string或者number
sex: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]).isRequired,
// // 定义对象
obj: PropTypes.shape({
name: PropTypes.string
})
}
// 设置默认值
Test.defaultProps = {
name: 'default',
sex: 'zzz',
obj: {name: 'yyy'}
}
propTypes 与 defaultProps可以写成静态属性的方式与上诉方式是相同
export default class Test extends Component {
static propTypes = {
// props下有一个name属性,这个属性是string类型,而且这个属性必须有值
name: PropTypes.string.isRequired,
// sex(男女这种字符串, 0,1这种数字), sex是string或者number
sex: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]).isRequired,
// // 定义对象
obj: PropTypes.shape({
name: PropTypes.string
})
}
static defaultProps = {
name: 'default',
sex: 'zzz',
obj: { name: 'yyy' }
}
render() {
console.log(this.props)
return (
<div>Test</div>
)
}
}
tsx版本
没有儿子的版本
import React, { Component } from 'react'
import classnames from 'classnames'
import './Tabs.css'
type tabprops = {
active: number
list: {text: string, val: number}[]
onChange?: (val:number) => void
}
export default class Tabs extends Component<tabprops, any> {
render() {
const { list, active } = this.props
console.log(this.props)
return (
<ul>
{list.map(item => <li onClick={() => this.props.onChange(item.val)} className={classnames({active: item.val === active})} key={item.val}>
{item.text}
</li>)}
</ul>
)
}
}
当这个组件有儿子的时候,props下一定会有一个属性(children),这个属性不需要你定义,请借助react提供的一个类型(PropsWithChildren)进行定义
type layouttype = {
name?: string,
}
export default class Layout extends Component<PropsWithChildren<layouttype>, any> {
}
深入组件渲染流程-生命周期
单个组件的生命周期
- 挂载
- 整个过程:类组件实例化(初始化props),调用render方法,把jsx变成真实的dom
- 初始化props,然后执行new App(props) (), 进入App的constructor
- 初始化之后得到实例,实例.render(), render
- rende执行,jsx变成真实的dom节点,渲染到页面上。
- 第一次页面渲染完成,进入生命周期:componentDidMount。该生命周期作用如下:
- 操作dom节点:初始化echarts, 初始化swiper
- 发起请求通常在这里
- 订阅: 订阅定时器, 订阅一些事件(监听window的resize事件)
- 更新
- 触发更新的条件: state props forceUpdate
- shouldComponentUpdate(nextProps,nextState)
- nextState,nextProps 即将更新的state或者props
- 返回值: true, false
- render
- 渲染页面
- componentDidUpdate(prevProps, prevState): 更新完成!
- prevProps, prevState 上一次的props和state
- 卸载
- componentWillUnmount
- 取消订阅,清除定时器~~~~
- componentWillUnmount
父子组件的生命周期执行顺序
- 挂载阶段
- 父组件的构造函数 --》 父组件的render --》子组件的构造函数 --》子组件的render --》 子组件的componentDidMount--》 父组件的componentDidMount
- 更新阶段
- 父组件的shouldComponentUpdate --> 父组件render --》 子组件shouldComponentUpdate --》 子组件的render --》 子组件 componentDidUpdate --> 父组件componentDidUpdate
- 卸载阶段
- 父组件componentWillUnmount --》 子组件 componentWillUnmount
获取dom节点 【这个要会哟】
你可以用原生的方式【不推荐哈】
ref的方式
refs的方式 ---》 类比vue2的方式 【废弃】
ref写成函数的方式 【主流】
createRef创建一个ref变量的方式 --》类比vue3的方式 【掌握】
export default class Life extends Component { h2 = null // 这是ts设置类型的方式【了解】 // h3Ref = createRef<HTMLHeadingElement>() h3 = createRef() componentDidMount() { // 1. refs的方式 -- 跟vue2一模一样 [废弃] this.refs.h1ref // 2. ref写成函数 this.h2 // 3. 借助createRef方法的方式 this.h3.current } render() { console.log('render~~~') return ( <div id='life'> { /* 这个节点加载完成之后,会往实例的refs属性里面添加:{ h1ref: h1这个节点} */ } <h1 ref="h1ref">111</h1> {/* 形参的ref就是h2 */} <h2 ref={ ref => { this.h2 = ref } }>222</h2> <h3 ref={this.h3Ref}>333</h3> </div> ) } }
受控(非受控)表单(v-model的实现)
将组件的状态保存在state里
将状态与表单绑定在一起
表单输入发生变化重新给状态设置值
export default class Input extends Component { state = { acc: 'test', pwd: 'test' } // 拿到事件对象,拿到事件对象.target 再拿input的value accInput = (e) => { this.setState({ acc: e.target.value }) } pwdInput = () => { } submit = () => { console.log(this.state) } render() { return ( <> <div> 账号<input onInput={this.accInput} value={this.state.acc} type="text"></input> </div> <div> 密码<input onInput={e => this.setState({pwd: (e.target as HTMLInputElement ).value})} value={this.state.pwd} type="text"></input> </div> <div> <button onClick={this.submit}>提交</button> </div> </> ) } }
深入setState
setState是异步的
state = { x: 0, y: 5, z: 10 } add = () => { const {x, y, z} = this.state // react有一个更新队列(数组),接收setState的操作。 最后做一次批处理,然后再更新页面 // 1. 在react18 this.setState操作是异步的 this.setState({ x: x + 1 }) // 此时x并没有加1,还是0 console.log(this.state.x) }
setState第二个参数为回调,可以实现Vue的nexttick的效果
- 次状态修改完成,而且重新执行render,页面渲染成功,进入componentDidUpdate, 然后进入此回调
- 就算shouldComponentUpdate返回false,还是会进入此回调
add = () => { const {x, y, z} = this.state // react有一个更新队列(数组),接收setState的操作。 最后做一次批处理,然后再更新页面 // 1. 在react18 this.setState操作是异步的 // 2. setState有第二个参数,是一个函数。函数什么时候执行了,次状态修改之后,页面更新完成,之后才执行 this.setState({ x: x + 1 }, () => { // 修改了此状态,页面更新完毕之后,执行次回调!!! ---》 vue的nexttick 【重点】 // 就算shouldComponentUpdate返回的也是false,这个回调函数也会执行 【了解】 console.log(this.state.x) }) }
函数组件
- 就是一个函数: 函数返回一段jsx
- 优点: 语法简单, 函数组件写起来没有this,性能更好
- 局限: 没有自己的状态, 没有自己的生命周期
- 场景: 如果我们的组件不需要生命周期、不需要有自己的状态,就可以用他.例如:tabs组件就非常适合用函数组件
export default function ChildFun(props) {
const { active, list, onChange} = props
return (
<ul>
{
list.map(item => <li onClick={() => onChange(item.value)} className={classNames({active: item.value === active})} key={item.value}>{item.label}</li>)
}
</ul>
)
}
以上是重构tab是的案例,更加简单
props校验
js版本的校验与类类型一直都是借助第三方的包: prop-types
// 函数类型就是对象 ChildFun.propTypes ChildFun.defaultProps
ts版本
校验props,就是校验一个函数的参数的类型。直接使用:
ts下设置默认值,还是使用defaultProps
type tabitem = { label: string value: number } type tabsprops = { active: number, list: tabitem[], onChange?: (val:number) => void } export default function ChildFun(props:tabsprops) { const { active, list, onChange} = props return ( <ul></ul> ) } // 设置了一个默认props变量,并且使用tabsprops类型进行约束 const defaultProps:tabsprops = { list: [ { label: '全部111', value: 0 }, { label: '男', value: 1 }, { label: '女', value: 2 }, ], active: 0, onChange: f => {} } // 然后把这个约束过后的变量赋值给ChildFun.defaultProps ChildFun.defaultProps = defaultProps
hook
什么是react的hook
是一个一个以use开头的函数,是react16.8之后的特性。主要是让函数组件有状态或者其他的react特性(生命周期)
useState的使用
让函数组件有状态
基本用法: const [ count, setCount ] = useState(0)
参数
- 状态的初始值
返回值
- 类型:数组,长度为2
- 第一个是:状态
- 第二个是: 修改此状态的专有函数
sueState与ts结合
- 设置状态的类型
type usertype = {
name: string
age: number
}
const [count, setCount] = useState<number>(0)
const [user, setUser] = useState<usertype>({name: '张三', age: 24})
useEffect
useEffect就是让函数组件拥有生命周期,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。 学习得过程中,你可以把useEffect类比这vue3的watchEffect来学
语法:
- 参数1: 回调函数, 参数2: 可以不传,可以传空数组, 也可以传具体的数组: [状态, props]
- useEffect的回调都会在页面加载完之后,执行一次
function useEffect(effect: EffectCallback, deps?: DependencyList): void;
componentDidMount
useEffect(() => {
}, [])
第二个参数传空数组,代表所有状态都不监听。 页面加载完会默认执行一次,以后都不会再执行。
componentDidUpdate
useEffect(() => {
})
第二个参数不传,代表监听所有。所有组件的任何一个state发生变化,props发生变化,都会执行。与componentDidUpdate 唯一区别就是一开始会默认执行一次
useEffect(() => {
}, [状态1, 状态2])
监听状态1和状态2, 然后两者只要其中一个发生变化,都会进入它的回调.
componentWillUnmount
useEffect的回调函数,然后一个函数。组件卸载的时候就会执行返回的那个函数
useEffect(() => {
return () => {}
}, [])
自定义hook
与vue3.2一样的思路,就是将一部分功能导出去,然后使用函数进行包裹。然后再引入使用
import { useState } from 'react'
export function useLogin() {
const [acc, setAcc] = useState('xxx')
const [pwd, setPwd] = useState('xxx')
const submit = () => {
console.log({acc, pwd})
}
return { acc, setAcc, pwd, setPwd, submit }
}
自定义hook
import { useLogin } from './hook/useUser'
const { acc, setAcc, pwd, setPwd, submit } = useLogin()
使用
react-router-dom(v6)
使用步骤
- 安装 yarn add react-router-dom
一级路由的划分
路由容器
import React from 'react' import ReactDOM from 'react-dom/client' import App from './App' import { HashRouter } from 'react-router-dom' ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( <HashRouter> <App /> </HashRouter> )
HashRouter为hash模式的路由容器,所有跟路由相关的组件需要放在其下面,所以建议直接在main.tsx中包裹App
Routes
- 包裹Route,根据地址决定渲染哪个组件
Route
- 将地址与react组件一一对应起来
404配置方式
<Route path='/*' element={<h1>404</h1>}></Route>
重定向
<Route path='/' element={<Navigate to={'/login'}></Navigate>}></Route>
借助Navigate来实现重定向
import React, { Component } from 'react'
import { HashRouter, Route, Routes,Link, Navigate } from 'react-router-dom'
import Login from './views/Login'
import Home from './views/Home'
export default class App extends Component {
render() {
return (
<>
{/* 需要一个路由的容器:HashRouter 表示hash模式 */}
<Link to={'/home'}>首页</Link> <Link to={'/login'}>登录</Link>
{/* 根据地址决定渲染哪一个Route */}
<Routes>
{/* 可以划分路由: 地址与react组件一一对应起来 */}
{/* 重定向,通过Navigate 把 / 重定向到 /login */}
<Route path='/' element={<Navigate to={'/login'}></Navigate>}></Route>
<Route path='/login' element={<Login></Login>}></Route>
<Route path='/home' element={<Home></Home>}></Route>
{/* 404 */}
<Route path='/*' element={<h1>404</h1>}></Route>
</Routes>
</>
)
}
}
二级的配置
子路由配置,配置在相应的Route下面
<Route path='/home' element={<Home></Home>}> {/* /home下面的二级路由 */} <Route path='movie' element={<Movie></Movie>}></Route> <Route path='cinema' element={<Cinema></Cinema>}></Route> </Route>
响应的地方设置路由出口
import React from 'react' // Outlet 类比成vue的 router-view 路由出口 import { Outlet, Link } from 'react-router-dom' export default function Home() { return ( <div> <Link to={'/home/movie'}>movie</Link> <Link to={'/home/cinema'}>cinema</Link> <Outlet /> </div> ) }
Outlet为路由出口