构建react项目
react提供create-react-app脚手架,默认把webpack的打包规则处理好了,把一些项目需要的基本文件也都创建好了。
1.create-react-app的运用
安装脚手架
npm i create-react-app -g(mac前面需要设置sudo)create-react-app --v检查安装情况
创建react工程化的项目:create-react-app demo
一个react项目中,默认会安装:
react:react框架的核心
react-dom:react视图渲染的核心,基于react构建webapp(html页面)。
react-native:构建和渲染app的
react-scripts:脚手架为了让项目目录看起来干净一些,把webpack打包的规则及相关的插件都隐藏到了node_modules目录下,react-scripts就是脚手架中自己对打包命令的一种封装,基于它打包,会调用node_modules中的webpack等进行处理。
JSX语法
JSX:JavaScript and xml(html) 把js和html混合在一起
-
vscode如何支持jsx语法(格式化,快捷提示)
- 创建的js文件,我们把后缀名设置为jsx即可,这样js文件中就可以支持jsx语法了
- webpack打包的规则中,也是会对jsx这种文件按照js的方法进行处理。
-
在html中嵌入js表达式,需要基于‘{}语法’
- js表达式:执行有结果的(变量,数学运算,三元运算符,循环:借助数组的迭代方法处理如map)
-
在ReactDOM.createRoot()的时候,不能直接把HTML/BODY为根容器,需要指定一个额外的盒子root
-
每一个构建的视图,只能有一个根节点。
- React给我们提供了一个特殊的标签:React.Fragment空文档标记标签<></>
-
{}胡子语法中嵌入不同的值,所呈现出来的特点
- number/string:值是啥,就渲染什么
- boolean/null/Symbol/BigInt/undefined:渲染内容是空
- 不支持渲染普通对象
- 数组对象:把数组的每一项都分别拿出来渲染(并不是变为字符串渲染,中间没有逗号)
- 函数对象:不支持在{}中渲染,但是可以作为函数组件渲染。
- 除数组对象外,其余对象一般都不支持在{}中渲染,但是也有特殊情况:
- jsx虚拟dom对象
- 给元素设置style行内样式,要求必须写成一个对象的形式
-
给元素设置样式
行内样式:
root.render(
<>
<h1 style={{height:'100px',color:'red'}}>123</h1>
</>
)
设置样式类名:需要把class替换成className
JSX底层处理机制
1. 把我们编写的jsx语法,编译为虚拟dom对象(virtualDOM)
- 虚拟dom对象:框架自己内部构建的一套对象体系,基于这些属性描述,我们所构建视图中的dom节点的相关特征。
- 基于babel-preset-react-app语法包,把JSX编译为React.createElement(ele,attr,...children)这种格式
1.ele:元素标签名或组件
2.attr:属性,如果没有设置任何属性则为null
3.children:从第三个及以后的参数,表示当前元素的子节点或子组件
- 再把createElement方法执行,创建出virtualDOM。
virtualDOM = {
$$typeof:Symbol(react.element),
ref:null,
key:null,
type:标签名,
props:{
元素相关属性,
children:子节点信息
}
}
2. 把构建的virtualDOM渲染为真实dom
- 真实dom:浏览器页面中,最后渲染出来,让用户看得见的dom元素
- 基于ReactDOM中的render方法处理的
react16:
ReactDOM.render(
<>...</>,document.getElementById('root')
)
react18:
const root = ReactDOM.createElement(document.getElementById('root'))
root.render(<>...</>)
3. 补充:第一次渲染页面是直接从virtualDOM->真实dom;但是后期视图更新的时候,需要经过一个dom-diff的对比,计算出补丁包PATCH(两次视图差异的部分),把PATCH补丁包进行渲染。
render函数的实现
/*封装一个遍历对象属性的方法(包括私有属性)
基于传统的for in循环,存在一些弊端(性能较差,只能迭代可枚举,非Symbol类型的属性...)
Object.getOwnPropertyNames(arr) -> 获取对象非Symbol类型的私有属性
Object.getOwnPropertySymbols(arr) -> 获取Symbol类型的私有属性
获取所有的私有属性:
1.let keys = Object.getOwnPropertyNames(arr).contact(Object.getOwnPropertySymbols(arr))
2.let keys = Reflect.ownKeys(arr) 不兼容IE
*/
const each = function each(obj,callback){
if( obj === null || typeof obj !== 'object') throw new TypeError('obj is not a object')
if( typeof callback !== 'function') throw new TypeError('callback is not a function')
let keys = Reflect.ownKeys(obj)
keys.forEach(key =>{
let value = obj[key]
callback(value,key)
})
}
/*render:把虚拟DOM变为真实DOM*/
function render(virtualDOM,container){
let { type,props} = virtualDOM;
if(typeof type == 'string'){ //动态创建一个标签
let ele = document.createElement(type)
each(props,(value,key)=>{
//className的处理:value存储的是样式类名
if(key === 'className'){
ele.className = value
return
}
//style的处理:value存储的是样式对象
if(key === 'style'){
each(value,(val,attr)=>{
ele.style[attr] = val;
})
return
}
//子节点处理:value存储的是children属性值
if(key === 'children'){
let children =value;
if(!Array.isArray(children)) children = [children]
children.forEach(child =>{
//子节点是文本节点直接插入
if(typeof child == 'string'){
ele.appendChild(document.createTextNode(child))
return
}
//子节点又是一个virtualDOM:递归处理
render(child,ele)
})
return
}
ele.setAttribute(key,value)
})
//把创建的标签添加到指定的容器中
container.appendChild(ele)
}
}
函数组件
创建函数组件
1.创建组件:在src目录中,创建一个xxx.jsx的文件,就是要创建的一个组件;我们在此文件中,创建一个函数,让函数返回jsx视图或jsx元素,virtualDOM对象;这就是创建一个函数组件~! 2.调用组件:基于ES6Module规范,导入创建的组件,然后像写标签一样调用这个组件即可~! 3.命名:组件的名字,一般采用PascalCase大驼峰命名法。
调用组件的时候,可以给调用的组件设置各种属性
<DemoOne style={{ fontSize: 15px }} className="box" x={10} title={[100,200,300]}/>
1.如果设置的属性值不是字符串格式,需要基于{}进行嵌套。
2.调用组件的时候,我们可以把一些数据基于属性props的方式。传给组件!
函数组件渲染机制
- 基于babel-preset-react-app 把调用的组件转换为createElement格式。
- 把createElement方法执行,创建出一个virturalDOM对象。
- 基于root.render把virtualDOM转为真实的DOM。 type不再是一个字符串,而是一个函数此时: - 把函数执行 - 把virtualDOM中的props,作为实参传给函数。 - 接受函数执行的返回结果(也就是当前组件的virtualDOM对象)。 - 最后基于render把组件返回的DOM变为真实DOM,插入到root容器中 。
函数组件props
- 调用组件,传递进来的属性是只读的(原理是对象被冻结了)
- 作用:父组件调用子组件的时候,可以基于属性,把信息传递给子组件,子组件接受相应的属性值,呈现出不同的效果,让组件的复用性更强。
- 虽然对于传递的props不能直接修改,但是可以做一些规则校验
- 设置默认值 函数组件名.propTypes ={ x:0 ... }
- 设置其他规则,例如:数据格式,是否必传... (依赖于官方的一个插件:prop-types)
import PorpTypes form "prop-types";
函数组件名.propTypes ={ title:PorpTypes.string.isRequired, x:PorpTypes.number, y:PorpTypes.oneOfType([PorpTypes.number,PorpTypes.bool])多种校验规则中的一个 }
传进来的属性,首先会经历规则的校验,不管校验成功还是失败,最后都会吧属性给props,只不过不符合设定规则,控制台会抛出警告错误。 - 如果想修改传过来的值可以把值赋值给一个变量,去修改变量的值。
Hooks
1.useState
React Hook函数之一,目前是在函数组件中使用状态,并且后期基于状态的修改,可以让组件更新。 let [num,setNum] = useState(val);
- 执行useState,传递的val是初始的状态值。
- 执行这个方法,返回结果是一个数组:[状态值,修改状态的方法]
- 执行setNum
const Demo =function Demo(){
let [num,setNum] = useState(0);
const click = ()=>{
setNum(num + 10)
}
return <Button onClick = {click}></Button>
}
useState底层处理机制
函数组件的每一次渲染(或者更新),都是把函数重新执行,产生一个全新的“私有上下文”。
- 内部的代码也需要重新执行
- 涉及的函数需要重新的构建(这些函数的作用域,是每一次执行函数组件产生的闭包)
- 每一次执行函数组件,也会把useState重新执行,但是:
执行useState,只有第一次设置的初始值会生效,其余以后再执行,获取的状态都是最新的状态值(而不是初始值)
返回的修改状态的方法,每一次都是返回一个新的。
useState优化机制
useState自带了性能优化的机制:
- 每一次修改值的时候,会拿最新要修改的值和之前的状态值做比较(基于Object.is做比较)
- 如果发现两次的值是一样的,则不会修改状态,也不会让视图更新(可以理解为:类似于PureComponent,在shouldComponent中做了浅比较和优化)
const Demo =function Demo(){
let [num,setNum] = useState(10);
const click = ()=>{
for(let i=0;i<10;i++){
flushSync(()=>{
setNum(num+1)
})
} //这里只会执行一次,num的结果是11
setNum(10) //设置为原来的值,不会渲染
}
return <Button onClick = {click}>新增</Button>
}
const Demo =function Demo(){
let [num,setNum] = useState(10);
const click = ()=>{
for(let i=0;i<10;i++){
setNum((pre)=>{
return pre + 1;
}) //函数只更新一次,但是num的值是20
}
}
return <Button onClick = {click}>新增</Button>
}
2.useEffect
在函数组件中,使用生命周期函数
useEffect(callback):
- 第一次渲染完毕后,执行callback,等价于componentDidMount
- 在组件每一次更新完毕后,也会执行callback,等价于componentDidMount
useEffect(callback,[]):
- 只有第一次渲染完毕后,才会执行callback,每一次视图更新完毕后,callback不再执行。
- 类似于componentDidMount
useEffect(callback,[依赖的多个状态]):
- 第一次渲染完毕会执行callback
- 当依赖的状态值(或者多个依赖状态中的一个)发生改变,也会触发callback执行
- 但是依赖的变化如果没有变化,在函数更新的时候,callback是不会执行的
useEffect(()=>{ return ()=>{} 返回的小函数,在组件释放的时候执行 })
const Demo =function Demo(){
let [num,setNum] = useState(10);
useEffect(()=>{
console.log(111) //必须在函数的最外层上下文中调用,不能把其嵌入到条件判断,循环等操作语句中
})
const click = ()=>{
setNum(num+1)
}
return <Button onClick = {click}>新增</Button>
}
useEffect细节处理
- 第一次渲染完毕后,从服务器异步获取数据
const data = ()=>{
return new Promise(reslove =>{
setTimeout(()=>{
reslove([10,210])
})
})
}
const Demo =function Demo(){
let [num,setNum] = useState(10);
useEffect(()=>{
data().then(r=>{
console.log(r)
})
},[])
const click = ()=>{
setNum(num+1)
}
return <Button onClick = {click}>新增</Button>
}
useEffect和useLayoutEffect的区别
useEffect:第一次真实DOM已经渲染,组件更新会重新渲染真实的DOM;所以频繁切换的时候,会出现样式或内容闪烁。
useLayoutEffect:第一次真实DOM还未渲染,遇到cb中修改了状态,视图立即更新,创建出新的virtualDOM,然后和上次一的virtualDOM合并在一起渲染为真实DOM。也就是此类需求下,真实DOM只渲染一次,不会出现内容或样式的闪烁。
视图更新步骤
- 基于babel-preset-react-app把JSX编译为createElement格式
- 把createElement执行,创建出virtualDOM
- 基于root.render方法把virtualDOM变为真实DOM对象
useLayoutEffect阻塞第四步操作,先去执行Effect链表中的方法(同步操作)
useEffect第四步操作和Effect链表中的方法执行,是同时进行的(异步操作) - 浏览器渲染和绘制真实DOM
3.useRef
作用一:函数组件获取DOM元素实例
第一种
const Demo =function Demo(){
let [num,setNum] = useState(10);
let box;
useEffect(()=>{
console.log(box)
},[])
return <Button onClick = {click} ref={(x)=> box = x}>新增</Button>
}
第二种
const Demo =function Demo(){
let [num,setNum] = useState(10);
let box = React.createRef();
useEffect(()=>{
console.log(box.current)
},[])
return <Button onClick = {click} ref={box}>新增</Button>
}
第三种 useRef
const Demo =function Demo(){
let [num,setNum] = useState(10);
let box = useRef(null);
useEffect(()=>{
console.log(box.current)
},[])
return <Button onClick = {click} ref={box}>新增</Button>
}
React.createRef和useRef的区别
useRef再每一次组件更新的时候(函数重新执行),再次执行useRef方法的时候,不会创建新的ref对象,获取到的还是第一次创建的ref对象
React.createRef在每一次组件更新的时候,都会创建一个全新的ref对象,比较浪费性能,但是在类组件中就不会有这种问题
作用二:获取子组件的dom元素
函数组件可以使用ref访问类组件的实例,但是不能直接反问函数子组件的实例,需要通过React.forwardRef函数才能访问。
//类组件可以直接访问
class Child extends React.Commponent{
state = {x:1000}
render(){
<span>{this.state.x}</span>
}
}
//函数组件需要用React.forwardRef
const Child = React.forwardRef(
function Child(prop,ref){
console.log(prop) //prop中没有ref
return <span ref={ref}>子组件</span>
}
)
const Demo =function Demo(){
let [num,setNum] = useState(10);
let box = useRef(null);
useEffect(()=>{
console.log(box.current)
},[])
return <Child ref={box} />
}
作用三:获取函数子组件的方法或状态
使用useImperativeHand函数获取
//函数组件需要用React.forwardRef
const Child = React.forwardRef(
function Child(prop,ref){
let [text,setText] = useState('你好');
const submit = ()=>{}
useImperativeHand(ref,()=>{
//在这返回的内容都可以被父组件ref对象获取到
return {
text,
submit
}
})
return <span>子组件</span>
}
)
const Demo =function Demo(){
let box = useRef(null);
useEffect(()=>{
console.log(box.current)
},[])
return <Child ref={box} />
}
4.useMemo
函数组件的每一次更新,都是把函数重新执行
1.产生一个新的闭包
2.内部的代码也要重新执行一遍
如果我们修改了x,那么对应的total的逻辑也会重新执行,比较消耗性能,影响更新速度。
诉求:在函数每一次重新执行的时候,如果依赖的状态值没有发生变化,我们此操作逻辑不应该去执行。
const Demo =function Demo(){
let [supNum,setSupNum] = useState(10);
let [x,setX] = useState(0);
let total = supNum,ratio
if(total>0) ratio = supNum*100.toFixed(2) + '%'
//使用useMemo
/*
1.第一次渲染组件的时候,cb会执行
2.后期只有依赖的值发生改变。cb才会执行
3.每一次会把cb执行的返回结果赋值给xxx
4.useMemo具备“计算缓存”,在依赖的状态值没有发生改变,cb没有触发执行的时候,xxx获取的是上次一次计算出来的结果,和vue的计算属性非常的类似。
*/
let ratio = useMemo(()=>{
let total = supNum,ratio;
if(total>0) ratio = supNum*100.toFixed(2) + '%';
return ratio
},[supNum,setSupNum])
return <div>
<p>支持人数:{supNum}</p>
<p>支持比例: {ratio}</p>
<p>{x}</p>
<div onClick = {()=> setSupNum(supNum + 1)}>支持<div/>
<div onClick = {()=> setX(x + 1)}>别的操作<div/>
</div>
}
5.useCallback
useCallback可以保证函数组件的每一次更新,不再把里面的小函数重新创建,用的都是第一次创建的。
const Demo =function Demo(){
let [num,setNum] = useState(10);
/*
1.组件第一次渲染,useCallback执行,创建一个函数cb,赋值给xxx
2.组件后续每一次更新,判断依赖的状态值是否改变,如果改变,则重新创建新的函数堆,赋值给xxx;但是如果依赖的状态没有更新或者没有设置依赖([]),则xxx获取的一直是第一次创建的函数堆,不会创建新的函数出来。
3.或者说,基于useCallback,可以始终获取第一次创建的函数的堆内存地址。
*/
const handle = useCallback(()=>{
//.....一些操作
},[])
const click = ()=>{
setNum(num+1)
}
return <Button onClick = {click}>新增</Button>
}
useCallback使用场景:
- 父组件嵌套子组件,父组件需要把一个函数基于属性传递给子组件,此时传递的这个函数,可以使用useCallback。
class Child extends React.PureComponent{
render(){
return <div>子组件</div>
}
}
const Child = React.memo(function Child(prop){
return <div>子组件</div>
})
/*
1.传递给子组件的属性,每一次需要是相同的堆内存地址,基于useCallback
2.在子组件内部也要做一个处理,验证父组件传递的属性是否发生改变,如果没有变化,则让子组件不能更新,有变化才需要更新,继承React.PureComponent即可。函数组件是基于React.memo对新老传递的属性做比较,如果不一致,才会把函数执行,如果一致,子组件不更新。
*/
const Demo =function Demo(){
let [num,setNum] = useState(10);
const handle = useCallback(()=>{
//.....一些操作
},[])
const click = ()=>{
setNum(num+1)
}
return
<>
<Child handel={handle} />
<Button onClick = {click}>新增</Button>
</>
}
React复合组件通信方案
React 样式私有化处理
1. 使用内联样式
2. 样式表
|- views <br>
|- box <br>
|- span <br>
3.CSSModules
1.把我们的样式全部写在xx.modules.css文件中,这样的文件是css文件,不能使用less,sass这样的预编译语言。
2.我们在组件中,基于ES6Module模块规范导入进来。
import style from 'xx.modules.css';
/*
style{
key(在css中写的类名):value(经过webpack编译的类名,Nav_box_c6ESF)
}
*/
3.我们编写的css样式也会被编译,所有之前的样式,也都编译为混淆后的类名了
4.我们在组件中所有元素的样式类,基于style.xxx去操作。
注意:
:global(.aa){
color:red; //不会被webpack编译
}
.list{
font-size:16px;
}
.link {
composes: list; //样式继承
color:pink;
}
//在组件中使用
return <span className={style.link}>aa</span>
4.React Jss
基本使用
import { createUseStyles} from "react-jss"
//返回结果是一个自定义hook函数
const useStyle = createUseStyles({
box:{
background:"red",
"&:hover" :{
color:"red"
}
}
item:{
'& a':{ //相当于 .item a
color:"blue"
}
}
})
动态样式
const useStyle = createUseStyles({
box:{
background: props => props.background,
"&:hover" :{
color:"red"
}
}
item:{
'& a': props =>{
return {
fontSize:props.size+'px'
}
}
}
})
const Demo =function Demo(){
let {box} = useStyle({
size:16,
background:"black"
});
return
<>
<Button className = {box}>新增</Button>
</>
}
注意:
不能直接在类组件中使用,可以通过代理组件实现。
React高阶组件
利用js中的闭包(柯理化函数)实现的组件代理,我们可以在代理组件中,经过业务逻辑的处理,获取一些信息,最后基于属性等方案,传递给我们最终要渲染的组件。
const Demo =function Demo(props){
cosnole.log(props)
return <div>demo</div>
}
const ProxyTest = function ProxyTest(Component){
return function HOC(props){
return <Component {...props}/>
}
}
export default ProxyTest(Demo);
//把函数执行的返回结果(应该是一个组件),基于ES6Module规范导出
//当前案例中,我们导出的是HOC:higher-order-components