干嘛用的
useDeferred的注释了写明了使用路径 用户输入 到 获取数据
加上官网也写了useDeferredValue
是一个和Suspense
结合使用的方法。
Suspense
我们可以先回顾一下<Suspense>
如何使用的
父组件
javascript
const Demo = () => {
const [value, setValue] = useState("");
return (
<>
<input
value={value}
onChange={(v) => {
setValue(v.target.value);
}}
/>
<Suspense fallback={<>loading</>}>
<SearchResults value={value} />
</Suspense>
</>
);
};
子组件
javascript
function SearchResults(props: { value: string }) {
const random = use(fetchData(props.value));
return <div>{random + " " + props.value}</div>;
}
use钩子
use钩子接受一个promise对象,在本文中promise对象来自下面的fetchData方法。在use中,我们会为这个promise对象增加三个属性,这三个属性分别就对应了promise本身的状态。当我们的promise的状态不为fulfilled
时,就向外throw promise或者error,来触发外层的suspense渲染fallback。当promise的状态为fulfilled
时,则返回promise实际的value。
typescript
function use(
promise: Promise<unknown> & {
status: string;
value: unknown;
reason: unknown;
}
) {
if (promise.status === "fulfilled") {
return promise.value;
} else if (promise.status === "rejected") {
throw promise.reason;
} else if (promise.status === "pending") {
throw promise;
} else {
promise.status = "pending";
promise.then(
(result) => {
promise.status = "fulfilled";
promise.value = result;
},
(reason) => {
promise.status = "rejected";
promise.reason = reason;
}
);
throw promise;
}
}
fetchData方法
我们通过延迟三秒来模拟接口返回的情况,在实际的业务场景中,就是我们请求数据的方法
typescript
const map = new Map();
const fetchData = (str: string) => {
if (!map.has(str)) {
map.set(
str,
new Promise((resolve) => setTimeout(() => resolve(Math.random()), 3000))
);
}
return map.get(str);
};
简单来说,我们通过use钩子对请求方法返回的promise进行二次包装,目的就是为了当promise处于未完成的状态时,保证<Suspense>
渲染fallback。<Suspense>
这个组件本身会在其children
第一次throw promise
时,为这个promise注册一个回调,在promise完成时重新渲染children
知道了Suspense
怎么用,我们再看看如何结合deferredValue。
useDeferredValue
其实非常简单,我们修改一下父组件看看效果。
ini
const Deferred = () => {
const [value, setValue] = useState("");
const deferred = useDeferredValue(value);
return (
<>
<input
value={value}
onChange={(v) => {
setValue(v.target.value);
}}
/>
<Suspense fallback={<>fallback</>}>
<SearchResults value={deferred} />
</Suspense>
</>
);
};
随着我们的输入,不断发起请求,然后进入到fallBack中
可以看到随着用户输入不再会渲染Suspense
的fallback了。可以理解为用户的输入的优先级较高,因此input
中的onChange
会高优先级执行,导致了函数组件的重新执行,但是deferred
由于被useDeferredValue
包裹,因此会延迟更新,通过下面的动图可以看到每次输入都会引起组件的更新,但是并不会引起deferred
的值的更新。当我们停止输入后最终deferred
和value
的值同步了。
这时候会发现,把Suspense
去掉也是一样的效果。其实是react帮我们兜底了,大家可以试一下替换SearchResults。当我们向外throw promise时,react也会帮我们进行处理,这个行为与Suspense
无关
javascript
let a = true;
export default function SearchResults(props: { value: string }) {
// const random = use(fetchData(props.value));
if (a) {
throw new Promise((res) =>
setTimeout(() => {
a = false;
res(1999);
}, 5000)
);
}
return <div>{"random" + " " + props.value}</div>;
}
useTransition
useTransition则不需要传递参数,返回[isPending, setTransition]
其中setTransition
使用时需要传递一个函数,这个函数会以低优先级的方式执行,并且具有合并执行的特效(多次调用只生效一次)。乍一听和useDeferredValue
类似。
实际使用时setTransition
则更加灵活。因为我们在使用useDeferredValue
时,react帮我们决定了何时为这个deferred值附新值,但是我们可以通过setTransition
来直接控制何时进行低优先级操作。而useDeferredValue
的使用场景更多的是原值在组件中不可控,比如说我们上面的例子中子组件接受props,把props通过useDeferredValue
进行包裹,因为props的控制权不在子组件中。
同样的,受控组件的onchange也最好使用useState
的set方法。
ini
const Transition = () => {
const [value, setValue] = useState("");
const [transedValue, setTransedValue] = useState("");
const [, setTransition] = useTransition();
return (
<>
<input
value={value}
onChange={(v) => {
setValue(v.target.value);
setTransition(() => {
setTransedValue(v.target.value);
});
}}
/>
<Suspense fallback={<>fallback</>}>
<SearchResults value={transedValue} />
</Suspense>
</>
);
};
上面这段代码和使用useDeferredValue
时的效果应该是一样的
接下来再看一下useTransition
的批处理特性。
ini
const Transition = () => {
const [value, setValue] = useState("");
const [transedValue, setTransedValue] = useState("");
const [, setTransition] = useTransition();
return (
<>
<input
value={value}
onChange={(v) => {
setTransition(() => {
setValue(v.target.value);
setTransedValue(v.target.value);
});
}}
/>
<Suspense fallback={<>fallback</>}>
<SearchResults value={transedValue} />
</Suspense>
</>
);
};
可以看到我们不停的输入,但是由于批处理,很多输入被忽略了。这也是为什么受控组件不适合useTransition
的原因。但是如果我们在setTransition
中增加一些输出,会发现输出并没有被合并,因为函数其实是照常执行的,只是如果在执行时有高优先级的任务,那么会被高优先级的任务中断。