itv_react

345 阅读24分钟

React18新增了那些特性

  • 允许组件渲染undefined

  • 自动批处理: 关闭自动批处理React.flushSync(() => { setState({xx: xx})})

    • 批处理是 React 在幕后所做的事情,许多开发人员都没有意识到这一点。当您注意您的开发者控制台时,您会意识到,如果您在同一个事件处理函数中运行一系列的状态更新,即使您有两个状态更新,React 也只会重新渲染 UI 一次。这意味着 React 将它们批量(或分组)在一起,因此与其不断重复状态更新并重新渲染 UI 的步骤,它会同时执行状态更新并只渲染 UI 一次。
    • 批处理是一种很好的机制,可以保护我们免受不必要的 UI 重新渲染,但 React 17 仅在事件处理函数中实现。而 Promise、异步代码(setTimeout等)或者原生事件处理函数的使用会打破这种行为(在这些场景中批处理会失效)。在 React 18 中,自动批处理会在原生事件处理函数、Promise 链和异步代码自动完成。
  • 新增Api startTransition 非紧急更新 接收的回调中的更新都会被认为是非紧急处理, 避免任务长时间执行阻塞UI渲染。

    • 以前,React 有一个非常重要的规则:任何东西都不能干扰渲染。一旦组件状态改变并触发了重新渲染,就无法阻止它,直到组件重新渲染,页面才能响应(交互事件)。对于新的更新,每个状态更新都被归类为以下两类之一:要么是紧急更新 (urgent update),要么是过渡更新(transition update,简称为 transition)。
    • 紧急更新是用户直觉上期望在心跳中响应的操作,例如鼠标单击或按键。过渡更新是一些延迟可以接受的并且在很多时候都可以预期的操作,例如搜索查询。startTransition API 用来将内部的 setState 调用标记为transitions,这意味着他们可中断的
    • 在React 18以前的版本以及React 18默认情况下(为了向前兼容),所有的更新都会认为是紧急更新。而startTransition提供api给用户来手动将某些更新标记为非紧急更新,从而避免浪费时间去渲染不必要的内容
     import React, { useState, useEffect, useTransition } from 'react';
    
     const App: React.FC = () => {
       const [list, setList] = useState<any[]>([]);
       const [isPending, startTransition] = useTransition();
       useEffect(() => {
         // 使用了并发特性,开启并发更新
         startTransition(() => {
           setList(new Array(10000).fill(null));
         });
       }, []);
       return (
         <>
           {list.map((_, i) => (
             <div key={i}>{i}</div>
           ))}
         </>
       );
     };
    
     export default App;
    
  • useDeferredValue 允许变量延迟更新, React 将尝试尽快更新延迟值。如果在给定的 timeout 期限内未能完成,它将强制更新

    import { useState, useDeferredValue } from "react";

    export default function App() {
      let [value, setValue] = useState(0);
      const deferredValue = useDeferredValue(value, { timeoutMs: 2000 });

      return (
        <div className="App">
          <div>{deferredValue}</div>
          <button onClick={()=>{setValue(deferredValue+1)}}>click me</button>
        </div>
      );
    }
  • 未捕获的 Suspense
    • 在 React 17 中,如果一个组件尝试异步加载,例如一个组件通过 React.lazy 加载,它会在它上面找到最近的 Suspense 边界,并渲染 fallback UI ,直到组件被加载完成。然而,如果它的上面没有 Suspense 边界,则会抛出一个错误。在之前的版本中,如果一个 Suspense 边界没有传递 fallback prop ,或者传递的 fallback 是 nullundefined ,整个 Suspense 会被忽略,并使用下一个 Suspense 边界。如果没有更多边界,则会抛出一个错误。在 React 18 中,即使 fallback 传递 nullundefined ,也会使用这个 Suspense 边界,并且不渲染任何东西
参考文章

react 多次setstate输出结果

React18+

class组件

  // react 18版本 异步函数,setTimeout,原生事件中也会自动批处理
  private handleClick = () => {
    // this.setState({ count: this.state.count + 1 });
    // this.setState({ count: this.state.count + 1 });
    // this.setState({ count: this.state.count + 1 });
    // console.log(this.state.count); // count === 0,  页面count是 1

    // this.setState((prev) => ({ count: prev.count + 1 }));
    // this.setState((prev) => ({ count: prev.count + 1 }));
    // this.setState((prev) => ({ count: prev.count + 1 }));
    // console.log(this.state.count); // count === 0, 页面count是 3

    // setTimeout(() => {
    //   this.setState({ count: this.state.count + 1 });
    //   this.setState({ count: this.state.count + 1 });
    //   this.setState({ count: this.state.count + 1 });
    //   console.log(this.state.count, '内部'); // 1s后 输出内部count 0 页面count是1
    // }, 1000);

    // console.log(this.state.count, '外部'); // 立马输出外部count 0

    setTimeout(() => {
      this.setState((prev) => ({ count: prev.count + 1 }));
      this.setState((prev) => ({ count: prev.count + 1 }));
      this.setState((prev) => ({ count: prev.count + 1 }));
      console.log(this.state.count, '内部'); // 1s后 输出内部count 0 页面count是3
    }, 1000);
  };

  public render() {
    return (
      <div>
        <Button type='primary' onClick={this.handleClick}>点击</Button>
        <p>{this.state.count}</p>
      </div>
    );
  }

hooks组件

  const handleClick = () => {
    // setCount(count + 1);
    // setCount(count + 1);
    // setCount(count + 1);
    // console.log(count); // count === 0, 页面count是 1

    // setCount(prev => prev + 1);
    // setCount(prev => prev + 1);
    // setCount(prev => prev + 1);
    // console.log(count); // count === 0, 页面count是 3

    // setTimeout(() => {
    //   setCount(count + 1);
    //   setCount(count + 1);
    //   setCount(count + 1);
    //   console.log(count); // count === 0, 页面count是 1
    // }, 1000);

    setTimeout(() => {
      setCount(prev => prev + 1);
      setCount(prev => prev + 1);
      setCount(prev => prev + 1);
      console.log(count); // count === 0, 页面count是 3
    }, 1000);
  };
React18以下

class组件

  // react 18以下版本
  private handleClick = () => {
    // this.setState({ count: this.state.count + 1 });
    // this.setState({ count: this.state.count + 1 });
    // this.setState({ count: this.state.count + 1 });
    // console.log(this.state.count); // count === 0, 页面count是 1

    // this.setState((prev) => ({ count: prev.count + 1 }));
    // this.setState((prev) => ({ count: prev.count + 1 }));
    // this.setState((prev) => ({ count: prev.count + 1 }));
    // console.log(this.state.count); // count === 0, 页面count是 3

    setTimeout(() => {
      this.setState({ count: this.state.count + 1 });
      this.setState({ count: this.state.count + 1 });
      this.setState({ count: this.state.count + 1 });
      console.log(this.state.count, '内部'); // 1s后 输出内部count 3, 页面count 是3
    }, 1000);
  };

  public render() {
    return (
      <div>
        <Button type='primary' onClick={this.handleClick}>点击</Button>
        <p>{this.state.count}</p>
      </div>
    );
  }

hooks组件

  const handleClick = () => {
    // setCount(count + 1);
    // setCount(count + 1);
    // setCount(count + 1);
    // console.log(count); // count === 0, 页面count是 1

    // setCount(prev => prev + 1);
    // setCount(prev => prev + 1);
    // setCount(prev => prev + 1);
    // console.log(count); // count === 0, 页面count是 3

    // setTimeout(() => {
    //   setCount(count + 1);
    //   setCount(count + 1);
    //   setCount(count + 1);
    //   console.log(count); // count === 0, 页面count是 1
    // }, 1000);

    setTimeout(() => {
      setCount(prev => prev + 1);
      setCount(prev => prev + 1);
      setCount(prev => prev + 1);
      console.log(count); // count === 0, 页面count是 3
    }, 1000);
  };

什么是虚拟DOM,怎么构成的

虚拟DOM是真实DOM在内存中的表示,简单来说,虚拟DOM就是个对象,由tag,props,children构成

  <div id="app">
    <p class="text">hello world!!!</p>
  </div>

  // 可转化成下面的虚拟DOM表示
  {
    tag: 'div',
    props: {
      id: 'app'
    },
    chidren: [
      {
        tag: 'p',
        props: {
          className: 'text'
        },
        chidren: [
          'hello world!!!'
        ]
      }
    ]
  }

上面对象就是我们说的虚拟DOM,可表示成树形结构,原生DOM浏览器厂商需要实现众多的规范(各种 HTML5 属性、DOM事件,即使创建一个空的 div 也要付出昂贵的代价。虚拟 DOM 提升性能的点在于 DOM 发生变化的时候,通过 diff 算法比对 JavaScript 原生对象,计算出需要变更的 DOM,然后只对变化的DOM进行操作,从而更新整个视图

react

主流的虚拟DOM库都有一个h函数,用于将虚拟DOM,转化为真实DOM, react通过babel把jsx转换成h函数形式,即: react_createElement函数, 最后调用render函数将虚拟DOM插入htmlDOM树中,渲染页面。 示例代码通过babel编译jsx转换成react可处理的结构


  const Page = () => {
    return <div 
            className="levi"
            style={{margin: 100}} 
            onClick={onClick}
            >hello world</div>
    }

  export default Page

  // babel编译后
  const Page = () => {
    return /*#__PURE__*/_react.default.createElement("div", {
      className: "levi",
      style: {
        margin: '100px'
      },
      onClick: onClick
    }, "hello world");
  };

  var _default = Page;

谈谈你对DIFF算法理解

顾名思义:就是比较新老VDOM变化,把变化的部分更新到视图上
diff的优化策略

  • 针对于tree diff优化,react会忽略DOM节点跨层级的移动操作
  • 针对于组件 diff优化,如果是不同的组件就会生成不同的组件,所以如果是不同的组件,就会被标记要修改,而不再细致的比较
  • 针对于同一层级的一组子节点,可以通过对应的key进行区别
tree diff

针对于dom跨层级移动操作,diff算法会对相同层级的节点比较,如果某个节点不存在了,那么这个节点和其子节点是不会再有比较的,react只会考虑同层级的节点的位置变化,而对于不同层级的节点,只有创建和删除。如果某个节点下发现某个子节点A消失了,就会直接销毁子节点A。

component diff

如果是同一类型的组件,则会比较虚拟dom树,如果不是同一类型,那么这个组件将被标记为dirty组件,从而替换整个组件下的所有子节点。如果是同一类型的组件,可能存在虚拟dom没有任何变化,因此react通过shouldComponentUpdate来判断这个组件是否需要进行diff运算, 如下图,一旦react判断D和G是不同类型的组件,就不会比较两者的结构,而是直接删除componentD,重新创建组件G及其子节点

element diff

当节点处于同一级时,diff提供三种节点操作:插入,移动,删除, 如下图,老集合节点顺序为:a,b,c,d,新节点的顺序为:b,a,d,c。如果是传统的diff操作,那么就会对比老A和新B,发现B!=A于是创建并插入B至最新的集合中,删除老集合中的A,以此类推。

React提出,通过key来判断同一层级的同组子节点。如果是相同的节点,只需要移动位置即可 react主要用key来区分组件,相同的key表示同一个组件,react不会重新销毁创建组件实例,只可能更新。如果key不同,react会销毁已有的组件实例,重新创建组件新的实例

生命周期有哪些

react > 16.3版本最新的生命周期,

移除了3个生命周期

  • componentWillMount
  • componentWillReciveProps
  • componentWillUpdate

新增了2个生命周期

  • static getDerivedStateFromProps
  • getSnapshotBeforeUpdate

我们将react生命周期化为为三个阶段,分别是

  • 挂载阶段 Mouting
  • 更新阶段 Update
  • 卸载阶段 unMount
挂载阶段

组件初始化阶段,这个阶段只会执行一次,有以下生命周期方法,顺序如下

  • constructor
  • staic getDerivedStateFromProps
  • render
  • componentDidMount

constructor 构造函数,初始化阶段被执行,如果使用了构造函数,需要在内部执行super(props),不然会报错, 在构造函数常做以下2件事情

  • 初始化state对象
  • 自定义事件(合成事件)绑定this
  class Levi extends React.Component{
    constructor(props) {
      super(props);
      
      this.handleClick = this.handleClick.bind(this);
    }
  }

getDerivedStateFromProps react实例的静态方法,不能在该方法中使用this,接受2个参数: 即使 getDerivedStateFromProps(nextProps, prevState) 这个函数会返回一个对象用于更新当前state,如果返回null,则不更新state

初始化组件数据时, 我们有时需要将组件接收的参数 props 中的数据添加到它的 state 中, 期望组件响应 props 的变化。 同时组件接收的 props 数据是只读的, 不可变的, 禁止修改的. 当组件接收了新的 props 时, constructor 函数中数据初始化行为并不会再次发生. 于是我们想要在 getDerivedStateFromProps 生命周期函数中获取新的 props 并且调用 setState() 来更新数据

  class Levi extends React.Componet {
    state: {
      age: 20
    }

    static getDerivedStateFromProps(nextProps, prevState) {
      if (nextProps.currentRow !== prevState.lastRow) {
        return {
            lastRow: nextProps.currentRow
        }
      }

      return null
    }
  }

render react最核心方法,一个组件中必须有这个方法,是一个纯函数,返回需要被渲染的元素,不要这个方法包含业务逻辑,例如数据请求,可能会造成重复渲染,页面卡死

componentDidMount 组件装修后调用,常用于数据请求,获取并操作DOM。可以写一些订阅操作,但是记得在componentWillUnMount 取消订阅

更新阶段

组件props、state发生改变,会触发该阶段

  • getDevrvedStateFromProps
  • shouldCompoentUpdate
  • render
  • getSnapShotBeforeUpdate
  • componentDidUpdate

getDevrvedStateFromProps 同上

shouldCompnentUpdate 接受2个函数 为 shouldCompnentUpdate(nextProps, nextState)会返回一个boolean, true表示执行更新,页面渲染, false表示页面不会渲染, 默认返回true 注意当我们调用forceUpdate并不会触发此方法。不建议手工编写shouldCompnentUpdate逻辑,除非牵扯到复杂的深度比较。shouldComponentUpdate默认浅层比较props,state, 可在方法内部自定义比较逻辑>,官方推荐使用 React.PureComponent<浅层比较props,state>

render 同上

getSnopShotBeforeUpdate 这个方法在render之后、componentDidUpdate之前,它使您的组件可以在可能更改之前从DOM捕获一些信息(例如滚动位置。 有2个参数: getSnapShotBeforeUpdate(prevProps, prevState), 函数会返回一个值,会作为第三个参数传递给componentDidUpdate. 如果没有返回值,请返回null,不然控制台会有警告,并且componentDidUpdate不能缺少,不然也会警告。


    // 限制聊天气泡框scrollTop: https://kinyaying.github.io/webpack-optimize/dist/#/snapshotSampleOptimize
  class Levi extends React.Componet {
    state =  {
       newList: []
    }
    
    leviRef = React.crateRef();
         
    getSnapshotBeforeUpdate(prevProps, prevState) {
      if (prevProps.list.length < this.state.newList.length) {
        return this.leviRef.scrollHeight;
      }

      return null;
    }

    componentDidUpdate(prevProps, prevState, perScrollHeight) {
      if(!snopShot) {
        console.log('这里是getSnapShotBeforeUpdate 拿到返回值')
        
        const curScrollTop= this.leviRef.scrollTop;
        if (curScrollTop < 5) return ;
        this.leviRef.scrollTop = curScrollTop + (this.rootNode.scrollHeight  - perScrollHeight);   //加上增加的div高度,就相当于不动
      }
    }
  }

componentDidUpdate 发生在getShapShotBeforeUpdate之后,接受3个参数分为: componentUpdate(prevProps, prevState, snapshot) 在这个函数里面可以进行DOM操作和数据请求,但是请注意,如果setState。 一定要写if判断逻辑,不然会造成死循环,页面卡死

参考文章

卸载阶段
  • componentWillUnmount

componentWillUnmount 组件卸载时调用, 可取消订阅、清除定时器、清除组件内部引用store的数据等操作

setState

假如一次事件中触发一次如上 setState ,在 React 底层主要做了那些事呢?

  • 首先,setState 会产生当前更新的优先级(老版本用 expirationTime ,新版本用 lane )。
  • 接下来 React 会从 fiber Root 根部 fiber 向下调和子节点,调和阶段将对比发生更新的地方,更新对比 expirationTime ,找到发生更新的组件,合并 state,然后触发 render 函数,得到新的 UI 视图层,完成 render 阶段。
  • 接下来到 commit 阶段,commit 阶段,替换真实 DOM ,完成此次更新流程。
  • 此时仍然在 commit 阶段,会执行 setState 中 callback 函数,如上的()=>{ console.log(this.state.number) },到此为止完成了一次 setState 全过程。
  • setState 只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout 中都是同步的
  • setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果
  • 在原生事件和setTimeout 中不会批量更新。在React事件系统中如果对同一个值进行多次 setState , setState 的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新
说明

当执行 setState() 时,会将需要更新的 state 浅合并后放入fiber状态队列,而不会立即更新 state,通过调度更新来批量更新 state

setState 是通过 enqueueUpdate 来执行 state 更新的,那 enqueueUpdate 是如何实现更新 state 的?继续往下走。 3、enqueueSetState 作用实际很简单,就是创建一个 update ,然后放入当前 fiber 对象的待更新队列中,最后开启调度更新,进入上述讲到的更新流程;

enqueueSetState(){
  /* 每一次调用`setState`,react 都会创建一个 update 里面保存了 */
  const update = createUpdate(expirationTime, suspenseConfig);
  /* callback 可以理解为 setState 回调函数,第二个参数 */
  callback && (update.callback = callback) 
  /* enqueueUpdate 把当前的update 传入当前fiber,待更新队列中 */
  enqueueUpdate(fiber, update); 
  /* 开始调度更新 */
  scheduleUpdateOnFiber(fiber, expirationTime);
}

4、enqueueUpdate 如果当前正处于创建/更新组件的过程,就不会立刻去更新组件,而是先把当前的组件放在 dirtyComponent 里,这里也很好的解释了上面的例子,不是每一次的 setState 都会更新组件。否则执行 batchedUpdates 进行批量更新组件

function enqueueUpdate(component) {
 // ...  
 if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }

  dirtyComponents.push(component);
}

5、batchedUpdates 是将当次所有的 dirtyComponent 遍历,执行其 updateComponent 来更新组件

参考文章

类组件和函数组件区别

类组件就是常说的class组件,有声明周期以及state
函数式组件:无声明周期,可接受props,常用来作为UI组件,

函数式组件性能比类组件要高,适用于表现层,类组件包含声明周期以及state特性,可处理复杂的逻辑,适用于逻辑层。react初衷偏向于使用函数式组件,这样能更快的渲染。hooks横空出世

react ref作用

dom元素、组件真正实例的引用,其实就是react.render()函数执行后,返回的组件实例你不能在函数组件上使用 ref 属性,因为他们没有实例

  • 为什么会用到refs 当要处理DOM元素的时候,例如元素focus,文本的选择,媒体的播放,第三方dom库集成,特性情况获取元素的宽高度等,这些是react无法控制的局面,所以需要用到ref

  • 如何创建ref

    <!-- react createRef -->
    class Levi extends React.Component {
      constructor(props) {
        this.leviRef = React.createRef();
      }
    
      private init() {
        console.log(this.leviRef)
      }
    
      render() {
        return <div ref={this.leviRef}>hello world</div>;
      }
    }
    
    <!-- hooks -->
    const Page = () => {
      const leviRef = useRef(initialVal);
    
      const fetch = useCallback(() => {
        console.log(leviRef.current)
      }, [])
    
      return  <div ref={this.leviRef}>hello world</div>;
    }
    
  • 访问ref 当ref传递给render元素时,対该元素的引用可在ref中的current获取到

const ref = this.leviRef.current

forwardRef

React.forwardRef 会创建一个React组件,这个组件能够将其接受的 ref 属性转发到其组件树下的另一个组件中。这种技术并不常见,但在以下两种场景中特别有用

  • 转发 refs 到 DOM 组件
  • 在高阶组件中转发 refs


// 高阶组件
    function logProps(Component) {
      class LogPropsContainer extends React.Component {
        componentDidUpdate(prevProps) {
          console.log('old props:', prevProps);
          console.log('new props:', this.props);
        }

        render() {
          const {forwardedRef, ...rest} = this.props;
          // 将自定义的 prop 属性 “forwardedRef” 定义为 ref
          return <Component ref={forwardedRef} {...rest} />;    }
      }

      // 注意 React.forwardRef 回调的第二个参数 “ref”。
      // 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
      // 然后它就可以被挂载到被 LogProps 包裹的子组件上。
      return React.forwardRef((props, ref) => {    return <LogPropsContainer {...props} forwardedRef={ref} />;  });
   }
  
    class FancyButton extends React.Component {
        focus() {
        // ...
        }

        render() {}
    }

    // 我们导出 LogProps,而不是 FancyButton。
    // 虽然它也会渲染一个 FancyButton。
    export default logProps(FancyButton);
 

    // App.tsx
    import FancyButton from './xxx/xxx'
    
    const ref = React.createRef();
    <FancyButton
      label="Click Me"
      handleClick={handleClick}
      ref={ref}/>;
  

什么是高阶组件?

高阶组件可接受react组件作为参数并返回一个新的组件的函数, 高阶组件可用于以下场景

  • 渲染劫持
  • 增强props
  • 代码复用,逻辑抽象
  • 内部state抽象、操作
  • 可用过ref访问该组件的实例

const HOCComponent = (WrapComponent) => {
  return class HOCComponent extends React.Component {
      state = {
        name: 'levi',
        age: 20
      }
      levi = React.createRef();

      componentDidMount() {
        console.log(this.levi.current); // 打印组件实例
        this.setState({age: 21}) // fecth data
      }

      render() {
        const newProps = {
          area: 'sh'
        }

        // 起到权限控制作用,不用再每个月页面里面判断
        if(this.state.age < 18) {
          return <div>未成年人无权限进入</div>
        }

        return <div><WrapComponent {...newProps} {...state} ref={this.levi} /><div>
      }
  }
}

什么是React.Context

context的作用就是实现跨层级的组件数据传递, context提供了一种在组件之间可以共享值的方法,而无需通过树的每个级显示传递props
优点: 跨组件访问数据
缺点:react组件树中某个上级组件shouldComponentUpdate返回false以后,当context更新,是不会引起下级组件的更新

函数式组件React.useContext上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。 即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染

如何创建
  // 创建context上下文,可单独抽离成一个文件
  const LeviInfoContext = React.createContext({ name: 'levi', age: 20, run: () => console.log('run') });

  // 提供provider包含内组件消费, 接受一个默认值value
  class App extends React.Component {
    public render() {
      return (
        <LeviInfoContext.Provider value={{ name: 'levi', age: 21 }}>
          <App />
        </LeviInfoContext.Provider>
      );
    }
  }

  // 类组件使用consemer消费
  import LeviInfoContext from 'xxxx';
  class Children extends React.Component {
    public render() {
      return <LeviInfoContext.consumer>{(baseObject) => <div>i am child</div>}</LeviInfoContext.consumer>;
    }
  }

  // 函数式组件使用 React.useContext。接受React.createContext返回的值
  const Page = () => {
    const info = React.useContext(LeviInfoContext);

    return <div>11111{info.name}</div>;
  };

从Context源码实现谈React性能优化

如何避免组件重复渲染

React常见问题就是重复渲染,React提供了2个方法,避免重复渲染

  • React.memo(component, equalFn?) // equalFn(prevProps, nextProps)
  • React.PureComponent
  • React.useMemo(fn: () => Component, deps) <函数式组件常用>

  const ComponentByMemo = React.memo(compoennt, (prevProps, nextProps) => {
    //  如果 props 相等,默认回收函数 会返回 true,并且不更新;如果 props 不相等,则返回 false,组件更新
    // 这与 shouldComponentUpdate 方法的返回值相反, shouldComponent 返回true的话,更新,反正不更新
  } )

  class Levi extends React.PureComponent {
    render() {
      return <div>levis</div>
    }
  }

  const ComponentByUseMemo = React.useMemo()

React.memo默认情况下浅比较props, 对于复杂props比较,可以通过自定义equalFn比较,如果equalFn返回true,组件不更新,反之更新

如何提高react性能

  • 抽离公共代码到单独文件中,使用路由懒加载
  • 在列表和表格中使用key,这样React渲染更新速度更快
  • 适当的使用shouldComponentUpdate/React.memo,避免了子组件不必要的渲染

补充前端性能优化

  • 减少http请求,缓存相关的ajax数据
  • 图片优化,压缩图片
  • css优化,提取共有css代码
  • js优化,提取公共js代码
  • 使用CDN内容分发网络
  • 使用响应的打包工具:webpack,gulp压缩代码
  • DOM渲染优化。重绘不一定引起回流,但是回流会重绘。
    • 合并多次的DOM操作为单次的DOM操作
    • 动画设置positon: absolute
    • ....

什么是React.Fiber

React小册-Fiber

react 事件系统

React小册-事件原理

hooks面试题

为什么hooks不能写在if,循环等语句

首先,可能都听过react的 Fiber 架构,其实一个 Fiber节点就对应的是一个组件。对于 classComponent 而言,有 state 是一件很正常的事情,Fiber对象上有一个 memoizedState 用于存放组件的 state。ok,现在看 hooks 所针对的 FunctionComponnet。 无论开发者怎么折腾,一个对象都只能有一个 state 属性或者 memoizedState 属性,可是,谁知道可爱的开发者们会在 FunctionComponent 里写上多少个 useStateuseEffect 等等 ? 所以,react用了链表这种数据结构来存储 FunctionComponent 里面的 hooks。比如:

function App(){
    const [count, setCount] = useState(1)
    const [name, setName] = useState('chechengyi')
    useEffect(()=>{
        
    }, [])
    const text = useMemo(()=>{
        return 'ddd'
    }, [])
}
复制代码

在组件第一次渲染的时候,为每个hooks都创建了一个对象

type Hook = {
  memoizedState: any,
  baseState: any,
  baseUpdate: Update<any, any> | null,
  queue: UpdateQueue<any, any> | null,
  next: Hook | null,
};
复制代码

最终形成了一个链表。

这个对象的memoizedState属性就是用来存储组件上一次更新后的 state,next毫无疑问是指向下一个hook对象。在组件更新的过程中,hooks函数执行的顺序是不变的,就可以根据这个链表拿到当前hooks对应的Hook对象,函数式组件就是这样拥有了state的能力。当前,具体的实现肯定比这三言两语复杂很多。

所以,知道为什么不能将hooks写到if else语句中了把?因为这样可能会导致顺序错乱,导致当前hooks拿到的不是自己对应的Hook对象

hooks原理

如果我们将上述demo其中的一个 useRef 放入条件语句中,

 let curRef  = null
 if(isFisrt){
  curRef = useRef(null)
 }
复制代码

hoo11.jpg

因为一旦在条件语句中声明hooks,在下一次函数组件更新,hooks链表结构,将会被破坏,current树的memoizedState缓存hooks信息,和当前workInProgress不一致,如果涉及到读取state等操作,就会发生异常。

react为什么要引入hooks

  • 大型的组件难以拆分,组件之间逻辑难以复用
  • 难以理解的class语法,忘记事件绑定this,生命周期多,顺序得记住。 相对类组件,函数式组件又过于简陋,没有生命周期钩子,没有类组件的状态state。但是函数式组件上手简单,友好直观
  import React, {useState} from 'react';

  const Levi = () => {

    const [count, setCount] = useState<number>(0);

    const handleClick = useCallback(() => {
      setCount(count+1)
    }, [])

    return <Button onclick={handleClick}>点击</Button>
  }

常用hooks

  • useState()状态钩子。为函数组建提供内部状态
      const Page = () => {
        const [name, setName] = useState<string>('levi')
      };
    
  • useContext()共享钩子。该钩子的作用是,在组件之间共享状态。 可以解决react逐层通过Porps传递数据,它接受React.createContext()的返回结果作为参数,使用useContext将不再需要Provider 和 Consumer
      const Page = () => {
        const info = React.useContext(LeviInfoContext);
    
        return <div>11111{info.name}</div>;
      };
    
  • useReducer()状态钩子。Action 钩子。useReducer() 提供了状态管理,其基本原理是通过用户在页面中发起action, 从而通过reducer方法来改变state, 从而实现页面和状态的通信。使用很像redux
      const initialState = { count: 0 };
    
      function reducer(state, action) {
        switch (action.type) {
          case 'increment':
            return { count: state.count + 1 };
          case 'decrement':
            return { count: state.count - 1 };
          default:
            throw new Error();
        }
      }
    
      function Counter() {
        const [state, dispatch] = useReducer(reducer, initialState);
        return (
          <>
            Count: {state.count}
            <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
            <button onClick={() => dispatch({ type: 'increment' })}>+</button>
          </>
        );
      }
    
  • useEffect()副作用钩子。它接收两个参数, 第一个是进行的异步操作, 第二个是数组,用来给出Effect的依赖项
      const Page = () => {
        useEffect(() => {
          const initial = async () => {
            // fetch data
            await Promise.resolve();
          }
        }, [])
    
      return <div>11111{info.name}</div>;
    };
    
  • useRef()获取组件的实例;渲染周期之间共享数据的存储(state不能存储跨渲染周期的数据,因为state的保存会触发组件重渲染),useRef传入一个参数initValue,并创建一个对象{ current: initValue }给函数组件使用,在整个生命周期中该对象保持不变
      const Page = () => {
        const leviRef = React.useRef(initialVal)
    
        const handleClick = useCallback(() => {
          levi.current.focus();
        }, [])
    
        return <input ref={leviRef} onClick={handleClick} />
      },
    
  • useMemo和useCallback可缓存函数的引用或值,useMemo缓存数据(所有对象、包括函数),useCallback缓存函数,两者是Hooks的常见优化策略,useCallback(fn,deps)相当于useMemo(()=>fn,deps)
        const Page = () => {
          const leviRef = React.useRef(initialVal)
          
          const handleClick = useCallback(() => {
            levi.current.focus();
          }, [])
    
          return React.useMemo(() = <input ref={leviRef} onClick={handleClick} />, [])
      },
    

Hooks相比HOC和Render Prop有哪些优点?

HOC都属于一种开发模式,将复用逻辑提升至父组件,容易嵌套过多、过度包装
hooks是React官方api, 将复用逻辑提升组件顶层,而不是提升至父组件中,避免造成多层嵌套

hooks 里面有哪些优化方案

  • deps依赖项不过过多,尽量不要超出3个
  • 多个相关state,合并成对象形式处理,不然会导致hooks内部出现多个state,难以维护
  • 在 useCallback 内部使用了更新state状态,可以考虑使用 setState callback 减少依赖
      const useValues = () => {
        const [values, setValues] = useState({});
    
        const updateData = useCallback((nextData) => {
          setValues((prevValues) => ({
            data: nextData,
            count: prevValues.count + 1,    
          })); // 通过 setState 回调函数获取最新的 values 状态,这时 callback 不再依赖于外部的 values 变量了,因此依赖数组中不需要指定任何值
        }, []); // 这个 callback 永远不会重新创建
    
        return [values, updateData];
      };
    
    
  • 可有ref来保存变量(组件整个生命周期ref会保持不变)
  • 编写自定义hooks返回可以选择元祖类型,如果超出3个,建议考虑对象形式返回
  • 自定义hooks返回的值,要保持引用的一致性
      function Example() {
        const data = useData();
        const [dataChanged, setDataChanged] = useState(false);
    
        useEffect(() => {
          setDataChanged((prevDataChanged) => !prevDataChanged); 
          // 当 data 发生变化时,调用 setState。
          // 如果 data 值相同而引用不同,就可能会产生非预期的结果。 
        }, [data]);
    
        console.log(dataChanged);
    
        return <LeviComponent data={data} />;
      }
    
      const useData = () => {
        // 获取异步数据  
        const resp = getAsyncData([]);
    
        // 处理获取到的异步数据,这里使用了 Array.map。
        // 因此,即使 data 相同,每次调用得到的引用也是不同的。  
        const mapper = (data) => data.map((item) => ({...item, selected: false}));
    
        return resp ? mapper(resp) : resp;
      };
    

useCallback 是用来干嘛的

缓存函数

useEffect和useLayoutEffect区别

  • useEffect 渲染之后异步执行,不会阻断浏览器的渲染。异步执行
  • useLayoutEffect 渲染之前同步执行,会阻塞浏览器的渲染,一些DOM操作可以放在useLayoutEffect执行。同步执行

react新版生命周期getDrivedStatefromprops为什么是静态的

react16.4版本引入,代替换来的componentWillReceiveProps, 当props或state改变的时候,派生出新的state,是一个纯函数。之所以设置为静态方法,在函数内部不能获取this,防止setState造成死循环。

实现一个自定义hooks

注意: 自定义hooks以use开头、或者函数名开头大写(react会认为这是一个组件)。不会react-hooks校验规则会报错

  // 获取当前文档标题 && 设置文档标题
  import { useState, useEffect, useCallback } from 'react';
  const useTitle = () => {
    const [title, setTitle] = useState<string>(document.title);

    useEffect(() => {
      document.title = title;
    }, [title]);

    const setDocumentTitle = useCallback(newTitle => {
      setTitle(newTitle);
    }, []);

    return [title, setDocumentTitle];
  };

  export default useTitle;

  // fetch data
  import { useState, useCallback } from 'react';
  const useFetchCity = () => {
    const [city, setCity] = useState<any[]>([]);
    const [loading, setLoading] = useState<boolean>(false);

    const getAllCity = useCallback(async () => {
      setLoading(true);

      const res = await new Promise(resolve => {
        setTimeout(() => {
          resolve([1, 2, 3, 4]);
        }, 2000);
      });

      setCity(res);
      setLoading(false);
    }, []);

    return [loading, city, getAllCity];
  };

  export default useFetchCity;
usePrevious
   function usePrevious (value) {
       const ref = useRef();
       
       useEffect(() => {
           ref.current = value
       }, [value])
       
       return ref.current;
   }
useDebounce
function useDebounce(fn, delay, dep = []) {
  const { current } = useRef({ fn, timer: null });
  useEffect(function () {
    current.fn = fn;
  }, [fn]);

  return useCallback(function f(...args) {
    if (current.timer) {
      clearTimeout(current.timer);
    }
    current.timer = setTimeout(() => {
      current.fn.call(this, ...args);
    }, delay);
  }, dep)
}

mobx & redux 区别

共同点

  • 为了解决数据状态管理混乱,维护统一数据状态
  • 支持将store与React组件连接,如react-redux,mobx-react
  • 某一状态只有一个可信数据来源(通常命名为store,指状态容器)

区别

  • redux将数据保存在单一的store中,mobx将数据保存在分散的多个store中
  • redux数据变更后,需要手动处理变化后的操作。mobx使用observable保存数据,数据变化后自动处理响应视图变更
  • redux使用不可变状态,这意味着状态是只读的,不能直接去修改它,需要使用纯函数,并且返回一个新的状态。mobx中的状态是可变的,可以直接对其进行修改
  • mobx相对来说比较简单,mobx更多的使用面向对象的编程思维。redux会比较复杂,需要掌握函数式编程思维,同时需要借助一系列的中间件来处理异步和副作用