React笔记——object

258 阅读14分钟
构建react项目

react提供create-react-app脚手架,默认把webpack的打包规则处理好了,把一些项目需要的基本文件也都创建好了。

1.create-react-app的运用

安装脚手架

  1. npm i create-react-app -g (mac前面需要设置sudo)
  2. 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混合在一起

  1. vscode如何支持jsx语法(格式化,快捷提示)

    1. 创建的js文件,我们把后缀名设置为jsx即可,这样js文件中就可以支持jsx语法了
    2. webpack打包的规则中,也是会对jsx这种文件按照js的方法进行处理。
  2. 在html中嵌入js表达式,需要基于‘{}语法’

    • js表达式:执行有结果的(变量,数学运算,三元运算符,循环:借助数组的迭代方法处理如map)
  3. 在ReactDOM.createRoot()的时候,不能直接把HTML/BODY为根容器,需要指定一个额外的盒子root

  4. 每一个构建的视图,只能有一个根节点。

    • React给我们提供了一个特殊的标签:React.Fragment空文档标记标签<></>
  5. {}胡子语法中嵌入不同的值,所呈现出来的特点

    • number/string:值是啥,就渲染什么
    • boolean/null/Symbol/BigInt/undefined:渲染内容是空
    • 不支持渲染普通对象
    • 数组对象:把数组的每一项都分别拿出来渲染(并不是变为字符串渲染,中间没有逗号)
    • 函数对象:不支持在{}中渲染,但是可以作为函数组件渲染。
    • 除数组对象外,其余对象一般都不支持在{}中渲染,但是也有特殊情况:
      • jsx虚拟dom对象
      • 给元素设置style行内样式,要求必须写成一个对象的形式
  6. 给元素设置样式

行内样式:
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的方式。传给组件!
函数组件渲染机制
  1. 基于babel-preset-react-app 把调用的组件转换为createElement格式。
  2. 把createElement方法执行,创建出一个virturalDOM对象。
  3. 基于root.render把virtualDOM转为真实的DOM。 type不再是一个字符串,而是一个函数此时: - 把函数执行 - 把virtualDOM中的props,作为实参传给函数。 - 接受函数执行的返回结果(也就是当前组件的virtualDOM对象)。 - 最后基于render把组件返回的DOM变为真实DOM,插入到root容器中 。
函数组件props
  1. 调用组件,传递进来的属性是只读的(原理是对象被冻结了)
  2. 作用:父组件调用子组件的时候,可以基于属性,把信息传递给子组件,子组件接受相应的属性值,呈现出不同的效果,让组件的复用性更强。
  3. 虽然对于传递的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);

  1. 执行useState,传递的val是初始的状态值。
  2. 执行这个方法,返回结果是一个数组:[状态值,修改状态的方法]
  3. 执行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细节处理
  1. 第一次渲染完毕后,从服务器异步获取数据
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使用场景:

  1. 父组件嵌套子组件,父组件需要把一个函数基于属性传递给子组件,此时传递的这个函数,可以使用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