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等工作可以暂停恢复,这样可以更加灵活的调度资源用于计算与渲染,尽可能的降低因过于复杂和密集的计算导致浏览器的渲染卡顿。

相关推荐
Rattenking43 分钟前
React 源码学习01 ---- React.Children.map 的实现与应用
javascript·学习·react.js
熊的猫2 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
小牛itbull6 小时前
ReactPress:重塑内容管理的未来
react.js·github·reactpress
FinGet17 小时前
那总结下来,react就是落后了
前端·react.js
王解21 小时前
Jest项目实战(2): 项目开发与测试
前端·javascript·react.js·arcgis·typescript·单元测试
AIoT科技物语2 天前
免费,基于React + ECharts 国产开源 IoT 物联网 Web 可视化数据大屏
前端·物联网·react.js·开源·echarts
初遇你时动了情2 天前
react 18 react-router-dom V6 路由传参的几种方式
react.js·typescript·react-router
番茄小酱0012 天前
ReactNative中实现图片保存到手机相册
react native·react.js·智能手机
王解2 天前
Jest进阶知识:深入测试 React Hooks-确保自定义逻辑的可靠性
前端·javascript·react.js·typescript·单元测试·前端框架
小牛itbull2 天前
ReactPress—基于React的免费开源博客&CMS内容管理系统
前端·react.js·开源·reactpress