react开发技巧小抄

React那些事儿

Hooks是什么

  1. hooks是一个消息通知机制(如git hooks, webpack hooks、操作系统的hooks),在react运行的某个时机时通知做一些操作。
  2. 拥抱函数式编程,为了让用户以最小的代价实现"关注点分离原则",达到最初的设计理念,即"component = f(data)"。
  3. 几乎重新定义了react的写法,用户可以不用再去关心程序运行的生命周期,只需要关注状态的改变即可。

避免竞争条件(Race Condition)导致的未知错误

相信大家在使用reactuseState更新状态时,应该最长使用如下写法吧:

tsx 复制代码
import { useState } from "react";

const App = () => {
  const [age, setAge] = useState<number>(0);

  return (
    <div>
      {age}
      <button onClick={() => setAge(age + 1)}>+</button>
    </div>
  );
};

export default App;

上面的代码再大多数情况下是没有什么问题的,点击+按钮后都能够让age不断累加,但是,在某些特殊场景下,就可能出现一些异常情况,如:

tsx 复制代码
import { useState } from "react";

const App = () => {
  const [age, setAge] = useState<number>(0);

  const handleClick = () => {
    setTimeout(() => {
      setAge(age + 1);// 1
    }, 100);
    setTimeout(() => {
      setAge(age + 1);// 1
    }, 50);
  };

  return (
    <div>
      {age}{/** 1 */}
      <button onClick={handleClick}>+</button>
    </div>
  );
};

export default App;

如上述情况,我们点击+号之后,虽然在回调函数中执行了两次setAage操作,但实际上页面展示的依然是1,这是为什么呢?这是由于我们的age变量是从环境中来的,而由于setTimeout形成了闭包,导致我们age的值始终都是0。这样就会出现明明我调用了两次setAage,但却只加到了1的奇怪现象。

上述这种现象,其实就是所谓的竞争条件(Race Condition),由于闭包内的变量优先级高于闭包外变量,导致没有正常完成更新。

那么,我们要如何解决上述问题呢?其实也很简单:

tsx 复制代码
import { useState } from "react";

const App = () => {
  const [age, setAge] = useState<number>(0);

  const handleClick = () => {
    setTimeout(() => {
      setAge(age => age + 1);// 2
    }, 100);
    setTimeout(() => {
      setAge(age => age + 1);// 1
    }, 50);
  };

  return (
    <div>
      {age}{/** 2 */}
      <button onClick={handleClick}>+</button>
    </div>
  );
};

export default App;

上面只是更换了更新age的方式,从直接传入新值改成传入一个函数,这个函数会接收当前上下文中age的上一个状态的值,并返回下一个状态,这样就能够始终保证,我们用来累加的age一定是当前的最新状态了。

Hooks不能在分支语句中使用的分析

首先,我们得先搞清楚,Hooks的实现原理,才能够弄明白为什么Hooks不能在分支语句中使用。

我们可以简单的理解为,当我们使用useEffect|useState|useRefHooks时,react内部实际上开辟了一个存储空间,并为每一次的Hooks调用打上一个编号,如果一个编号上没有存储任何东西,则会初始化一个引用并存储其中,如果下一次相同编号的的Hooks则无需重新初始化,直接使用该引用即可。如果我们将Hooks放在一个分支语句或回调函数当中,就可能出现刚开始存在这个编号的Hooks,下一次渲染又不存在的情况。这会导致他们的编号不一致,从而导致记录的Hooks出现混乱。

总结一下:实际上,我们的Hooks可以理解成是程序的一个声明,而流程控制,如if-else不是声明的一部分,如果我们把Hooks放在流程控制当中,会导致程序声明和逻辑的混乱。React内部通过Hooks的词法顺序来区分不同的Hooks

tsx 复制代码
import { useState, useEffect, useRef } from "react";

const App = () => {
  const [age, setAge] = useState<number>(0);
  const button = useRef(null);
  
  useEffect(() => {
    console.log('init')
  }, []);

  const handleClick = () => {
    setTimeout(() => {
      setAge(age => age + 1);// 2
    }, 100);
    setTimeout(() => {
      setAge(age => age + 1);// 1
    }, 50);
  };

  return (
    <div>
      {age}{/** 2 */}
      <button ref={button} onClick={handleClick}>+</button>
    </div>
  );
};

export default App;

// 上述代码中的Hooks,在react内部存储可以看成
[useState, useRef, useEffect]
// 当每次重新渲染时,读取到useState时,react会到存储空间中的第一位查找引用,读取到useRef时,将会去存储空间的第二位查找引用...,假如说,某个hooks存在分支语句中,如:

import { useState, useEffect, useRef } from "react";

const App = () => {
  const [age, setAge] = useState<number>(0);
  let button
  if(age === 0) {
		button = useRef(null);    
  }

  
  useEffect(() => {
    console.log('init')
  }, []);

  const handleClick = () => {
    setTimeout(() => {
      setAge(age => age + 1);// 2
    }, 100);
    setTimeout(() => {
      setAge(age => age + 1);// 1
    }, 50);
  };

  return (
    <div>
      {age}{/** 2 */}
      <button ref={button} onClick={handleClick}>+</button>
    </div>
  );
};

export default App;

// 那么,当age不为0时,无法进入分支语句,因此没有了useRef定义,但是原本的存储空间依然还是:
[useState, useRef, useEffect]
// 这样就会导致原本useEffect在存储空间中的编号是3,但是现在编号变成了2,读取到了原本应该是useRef的引用,导致系统异常。

子组件传递信息给父组件

react中,父组件传参给子组件很简单,通过属性的方式就可以轻松实现,如果要子组件传递参数给父组件,我们通常会用以下两种方式:

ref

tsx 复制代码
fucntion Child(props) {
  return (
  	<>
    	<input ref={props.inputRef} />
    </>
  )
}

function Parent() {
  cosnt inputRef = useRef(null);
  useEffect(() => {
		console.log(inputRef.current.value);
  }, []);
  return (
  	<Child inputRef={inputRef} />
  );
}

callback

tsx 复制代码
fucntion Child(props) {
  return (
  	<>
    	<input onChange={e => props.onChange(e.target.value)} />
    </>
  )
}

function Parent() {
  
  const handleChange = (value) => {
    console.log(`value: ${value}`);
  }
  return (
  	<Child onChange={handleChange} />
  );
}

容器组件传参

父子组件之前相互传参我们都清楚,那么,假如说我们要通过一个容器组件,把参数传递给他的children要怎么传呢?

tsx 复制代码
fucntion Child(props) {
  return (
  	<>
			{props.x}
    </>
  )
}

function Parent({children}) {
  
  return (
		<div className="container">
    	{children}
    </div>
  );
}

function App() {
  // 如果我要将x传递给他的所有子组件,要怎么实现呢?
  return <Parent x={1}>
  	<Child />
  </Parent>
}

我们可以借助cloneElement完成

tsx 复制代码
fucntion Child(props: {x?: number}) {
  return (
  	<>
			{props.x}
    </>
  )
}

function Parent({children: JSX.Element, x: number}) {
  
  return (
		<div className="container">
      {/* 克隆子节点,并将属性x赋值给所有子节点 */}
    	{React.cloneElement(children, {x})}
    </div>
  );
}

function App() {
  return <Parent x={1}>
  	<Child />
  </Parent>
}

避免爆栈(StackOverflow)

tsx 复制代码
function APP() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    // 由于这个useEffect有count依赖,一旦count更新就会被重新触发,如果我们在这里又触发了setCount操作,
    // 就会导致程序陷入死循环而导致爆栈,这一点大家需要注意
    setCount(x => setCount(x));
  }, [count]);
}

同时兼容受控与非受控的组件

受控组件简单的说就是他的状态交由外部改变,通过外部传入的value改变组件内部的状态,常见与表单组件。虽然受控组件更加灵活,但也一定程度上违反了"最小知识原则",用户在使用一个组件时的学习成本变高。那么,有没有办法让一个组件即能够兼容受控组件的灵活,又能能兼容非受控组件的简单易用呢?也就是说,能否实现一个既可以受控,又可以非受控的组件。

tsx 复制代码
import { useEffect, useState } from "react";

export type MyInputPorps = {
  value?: string;
  defaultValue?: string;
  onChange?: (value: string) => void;
};

function MyInput({ value, defaultValue, onChange }: MyInputPorps) {
  // 受控组件和非受控组件的标志是有没有传value属性,如果传了就是受控组件,没传就是非受控组件
  const controlled = typeof value !== "undefined";
  const [_value, setValue] = useState(
    (controlled ? value : defaultValue) || ""
  );

  useEffect(() => {
    if (_value !== defaultValue) {
      onChange?.(_value);
    }
  }, [_value]);

  return (
    <input
      value={controlled ? value : undefined}
      defaultValue={defaultValue}
      onChange={(e) => {
        if (controlled) {
          onChange?.(e.target.value);
          return;
        }
        setValue(e.target.value);
      }}
    />
  );
}

const App = () => {
  const [val, setVal] = useState("name");
  return (
    <div>
      <fieldset>
        <legend>非受控组件</legend>
        <MyInput defaultValue="123" />
      </fieldset>
      <fieldset>
        <legend>受控组件</legend>

        <MyInput
          value={val}
          onChange={(value) => {
            console.log(value);
            setVal(value);
          }}
        />
      </fieldset>
    </div>
  );
};

export default App;

Hooks优化版

tsx 复制代码
import { useEffect, useState } from "react";

export type MyInputPorps = {
  value?: string;
  defaultValue?: string;
  onChange?: (value: string | undefined) => void;
};

export type UseValueOptions = {
  value?: string;
  defaultValue?: string;
  onChange?: (value: string | undefined) => void;
};
function useValue({
  value,
  defaultValue,
  onChange,
}: UseValueOptions): [string | undefined, (value: string | undefined) => void] {
  // 受控组件和非受控组件的标志是有没有传value属性,如果传了就是受控组件,没传就是非受控组件
  const controlled = typeof value !== "undefined";
  const [_value, setValue] = useState<string | undefined>(
    controlled ? value : defaultValue
  );

  useEffect(() => {
    if (controlled && _value !== value) {
      setValue(value);
    }
  }, [value]);

  useEffect(() => {
    if (!controlled && _value !== defaultValue) {
      onChange?.(_value);
    }
  }, [_value]);

  const setHandler = (value: string | undefined) => {
    if (!controlled) {
      setValue(value);
    } else {
      onChange?.(value);
    }
  };
  return [_value, setHandler];
}

function MyInput({ value, defaultValue, onChange }: MyInputPorps) {
  // 受控组件和非受控组件的标志是有没有传value属性,如果传了就是受控组件,没传就是非受控组件
  const controlled = typeof value !== "undefined";
  const [_value, setValue] = useState(
    (controlled ? value : defaultValue) || ""
  );

  useEffect(() => {
    if (_value !== defaultValue) {
      onChange?.(_value);
    }
  }, [_value]);

  return (
    <input
      type="text"
      value={controlled ? value : undefined}
      defaultValue={defaultValue}
      onChange={(e) => {
        if (controlled) {
          onChange?.(e.target.value);
          return;
        }
        setValue(e.target.value);
      }}
    />
  );
}

function MyInput2({ value, defaultValue, onChange }: MyInputPorps) {
  // 受控组件和非受控组件的标志是有没有传value属性,如果传了就是受控组件,没传就是非受控组件
  const [_value, setValue] = useValue({
    value,
    defaultValue,
    onChange,
  });
  return (
    <input
      type="text"
      value={value?_value:undefined}
      defaultValue={defaultValue}
      onChange={(e) => {
        setValue(e.target.value);
      }}
    />
  );
}

const App = () => {
  const [val, setVal] = useState("name");
  const [val2, setVal2] = useState("age");
  return (
    <div>
      <fieldset>
        <legend>非受控组件</legend>
        <MyInput defaultValue="123" />
      </fieldset>
      <fieldset>
        <legend>受控组件</legend>

        <MyInput
          value={val}
          onChange={(value) => {
            console.log(value);
            setVal(value as string);
          }}
        />
      </fieldset>
      <fieldset>
        <legend>非受控组件(Hooks)</legend>
        <MyInput2 defaultValue="333" />
      </fieldset>
      <fieldset>
        <legend>受控组件(Hooks)</legend>
        <MyInput2
          value={val2}
          onChange={(value) => {
            console.log(value);
            setVal2(value as string);
          }}
        />
      </fieldset>
    </div>
  );
};

export default App;

强制触发重绘

我们经常会遇到一些场景,数据更新之后,由于这个数据并不是直接放在状态当中,不会自动触发重绘,此时,我们可能需要手动强制触发一下重绘使得识图更新,这边推荐使用版本号重绘法

tsx 复制代码
function App() {
  const [, setVersion] = useState(0);
  const buz = useBuz();
  useEffect(() => {
    buz.on('refresh', () => {
      // 此时,我们监听到业务hooks通知的期望页面被刷新的消息时,我们通过更新version状态,依次来触发页面的强制刷新重绘
      setVersion(v => v + 1);
    });
    return () => {
      buz.off('refresh');
    };
  }, []);
  return (
  	<>
    	...
    </>
  )
}

Fiber是什么

  1. FiberReact Element的一个数据镜像
  2. Fiber 是一个Diff工作
  3. Fiber模拟了函数调用的关系(函数的递归调用与回溯的流程)

Fiber在处理更新时,实际并不是直接操作当前的Fiber节点本身,而是会将他copy一份出来,然后在这个克隆版本上进行修改,当所有的操作都完成之后,再将这个克隆的Fiber节点跟原本的节点进行Diff。最后根据Diff的结果将当前节点替换掉,完成页面更新。这个过程类似于我们使用Git进行版本管理时,修改的文件并不会直接加入到版本管理,而是克隆一个副本,把更改放到这个副本上,当执行Commit操作时才合并修改。本质上,这是一种处理并发的技巧,叫做:Copy on Write

Fiber更新的两个阶段

  • 计算阶段(可中断)
    • 计算Work In Progress Fiber,即上面说的当前Fiber的副本。需要更新的属性、节点之类的信息都会在这里计算
    • 进行Dom Diff,计算Dom的更新
  • 提交阶段(不可中断)
    • 提交所有的更新

Fiber的执行

驱动Fiber的执行有很多种情况,主要的3种是:

  • 根节点的渲染函数调用,即首次渲染(ReactDom.render(<App />, document.getElementById('root'))
  • setState
  • 属性的改变

当上述情况发生时,render库会驱动Fiber执行。

  • 如果是新节点,则创建Fiber树
  • **计算阶段:**如果是变更操作,那么计算Work In Progress Fiber
  • 计算阶段: render库执行,利用Diff算法计算更新
  • **提交阶段:**应用更新

Fiber的并行

并行就是指任务可以交替同时进行,看起来好像一起执行的,如果要做到这点,那么Fiber就必须得保证每个任务都是独立的。

理论上,在计算阶段,一切都是虚拟的,父Fiber节点和子Fiber节点可以是两个work。

之前说了,Fiber的执行类似深度优先搜索,在不断地递归与回溯的过程中完成工作,那么,这样就形成了依赖关系,是不是就没办法并行了呢?

实际上是可以的。

因为Fiber是先计算出所有的Work In Progress Fiber后再进行Diff操作的。

虽然在计算Work In Progress Fiber时不能并行,但只要计算出来后,后面的Diff操作和部分的更新操作都是可以并行执行的。

Fiber的价值

Fiber之所以花那么大的功夫让其支持并行,目的是为了让我们DiffUpdate等工作可以暂停恢复,这样可以更加灵活的调度资源用于计算与渲染,尽可能的降低因过于复杂和密集的计算导致浏览器的渲染卡顿。

相关推荐
凯哥爱吃皮皮虾7 小时前
如何给 react 组件写单测
前端·react.js·jest
每一天,每一步9 小时前
react antd点击table单元格文字下载指定的excel路径
前端·react.js·excel
screct_demo19 小时前
詳細講一下在RN(ReactNative)中,6個比較常用的組件以及詳細的用法
javascript·react native·react.js
光头程序员1 天前
grid 布局react组件可以循数据自定义渲染某个数据 ,或插入某些数据在某个索引下
javascript·react.js·ecmascript
limit for me1 天前
react上增加错误边界 当存在错误时 不会显示白屏
前端·react.js·前端框架
浏览器爱好者1 天前
如何构建一个简单的React应用?
前端·react.js·前端框架
VillanelleS1 天前
React进阶之高阶组件HOC、react hooks、自定义hooks
前端·react.js·前端框架
某哈压力大1 天前
基于react-vant实现弹窗搜索功能
前端·react.js
傻小胖1 天前
React 中hooks之useInsertionEffect用法总结
前端·javascript·react.js
flying robot2 天前
React的响应式
前端·javascript·react.js