面试篇(三)

0.1+0.2为什么等于0.3000000...4?

整数转成二进制是重复相除法:25 = (11001),25除2直到商为0,然后倒取余得二进制。

小数转成二进制是重复相乘法:0.1 = (0.0001100110011...),乘2,顺序取整数部分,直到小数部分为0。

0.1 * 2 = 0.2 = 0+ 0.2

0.2 * 2 = 0.4 = 0 + 0.4

0.4 * 2 = 0.8 = 0 + 0.8

0.8 * 2 = 1.6 = 1 + 0.6

0.6 * 2 = 1.2 = 1 + 0.2

0.2 * 2 = 0.4 = 0 + 0.4

....

顺序取 000110..., 无穷

小学我们学过,小数分为有限小数无限小数,无限小数又分为无限循环小数无限不循环小数
同样的,对于二进制小数来说,同样有有限小数和无限小数两大类。
那么重点来了,0.1和0.2在十进制小数里面是有限小数,但是使用二进制表示的时候就不是了。
不信你使用重复相乘法计算一下试试:

# 运算可以得到: 0.1=0.0001100110011... # 它实际上是一个无限循环小数 0.1=0.0(0011) # 括号的0011即是无限循环的部分 # 同理得到0.2的二进制 0.2=0.(0011) # 括号的0011即是无限循环的部分

所以计算机是无法准确表示0.1和0.2两个数的,只能无限逼近这两个数。
假设计算机使用10位精度,则计算机里面,0.1和0.2是这样子的。

0.1=0.0001100110(2) 0.2=0.0011001100(2) # 现在我们使用二进制加法,运算两个二进制数 0.0001100110 0.0011001100 ———————— 0.0100110010(2)

再把这个结果转换成十进制

0.0100110010(2) = 1*(1/4) + 1*(1/32) + 1*(1/64) + 1*(1/512) = 0.298828125 < 3

可以看到这个数值已经很接近3了,如果把精度再调高为16位、32位甚至是64位,会得到更加接近3的结果。这就解释了为什么在十进制中运算和在二进制中运算,会得到不一样的结果。
但是,这就完了吗?还没有。其实可以归纳总结到,不管精度是多少,这个结果应该是恒小于3的,而不可能是大于3的结果,因为在截取有效数时,总是把末尾的数值去掉了,那为什么在Python这里会大于3呢?
这主要是因为除了精度以外,计算机浮点数表示数据的时候还有对阶、零舍一入等等的操作,有可能会使得实际操作数比原操作数大一些。
在十进制中有四舍五入法。比如:

0.499999 约等于 0.5 0.41999 约等于 0.4

同样的,在二进制中,有零舍一入的操作。

0.1001(2) 约等于 0.10(2) 0.1011(2) 约等于 0.11(2)

所以在二进制运算中,为了进行有效数值的运算,还会对浮点数尾数进行零舍一入的操作,从而导致真实值和计算值的偏差。这就可以合理的解释在Python中0.1+0.2为什么等于0.3000…4了。

useImperativeHandle的使用场景

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用。

父、子组件在使用该hook时步骤如下:

  1. 父组件使用useRef(或createRef)创建一个ref对象,将这个ref对象赋给子组件的ref属性
  2. 子组件使用forwardRef包装自己,允许作为函数组件的自己使用ref。然后使用useImperativeHandle钩子函数,在第二个函数参数在内部返回状态,这个被返回的状态就可以被父组件访问到。
  3. 父组件使用创建的ref对象的current属性获取子组件暴露出的属性或方法。
import React, { useRef, useImperativeHandle } from 'react';
import ReactDOM from 'react-dom';

const FancyInput = React.forwardRef((props, ref) => {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} type="text" />
});

const App = props => {
  const fancyInputRef = useRef();
  return (
    <div>
      <FancyInput ref={fancyInputRef} />
      <button
        onClick={() => fancyInputRef.current.focus()}      // 调用子组件的方法
      >父组件调用子组件的 focus</button>
    </div>
  )
}
ReactDOM.render(<App />, root);

useContext的使用场景

useContext可以帮助我们跨越组件层级直接传递变量,实现数据共享。

import React, { useState, createContext, useContext } from "react";
const CountContext = createContext(0);

const Example = () => {
  const [count, setCount] = useState<number>(0);
  return (
    <div>
      <p>父组件点击数量:{count}</p>
      <button onClick={() => setCount(count + 1)}>{"点击+1"}</button>
      <CountContext.Provider value={count}>
        <Counter />
      </CountContext.Provider>
    </div>
  );
};

const Counter = () => {
  const count = useContext(CountContext);
  return <p>子组件获得的点击数量:{count}</p>;
};

export default Example;

useReducer

当state过多的时候,可以将state合并成一个state,通过action的方式取修改特殊的state字段。

import React, { useReducer } from "react";
const initialState = {
  count: 0
};
function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return { count: state.count + action.payload };
    case "decrement":
      return { count: state.count - action.payload };
    default:
      throw new Error();
  }
}
function App() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "increment", payload: 5 })}>
        +
      </button>
      <button onClick={() => dispatch({ type: "decrement", payload: 5 })}>
        -
      </button>
    </>
  );
}

useLayoutEffect

大部分情况下,使用 useEffect 就可以帮我们处理组件的副作用,但是如果想要同步调用一些副作用,比如对 DOM 的操作,就需要使用 useLayoutEffect,useLayoutEffect 中的副作用会在 DOM 更新之后同步执行,useLayoutEffect 会在 render,DOM 更新之后同步触发函数,会优于 useEffect 异步触发函数。

function App() {
  const [width, setWidth] = useState(0);
  useLayoutEffect(() => {
    const title = document.querySelector("#title");
    const titleWidth = title.getBoundingClientRect().width;
    console.log("useLayoutEffect");
    if (width !== titleWidth) {
      setWidth(titleWidth);
    }
  });
  useEffect(() => {
    console.log("useEffect");
  });
  return (
    <div>
      <h1 id="title">hello</h1>
      <h2>{width}</h2>
    </div>
  );
}

useId 

useId用于生成一个包含 :号的唯一id , 可在组件中多次使用, id是不重复的, 当项目中存在多个根应用时, 我们还可以给useId生成的id增加前缀 

增加前缀在 ReactDOM.createRoot 函数的第二个参数配置 identifierPrefix 字段即可, 在此说一下, react18已结废弃了 ReactDom.render方法, 改用root.render, ReactDOM.createRoot用于生成root根节点

const root = ReactDOM.createRoot(document.getElementById('root'), {identifierPrefix: 'app1-',})
mport React, { useId } from 'react'
 
function UseId() {
  const id1 = useId()
  const id2 = useId()
  const id3 = useId()
  const id4 = useId()
  const id5 = useId()
  return (
    <div style={{ height: 200, width: '100%', background: 'skyblue' }}>
      <h1>useId</h1>
      <p>id1 : {id1}</p>
      <p>id2 : {id2}</p>
      <p>id3 : {id3}</p>
      <p>id4 : {id4}</p>
      <p>id5 : {id5}</p>
    </div>
  )
}

// app1-r1
// app1-r2
// app1-r3
// ...

useTransition 

useTransition 又叫过渡, 他的作用就是标记非紧急更新, 这些被标记非紧急更新会在紧急更新完之后进行更新, useTransition 他的场景在应对渲染量很大的页面需要及时响应某些事件的情况, 接下来举个例子.

 思路: 从别的博主那里借鉴的, 准备一个进度条, 通过滑动进度条来显示进度条的进度并且渲染相同进度数量的div, 如果我们不对渲染进行优化那无疑页面会很卡, 此时使用过渡配合useMemo来缓存页面结构, diffing算法就会对比出少量的变化进行局部修改

import React, { useTransition, useState, useMemo } from 'react'
 
export default function UseTransition() {
  const [isPending, startTransition] = useTransition()
 
  const [rangeValue, setRangeValue] = useState(1)
  const [renderData, setRenderData] = useState([1])
  const [isStartTransition, setIsStartTransition] = useState(false)
  const handleChange = (e) => {
    setRangeValue(e.target.value)
    const arr = []
    arr.length = e.target.value
    for (let i = 0; i <= arr.length; i++) {
      arr.fill(i, i + 1)
    }
    if (isStartTransition) {
      startTransition(() => {
        setRenderData(arr)
      })
    } else {
      setRenderData(arr)
    }
  }
  const jsx = useMemo(() => {
    return renderData.map((item) => {
      return (
        <div
          style={{
            width: 50,
            height: 50,
            backgroundColor: `#${Math.floor(Math.random() * 16777215).toString(
              16
            )}`,
            margin: 10,
            display: 'inline-block',
          }}
        >
          {item}
        </div>
      )
    })
  }, [renderData])
  return (
    <div>
      <div style={{ textAlign: 'center' }}>
        <label>
          <input
            type="checkbox"
            checked={isStartTransition}
            onChange={(e) => {
              setIsStartTransition(e.target.checked)
            }}
          />
          useTransition
        </label>
        <input
          type="range"
          value={rangeValue}
          min={0}
          max={10000}
          style={{ width: 120 }}
          onChange={handleChange}
        />
        <span>进度条 {rangeValue}</span>
        <hr />
      </div>
      {jsx}
    </div>
  )
}

useDeferredValue 

useDeferredValue 需要接收一个值, 返回这个值的副本, 副本的更新会在值更新渲染之后进行更新, 以此来避免一些不必要的重复渲染. 打个比方页面中有输入框, 输入框下的内容依赖于输入框的值, 但是输入框是一个高频操作, 如果输入10次, 可能用户只想看到最终的结果那么中途的实时渲染就显得不那么重要了, 页面元素少点还好, 一旦元素过多页面就会及其的卡顿, 渲染引擎堵得死死的, 用户就会骂娘了, 此时使用useDeferredValue是一个很好的选择

import React, {
  useDeferredValue,
  useEffect,
  memo,
  useState,
  useMemo,
} from 'react'
 
const List = memo(({ count }) => {
  const [data, setData] = useState([])
  useEffect(() => {
    const data = []
    data.length = +5000
    for (let i = 0; i < data.length; i++) {
      data.fill(i + 1, i)
    }
    setData(data)
  }, [count])
 
  return (
    <div>
      {data.map((item) => (
        <p>{count}</p>
      ))}
    </div>
  )
})
 
export default function UseDeferValue() {
  const [inpVal, setInpVal] = useState('')
  const deferedValue = useDeferredValue(inpVal)
  console.log(inpVal, deferedValue)
  const memoList = useMemo(() => <List count={deferedValue} />, [deferedValue])
  return (
    <div>
      <h1>UseDeferValue</h1>
      <input
        type="number"
        value={inpVal}
        max={200000}
        onChange={(e) => setInpVal(e.target.value)}
      />
      {memoList}
    </div>
  )
}

useSyncExternalStore 

useInsertionEffect