为什么要放弃虚拟节点,使用fiber
yunlaiwu.github.io/blog/2017/0…
计算和比对Virtual DOM,最后更新DOM树,这整个过程是同步进行的,也就是说只要一个加载或者更新过程开始,那React就以不破楼兰终不还的气概,一鼓作气运行到底,中途绝不停歇。
虚拟dom转化为真实dom的流程
创建虚拟dom
jsx是createElement的语法糖,它用于创建虚拟dom。
function Hello() {
return <div>Hello, world!</div>;
}
//被转化为
function Hello() {
return React.createElement("div", null, "Hello, world!");
}
虚拟dom转化为真实dom
通过render函数转化,第一个参数是虚拟dom(JavaScript对象),第二个参数是容器。
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
1.Reconciliation(协调)阶段和Commit(提交)
Reconciliation
React根据最新的状态和属性生成一个新的虚拟DOM树。然后,React会将新的虚拟DOM树与旧的虚拟DOM树进行比较。React会通过一个称为"diffing"的算法来找出哪些部分需要更新。
-
只对同级的 react element 进行对比。如果一个 DOM 节点在前后两次更新中跨越了层级,那么 React 不会尝试复用它;
-
两个不同类型(type 字段不一样)的 react element 会产生不同的 react element tree。例如元素 div 变为 p,React 会销毁 div 及其子孙节点,并新建 p 及其子孙节点;
-
开发者可以通过 key 属性来暗示哪些子元素在不同的渲染下能保持稳
前面的描述是第一版描述,稍微有点不准确。以下是第二版描述
调和阶段是用来生成新的workInProgress Tree(它是Fiber Node组成的树)的。有人说,虚拟DOM转换为fiber的过程叫reconcile。
Commit
计算出的差异应用到实际的DOM上
经过上面的对比找出了「差异」之后,React 知道了“哪些 react element 要被删除”、“哪些 react element 需要添加子节点”、“哪些 react element 位置需要移动”、“哪些 react element 的属性需要更新”等等的一系列操作,这些操作会被看作一个个更新任务(work)。每个 react element 自身的更新任务(work)会存储在与这个 react element 对应的 fiber node 中。
在 渲染阶段(Render phase) ,Reconciliation 会从 fiber node tree 最顶端的节点开始,重新对整棵 fiber node tree 进行 深度优先遍历,遍历树中的每一个 fiber node,处理 fiber node 中存储的 work。
前面的描述是第一版描述,稍微有点不准确。以下是第二版描述
commit阶段通过新的workInProgress Tree来更新真实Dom(这个过程不可中断)。
参考资料(第一版):juejin.cn/post/689124…
参考资料(第二版):juejin.cn/post/711413…
为什么要使用双缓存 Fiber tree:juejin.cn/post/699327… (虚拟dom直接转换为真实dom的话整个过程不可中断,但是构建 workInprogres 的过程可以中断,假设这个阶段还有更新,就会重新构建 workInprogresTree 。等到 workInprogres 完成之后,就会将 workInprogres 转化为真实 DOM。
effectList是什么?
EffectList的每一个元素是一个fiber节点(准确来说,所有effectTag不为空的fiber节点)。effectTag是fiber节点的一个属性,用来标记该fiber节点应该进行怎样的更新(新增,删除还是更新)
此外,除了需要更新的节点。如果节点有副作用,比如componentDidMount、componentDidUpdate和componentWillUnmount、useEffect里的代码都属于副作用。该节点也会被添加到EffectList。
它们在协调阶段生成,在提交阶段依次执行。
2 前端路由
为什么需要前端路由
在SPA应用中,在地址栏输入的路由不同,但返回的都是同一个页面。app会根据url的不同,映射不同的内容。
实现hash模式
记住一点window.location.hash可以获取当前hash值,也可以改变当前hash值。
window.addEventListener('hashchange')来监听hash值的改变
//最外层监听hash的改变,传给子组件
import React, { useState, useEffect } from 'react';
const HashRouter = ({ children }) => {
const [hash, setHash] = useState(window.location.hash);
useEffect(() => {
const onHashChange = () => {
setHash(window.location.hash);
};
window.addEventListener('hashchange', onHashChange);
return () => {
window.removeEventListener('hashchange', onHashChange);
};
}, []);
return <>{children(hash)}</>;
};
//Link组件用于改变hash值
const Link = ({ to, children }) => {
const handleClick = (e) => {
e.preventDefault();
window.location.hash = to;
};
return (
<a href={`#${to}`} onClick={handleClick}>
{children}
</a>
);
};
//Route根据hash值决定展示组件还是null
const Route = ({ path, component: Component, currentHash }) => {
const currentPath = currentHash.slice(1) || '/';
return currentPath === path ? <Component /> : null;
};
实现history模式
重点:window.addEventListener('popstate')可以监听路由变化。
唯一的区别是通过window.history.pushState改变路由。
const Link = ({ to, children }) => {
const handleClick = (e) => {
e.preventDefault();
window.history.pushState(null, '', to);
window.dispatchEvent(new Event('popstate'));
};
return (
<a href={to} onClick={handleClick}>
{children}
</a>
);
};
React-router-dom有哪些组件
<Router>包裹在最外层,<Swich>用来包裹<Router>.<Router>用来保存映射关系。<Link>用于跳转。
<Router>
<div>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/contact">Contact</Link>
</li>
</ul>
</nav>
{/* 使用 Switch 和 Route 定义各个页面对应的组件 */}
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/contact" component={Contact} />
</Switch>
</div>
</Router>
3.hook只能放在最外层
什么是hook
不编写类的情况下使用 state、生命周期等特性
如果放在条件判断里
不能保证每次重新渲染的时候,所有的hook调用顺序一致。
这里详细解释了为什么不能放在条件语句中:juejin.cn/post/711673…
每次重新执行函数式组件,会重新执行useState(),而useState()给你返回的state,实际上存储在一个数组中,可以通过values[index]来获取你需要的state。每次执行useState(),index会增加1。试想,如果我跳过了第一个useState(),执行第二个useState()时,index仍为0,那执行第二个useState()获取的就是第一个state的值。
4.渲染列表的时候,index不要作为key
要选用固定的值作为key。比如删除列表中第一个元素,导致整个列表的index减1,导致整个列表重新渲染。
5.useEffect和useLayoutEffect的区别是什么
共同点
useEffect 和 useLayoutEffect 都在组件渲染后执行。组件渲染后指DOM节点已经更新,但浏览器仍未绘制下一帧。
不同点
useEffect异步执行回调函数,不会阻塞浏览器渲染。
useLayoutEffect同步执行回调函数,会先执行完,才会执行浏览器渲染。
6.shouldComponentUpdate
类组件的一个方法,用于觉得props变更时,组件是否刷新。它会接受新的props和新的state,返回值为fasle表示不更新组件。
shouldComponentUpdate(nextProps, nextState) {
return fasle;
}
7.React.memo高阶组件
parentValue改变,导致父组件重新渲染。如果不用Memo包裹,子组件也会重新渲染。如果用Memo包裹,子组件不会重新渲染。
仅当childValue改变,子组件才会重新渲染。
import React, { useState, memo } from "react";
// 创建一个简单的子组件
const ChildComponent = (props) => {
console.log("ChildComponent rendered!");
return <div>Child Component: {props.value}</div>;
};
// 使用 React.memo 包裹子组件
const MemoizedChildComponent = memo(ChildComponent);
function ParentComponent() {
const [parentValue, setParentValue] = useState(0);
const [childValue, setChildValue] = useState(0);
return (
<div>
<div>
Parent Value: {parentValue}
<button onClick={() => setParentValue(parentValue + 1)}>Increment Parent Value</button>
</div>
<div>
Child Value: {childValue}
<button onClick={() => setChildValue(childValue + 1)}>Increment Child Value</button>
</div>
{/* 使用 memoized 子组件 */}
<MemoizedChildComponent value={childValue} />
</div>
);
}
export default ParentComponent;