React 基础语法总结(一)

1,172 阅读20分钟

render 方法的作用?

  • render 方法会负责把虚拟 DOM 变成真实 DOM 插入到容器中
  • react 元素是不可变的「是一个只读对象」,只能通过执行 render 方法,来进行对元素的更新渲染
  • react 只会更新数据改变的部分

render 更新的过程

  • 浏览器不识别,jsx 会被编译成 createElement 语法
  • 编译阶段是在 webpack 编译的时候,也就是打包的时候执行的「编译阶段不执行代码」
  • 打包后的代码在浏览器里执行的时候,会执行 React.createElement() 得到虚拟 DOM「执行时已经编译成了虚拟 DOM」,返回一个 JS 对象
  • 然后在通过 render 方法,它会负责把虚拟 DOM 变成真实 DOM 元素插入到 root 容器中

createElement 的实现步骤

  • webapck 编译之后,会生成 createElement 语法,通过编译可以拿到标签和属性
  • 参数
@param {*} type 标签名
@param {*} config 标签属性「class、id...」
@param {*} children 子标签
  • 实现步骤
      1. 判断 参数的个数是否大于 3
      • 大于 3,则子标签不止一个,需要把子标签转换为数组赋值给 children
      if( arguments.legnth > 3 ) {
        children = Array.prototype.slice.call(arguments, 2)
      }
      
      • 小于 3,直接赋值给 children「无需处理」:props.children = children;
      1. 返回 type 和 props 属性
        return {
            type,
            props
        }
    

key 属性的作用

-可以区分子标签,方便更新,提高效率

  • key 不能重复,必须是唯一的
  • 不唯一的 key 会导出重复更新或者是忽略更新
    • 在源码中有一个
        childrenmap = {
            key值:标签
        }
    
    • 一个 key 值对应一个元素,如果 key 值相同,在 childrenmap 中会被覆盖
    • 当更新的时候就会忽略掉重复的 key 元素,只保留最后一个元素
  • 当 DOM diff 比较的时候:
    • 如果没有 key ,那么就会根据索引来比较,性能非常差
    • 如果有唯一 key 值,拿到新值之后会根据 key 来跟旧的做比较,有的话进行位置的移动,没有则会创建并插入

React 如何更新?

  • 把旧的都删除,在插入全部的新元素「性能比较差」
  • react 会把老的虚拟 DOM 和新的虚拟 DOM 进行比较,这个也就是所谓的 DOM diff
    • 用最小的代码进行更新
    • 比较过程:进行对元素的移动,不会进行元素的删除和创建
    • 让 react 更能友好的进行比较,所以给每个元素加个唯一标签,通过 key 值确定每一个元素,从而进行比较,提高了性能

什么是 React 组件?

  • 可以将 UI 分成一些独立的、可复用的部件,这样你就只需要专注以构建每一个单独的部件
  • 组件从概念上类似于 JS 函数,他接受任意的参数「props」,并且返回用于描述页面展示内容的 react 元素

React 组件的分类?

函数式组件

  • function 组件,称为静态组件「函数式组件」
  • 函数组件接收一个单一的 props 对象并返回了一个 react 元素「虚拟 DOM 对象」
    • react 元素不仅可以是 DOM 标签,还可以是用户自定义的组件
      • 自定义组件的名称必须是首字母大写的
      • 原生组件小写开头,自定义组件大写字母开头的
      • 组件必须使用前先定义
      • 组件需要返回并且只能返回一个根元素
        • 根元素可以使用一个空标签
        • 也可以用一个小括号(),代表里面的内容是一个整体
      • 组件之间可以嵌套

函数式组件的更新

  • 虚拟 DOM 类型是自定义函数组件
  • 函数执行,把 props 参数传进去
  • 拿到返回值,也就是虚拟 DOM
  • 把虚拟 DOM 变成真实 DOM,然后处理属性...

类(定义的)组件

  • class 组件,称为动态组件「类组件」
  • React.Component 是 react 的一个内置组件
  • 编写 class 组件的时候,必须继承 react 的内置组件;
  • 每一个类组件都有一个 render 函数
  • 可以在构造函数里,并且只能在构造函数中给 this.state 赋值
  • 定义状态对象:this.state而不是一个方法名称
  • 状态对象可以存储属性和值
  • 状态更新,会引起组件的刷新
  • 改变状态需要使用 setState() 方法
  • 属性对象「props」:父组件给的,不能改变「只读的对象」

实现类组件渲染的过程

  • 解构类的定义和类的属性对象:let { type, props } = vdom;
  1. 创建类组件的实例:let classInstance = new type(props)
  2. 调用实例的 render() 方法,返回要渲染的虚拟 DOM:let renderVdom = classInstance.render();
  3. 根据虚拟 DOM 对象创建真实 DOM 对象:let dom = createDOM( renderVdom )
  4. 为了以后类组件的更新,把真实 DOM 挂载到了类的实例上:classInstance.dom = dom;
  5. 返回 dom:return dom;

实现类组件的更新

setState( newState ,function(){...})

  • 参数:
    • newState: 只传递需要修改的值
    • 回调函数:可以拿到更新后的新值
  • React 中更新视图,需要调用 setState 这个方法「react 提供的一个方法」
  • 不要在 render 中触发 setState ,会造成一个死循环
    • setState 触发,会执行 render 函数,render 中又触发 setState ... 无限循环,形成死递归
  • 在 react 中,事件的更新可能是异步的,是批量的,但不是同步的
  • 调用 state 之后状态并没有立刻更新,而是先缓存起来了
  • 等事件函数处理完成后,在进行批量更新,一起更新并重新渲染
    • 因为 jsx 事件处理函数是 react 控制的,只要归 react 控制就是批量,只要不归 react 管了,就是非批量

  • 实现类组件的更新过程
    1. 新状态和旧状态进行合并
    老的状态:let state = this.state;
    新老状态合并:this.state = {...state, ...newState};
    
    1. 调用子类的 render 方法,获取新的 state 的虚拟 DOM:
        let newVdom = this.render()
    
    1. 更新:updateClassComponent(this, newVodm)
    2. 实现更新方法
    function updateClassComponent( 类的名字,新虚拟 DOM ) {}
    
    • 类组件渲染时,把真实 DOM 挂载到了类上,所以通过类可以拿到老的真实 DOM:
    let dom = classInstance.dom
    
    • 执行 createDOM 方法把新的虚拟 DOM 变成真实 DOM:
    let newDOM = createDOM(newVdom);
    
    • 拿到新的真实 DOM 后,获取到旧 DOM 的父节点,把它的儿子节点替换成新的 DOM:
    oldDOM.parentNode.replaceChild( newDOM, oldDOM )
    
    • 最后再把新的 DOM 挂载到类上:
    classInstance.dom = newDOM;
    

判断是类组件还是函数组件的方法

  • 类组件中有 isReactComponent 属性,通过 isReactComponent 属性来判断,是否为类组件:值为 true,那么就是类组件

如何理解 setState?

  • react 数据更新页面不会渲染,需要通过 setState 来触发 render 函数来进行页面的更新渲染
  • setState 接收两个参数,第一个参数是传递的新数据,第二参数是一个回调函数,函数体中可以拿到更新后的状态
  • setState 大部分情况下是一个异步操作,执行 setState 并不会立即去更新数据
  • 如果多次调用 setState,react 内部会把所有需要更新的状态缓存的到数组中,批量更新
  • 批量更新的实现:循环数组,一次进行一个新状态替换旧状态的方式,来进行状态的更新,最后执行 render 方法,进行一个更新操作

Fragment

  • react 提供的一个组件,作为容器使用,不会在页面上进行渲染;
  • 一个组件返回多个子元素列表,Fragments 允许将子列表分组,不需要向 DOM 添加节点

React 中如何实现组件通信

  • 父子通信:props「属性」、ref「绑定 DOM」
  • 子父通信: props「方法」
  • 兄弟通信:状态提升、context、全局状态管理

状态提升

将 state 放在公有的父元素上,结合父子与子父实现通信

类组件中的上下文 context

  • 跨组件通信
    • 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树
    • 父组件设置了 context 之后,所有后代组件都可以拿到数据
  • 使用方式 1:React.createContext+myContext.Provider+contextProp
    • 创建上下文对象 myContext:React.createContext();
    • 使用 myContext.Provider 组件把父组件包裹起来,value 就是要传递给后代的属性:<myContext.Provider value={...}></myContext.Provider>
    • 在子组件中接收上下文对象 myContext:static contextProp = myContext;
    • 获取属性:this.context
  • 使用方式 2:childContextTypes+getChildContext+contextTypes
    • 引入propTypes:prop-types 第三方包
    • 父创建一个静态属性:static childContextTypes = {}
    • 给这个静态属性中规定后代所使用的属性类型限制「需要借助第三方包:prop-types」:static childContextTypes = {name「给子组件的属性名」:propTypes.string「规定属性类型」}
    • 设置给后代的属性,及属性值:getChildContext() {return:{name: xxx}}
    • 子组件中必须要接收,声明一下:static contextTypes = {name: propTypes.string}「想用什么属性在这里接收什么属性」
    • 然后在子组件中就可以使用:console.log( this.context )

在嵌套的子组件里修改上下文数据

  • Consumer:上下文对象的 api
  • 用 Consumer 组件包裹要渲染的组件
  • 函数的参数就是在父组件提供的数据
  • 在函数里返回要渲染的 jsx

全局状态管理

react 的生命周期

  • react 的生命周期的顺序:从外到里渲染,子组件比父组件优先挂载
  • 新增的生命周期和要废弃的不能同时使用
  • 初始化:第一个钩子函数
    • constructor(初始化)
  • 挂载
    • static getDerivedStateFromProps(props, state)
      • props:父组件传过来的数据
      • state:子组件中的私有属性
      • 在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用,会重新赋值,它返回一个对象来更新 state,如果返回 null 则不更新任何内容
    • componentWillMount(组件将要更新)「已废弃」
      • getDerivedStateFromProps 和 componentWillMount两个钩子不共存
    • componentDidMount(组件挂载完成)
      • react当中的AJAX请求 一般都是在这个钩子进行的
      • 只加载一次「不刷新页面」
  • 更新
    • componentWillReceiveProps(nextProps)
      • 数据变化的时候,会触发 componentWillReceiveProps 钩子,可以获取到将要更新的值
    • shouldComponentUpdate(nextProps, nextState)
      • 询问组件是否要更新
      • 参数:nextProps: 判断地址是否相同,然后在判断内容是否一样
      • 返回值:false 不更新,true 更新
      • 可以用来优化的一个钩子函数
    • componentWillUpdate()
      • 组件将要更新
      • 已废弃
    • componentDidUpdate()
      • 组件更新完成
      • 此钩子函数中不能调用 setState ,会造成死循环
  • 销毁
    • componentWillUnmount
      • 组件将要销毁
      • 一般用来清除定时器,或者是移除原生事件
    • componentDidCatch(error, info)
      • 捕获错误信息

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate -------以上在 17 版本中废除
  • static getDereviedStateFromProps:数据变化的时候触发
    • 必须加 static,是一个静态属性
    • 不能和要废弃的生命周期一起用
    • 必须有返回值,没有返回结果,返回 null
    • 执行顺序:初始化之后,渲染之前
    • 在数据改变之后也会执行「props/state」
    • 生命周期内没有 this ,不能获取当前组件对象
    • 参数:props/state,两个都是更新后的值
    • 返回值是 props 和 state 合并之后新的 state 对象
    • 返回值如果是 null,按照参数中的数据进行更新界面
    • 返回值是一个对象,按照返回值来更新界面
  • getSnapshopBeforeUpdate:更新前触发
    • 必须有返回值,没有返回结果,返回 null
    • 不能和要废弃的生命周期一起用
    • 必须和 DidUpdate 一起使用
    • 生明周期内可以使用 this,this 中存储的是修改后的数据
    • 参数中存储的是修改前的数据
    • return 的值会作为componentDidupdate第三个参数出现
    • 更新后可以拿到更新之前的数据
    • 页面跳转,返回上一个页面中的历史记录 -------------以上在 17 版本中新增的

react fiber 的了解

  • react 15 版本之前渲染方式是同步渲染,导致页面卡顿
  • react 16 版本之后渲染核心是引入了 fiber,实现异步渲染
    • fiber 就是将一个时间超长的任务分段,一段一段的执行,更新前阶段可以被其他优先级高的任务打断,更新后则不能,所以,AJAX 请求不能放在 componentWillMount 阶段,会出现重复请求的问题

fiber 优缺点?

  • 弊端:会导致更新之前的生命周期可能会被其他优先级高的任务打断,打断之后生命周期会重新执行,所以,我们一般不会再更新前的生命周期中做一些网络请求等 组件的渲染分成两个阶段:
  • 更新前:可以被其他优先级高的任务打断,render 之前的都可以被打断
  • 所以,AJAX 请求不能放在 componentWillMount 阶段,会出现重复请求的问题
    • componentWillMount
    • componentWillReceiveProps
    • componentWillUpdate -------以上在 17 版本中废除
    • static getDereviedStateFromProps:数据变化的时候触发
    • getSnapshopBeforeUpdate:更新前触发 -------以上在 17 版本中新增的
    • shouldComponentUpdate
  • 更新后:不会被打断,render 之后的生命周期都是第二阶段

常用的声明周期

  • constructor:初始化数据
  • componentDidMount:网络请求
  • componentWillUnmount:
  • shouldComponentUpdate:控制 render 函数是否执行

react 优化

  • shouldComponentUpdate:控制组件是否重新 render
  • PureComponent

路由

  • hash 路由和历史路由的区别
  • 路由实现的原理
    • 监听地址栏的改变
    • 根据地址栏的改变渲染组件
  • 在线上 hash 路由和历史路由的区别
    • 刷新之后会 404 「需要服务器端修改配置」
  • 重定向

全局状态管理

  • redux
  • react-redux
  • 异步中间件
  • 用途:
    • 多组件共享状态
    • 一个组件发生改变其他组件跟着变
  • 怎么用的?过程是怎么样的?
    • 全局创建状态管理对象 store
    • 创建 reducer:本质是一个函数,接收修改前的数据和 actions 参数
    • 在函数的内部根据 action 对修改前的数据进行修改,返回新数据
    • 将 store 对象和 reducer 进行关联
    • 在页面上通过 store 和 getState 获取全局状态值
    • 用户在组件里触发 actionsCreator 的方法,创建 action 通过 dispatch 发送给 reducer
    • 在需要更新的组件中做 subscribe 监听,控制组件重新渲染

diff 算法

数据发生改变,引起虚拟 dom 改变,对比修改前后的虚拟DOM,找出差异点,根据差异点更新真实 DOM

什么是纯函数?

输出只由输入决定

使用全局状态管理的时候,需要注意什么

  • 在 reducer 中不能修改原数据,可能会导致页面不更新
  • 解决方式:深拷贝「JSON.stringify/JSON.parse、递归、immutable 插件」、浅拷贝「Object.assign()」

全局状态管理的时候如何处理异步问题

异步中间件:redux-thunk

全局状态管理中将网络请求放在哪里

  • 组件中
  • actions:
    • 便于统一管理请求,减少冗余代码
    • 可以准确的检测到数据的更改时间

受控组件和非受控组件

  • 受控组件:原生的输入组件它的值受状态控制
  • 非受控组件:原生的输入组件的值不受状态控制

高阶组件

  • 高阶组件就是一个函数,传给它一个组件,它返回一个新的组件
  • 高阶组件的作用就是为了组件之间的代码复用
  • 被高阶组件处理过的组件,一般在 props 中获取数据
  • 应用场景:
    • 属性代理:经过高阶组件处理过的属性
    • 反向继承:
      • 场景:自定义组件中的文本,可以进行内容的扩展
      • 在高阶组件中,一般来说,子类继承父类,先执行父类的方法,在执行子类的方法。
      • 通过 super 可以调用父类的方法,从而实现反向继承
      • 克隆:React.cloneElement(目标元素, {新属性}, {儿子})
  • @装饰器可以用在高阶组件中,直接拿到最内部的组件执行结果

hoc 及如何做代码优化?都用到哪些地方了?

可以通过高阶组件封装公有代码,路由拦截器、路由懒加载、react-loadable、From.create、withRouter

shouldComponentUpdate

  • 当一个组件的 props 或者 state 变更,React 会将最新返回的元素与之前渲染的元素进行对比,以此决定是否有必要更新真实的 DOM,当他们不相同时 React 会更新该 DOM
  • 如果渲染的组件非常多时,可以通过覆盖生命周期方法 shouldComponentUpdate 来进行优化
  • shouldComponentUpdate 方法会在重新渲染前被触发,默认返回 true,可以在 shouldComponentUpdate 中返回 false 来跳过整个渲染过程,包括该组件的 render 及后续操作

PureComponent

  • 默认情况下,只要改了状态,那么所有的组件,不管它的属性变没变,都要更新
  • PureComponent:纯组件,当组件接收的参数没有发生任何改变的时候,当前组件不会再去执行 render 「类似于:静态组件中的 memo 函数」
  • PureComponent 只会进行一个浅层的比较:如果是对象的话,只看地址有没有发生改变,不看内容有没有发生变化

React Hooks

函数组件更新数据

静态组件更新数据:useState

var [count, setCount] = useState(初始值);

  • count:对应的初始值数据
  • setCount:用来更新数据的函数,形参是数据的新值
  • 不能再 if 或者 for 循环中使用
  • 不会自动合并没用到的属性及值;
    • 所以,需要我们自己手动合并「使用扩展运算符:...」:setState({...state, count: state.count+1});
  • 如果执行的函数体内是异步操作,那么我们每次执行每次执行都会形成一个闭包「变量被保护及保存起来了」,state 对应的值就是执行时的那个 state 值,不一定就是最新的 state 值
  • 通过给 setState 传一个回调函数的方式来获取新的 state 值
function minus() { 
    setTimeout(()=>{
        setState((state)=>{
            console.log(state);
            return {count: state.count+1}
        })
    }, 5000)

    // 同步操作没问题
    console.log(state.count)
}
  • 惰性初始化 state:初始化的操作只会在第一次执行:更新的时候就不在执行当前的初始化操作
let [state, setState] = useState(function () { 
    console.log('初始状态')
    return {count: 600}
});

静态组件更新函数:useCallback

  • useCallback( function(){}, [] );
  • 参数 1:第一个参数是要缓存的函数
  • 参数 2:执行函数时,需要的依赖;只有当依赖项发现变化才回去执行函数
  • 第二个参数不传,每次执行的函数体都是一样的,所以不会每次都要执行

useReducer:useState 的替代方案

  • useState 的替代方案。它接收一个:(state, action)=>newState 的 reducer,并返回当前的 state 以及配套的 dispatch 方法
  • 在某些场景下,useReducer 会比 useState 更适用,比如:state 逻辑比较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等
  • reducer 处理器,可以接收一个老状态,返回一个新状态
let [ state, dispatch ] = useReducer( (state, action)=>{
    switch(action.type) {
        case 'add':
            return {...state, number: n+1}
        default: 
            return state;
    }
}, {count: 100, name: "guyal_"} );

dispatch({ type: "add", n: state.count + 1 })
  • 参数:
    • state:更新之前的旧数据,任意类型的状态
    • action:传给 dispatch 的对象,动作,是一个对象,包含:type:类型「一般是字符串」,其他类型...
    • dispatch:派发函数
      • 是用来修改 state 的,通过让参数 1 执行的方式来修改 state
    • react 会使用 参数 1 函数执行的返回结果,来顶替老的 state

useContext:函数组件中的上下文

  • 创建上下文对象 myContext:React.createContext();
    • 使用 myContext.Provider 组件把父组件包裹起来,value 就是要传递给后代的属性:<myContext.Provider value={...}></myContext.Provider>
    • 在子组件中接收上下文对象 myContext:let { context } = useContext(myContext);

函数组件实现生命周期

useEffect:函数组件实现数据的加载及更新

useEffect(()=>{
    //页面挂载触发:比如,添加定时器
    return ()=>{
    //组件销毁触发:比如,清除定时器
    }
},[依赖项])	
  • 参数
    • 参数 1:回调函数,在初次加载完成和更新完成之后触发
      • 可以理解为是类组件的 componentDidMount 和 componentDidUpdate 的合成体
      • 加载和更新都会执行这个回调函数
      • 组件销毁的时候触发:回调函数中 return 的回调函数相当于销毁的生命周期
    • 参数 2:数组,放入的是依赖,只有依赖项发生改变的时候,才会执行回调函数
      • 第二个参数为空数组:只执行一次,相当于类组件的 componentDidMount
  • 副作用:相对于纯函数来说,影响当前作用域之外的作用域,那么这就是副作用
  • 纯函数:
    • 相同的输入会产生相同的输出
    • 不能修改本函数作用域之外的变量

useLayoutEffect + useRef

  • useEffect 不会阻塞浏览器渲染,而 useLayoutEffect 会阻塞浏览器渲染
  • useEffect 会在浏览器渲染结束绘制完成后执行,所以绘制的时候,没有移动
  • useLayoutEffect 则是在 DOM 更新完成后,浏览器绘制之前执行

缓存 Hook

memo:缓存数据

  • memo 处理过的组件 若传给组件的数据没有发生改变 那么组件就不会重新执行
  • 一般用来缓存值类型
  • memo 进行的是一个浅比较,对象和函数是无法使用 memo 来实现缓存的
Child = memo(Child);// 添加需要缓存的组件:Child 组件

useMemo:缓存对象类型

let data = useMemo(()=>{},[依赖项])

  • useMemo 一般用来缓存对象类型
  • 只有依赖项改变的时候 data 才会被给成新的地址
  • useMemo 需要结合 memo 函数进行优化
let data = useMemo(()=>{
  return {
    age:1000,
    name:name
  }
},[name]) // [依赖]

useCallback:缓存函数类型

let minus = useCallback(函数体,[依赖项]);

  • useMemo 一般用来缓存函数使用
  • 只有依赖发生改变的时候 minus才会被赋予一个新的函数地址
  • useCallback 需要结合 memo 函数进行优化
let minus = useCallback(()=>{
	setCount(--count)
},[name])

ref

createRef

  • 使用方式:
    • 创建私有属性:xxx=React.createRef()
  • 获取方式:this.xxx.current

pDom = React.createRef()

  • ref 只能用类组件,函数组件中没有 this
  • ref.current = 类的实例
  • ref 属性用于 HTML 元素,构造函数中使用 React.createRef() 创建的 ref 来接收 DOM 元素:<p ref='pDom'></p>
    • 通过 this.ref.pDom 来获取 p 标签
  • ref 属性值是一个箭头函数:ref = {(el)=>{this.xxx=el}}
  • 如果需要处理逻辑,那么就用函数
    • el 是 react 帮我们传递的实参
    • xxx 就是对应的组件

forwardRef

  • 函数组件可以使用 forwardRef 转发一下,ref 就可以使用了
  • forwardRef 将 ref 从父组件中转发到子组件中的 dom 元素上,子组件接收 props 和 ref 作为参数
  • useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值
  • 获取子组件中的某个元素
function Child(props:any, ref:any) {
  return (<>
    <h1 ref={ref}>子组件</h1>
    </>)
}

const Children1 = React.forwardRef(Child)

class App extends React.Component {
  createRef = React.createRef();
  render() {
    console.log(this.createRef);// 子组件中的 h1 
    return (
      <>
        <Children1 ref={this.createRef} />
      </>
    )
  }F
}

获取子组件中某个元素的步骤**:

  • 我们通过调用 React.createRef 创建了一个 React ref 并将其赋值给 createRef 变量。
  • 我们通过指定 createRef 为 JSX 属性,将其向下传递给 <Children1 ref={createRef}>
  • React 传递 createRef 给 Children1 内函数 (props, ref) => ...,作为其第二个参数。
  • 我们向下转发该 ref 参数到 <h1 ref={ref}>,将其指定为 JSX 属性。
  • 当 ref 挂载完成,ref.current 将指向 h1 DOM 节点。

自定义 Hook

  • 自定义 hooks,只要说一个函数以 use 开头,并且里面调用了别的 Hooks

性能优化

  • 渲染了长列表(上百甚至上千的数据),我们推荐使用“虚拟滚动”技术。这项技术会在有限的时间内仅渲染有限的内容,并奇迹般地降低重新渲染组件消耗的时间,以及创建 DOM 节点的数量。
  • React 优化最重要的策略就是减少组件的刷新,组件属性不变的情况下,不进行 render
    • 类组件:PureComponent
    • 函数组件:useCallback