8 个问题带你进阶 React

155 阅读8分钟

问题列表

  • 高阶组件(HOC) , render props 以及 hook 的对比和用处.
  • 虚拟 DOM 是什么?
  • react diff 原理, 如何从 O(n^3) 变成 O(n)
    • 为什么要使用 key , 有什么好处?
  • jsx 的原理
    • 自定义的 React 组件为何必须大写
  • setState 什么时候是同步,什么时候是异步?
  • React 如何实现自己的事件机制?
    • React 事件和原生事件有什么区别
  • 聊一聊 fiber 架构
  • React 事件中为什么要绑定 this 或者 要用箭头函数, 他们有什么区别

一. 高阶组件(HOC) , render props 以及 hook 的对比和用处.

创建 HOC 的方式

学习 HOC 我们只需要记住以下 2 点定义:

  1. 创建一个函数, 该函数接收一个组件作为输入除了组件, 还可以传递其他的参数
  2. 基于该组件返回了一个不同的组件.

代码如下:

function withSubscription(WrappedComponent, selectData) {  
returnclass extends React.Component {  
constructor(props) {  
super(props);  
this.state = {  
data: selectData(DataSource, props)  
};  
}  
// 一些通用的逻辑处理  
render() {  
// ... 并使用新数据渲染被包装的组件!  
return<WrappedComponent data={this.state.data} {...this.props} />;  
}  
};  
}

HOC 的优点

  • 不会影响内层组件的状态, 降低了耦合度

HOC 的缺点

  • 固定的 props 可能会被覆盖.
<MyComponent  
x="a"  
y="b"  
/>

这 2 个值,如果跟高阶组件的值相同, 那么 x,y 都会被来自高阶组件的值覆盖.

// 如果 withMouse 和 withPage 使用了同样的 props, 比如 x , y.  
// 则会有冲突.  
exportdefault withMouse(withPage(MyComponent));
  • 它无法清晰地标识数据的来源

withMouse(MyComponent) 它不会告诉你组件中包含了哪些 props , 增加了调试和修复代码的时间.

render props

功能: 将一个组件内的 state 作为 props 传递给调用者, 调用者可以动态的决定如何渲染.

创建 render props 的方式

  1. 接收一个外部传递进来的 props 属性
  2. 将内部的 state 作为参数传递给调用组件的 props 属性方法.

代码如下:

class Mouse extends React.Component {  
constructor(props) {  
super(props);  
this.state = { x: 0, y: 0 };  
}  
  
render() {  
return (  
<div style={{ height: '100%' }}>  
// 使用 render props 属性来确定要渲染的内容  
{this.props.render(this.state)}  
</div>  
);  
}  
}  
// 调用方式:  
<Mouse render={mouse => (  
<p>鼠标的位置是 {mouse.x},{mouse.y}</p>  
)}/>

缺点

  • 无法在 return 语句外访问数据

它不允许在 return 语句之外使用它的数据. 比如上面的例子, 不能在 useEffect 钩子或组件中的任何其他地方使用 x 和 y 值, 只能在 return 语句中访问.

  • 嵌套

它很容易导致嵌套地狱. 如下代码:

const MyComponent = () => {  
return (  
<Mouse>  
{({ x, y }) => (  
<Page>  
{({ x: pageX, y: pageY }) => (  
<Connection>  
{({ api }) => {  
// yikes  
}}  
</Connection>  
)}  
</Page>  
)}  
</Mouse>  
)  
};

基于上面的两种方式,都有他们的缺点,那我们来到最香的解决方案: hook

hook

我们来看一个例子:

const { x, y } = useMouse();  
const { x: pageX, y: pageY } = usePage();  
  
useEffect(() => {  
  
}, [pageX, pageY]);

从上面的代码可以看出, hook 有以下的特性. 它解决了上面 hoc 和 render props 的缺点.

  • hook 可以重命名

如果 2 个 hook 暴露的参数一样,我们可以简单地进行重命名.

  • hook 会清晰地标注来源

从上面的例子可以简单地看到, x 和 y 来源于 useMouse. 下面的 x, y 来源于 usePage.

  • hook 可以让你在 return 之外使用数据
  • hook 不会嵌套
  • 简单易懂, 对比 hoc 和 render props 两种方式, 它非常直观, 也更容易理解.

总结

在 hook、render props 和 HOC(higher-order components) 之间选择时,应该尽可能使用 hook.

二. 虚拟 DOM 是什么?

在 React 中, React 会先将代码转换成一个 JS 对象, 然后再将这个 JS 对象转换成真正的 DOM. 这个 JS 对象就是所谓的虚拟 DOM.

它可以让我们无须关注 DOM 操作, 只需要开心地编写数据,状态即可.

三. react diff 原理, 如何从 O(n^3) 变成 O(n)

  • 为什么是 O(n^3) ?

从一棵树转化为另外一棵树,直观的方式是用动态规划,通过这种记忆化搜索减少时间复杂度。由于树是一种递归的数据结构,因此最简单的树的比较算法是递归处理。确切地说,树的最小距离编辑算法的时间复杂度是 O(n^2m(1+logmn)), 我们假设 m 与 n 同阶, 就会变成 O(n^3)。 推荐阅读(为什么是 O(n^3))[1]:

  • react diff 原理

简单的来讲, react 它只比较同一层, 一旦不一样, 就删除. 这样子每一个节点只会比较一次, 所以算法就变成了 O(n).

对于同一层的一组子节点. 他们有可能顺序发生变化, 但是内容没有变化. react 根据 key 值来进行区分, 一旦 key 值相同, 就直接返回之前的组件, 不重新创建.

这也是为什么渲染数组的时候, 没有加 key 值或者出现重复key值会出现一些奇奇怪怪的 bug . 

除了 key , 还提供了选择性子树渲染。开发人员可以重写 shouldComponentUpdate 提高 diff 的性能。

推荐阅读(diff 原理)[2]

四. jsx 的原理

<div>Hello ConardLi</div>

实际上, babel 帮我们将这个语法转换成

React.createElement('div', null, `Hello ConardLi`)
自定义组件必须大写的原因.

babel 在编译的过程中会判断 JSX 组件的首字母, 如果是小写, 则为原生 DOM 标签, 就编译成字符串. 如果是大写, 则认为是自定义组件. 编译成对象.

为什么以下代码会报错?
return (<a></a><a></a>)

同样的, 因为我们是按照 React.createElement() 来创建组件, 所以只能有一个根节点. 如果你想要使用 2 个平行的节点, 可以用 <></> 来包裹. <></> 会被编译成 <React.Fragment/>. babel 转译如下:

c8070012fc43e608e417a61c42eb9610.png babel 转换[3]

五. setState 什么时候是同步,什么时候是异步?

这里的“异步”不是说异步代码实现. 而是说 react 会先收集变更,然后再进行统一的更新.

setState 在原生事件和 setTimeout 中都是同步的. 在合成事件和钩子函数中是异步的.

在 setState 中, 会根据一个 isBatchingUpdates 判断是直接更新还是稍后更新, 它的默认值是 false. 但是 React 在调用事件处理函数之前会先调用 batchedUpdates 这个函数, batchedUpdates 函数 会将 isBatchingUpdates 设置为 true. 因此, 由 react 控制的事件处理过程, 就变成了异步(批量更新).

六.  React 里面的事件机制.

我们先看看 冒泡捕获 的经典图:

da1c41dba8a86dfc90fcef2e92649cf6.png 在组件挂载的阶段, 根据组件生命的 react 事件, 给 document 添加事件 addEventListener, 并添加统一的事件处理函数 dispatchEvent.

将所有的事件和事件类型以及 react 组件进行关联, 将这个关系保存在一个 map 里. 当事件触发的时候, 首先生成合成事件, 根据组件 id 和事件类型找到对应的事件函数, 模拟捕获流程, 然后依次触发对应的函数.

如果原生事件使用 stopPropagation 阻止了冒泡, 那么合成事件也被阻止了.

七. 什么是 React Fiber

背景: 由于浏览器它将 GUI 描绘,时间器处理,事件处理,JS 执行,远程资源加载统统放在一起。如果执行 js 的更新, 占用了太久的进程就会导致浏览器的动画没办法执行,或者 input 响应比较慢。 react fiber 使用了 2 个核心解决思想:

  • 让渲染有优先级
  • 可中断 React Fiber 将虚拟 DOM 的更新过程划分两个阶段,reconciler 调和阶段与 commit 阶段. 看下图:

9e5c4b75710519375429632ea48b2262.png

一次更新过程会分为很多个分片完成, 所以可能一个任务还没有执行完, 就被另一个优先级更高的更新过程打断, 这时候, 低优先级的工作就完全作废, 然后等待机会重头到来.

调度的过程

requestIdleCallback

df17e42d40ac6145fc7af11aff764e79.png 首先 react 会根据任务的优先级去分配各自的过期时间 expriationTime . requestIdleCallback 在每一帧的多余时间(黄色的区域)调用. 调用 channel.port1.onmessage , 先去判断当前时间是否小于下一帧时间, 如果小于则代表我们有空余时间去执行任务, 如果大于就去执行过期任务,如果任务没过期. 这个任务就被丢到下一帧执行了. 由于 requestIdleCallback 的兼容性问题, react 自己实现了一个 requestIdleCallback 推荐阅读(司徒正美 React Fiber 架构)[5]

八. React 事件中为什么要绑定 this 或者要用箭头函数?

事实上, 这并不算是 react 的问题, 而是 this 的问题. 但是也是 react 中经常出现的问题. 因此也讲一下

<button type="button" onClick={this.handleClick}>Click Me</button>

这里的 this . 当事件被触发且调用时, 因为 this 是在运行中进行绑定的.this 的值会回退到默认绑定,即值为 undefined,这是因为类声明和原型方法是以严格模式运行。 我们可以使用 bind 绑定到组件实例上. 而不用担心它的上下文.

因为箭头函数中的 this 指向的是定义时的 this,而不是执行时的 this. 所以箭头函数同样也可以解决.

参考资料

[1]

推荐阅读(为什么是 O(n^3)): github.com/Advanced-Fr…

[2]

推荐阅读(diff 原理): www.infoq.cn/article/rea…

[3]

babel 转换: babeljs.io/repl

[4]

推荐阅读(动画浅析 React 事件系统和源码): www.lzane.com/tech/react-…

[5]

推荐阅读(司徒正美 React Fiber 架构): zhuanlan.zhihu.com/p/37095662

转载自高级前端进阶# 8 个问题带你进阶 React 文章来源于前端加加 ,作者广州peen