路由实现
- history.pushState 和 history.replaceState popstate
- hash hashchange
React key的作用
react key 帮助 React 识别哪些元素改变了,比如被添加或删除。一个元素的 key 最好是这个元素在列表中拥有的一个独一无二的字符串。
关于 React Re-Render
1、有四个原因会导致组件 re-render:状态变化、父组件 re-render、Context 变化和 Hooks 变化。 2、如何避免父组件 re-render 导致的 re-render?React.memo!使用 React.memo 后的组件只有 props 变更才会触发 re-render。对于 props 很多且没有很多子组件的组件来说,相比 re-render,检查 props 是否变更带来的消耗可能更大。
虚拟dom的渲染原理
为什么使用 vitrual dom
提高开发效率:只需要告诉 React 你想让视图处于什么状态,React 则通过 VitrualDom 确保 DOM 与该状态相匹配。你不必自己去完成属性操作、事件处理、DOM 更新,React 会替你完成这一切。让我们更关注我们的业务逻辑而非 DOM 操作,这一点即可大大提升我们的开发效率
提升性能:VitrualDom 的优势在于 React 的 Diff 算法和批处理策略,React 在页面更新之前,提前计算好了如何进行更新和渲染 DOM。VitrualDom 帮助我们提高了开发效率,在重复渲染时它帮助我们计算如何更高效的更新,而不是它比DOM操作更快
跨浏览器兼容:React 基于 VitrualDom 自己实现了一套自己的事件机制,自己模拟了事件冒泡和捕获的过程,采用了事件代理,批量更新等方法,抹平了各个浏览器的事件兼容性问题
跨平台兼容:VitrualDom 为 React 带来了跨平台渲染的能力。以React Native 为例子。React 根据 VitrualDom 画出相应平台的 ui 层,只不过不同平台画的姿势不同而已
虚拟DOM实现原理
1. JSX 和 createElement
在实现一个React组件时可以选择两种编码方式,第一种是使用JSX编写,第二种是直接使用React.createElement编写。JSX只是为 React.createElement(component, props, ...children) 方法提供的语法糖。所有的JSX 代码最后都会转换成React.createElement(...) ,Babel帮助我们完成了这个转换的过程。
2. 创建虚拟dom
createElement()函数- 处理 props
- 将特殊属性 ref、key 从config中取出并赋值
- 将特殊属性 self、source 从config中取出并赋值
- 将除特殊属性的其他属性取出并赋值给 props
- 获取子元素
- 获取子元素的个数 —— 第二个参数后面的所有参数
- 若只有一个子元素,赋值给 props.children
- 若有多个子元素,将子元素填充为一个数组赋值给 props.children
- 处理默认 props
- 将组件的静态属性 defaultProps 定义的默认 props 进行赋值
- 处理 props
- 返回 ReactElement 对象(所谓的虚拟DOM)有以下几个属性
- type:元素的类型,可以是原生html类型(字符串),或者自定义组件(函数或class)
- key:组件的唯一标识,用于Diff算法,下面会详细介绍
- ref:用于访问原生dom节点
- props:传入组件的props
- owner:当前正在构建的Component所属的Component
?typeof:是一个Symbol类型的变量,这个变量可以防止 XSS- self:指定当前位于哪个组件实例(非生产环境)
- _source:指定调试代码来自的文件(fileName)和代码行数(lineNumber)(非生产环境)
虚拟DOM转换为真实DOM
调用 ReactDOM.render(element, container[, callback]) 将组件进行渲染
1. 初始参数处理
- 将当前组件使用 TopLevelWrapper 进行包裹
- 判断根结点下是否已经渲染过元素,如果已经渲染过,判断执行更新或者卸载操作
- 处理 shouldReuseMarkup 变量,该变量表示是否需要重新标记元素
- 调用将上面处理好的参数传入 _renderNewRootComponent,渲染完成后调用callback
- 在 _renderNewRootComponent 中调用 instantiateReactComponent 对我们传入的组件进行分类包装
- ReactDOMEmptyComponent:空组件
- ReactDOMTextComponent:文本
- ReactDOMComponent:原生DOM
- ReactCompositeComponent:自定义React组件
- 他们都具备以下三个方法
- construct:用来接收 ReactElement 进行初始化
- mountComponent:用来生成 ReactElement 对应的真实 DOM 或 DOMLazyTree
- unmountComponent:卸载 DOM 节点,解绑事件
- 在 _renderNewRootComponent 中调用 instantiateReactComponent 对我们传入的组件进行分类包装
2. 批处理、事务调用
- 在 _renderNewRootComponent 中使用 ReactUpdates.batchedUpdates 调用 batchedMountComponentIntoNode 进行批处理
- 在 batchedMountComponentIntoNode 中,使用 transaction.perform 调用 mountComponentIntoNode 让其基于事务机制进行调用
3. 生成html
- 在 mountComponentIntoNode 函数中调用 ReactReconciler.mountComponent 生成原生DOM节点
- mountComponent 内部实际上是调用了过程1生成的四种对象的 mountComponent 方法
4. 渲染html
- 在 mountComponentIntoNode 函数中调用将上一步生成的 markup 插入 container 容器
- 在首次渲染时,_mountImageIntoNode 会清空 container 的子节点后调用 DOMLazyTree.insertTreeBefore
针对性的性能优化
在IE(8-11)和Edge浏览器中,一个一个插入无子孙的节点,效率要远高于插入一整个序列化完整的节点树
- React 通过 lazyTree,在IE(8-11)和 Edge 中进行单个节点依次渲染节点
- 在其他浏览器中则首先将整个大的 DOM 结构构建好,然后再整体插入容器
- 单独渲染节点时,React 还考虑了 fragment 等特殊节点,这些节点则不会一个一个插入渲染
React 事件机制
React 自己实现了一套事件机制,其将所有绑定在虚拟 DOM 上的事件映射到真正的 DOM 事件,并将所有的事件都代理到 document 上,自己模拟了事件冒泡和捕获的过程,并且进行统一的事件分发
React 自己构造了合成事件对象 SyntheticEvent,这是一个跨浏览器原生事件包装器,它抹平了各个浏览器的事件兼容性问题
- 它具有与浏览器原生事件相同的接口,包括stopPropagation() 和 preventDefault() 等等
- 在所有浏览器中他们工作方式都相同
setState
setState() 将对组件 state 的更改排入队列批量推迟更新,并通知 React 需要使用更新后的 state 重新渲染此组件及其子组件。其实setState实际上不是异步,只是代码执行顺序不同,有了异步的感觉
- 同步更新
React 引发的事件处理(比如通过onClick引发的事件处理)
React 生命周期函数
- 异步更新
绕过React通过 addEventListener 直接添加的事件处理函数
通过 setTimeout || setInterval 产生的异步调用
注意事项
- 使用 setState() 改变状态之后,立刻通过this.state拿不到最新的状态
可以使用 componentDidUpdate() 或者 setState(updater, callback) 中的回调函数 callback 保证在应用更新后触发,通常建议使用 componentDidUpdate()
- setState合并,在合成事件和声明周期钩子中,多次调用会被优化为一次
为了更好的感知性能,React 会在同一周期内会对多个 setState() 进行批处理。通过触发一次组件的更新来引发回流。后调用的 setState() 将覆盖同一周期内先调用 setState() 的值。所以如果是下一个 state 依赖前一个 state 的话,推荐给 setState() 传 function
- 当组件已被销毁,调用
setState,React会报错
将数据挂载到外部,通过props传入
在组件内部维护一个状态量isUnmounted,componentWillMount中标记为true,在setState前进行判断
setState()被调用之后,源码执行栈(react 参照版本 15.6.0)
- React 组件继承自 React.Component,而 setState 是 React.Component 的方法,因此对于组件来讲 setState 属于其原型方法
- 调用
enqueueSetState()将 state 放入_pendingStateQueue队列中;调用enqueueCallback()将 callback 放入_pendingCallbacks队列中; - 调用
enqueueUpdate()如果处于批量更新模式,也就是 isBatchingUpdates 为 true 时,将需要更新的 component 添加到 dirtyComponents 数组中,否则 调用batchedUpdates()执行批量更新 - 调用
transaction.perform()发起事务- 初始化:事务初始化阶段没有注册方法,故无方法要执行
- 运行:执行 setSate 时传入的 callback 方法,一般不会传 callback 参数
- 结束:执行
RESET_BATCHED_UPDATESFLUSH_BATCHED_UPDATES这两个 wrapper 中的 close 方法RESET_BATCHED_UPDATES在 close 阶段将 isBatchingUpdates 设置为falseFLUSH_BATCHED_UPDATES在 close 阶段,调用 runBatchedUpdates 方法,循环遍历所有的 dirtyComponents ,调用 updateComponent 刷新组件,并执行它的 pendingCallbacks , 也就是 setState 中设置的 callback
错误边界
组件内的 js 错误会导致 React 的内部状态被破坏,并且在下一次渲染时产生可能无法追踪的错误。部分 UI 的 js 错误不应该导致整个应用崩溃。
错误组件是一种 React 组件,可以捕获并打印发生在其子组件树任何位置的js错误,并且渲染出备用 UI, 而不是那些崩溃的子组件树。
- 可以捕获的错误: 渲染期间,生命周期方法和整个组件树的构造函数
- 无法捕获的错误: 事件处理,异步代码(setTimeout回调函数),服务端渲染,它自身抛出来的错误(并非它的子组件)
- class 组件中定义了一下两个生命周期方法中的任意一个(或两个)
- static getDerivedStateFromError()渲染备用UI
- componentDidCatch()打印错误信息
错误边界的工作方式类似于 js 的 catch{},不同的地方在于错误边界只针对React组件, 只有 class 组件才可以成为错误边界组件。
如果一个错误边界无法渲染错误信息,则错误会冒泡到最近的上级错误边界,类似于catch{}的工作机制。
自React 16起,任何未被错误边界捕获的错误将会导致整个React组件树被卸
- 把错误的UI留在那里比完全移除它更糟糕
- 同时有可能发现一些已存在应用中但未曾注意到的崩溃
不能处理事件处理方法中的错误
- 与render方法和生命周期方法不同,事件处理器不会再渲染期间触发如果他们抛出异常,React仍然能够知道需要在屏幕上显示什么
- 可以使用try{}catch{}进行处理
Suspense
- Suspense主要是为了解决两个问题:代码分割、数据获取
- 使用 React.lazy 函数像渲染常规组件一样使用动态引入的组件
- 配合 React.Suspense 来实现加载时的降级展示
- fallback 将在加载过程中进行展示
- 模块加载失败(如网络问题),会触发一个错误,可以通过错误边界来处理
- 错误边界
- static getDerivedStateFromError(error) 被设计用于捕获 Render Phase 中的异常,不希望其中产生副作用
- componentDidCatch(error, info)用于在 Commit Phase 阶段捕获异常
- 原理
- 通过 throw 一个 Promise
- 然后 React.Suspense 通过上文中处理子组件错误的生命周期函数捕获到它
- 在它没有 resolve 时渲染 fallback
- resolve 后渲染实际内容
- 同时该机制内部还做了缓存处理,如果包含缓存数据就不执行 throw,以防止多次重复副作用的执行。
HOC 高阶组件
- 定义
- 高阶组件是 增强函数,输入一个元组件,返回一个新的增强组件
- 主要作用是 代码复用,操作状态和参数
- 用法
- 为组件包裹一层默认参数
function proxyHoc(Comp) { return class extends React.Component { render() { const newProps = { name: 'tayde', age: 1, } return <Comp {...this.props} {...newProps} /> } } } - 为组件进行包装
function withMask(Comp) { return class extends React.Component { render() { return ( <> <Comp {...this.props} /> <div style={{ width: '100%', height: '100%', backgroundColor: 'rgba(0, 0, 0, .6)', }} </> ) } } } - 通过props将被包裹组件中的state依赖外层,例如转为受控组件
function withOnChange(Comp) { return class extends React.Component { constructor(props) { super(props) this.state = { name: '', } } onChangeName = () => { this.setState({ name: 'dongdong', }) } render() { const newProps = { value: this.state.name, onChange: this.onChangeName, } return <Comp {...this.props} {...newProps} /> } } } const NameInput = props => (<input name="name" {...props} />) export default withOnChange(NameInput) - 根据条件,渲染不同的组件
function withLoading(Comp) { return class extends Comp { render() { if(this.props.isLoading) { return <Loading /> } else { return super.render() } } }; }
- 为组件包裹一层默认参数
- 应用场景
- 权限控制,通过抽象逻辑,统一对页面进行权限判断,按不同的条件进行页面渲染
- 性能监控,包裹组件的生命周期,进行统一埋点
- 使用注意
- 纯函数: 增强函数应为纯函数,避免侵入修改元组件
- 避免用法污染: 应透传元组件的无相关参数与事件
- 命名空间: 为HOC增加特异性的组件名称,便于开发调试和查找问题
- 引入传递: 如果需要传递元组件的refs引用,使用React.forwardRef
- 静态方法: 元组件上的静态方法无法被自从传出,会导致业务层无法调用
- 函数导出
- 静态方法赋值
- 重新渲染: 由于增强函数每次调用是返回一个新组件,因此如果在Render中使用增强函数,就会导致每次都重新渲染整个HOC,而且之前的状态也会丢失
React Hook
React中通常使用类定义或者函数定义创建组件。React 16.8 版本推出了 HOOK,通过它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性
- 优点:
- 跨组件复用: 改造成本小,不会影响原来的组件层次结构和不会有传说中的嵌套地狱
- 类定义更为复杂:
- 时刻需要关注 this 的指向问题
- class 给组件预编译、代码压缩、热加载等工作带来了很多困难
- 每个生命周期函数都包含某个业务逻辑的一部分,每个业务逻辑又被分散在每个生命周期函数中
- 代码复用代价高,高阶组件的使用会使整个组件树变得臃肿
- 状态与 UI 隔离: 由于 HOOK 的特性,状态逻辑会变成更小的粒度,并且极容易被抽象成一个自定 HOOK,组件中的状态和 UI 变得更为清晰和隔离
- 注意
- 避免在 循环/条件判断/嵌套函数 中调用 hooks,保证调用顺序的稳定
- 只有 函数定义组件和 hooks 中可以调用 hooks,避免在 类组件或者普通函数 中调用
- 不能在 useEffect 使用 useState,React 会报错提示
- 类组件不会被替换或废弃,不需要强制改造类组件,两种方式能并存
- 重要钩子
- useState(initState)
- 返回一对值:当前状态和用来更新它的函数
- 类似类组件的 this.setState,但不会自动合并新旧 state
- useEffect(callback, [source])
- 给函数组件提供了执行副作用(如数据获取、订阅或手动操作 DOM 等)的能力
- 它和类组件中的 componentDidMount、componentDidUpdate 及 componentWillUnmount 具有等效的用途,只不过被合并成了一个 API
- useState(initState)
React16 Fiber
- Fiber
- 是对核心算法的一次重新实现
- 问题:
- 随着应用变得越来越庞大,整个更新渲染的过程开始变得吃力,大量的组件渲染会导致主进程长时间被占用,导致一些动画或高频操作出现卡顿和掉帧的情况
- 解决
- 解决同步阻塞的方法,通常有两种: 异步 与 任务分割。而 React Fiber 便是为了实现任务分割而诞生的。
- react 的渲染过程不再是一旦开始就不能终止的模式
- render phase 可以被高优先级打断
- commit phase 不会被打断
- 优先级策略:
- 文本框输入 > 本次调度结束需完成的任务 > 动画过渡 > 交互反馈 > 数据更新 > 不会显示但以防将来会显示的任务
- 当遇到进程阻塞的问题时,任务分割、异步调用 和 缓存策略 是三个显著的解决思路
- 废弃掉 componentWill*
- componentWillMount
- componentWillReceiveProps
- componentWillUpdate
- 添加方法
- static getDerivedStateFromProps(nextProps, prevState)
- getSnapshotBeforeUpdate(prevProps, prevState)
- static getDerivedStateFromError(error)
react 优化
- 用React的生产版本
- 使用开发者工具中的分析器对组件进行分析
- 虚拟化长列表react-window、react-virtualized,会在有限的时间内仅渲染有限的内容,并奇迹般地降低重新渲染组件消耗的时间,以及创建DOM节点的数量
- 避免重新渲染:
- 使用 React.memo 创建函数组件
- 使用pureComponent组件
- 在组件中实现shouldComponentUpdate方法
- 使用不可突变数据immutable.js
- 列表中使用稳定常量作为key,而不是index
- 事件处理函数this绑定放在constructor中
- 不滥用props,只传递component需要的props
- 使用react-loadable异步加载组件
- 事件节流和防抖
React 预防 XSS攻击
- 所有的用户输入都需要经过HTML实体编码,React会在运行时动态创建DOM节点然后填入文本内容
- 在React 应用中确保不会注入任何没显式编写的数据,所有的数据在页面渲染之前都会被转换成字符串,这防止 XSS 进攻
React组件间复用逻辑状态
1. 高阶组件
- 缺点
- 组件中属性难以溯源,并且存在属性覆盖的问题
- 高阶组件改变了当前组件的层级结构,在 React Dev 工具中看到的结构将会变得非常深,这会加大调试的难度。
- 步骤
- 对原组件进行一层包裹,并将需要的状态注入其 props
- 这样在每个需要用到 FriendStatus 的组件外层都使用该高阶组件包裹,就可以在组件内拿到所需的状态
function withFriendStatus(WrappedComponent) {
return class extends Component {
state = {
isOnline: null,
}
handleStatusChange = ({ isOnline }) => this.setState({ state });
componentDidMount() {
ChatAPI.subscribeToFriendStatus(this.props.friend.id, this.handleStatusChange);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(this.props.friend.id, this.handleStatusChange);
}
render() {
return <WrappedComponent {...this.props} isOnline={this.state.isOnline} />
}
}
}
@withFriendStatus
class FriendListItem extends Component {
// this.props.isOnline
}
2. Render Props
-
缺点
- 改变了原有组件的层级结构,可能会带来 Wrapper Hell 的问题
- 由于其写法直接包裹了原组件的 render 部分,在使用多层 Render Props 时也会使编码过程中产生 Wrapper Hell,加大了阅读难度
-
优点
- 使用了一个接收函数的 prop 解决了逻辑复用,状态将通过该函数参数返回,同时该函数将继续渲染原组件内容
-
步骤
- 在使用 Render Props 时,我们只需要将原组件的 render 内容移至逻辑复用组件的 render prop 属性的函数中,此时就可以拿到所需的状态
class FriendStatusProvider extends Component {
state = {
isOnline: null,
}
handleStatusChange = ({ isOnline }) => this.setState({ state });
componentDidMount() {
ChatAPI.subscribeToFriendStatus(this.props.friend.id, this.handleStatusChange);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(this.props.friend.id, this.handleStatusChange);
}
render() {
return this.props.children(this.state.isOnline);
}
}
class FriendListItem extends Component {
render() {
return (
<FriendStatusProvider friend={this.props.friend}>
{isOnline => (
// 原有组件 render 内容
)}
</FriendStatusProvider>
);
}
}
3. 自定义 Hook
- 优点
- 自定义 Hook 没有改变原有组件层级结构,避免了 Wrapper Hell
- 同时轻量、完全独立,易于复用及社区共享
- 步骤
- 将要复用的逻辑提取到一个函数中,它被称作自定义 Hook
- 在使用自定义 Hook 时也和调用函数相同,它和将自定义 Hook 中代码拷贝至调用处的执行结果相同
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
// ...
);
}