请求瀑布流是如何出现的
终于,进入严肃的编码时间了。我们现在有了需要被移动的块,也知道他们是如何被组合起来的,就可以写问题项追逐app了。让我们从刚刚的示例代码开始写:
const App = () => {
return (
<>
<Sidebar />
<Issue />
</>
);
};
const Sidebar = () => {
return; // some sidebar links
};
const Issue = () => {
return (
<>
<Comments />
</>
);
};
const Comments = () => {
return; // some issue comments
};
然后,我们把获取数据的操作放进一个钩子里:
export const useDtat = (url) => {
const [state, setState] = useState();
useEffect(() => {
const dataFetch = async () => {
const data = await (await fetch(url)).json();
setState(data);
}
dataFetch();
}, [url])
return { data: state };
}
然后,就在Comment组件可以快乐的调用了:
const Comments = () => {
// fetch is triggered in useEffect there, as normal
const { data } = useData('/get-comments');
// show loading state while waiting for the data
if (!data) return 'loading';
// rendering comments now that we have access to them!
return data.map((comment) => <div>{comment.title}</div>);
};
然后,就在Issue组件也类似:
const Issue = () => {
// fetch is triggered in useEffect there, as normal
const { data } = useData('/get-issue');
// show loading state while waiting for the data
if (!data) return 'loading';
// render actual issue now that the data is here!
return (
<div>
<h3>{data.title}</h3>
<p>{data.description}</p>
<Comments />
</div>
);
};
还有在App组件调用:
const App = () => {
// fetch is triggered in useEffect there, as normal
const { data } = useData('/get-sidebar');
// show loading state while waiting for the data
if (!data) return 'loading';
return (
<>
<Sidebar data={data} />
<Issue />
</>
);
}:
然后,就完成了~
代码示例: advanced-react.com/examples/14…
但这个应用有一个小问题,非常的慢。
我们这个示例,是一个典型的瀑布流请求。还记得React的生命周期吗,只有当组件被return时,它们才会变挂载,渲染,然后在触发获取数据的钩子。在我们的例子中,每一个组件在等待数据加载时,都是显示 loading。只有当数据加载完成后,才会切换为另一个组件,再触发其获取数据的钩子,并如此循环往复。
如果你想向用户尽快展示页面,使用瀑布流不是最好的方案。接下来,我们将会介绍处理瀑布流问题的方案。
如何处理瀑布流问题
Promise.all 方法
最简单的方法,就是尽可能早地获取所有数据。在这个示例中,就是在App组件处获取所有数据。但要注意的是,我们仅仅是普通的调用,是远远不够的。这样的代码是不行的:
useEffect(async () => {
const sidebar = await fetch('/get-sidebar');
const issue = await fetch('/get-issue');
const comments = await fetch('/get-comments');
}, [])
这样的代码不过是另一个瀑布流。加载所有接口的时间是 1s + 2s + 3s = 6s。我们需要一次性调用所有接口。如此一来,我们只需要等待三秒,获得了50%的性能提升!
其中一个方法是 Promise.all:
useEffect(async () => {
const [sidebar, issue, comments] = await Promise.all([
fetch('/get-sidebar'),
fetch('/get-issue'),
fetch('/get-comments'),
]);
}, []);
之后,要把所有的值作为状态存在父组件中,再把这些状态作为属性传给子组件。
const useAllData = () => {
const [sidebar, setSidebar] = useState();
const [comments, setComments] = useState();
const [issue, setIssue] = useState();
useEffect(() => {
const dataFetch = async () => {
// waiting for allthethings in parallel
const result = (
await Promise.all([
fetch(sidebarUrl),
fetch(issueUrl),
fetch(commentsUrl),
])
).map((r) => r.json());
// and waiting a bit more - fetch API is cumbersome
const [sidebarResult, issueResult, commentsResult] =
await Promise.all(result);
// when the data is ready, save it to state
setSidebar(sidebarResult);
setIssue(issueResult);
setComments(commentsResult);
};
dataFetch();
}, []);
return { sidebar, comments, issue };
};
const App = () => {
// all the fetches were triggered in parallel
const { sidebar, comments, issue } = useAllData();
// show loading state while waiting for all the data
if (!sidebar || !comments || !issue) return 'loading';
// render the actual app here and pass data from state to children
return (
<>
<Sidebar data={state.sidebar} />
<Issue
comments={state.comments}
issue={state.issue}
/>
</>
);
};
代码示例: advanced-react.com/examples/14…
如此一来,应用启动就快了很多。
并行Promises 方法
但是,如果我不想等到所有接口加载完数据才展示内容,该怎么办。在我们这个例子中,Comment的数据是最慢获取的,也是最不重要的。为了等待这个数据,而给用户造成卡顿,真的值得吗?我们可以一起调用所有的接口,同时又保证这些接口互相独立吗?
当然可以!我们只需要将那些使用 async/await 语法的获取操作转换为合适的传统 Promise 形式,并在 then 回调函数中保存数据。
fetch('/get-sidebar')
.then((data) => data.json())
.then((data) => setSidebar(data));
fetch('/get-issue')
.then((data) => data.json())
.then((data) => setIssue(data));
fetch('/get-comments')
.then((data) => data.json())
.then((data) => setComments(data));
如此一来,每一个数据获取都是独立的。那么App就可以先展示Siderbar和Issue组件的内容了。
const App = () => {
const { sidebar, issue, comments } = useAllData();
// show loading state while waiting for sidebar
if (!sidebar) return 'loading';
// render sidebar as soon as its data is available
// but show loading state instead of issue and comments while we're waiting for them
return (
<>
<Sidebar data={sidebar} />
{/*render local loading state for issue here if its data not available*/}
{/*inside Issue component we'd have to render 'loading' for empty comments as well*/}
{issue ? <Issue comments={comments} issue={issue} /> : 'loading''}
</>
)
}
在这里,一旦Sidebar、Issue和评论Comments组件的数据可用,我们就立即渲染它们 —— 这与初始的瀑布流模式行为完全相同。但由于我们并行发起了这些请求,总体等待时间将从 6 秒降至仅 3 秒。我们在保持应用程序行为不变的情况下,大幅提升了其性能!
代码示例: advanced-react.com/examples/14…
但是,需要注意的是,我们在组件的最顶端触发了三次状态修改,也就触发了三次重新渲染。这些不必要的重新渲染,也会影响应用的性能,但也要视应用的大小、组件在组件树的位置而定。