react(上)
主流函数组件,但是类组价也要用,都要会
- 特点:
- 声名式: 就像写 HTML一样,react负责渲染UI , 数据的声明
- 基于组件开发,可以保持状态和DOM的分离
- web, 小程序都可用
简单使用
简单应用不适用脚手架:
1,进入文件
创建 HTML, 引入配置文件,
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b0hMiW7v-1690027946541)(C:\Users\30641\AppData\Roaming\Typora\typora-user-images\image-20230710210149417.png)]
注意 引入的顺序不能颠倒,1) react.js 是react的核心库文件,此文件用来穿件虚拟DOM,已经相关的方法, 2) react-dom.js 将虚拟dom转为真实dom,解析出来,渲染到页面中
2,指定挂载节点
<div id="root"></div>
3, 编写业务
<script> let rooDom = documnet.getElementById("root") // 创建一个vnode对象 let vnode = React.createElement( 'h3', // 要创建的标签名 {}/null, // 标签的属性,可以为空对象,或者null '你好react,标签的内容' ) // 解析vnode为真实dom,并渲染到页面中 1, ReactDOM.render() // 在react18之前使用,18也支持,但是不支持渲染新特性 // createRoot(挂载节点) // render(渲染组件) 2, ReactDOM.createRoot(rootDom).render(vonde) </script>
4,小案例
const rootDom = document.getElementById("root") let el = React.createElement let vnode = el( 'ul', // 标签名 null, // 标签属性,可以是空对象或者是null ,如果写style样式,只能写对象 // 事件一定要用小驼峰命名法,onClick el("li" , null , el("span" , {title:"aaa" , style:{color:'red' }} , "张三"), el("button" , null , "查看"), el("button" , {onClick: ()=> console.log(123)} , "删除") ), el("li" , null , el("span" , null , "框死"), el("button" , null , "查看"), el("button" , null , "删除") ), el("li" , null , el("span" , null , "王五"), el("button" , null , "查看"), el("button" , null , "删除") ), ) ReactDOM.createRoot(rootDom).render(vnode)
非常的繁琐且不直观,所以引入
jsx
jsx
jsx 编译
<!-- jsx编译为js -->
<script src="./js/babel.js"></script>
<!-- 3,编写业务 -->
<!-- 如果用到了jsx,则在浏览器直接运行HTML文件时,一定要设置,script的type -->
<!-- type 不同,浏览器对应的工作性质也不一样 -->
<script type="text/babel">
const rootDom = document.getElementById("root")
// 创建得到一个 vnode对象,jsx 是js的增强版本 , 他对字符窜进行了增强
// jsx 在写HTML时一定要符合xml规范
// xml规范,严格标识语言,属性名要小写且用引号括起来,标签必须为双标签,又开始和闭合
// jsx要求,内置html标签名为小写,自定义组件名为首字母大写,属性名称为小写且只要用引号括起来,要有结束和开始
// jsx不能直接解析运行,所以要先将jsx转为js ====》 通过引入的 babel 库来完成对js的编译
// 可以吧他理解为一个组件中的UI部分
const vnode = (
<ul>
<li>
<span>张三</span>
<button>查看</button>
</li>
<li>
<span>张无</span>
<button>查看</button>
</li>
</ul>
)
ReactDOM.createRoot(rootDom).render(vnode)
</script>
变量绑定
const username = "张三aaa"
const fn = function () {
console.log("aaa")
}
const age = 22
const vnode = (
<div>
{/*绑定变量,{变量}*/}
<div>{username}</div>
{/* 表达式*/}
<div>{1 + 3}</div>
{/* 调用内置方法*/}
<div>{username.substr(0, 3)}</div>
{/* 调用自定义函数 */}
<div>{fn()}</div>
{/*不支持声明变量,if语句,while等块级语句*/}
{/*支持三目运算,他很重要*/}
<div>{age < 20 ? <h3>而已</h3> : <h3>老人</h3>} </div>
</div>
)
动态属性
脚手架
// 安装webpack切记不要全局安装
npm i -D webpack webpack-cli webpack-dev-server ...
npm i -S react react-dom
// 创建一个react项目
npx create-react-app 项目名
// 新建webpack配置文件
webpack.config.js
// 五个核心的配置
- mode , -entry ,-output , -module:rules ,-plugins
// 工程化后的REact目录
public index.html
src App.js index.js
组件
函数组件
// 函数组件 // 本质上还是一个函数 // 1, 函数名称首字母大写。jsx要求自定义组件在调用时首字母要大写 // 2, 他会有一个形参,可以接受任意类型的数据,可以用它来控件自父组件数据 // 3, 返回必须包括一个jsx , 且jsx必须有顶层元素包裹,还可以返回null==》 r18之后 // 4, 定义的函数一定要默认导出 // rfc 快捷键生成 import React from 'react'; const 01 = () => { return ( <div> // 导出的内容 </div> ); } export default 01;
类组件
import React from 'react';
// 定义一个类组件
// 1, 只能使用es6的类来定以 , 类名必须首字母大写
// 2, 必须继承一个react.component父类
// 3 , 此类中必须重写render 方法, 此方法一定要返回一个 jsx/null / undefined
// 4, 此类要导出
// 用 rcc 来快捷创建
class App extends React.Component{
render(){
return (
<div>
<h2>jjjjsdfasdf</h2>
</div>
)
}
}
export default App;
// 函数具有二义性,函数,类都可以 function Person(){ // 用new。target来禁用函数,他不能被new,只能当成函数来调用 if(new.target){ throw new Error("错误") } } console.log( new Person()) // 报错
props—父子间传值
函数组件的父向子传值
import React from 'react';
// 子组件接收父组件传过来的数据 , 通过函数的形参来接受
// 形参可以接受任意类型的数据得到的是一个对象
const Child = props => {
console.log(props)
props.title = "aaa" // 直接报错,props是一个只读的属性,不能被修改,要想修改只能是父组件来完成,是一个单项数据,父流向子
return (
<div>
<h5>child 组件 --- {props.title}</h5>
</div>
);
}
// props 是一个对对象 可以结构赋值,运用更加的简单
const Child = ({title = "解构时可以赋予初始值"}) =>{
return (
<div>
<div> {title} </div>
</div>
)
}
const App = () => {
const title = "我是父朱健传过来的值"
return (
<div>
<h3>App组件</h3>
{/*
父组件通过自定义属性来向子组件传递数据,props
调用子组件,直接将子组件类名当标签名用
*/}
< Child title={title}/>
</div>
);
}
export default App;
类组件的父向子传值
import React, { Component } from 'react';
class Child extends Component {
render() {
// 类组件中子组件通过在成员属性 this.props中获取父组件传过来的自定义属性值
// this.props 他的值为一个对象, 也不可修改
// const 定义一个不可改变的常量
const { title } = this.props
return (
<div>
<h3>child --- {title} </h3>
</div>
);
}
}
class App extends Component {
// 成员属性
title = "我是父组件传过来的值"
render() {
return (
<div>
<h3>app</h3>
{/*
父组件向子组件传值时,通过自定义属性来完成,props传任意类型的值
*/}
<Child title={this.title} />
</div>
);
}
}
export default App;
事件处理
事件绑定(函数)
import React from 'react';
const App = () => {
return (
<div>
{/*
1, 事件名要用下驼峰
2, 绑定的事件方法不要写小括号
3 如果要写小括号,绑定的一定要是一个函数体
*/}
<button onClick={clickHandler}>点击事件</button>
<button onClick={clickHandlerw()}>点击事件</button>
</div>
)
function clickHandler() {
console.log(111)
}
}
// 写在下面一定要写function,下载return上面可以用箭头函数
// 柯理化函数,函数返回函数
// function clickHandlerw(){
// return function(){
// console.log(111)
// }
// }
// 推荐写法:
const clickHandlerw = () => () => {
console.log("2222")
}
export default App;
事件绑定(类)
import React, { Component } from 'react';
class App extends Component {
// 正常用,调用时不加小括号
// clickHandler(){
// console.log("111")
// }
// 加小括号调用
// clickHandler(){
// return () => {
// console.log("222")
// }
// }
count = 100
clickHandler() {
console.log(this.count)
}
render() {
return (
<div>
{/*
类组件中会有this指向问题,用bind 不会立即执行
*/}
<button onClick={this.clickHandler.bind(this)}>点击事件</button>
</div>
);
}
}
export default App;
合成事件
- 为什么出现这个技术?
**性能优化:**使用事件代理统一接收原生事件的触发,从而可以使得真实 DOM 上不用绑定事件。React 挟持事件触发可以知道用户触发了什么事件,是通过什么原生事件调用的真实事件。这样可以通过对原生事件的优先级定义进而确定真实事件的优先级,再进而可以确定真实事件内触发的更新是什么优先级,最终可以决定对应的更新应该在什么时机更新。
**分层设计:**解决跨平台问题,抹平浏览器差异。
如何解释合成事件一定不在挂载点当中,但全部打印后,合成事件的捕获阶段先打印,说明挂载点已经被劫持,
但因顺序为 : body–》 顶层元素合成 — 》 元素合成 —》 挂载点原生 —》 元素原生 —》 元素合成冒 —》 顶层合成冒 —》 挂载原冒 —》 bogy冒
import React, { Component } from 'react';
class App extends Component {
// 合成事件 r17之前只对冒泡做劫持,捕获阶段不处理,18之后才处理
// 冒泡阶段
click() {
console.log("合成-冒泡-click")
}
// 捕获阶段
clickCapture() {
console.log("合成-捕获-clickCapture")
}
// 生命周期方法,类似与vue中的mounted,虚拟dom编译为真实的dom,挂载到节点后执行
componentDidMount() {
// 一下皆是原生事件
document.getElementById("btn").addEventListener("click",
() => {
console.log("原生 - 捕获 -click1")
},
true // 表示捕获阶段执行
)
document.getElementById("btn").addEventListener("click",
() => {
console.log("原生 - 冒泡click2")
},
false // 表示冒泡阶段执行
)
document.getElementById("root").addEventListener("click",
() => {
console.log("root - 捕获 -click1")
},
true // 表示捕获阶段执行
)
document.getElementById("root").addEventListener("click",
() => {
console.log("root - 冒泡click2")
},
false // 表示冒泡阶段执行
)
document.body.addEventListener("click",
() => {
console.log("body - 捕获 -click1")
},
true // 表示捕获阶段执行
)
document.body.addEventListener("click",
() => {
console.log("body - 冒泡click2")
},
false // 表示冒泡阶段执行
)
}
// 事件的执行顺序从:body捕获---》 合成捕获 --》root捕获---》 原生事件捕获 ---》 原生冒泡 ---》 合成冒泡 ---》 root冒泡 ---》 body冒泡
render() {
return (
<div>
<button id='btn' onClick={this.click} onClickCapture={this.clickCapture}>点击事件</button>
</div>
);
}
}
export default App;
阻止事件传播
捕获阶段 root --- son
document.getElementById("root").addEventListener("click" , ()=>{
console.log("root -- 1")
} , true )
document.getElementById("son").addEventListener("click" , ()=>{
console.log("son -- 1")
} , true )
// 冒泡阶段 son -- root
document.getElementById("root").addEventListener("click" , ()=>{
console.log("root -- 1")
} , false )
document.getElementById("son").addEventListener("click" , ()=>{
// 阻止事件继续传递 , 但当前绑定事件的元素会继续执行
// event.stopPropagation()
// 阻止事件继续传递,并且还会阻止当前元素中其他的未执行函数,当前的事件执行
event.stopImmediatePropagation()
console.log("son -- 1")
} , false )
document.getElementById("son").addEventListener("click" , ()=>{
console.log("son -- 2")
} , false )
document.getElementById("son").addEventListener("click" , ()=>{
console.log("son -- 3")
} , false )
网络请求
/api/v1/getNowPlayingFilmList?cityId=110100&pageNum=1&pageSize=10 电影网站的数据地址
- 安装插件:
npm i -S axios
解决跨的方案: 浏览器同源策略的问题, // cors: 在h5 之后刘篮球对跨域也进行了修订,只要你的响应头中有规定的信息则允许跨域 // 主流,cors. 利用响应头中的信息,让浏览器允许网络跨域 jsonp , 代理,利用HTML标签,websocket , postmessage window.domain react 的代理实现跨域问题: ==》 在src文件下,设置 setupProxy.js 文件 并通过安装 http-proxy-middleware ==> `npm i -D http-proxy-middleware` // 结构的同时起一个别名,为了调用的时候方便: const {createProxyMiddleware:proxy} = require('http-proxy-middleware') // app 对象,他是一个Expres实例对象 module.exports = app =>{ // 代理,一API开头就走代理 app.use( '/api', proxy({ target:"https://api.iynn.cn/film", changeOrigin:true , pathRewrite:{ // 充新定义 } }) ) // app.get('api/aa' , (req,res) => { // res.send({ // code: 0, // msg: "ok", // data:1 // }) // }) }
网络请求数据更新到视图
// 父组件
import React, { Component } from 'react';
// 导入api方法,也就是获取到里面的值
import { getNowPlayingFilmApi } from './api/filmApi'
// 导入子组件
import Loading from './components/Loading';
class App extends Component {
state = {
films: [],
total: 0
}
async componentDidMount() {
let {films , total} = await getNowPlayingFilmApi()
this.setState({
films,
total
})
}
render() {
const { films } = this.state
return (
<div>
<h3>app组件</h3>
<ul>{
films.length === 0 ?
// child 插槽
( <Loading >
<h1>loading...</h1>
</Loading>) :
(films.map(({ filmId, name }) => <li key={filmId}> { name } </li> )) *注意只有一个{} ,区别vue的双花括号*
}</ul>
</div>
);
}
}
export default App;
// 子组件
import React, { Component } from 'react';
class Loading extends Component {
render() {
const { child } = this.props
return (
<div>
//示例中的用法是将<h1>loading...</h1>作为child传递给<Loading>组件。这样,在Loading组件中,如果child存在,它将渲染child,否则会显示默认的"加载中。。。"文本(<h3>加载中。。。</h3>)。
{child ?? <h3>加载中。。。</h3>}
</div>
);
}
}
export default Loading;
父子间传值
props实现
- 实现父子之间的传值,事件上都是父组件向子组件传递数据和方法, 子在接受了付的数据后,用父组件传过来的方法修改接受的值,父组件中对对应的值也就会随着改变 如果是子自己的值怎么传给父?
import React, { Component } from 'react'; // 限制值得类型 import { number,func } from 'prop-types' class App extends Component { state ={ count : 1000 } addCount = n =>{ this.setState(state => ({ count: state.count + n })) } render() { const {count} = this.state return ( <div> <h3>父组件 ---{count}</h3> <hr /> {/* 父向子传值 通过自定义属性 Props传值,他是一个单向数据流 */} <Child count={count} addCount={this.addCount} /> </div> ); } } class Child extends Component { // 限制类型 static propTypes ={ count:number, addCount: func } setCount =()=>{ // 子相父传值 vue: emit // 在react中,可以让父修改当前的数据的方法也传过来,然后再子组件中调用 , this.props.addCount(1) } render() { const { count } = this.props //自接受父传来的值 return ( <div> <h3>child组件 -- {count}</h3> <button onClick={this.setCount}>子组件修改count值</button> </div> ); } } export default App;
ref实现
import React, { Component, createRef } from 'react'; class Child extends Component { state = { count: 100 } // 子组件中的修改数据方法,可以再绑定ref后在父组件中通过 this.childInstance(属性名).current.setCount(10) 来调用 setCount = (n , cb) => { this.setState(state =>({ count : state.count + n }) , ()=>{ cb(this.state) // 用来让父组件同步的获取当前的state中的值 }) // 在修改数据的同时还可以向父组件传别的值 return 200 } render() { const { count } = this.state return ( <div> <h3>子组件 ---{count} </h3> </div> ); } } class App extends Component { childRef = createRef() getChildInstance = () => { console.log(this.childRef.current) // 获得子组件实例 // let n =this.childRef.current.setCount(10) // 就收子组件传过来的值 // console.log(n , this.childRef.current.state.count) //获取子组件的值,但得到的是修改前的一次,因为是异步的 let n = this.childRef.current.setCount(10, state => { console.log(state) // 传入的cb回调函数就是当前的箭头函数,在子组件中的回调函数中执行的时候调用当前的函数,具体的 state值就是在组件中出入的值, 此片段中传入的就是 this.state 也就是子组件中的当前的 state 值 }) console.log(n) } render() { return ( <div> <h3>父组件 </h3> <button onClick={this.getChildInstance}>获取子组件</button> {/* 通过给自定义组件(子组件)绑定ref属性,来回去当前的组件实例 通过实例对象,完成组件中的方法和属性的调用,实现父子间的传值 绑定之后,父组件中就可以通过ref来获取子组件中的属性或者方法 ref只能是在父组件中给调用的子组件绑定ref属性,也就是父中的子绑定,只此一种情况 */} <Child ref={this.childRef} /> </div> ); } } export default App;
兄弟组件传值[状态提升]
贼简单,就是把兄弟间要用的数据在父组件中定义,方法也在父组件中定义,给一个子传值,给另一个传方法,或者是按需求个传个的
简单案例
import React, { Component } from 'react';
// 兄弟组件之间的传值,将兄弟共用信息提升到父组件中,
class Child1 extends Component {
render() {
// 父子间传值,自接受父的传值 this.props
const {count} = this.props
return (
<div>
<h3>Child1 --- {count}</h3>
</div>
);
}
}
class Child2 extends Component {
render() {
return (
<div>
<h3>Child2</h3>
{/*
第一种写法无法给接收到的方法中传入参数
一般都用第二种,可以传参,也可以不穿,看需求
*/}
{/* <button onClick={this.props.setCount}>修改值++</button> */}
<button onClick={() => this.props.setCount(10)}>++修改值++</button>
</div>
);
}
}
class App extends Component {
state={
count:100
}
setCount = n =>{
this.setState(state =>({
count : state.count + n
}))
}
render() {
const {count} = this.state
return (
<div>
<h3>父组件</h3>
<hr />
{/*
兄弟组件,通信可以将公用的数据提升到父组件中,实现共享
*/}
<Child1 count={count} />
<Child2 setCount={this.setCount}/>
</div>
);
}
}
export default App;
跨组件通信
- 实现组件的跨层级通信 , 生产和消费
- 在祖先节点中生产数据,data , 写在context对象当中,
- 在要通信的子孙节点中通过context对象获取数据 , 值 方法
- 必须要注意一点, 要先有数据才能消费数据,先生产在获取 , 且一个项目中可以有多个 context 对象
// 创建 context对象
import { createContext } from "react";
const ctx = createContext()
// provider 是将组件爱你中的state数据提供到子孙组件中去, 生产或者发布的过程
// consumer 消费
export const {Consumer , Provider} = ctx
export default ctx
//在组件中调用 index文件
import React, { Component } from 'react';
// ctx 是export default 默认导出的, { 解构是按需导出的 } , 默认导出的一定要写在按需导入的前面
import ctx, { Consumer, Provider } from "./context/appContext"
class Child1 extends Component {
render() {
return (
<div>
<h3>Child1</h3>
{/*
子孙组件在接收 祖先组件 context 传下来的值时 用consumer标签包裹 data
*/}
<Consumer>
{data =>{
return <h3> Child1组件 ---- count值: {data.count}</h3>
}}
</Consumer>
</div>
);
}
}
// 推荐的消费方案
class Child2 extends Component {
// 在类中的静态属性contextType赋值了context对象后,他就可以在后续的成员属性中用 this.context得到context对象
// 简单说就是子组件可以通过设置静态的属性 contextType = ctx 让子孙组件可以获得祖先组件中的 context 对象,并使用对象中的值和方法 , 来修改祖先中的值,
static contextType = ctx
addCount = () => {
console.log(this.context.setCount);
this.context.setCount(10);
}
render() {
return (
<div>
<button onClick={this.addCount}>++</button>
</div>
);
}
} class App extends Component {
// 祖先state数据,要给context对象来向下传递然后去消费
state = {
setCount:(n) => this.setState(state => ({count: state.count + n })),
count: 100
}
// 为了方便传值,也为了代码的优雅,可以将值和方法,全部都写在state中
// setCount = (n=1) =>{
// this.setState(state=>({
// count: state.count + n
// }))
// }
render() {
return (
<div>
<h3>app</h3>
<hr />
{/*
向下传值的时候用provider标签包裹 , 子组件,直接在provider中传入向下传递的值
和父子传值一样,无论是向后代组件传值还是方法,都是将从祖宗组件向下传值
*/}
{/* <Provider value={{title:"测试"}}> */}
{/* <Provider value={this.state}> */}
<Provider value={{...this.state , setCount: this.setCount}}>
<Child1 />
<hr />
<Child2 />
</Provider>
</div>
);
}
}
export default App;
高阶组件(类组件用的多,函数用别的)
高阶组件的基础应用及相关的问题
本质上就是一个函数,传给他一个组件,返回一个新的组件,相当于给手机加了一个手机壳
扩展: js中高阶函数==》 1,把函数当做形参 2,返回一个函数
- 实现步骤:
- 创建一个函数, 指定函数的参数,参数应该 应大写字母开头,参数中要穿入一个组件
- 在函数内部创建一个类组件,或者函数组件
- 常见的作用:
- 进行权限控制
- 访问统计
- 统一布局
缺点: 增加了组件的层级,影响性能 , 并且还会影响Props的传值
// 配置高阶组件函数 文件 src/hoc/withLoad.js
import React , {Component , Fragment} from "react"
// Fragment 是一个内置组件,它可以当作组件元素的顶层元素,但是他不会解析成html元素,就相当于只是一个占位文件
// fragment使用频率很高,,react提供了加血方法: <></> 且 简写无需导包
// 高阶他就是一个函数, 把组件当做参数传过来,对传入的组件进行增强后 返回
// Cmp就是接收到的组件,
const withLayout = Cmp =>{
console.log(new Cmp())
// 返回一个类组件
// return class Layout extends Component {
return class extends Component { // 返回值的类直接就用在了组件中, 所以直接返回一个匿名的类即可
render (){
return (
// <div className="hoc">
// <Fragment>
<>
{/* 采用fragment后,相当于直接将fragment标签中的内容写在了引入的父组件的div中,也就是少了原本的 div标签*/}
<h3>公用的组件头部</h3>
<hr />
{/*
用高阶组件包裹组件
*/}
<Cmp />
<hr />
<h3>公共组件的尾部</h3>
</>
// </Fragment>
// </div>
)
}
}
}
export default withLayout
// 被高阶组件调用的组件 , 通常是摸个页面的部分,调用后给此组件添加了一个新的布局
import React, { Component } from 'react';
// react 没有配置路径别名,只能写相对地址
import withLayout from 'src/hoc/withLayout';
class Home extends Component {
displayName='home'
render() {
return (
<div className='home'>
<h3>首页面</h3>
</div>
);
}
}
// 使用高阶组件来增强组件时 , 导出时要用高阶组件将导出的类包裹, 因为高阶组件是一个函数,返回的是一个对组件增强后的类, 所以在app祖宗组件中导入的也就是高阶组件返回的 包裹后的 深层组件
// 缺点: 改变了原组件的层级,加深了
export default withLayout(Home);
- 如何解决高阶组件中的Props丢失问题:
- 实际上就是在高阶组件的 方法中加一步, 先对祖先组件传过来的Props值深复制出来,对值进行相应的修改,然后将修改后的值 ,在通过接受的组件参数,传给组件,简单说高阶组件函数,相当成了数据的处理和转送中间层 修改值的时候不能直接对Props修改,要采用深复制的方法,克隆数据对克隆数据进行修改,并将克隆后的数据传递出去
- 在子组件中,正常接收数据this.props,但要明白此时的数据时处理过后的高阶数据,不是元数据
高阶组件的 类组件的封装方法
const withLayout = Cmp => {
return class extends Component {
render() {
const arts = cloneDeep(this.props)
arts.phone = arts.phone.slice(0, 3) + "****" + arts.phone.slice(7)
return (
<>
<h3>我是公共头部</h3>
<hr />
{/* <Cmp {...this.props}/> */}
{/* 推荐用法 */}
<Cmp {...arts} />
<hr />
<h3>我是公共低部</h3>
</>
)
}
}
}
函数组件的写法
const withLayout = Cmp => props =>{
// 在高阶组件函数中对数据进行处理 , 注意修改数据时,不能修改Props中的数据
// 第一种处理方法 ,不常用
// let phone = props.phone
// phone = phone.slice(0,3) + "*****" + phone.slice(7)
// 第二种, 使用深层克隆,将Props的数据当一份出来
const arts = cloneDeep(props)
arts.phone = arts.phone.slice(0,3) + "****" + arts.phone.slice(7)
return (
<>
<h3>我是公用头部</h3>
<hr />
{/* 第一中处理方法,直接用修改后的值 */}
{/* <Cmp phone={phone} /> */}
{/* 当高阶组件对数据不进行处理时,则直接用展开语法,获取 各项数据即可 */}
{/* <Cmp {...props} /> */}
{/* 第二种处理方法 */}
<Cmp {...arts}/>
<hr />
<h3>我是公用底部</h3>
</>
)
}
子组件使用时
class Home extends Component {
displayName='home'
// console.log() 在类组件中,一切的执行语句都要写在,render中
render() {
// 在使用了高阶组件之后,Props的数据会丢失,不能在直接使用
/*
要想获取数据,要先在高阶组件中进行数据出来 , 并且在高阶组件中修改后的值,手动的将修改后的值传入到 调用的组件中,也就是 Cmp中, 接着在子组件中,this.props 接受的值可以理解为是 高阶组件传过来的修改后的值,不是父组件直接传过来的值
*/
console.log("phone" , this.props)
const {phone} = this.props
return (
<div className='home'>
<h3>首页面 --- {phone}</h3>
</div>
);
}
}
高阶组件的反向继承 === 可以理解为对子组件重写的方案
- 一般前端很少用到,但控制翻转能用到
- 继承: 先执行父,在执行子 反向继承: 先执行子,在执行父
- 作用: 用来渲染劫持 和 操作 state数据
用我自己的理解简单总结一下: 在app祖宗组件中,接受传入的子组件,这个子组件是经过高阶组件增强的,所谓增强就是原来的比较纯净的子组件,通过高阶组件增添了,属性、方法,设置app组件传过来的值,注意区别上面的解决Props传值丢失问题 , 此时的高阶组件在返回类时,继承的是接收到的子组件类型,可以理解为返回的值就是子组件类的扩展类,或者说增强版的子组件,app接受的就是这个增强版(改造后的)子组件 , 示例代码如下
子组件,就像个模版花瓶,任高阶组件调教
import React , { Component } from "react"
import withBtn from "../../hoc/withBtn"
class Btn extends Component{
render(){
console.log(this.props)
return <button></button>
}
}
export default withBtn(Btn) // 导出的就是调教好的
app祖宗组件,给高阶组件提供调教的值,方法
class App extends Component {
clickHandler = () => {
console.log(1111222333)
}
render() {
return (
<div>
// title onClick 都是要穿的值
<Btn title="打印" onClick={
this.clickHandler
}/>
</div>
);
}
}
高阶组件对子组件进行具体的 增强
const withBtn = Cmp => {
// 这里继承的不是component 而是继承的 需要增强的类型,继承此类型以后,可以获得祖宗节点的穿的值
return class extends Cmp {
render() {
const ele = React.cloneElement(super.render(), {
onClick:this.props.onClick
},
this.props.title)
return (
<>
// app 组件渲染的组件就是这个ele
{ele}
</>
)
}
}
}
memoization (计算属性 - 记忆组件)
- 两次的计算结果相同返回结果,结算的结果不一样就返回新的结果
- 安装: npm i -S memoize-one
import React, { Component } from 'react';
// 缓存组件 -- 计算属性
// 如果下一次的计算的依赖性没有发生改变,就会用上一次的结果缓存
// 是一个性能优化函数,可以起到一定的性能提升,也可以不用
import memoizeOne from 'memoize-one';
class App extends Component {
state = {
n1: 1,
n2: 2
}
sum = (n1, n2) => {
console.log("sum")
return n1 + n2
}
// 高阶函数: 他就是一个高阶函数,形参是一个函数
total = memoizeOne((n1, n2) => {
console.log('total')
return n2 + n1
})
render() {
const { n1, n2 } = this.state
return (
<div>
<span>{n1}</span>
<span>+</span>
<span>{n2}</span>
<span>=</span>
{/* this.sum 执行了三次,打印了三个sum, total执行了一次,因为三次的结果都相同 */}
<span>{this.sum(n1, n2)}</span>
<span>{this.sum(n1, n2)}</span>
<span>{this.sum(n1, n2)}</span>
<h3>{this.total(n1,n2)}</h3>
<h3>{this.total(n1,n2)}</h3>
<h3>{this.total(n1,n2)}</h3>
</div>
);
}
}
export default App;
portals
portals提供的是将子组件渲染到父组件以外的Dom的方案
- 在APP组件中的调用方法是一样的,但如果想让子组件挂在到别的Dom节点中,需要改变写法
基础了解,应用时 ,多数是挂在到动态生成的节点中去 ,下面是死的 import React, { Component } from 'react'; // 一个让子组件挂在到指定的元素节点中方法 import { createPortal } from 'react-dom'; class Child extends Component { render() { // 常规应用,挂在到的是root节点 // return ( // <> // <h3>弹出层</h3> // </> // ); return createPortal( <> <h3>弹出层</h3> </>, // 声明要挂载到的节点 document.getElementById('dialog') ) } } class App extends Component { render() { return ( <div> <h3>app</h3> <hr /> <Child /> </div> ); } } export default App;
实际中的应用
import React, { Component } from 'react'; // 创建一个让组件挂在到指定的元素节点中 import { createPortal } from 'react-dom'; class Child extends Component { // 生成弹窗的挂载节点 constructor(props){ super(props) // 在创建类实例的时候,将节点和方法都结构到对应的元素中 const [el , remove] = this.createContainer() } // 生成挂载节点,返回的是挂载节点和删除节点的方法 createContainer(){ this.container = document.createElement('div') document.body.appendChild(this.container) return [()=> this.container.remove() , this.container] } // 如果要关闭弹窗直接注销生成的动态挂载节点即可, 直接调用 remove 方法 onClose = ()=>{ this.remove() } render() { return createPortal( <> <h3>弹出层</h3> <button onClick={this.onClose}>关闭</button> </>, // 声明要挂载到的节点 // document.getElementById('dialog') this.container ) } } class App extends Component { state= { visable : false, dt: 1 } render() { return ( <div> <h3>app</h3> <button onClick={()=>{ this.setState({ visable:true , dt: Date.now() // 为了点击时 ,让代码发生变化,否则就不会触发二次显示 }) }}>显示</button> <hr /> {this.state.visable ? ( <Child key={this.state.dt}/> ): (<></>) } </div> ); } } export default App;
装饰器
用来包装或者扩展代码的功能,装饰器以:
@
符号开头的特属于法只能用在面向对象中,且需要用过bable编译才能在浏览器正常的运行, 函数中不用
目前还在提案阶段,react没有内置方法,需要在webpack配置中进行修改
两种方法:
1, npm run eject 在react工程化中暴露出webpack配置, 但是次日方案不可逆,出错就寄了,不推荐
2:
npm i -D @craco/craco@7 修改或覆盖webpack配置 在根目录中创建 craco.config.js 文件,对webpack进行配置 此文件用 commonjs的规范,对已有的webpack进行修改和增量配置 , 修改后重启应用 npm i -D @babel/plugin-proposal-decorators@7
类装饰器 , 类成员装饰器
类装饰器
target就是当前装饰的类 person
const decator =Target => {
console.log(Target)
// 扩展静态方法和属性
Target.staticMsg = '我是一个静态属性'
// 扩展动态属性和方法 这是添加给此类创建的实例化对象的,当前类本身不能使用此属性
Target.prototype.msg = "成员属性"
}
装饰器的语法形式为@decator,其中decator是一个函数,接收类或类方法作为参数,并返回一个新的被修改后的类或方法。装饰器可以在类的声明之前或类方法的定义之前使用。
@decator
class Person {}
类成员装饰器
// target 当前的装饰类的实例对象
// key装饰的成员名称
// description 对于成员的信息的描述,是一个对象
status已被弃用
const readonly = status= (target, key , description ) => {
console.log(target, key , description)
description.writable = !status
}
class Person {
@readonly(true)
title="你好"
}
const p = new Person()
p.title = "adv"
console.log(p.title)
装饰器方法调用高阶组件
与传统的高阶组件的调用方法基本一样的,就改了两个地方 装饰器的引入@高阶函数, 导出时正常导出,无须方法包裹 , 仅此两处
在定义好 高阶组件的函数后,在子组件中直接采用装饰器引入即可
import React, { Component } from 'react';
import withLayout from 'src/hoc/withLayout';
const fun = () => {
console.log("第二个装饰器")
}
// 装饰器可以有多个,执行顺序为: 水平方向: 从右向左,垂直方向:从下向上
@withLayout @fun
class Loading extends Component {
render() {
const { child } = this.props
return (
<div>
{child ?? <h3>加载中。。。</h3>}
</div>
);
}
}
export default Loading;
react项目支持配置
- 修改或覆盖webpack配置
- 在react工程中,暴露出webpack配置,npm run eject ,缺点是他不可逆, 暴露后就不能退回,
npm i -D @craco/craco@7
==》 进行或覆盖webpack的配置npm i -D @babel/plugin-proposal-decorators@7
bable配置
样式
- react 不能像vue那样将排版,逻辑,样式全都写在一个文件中,所以样式需要单独解决
- 写样式时需要注意的问题: 类名重复时会发生样式的覆盖,特别是祖孙组件之间
- 解决方法有四种(样式隔离的方法):
- 按照命名规范起样式别名: 模块名-样式名… 这是一定可以解决问题的方法
- cssModel 方案, 把css解析成js文件,但css文件命名有要求: XXX.module.css/scss ,在组件中引用时: import style(自己起名) from “文件地址” , className={style.具体的类名}
- cssinjs(热门方案) 在js中写css,需要引入第三方的库(styled-components), 只需要定义 一个组件名,并指定其解析后的类型,然后用模版字符串定义他的样式,最后按需导入接口,用法和组件的用法一样,双标签包裹后定义内容
- 用sass进行嵌套隔离,但是在最外面,名称要用 模块名-样式名 , 保证不重名
模版字符串扩展的 函数新语法
// 模范字符使用方法
function fn(...args){
console.log(args)
}
fn(`asdafgasdfwe`) // [asdfasdf]
let num = 100
let name = "嘿嘿"
fn(`asdlf${num}jals${name}d`) // [ ['asdlf' , "jals" , "d"] , 100 , "嘿嘿"]
// ${} 将数组拆分开,但仍为一个数组内的内容,添加的变量,单独为一个元素
// 剩余参数法 strs 接收第一个参数,其他的将以数组形式传入
这就是定义的第三种cssinjs方法
function fn2(strs , ...args){
console.log(strs , args)
let out = ''
strs.forEach((item, index) =>{
out += item + (args[index] ?? '')
})
console.log(out)
const style = document.createElement('style')
style.innerHTML= out
document.head.appendChild(style)
}
// fn2(1,[2,3,4]) // 1,[2,3,4]
区别 fn2(`aa${num}aaa`) 和 fn2`aa${num}aaa` 两者不同
// 在使用模版字符串传值的时候,加小括号,就相当于整个模版字符串的值就是第一个参数 , 这个过程是,模版字符串先解析模版中的占位符中的动态变量,将他的值传进来,然后和静态部分组成一个新的字符串,作为参数传入方法中,作为第一个参数(字符串参数)
// !! fn2(`asdlf${num}jals${name}d`)
// 下面是js新语法: 若要提供自定义函数,需在模板字面量之前加上函数名(结果被称为带标签的模板)。此时,模板字面量被传递给你的标签函数,然后就可以在那里对模板文本的不同部分执行任何操作
// strs得到的是模版字符串的静态部分,args得到的是动态部分并将他们放在一个数组中
// fn2`asdlf${num}jals${name}d`
fn2`
.three{
color: purple;
font-size:20px;
background-color: green
}
`
四种方法详解 (样式就是正常的样式书写)
app 祖宗组件
import React, { Component } from 'react';
import Myhead from "./components/Myheader/index"
// 一
import "./app.css"
// 二
import style from "./style.module.css"
// 方法三 手撸法,很鸡肋,没有样式隔离,依旧有重名时的样式覆盖问题
function appStyle(strs , ...args) {
let cs = ''
strs.forEach((item,index) =>{
cs += item + (args[index] ?? "")
})
const style = document.createElement("style")
style.innerHTML= cs
document.head.appendChild(style)
}
appStyle`
.three{
background-color:pink
}
`
class App extends Component {
render() {
return (
<div>
{/* 失败版本,命名重复,只有app的样式生效 */}
<h3 className='title'>app</h3>
{/* 方法一 , 命名规范法 */}
<h3 className='app-title'>app方法一</h3>
{/* 方法二: cssmodule解析成js */}
<h3 className={style.title}>app二</h3>
{/* 方法三(手撸版): cssinjs 没有引入一个styled-compoents库时的用法,然后用模版字符串解析定义好的样式模版字符串 , 需要注意的是此方法和白给失败方法没有区别 , 依旧有重复类名样式就覆盖的问题,且子组件的权重最高,因为子组件后加载可能 */}
<h3 className='three'>app三</h3>
<hr />
<Myhead />
</div>
);
}
}
export default App;
// 子组件
import React, { Component } from 'react';
// 第一种:按命名规范命名
import "./style.css"
// 用第二种方法,将css解析成js
import style from "./style.module.css"
// 第三种方法
// styled-components 他 是实现了cssinjs的技术库
// 在没有引入技术库时的用法
// 剩余参数法 strs 接收第一个参数,其他的将以数组形式传入
function fn2(strs , ...args){
console.log(strs , args)
let out = ''
strs.forEach((item, index) =>{
out += item + (args[index] ?? '')
})
console.log(out)
const style = document.createElement('style')
style.innerHTML= out
document.head.appendChild(style)
}
fn2`
.three{
color: purple;
font-size:20px;
background-color: green
}
`
class Head extends Component {
render() {
return (
<div>
{/* 会发生样式重叠的写法, 组件的class名相重复,样式只会有最后一个会生效 */}
<h3 className='title'>我是头部失败版</h3>
{/* 第一种方案: 起不重复的名字 */}
<h3 className='min-title'>我是头部一</h3>
{/* 第二种方法 */}
<h3 className={style.title}>我是头部二</h3>
{/* 方法三: cssinjs方法用到es6中的模版字符串 */}
<h3 className='three'>我是头三</h3>
</div>
);
}
}
export default Head;
styled-components(cssinjs方法)
安装技术库 npm i -S styled-components
// cssinjs 方法: 把css写在js中
// style-component库来实现
// 好处: 把样式当做组件来使用,定义的样式有隔离性
import styled from "styled-components"
// 用styled-components创建一个组件名为Div(这个随便起) ,在编译后的HTML元素名称为div, 且随机生成一个类名,保证类名不重复
// 此Div中的样式为字符串模版中的样式、样式的写法和scss一样
// 要想样式有提示: 安装插件: vscode-styled-componets 即可
export const H3 = styled.h3`
color : red;
font-size: 30px;
` // 看清楚 ,这是模版字符串
import React, { Component } from 'react';
// 第三种方法, 引入技术库后 , 样式有很多,按需导入即可
import { H3 } from "./style"
class Head extends Component {
render() {
return (
<div>
{/* 方法三: 最终版 , 将样式当组件来用 */}
<H3>我是头三终极版</H3>
</div>
);
}
}
export default Head;
cssinjs的样式嵌套 , 样式继承 ,接收props传值
因为cssinjs中定义样式是当成子组件再用,所以可以接受父组件的传值
import styled from "styled-components"
export const Div = styled.div`
color : red;
font-size: 30px;
h3{
color: pink;
background-color: plum;
& 表示当前的单个元素
&:hover{
color: black;
}
}
div{
width: 200px;
height: 200px;
background-color: blue;
}
`
// 接收Props传值
export const ContentBtn = styled.button`
font-size: 20px;
height: 50px;
line-height: 50px;
border: 1px solid red;
border-radius: 5px;
outline: none;
如果所设置的样式带单位,就将单位写在{}外 ,如下
color:${props => props.color ?? "red"}px
`
// 样式继承
// 很简单就将之前定义好的组件名写在小括号里即可,会继承编译后的HTML元素和样式
export const Btn = styled(ContentBtn)`
font-size: 14px;
`
//使用:
import {Div , Btn , ContentBtn} from "地址"
<Div>
<h3>我是头三终极版</h3>
<div>世纪东方卡拉胶猎杀对决发啦手机打理发卡</div>
</Div>
<Btn>按钮</Btn>
{/* 传值 */}
<ContentBtn color="red">按钮</ContentBtn>
第四种解决css隔离的方法: scss样式,这是最重要的方法
创建scss文件,在安装scss处理器, npm i -D sass , 引入文件即可
sass分为 dart-sass[主流解析器,npm下载即可] / node-sass解析器,现在不好用要梯子
子组件的样式引用 components文件中的组件
import React, { Component } from 'react';
import "./style.scss"
import style from "./scss.module.scss"
class Scss extends Component {
render() {
return (
<div>
{/* 还是要注意重名问题,组件中元素的calss名要沟通好别重复 */}
<div className='my-header'>
<div className='title'>我是一个标题</div>
</div>
{/* 也可以使用 XXX.module.scss */}
{/* 但是要注意两点,使用是要用 className={引入名.XXX} 有-的非正常属性名要用函数签名 , {style['XX-XX']} */}
<div className={style['my-headd']}>
<div className={style.title}>我是第二个标题</div>
</div>
</div>
);
}
}
export default Scss;
//app组件正常引入即可
import React, { Component } from 'react';
import Myhead from "./components/Myheader/scss"
class App extends Component {
render() {
return (
<div>
<Myhead />
</div>
);
}
}
export default App;
export const ContentBtn = styled.button`
font-size: 20px;
height: 50px;
line-height: 50px;
border: 1px solid red;
border-radius: 5px;
outline: none;
//如果所设置的样式带单位,就将单位写在{}外 ,如下
color:${props => props.color ?? "red"}px
`
// 样式继承
// 很简单就将之前定义好的组件名写在小括号里即可,会继承编译后的HTML元素和样式
export const Btn = styled(ContentBtn)`
font-size: 14px;
`
//使用:
import {Div , Btn , ContentBtn} from "地址"
<Div>
<h3>我是头三终极版</h3>
<div>世纪东方卡拉胶猎杀对决发啦手机打理发卡</div>
</Div>
<Btn>按钮</Btn>
{/* 传值 */}
<ContentBtn color="red">按钮</ContentBtn>
第四种解决css隔离的方法: scss样式,这是最重要的方法
创建scss文件,在安装scss处理器, npm i -D sass , 引入文件即可
sass分为 dart-sass[主流解析器,npm下载即可] / node-sass解析器,现在不好用要梯子
子组件的样式引用 components文件中的组件
import React, { Component } from 'react';
import "./style.scss"
import style from "./scss.module.scss"
class Scss extends Component {
render() {
return (
<div>
{/* 还是要注意重名问题,组件中元素的calss名要沟通好别重复 */}
<div className='my-header'>
<div className='title'>我是一个标题</div>
</div>
{/* 也可以使用 XXX.module.scss */}
{/* 但是要注意两点,使用是要用 className={引入名.XXX} 有-的非正常属性名要用函数签名 , {style['XX-XX']} */}
<div className={style['my-headd']}>
<div className={style.title}>我是第二个标题</div>
</div>
</div>
);
}
}
export default Scss;
//app组件正常引入即可
import React, { Component } from 'react';
import Myhead from "./components/Myheader/scss"
class App extends Component {
render() {
return (
<div>
<Myhead />
</div>
);
}
}
export default App;