还在转自大佬的笔记
写于 2019年12月31日17:58:14
明天就是 2020 俗称的 ABAB,我不可能回到过去,那就走向未来吧。
———————————————————————————————
其实React 属于自己的发明并不多,比如虚拟DOM、组件化思想并不是Facebook原创。但JSX是React真正的创造,我认为这是React最
"伟大"的发明就是 “JSX”,
JSX是 react 的骨骼,它搭起了 React 应用的组件,是整个项目的组件框架基础。
"不就是HTML in JS"吗? 有什么神奇之处呢?请继续阅读。
JSX就是丑陋的模板
直观上来看,JSX 是将 HTML 直接嵌入在了JS代码里面,这是刚开始接触React时,很多人最不能接受的设定。因为前端开发者被“表现和逻辑层分离”这种思想“洗脑”太久了:表现和逻辑耦合在一起,在某种程度上是一种混乱和困扰。
但是从现在发展来看,JSX完全符合“真香定律”:JSX 让前端实现真正意义上的组件化成为了可能。
可能有读者认为JSX很简单,但是你真的理解它了吗?试着回答这么几个问题:
如何在jsx中调试代码
为什么JSX中不能直接使用 if else
在回答这些问题之前,先来看看 JSX 是如何实现条件渲染的。
JSX 多种姿势实现条件渲染
很常见的一个场景: 渲染一个列表。但是需要满足:列表为空数组时,显示空文案“Sorry, the list is empty”。同时列表数据可能通过网络获取,存在列表没有初始值为null的情况。
JSX 实现这种条件渲染最简洁的手段就是三目运算符:
const list = ({list}) => {
const isNull = !list
const isEmpty = !isNull && !list.length
return(
<div>
{
isNull
? null
:(
isEmpty?<p> Sorry, the list is empty </p>
:<div>
{
list.map(item => <ListItem item = {item} />)
}
</div>
)
}
</div>
)
}
但是我们多加几个状态那么立刻变成回调地狱现场,下面伪代码只是为了演示理解使用:
const list = ({isLoading, list. error}) => {
return (
<div>
{
condition1
? <Component1 />
: (
condition2
? <Component2 />
: (
condition3
? <Component3 />
: <Component4 />
)
)
}
</div>
)
}
那么正常的我们在逻辑代码如何破解这种嵌套呢? 我们常用的手段就是抽离出来 render function:
const getListContent = (isLoading, list, error) => {
console.log(list)
console.log(isLoading)
console.log(error)
// ...
return ...
}
const list = ({isLoading, list, error}) => {
return (
<div>
{
getListContent(isLoading, list, error)
}
</div>
)
}
甚至直接使用IIFE
备注: (IIFE 立即调用函数)
const list = ({isLoading, list, error}) => {
return (
<div>
{
(() => {
console.log(list)
console.log(isLoading)
console.log(error)
if (error) {
return <span>Something is wrong!</span>
}
if (!error && isLoading) {
return <span>Loading...</span>
}
if (!error && !isLoading && !list.length) {
return <p>Sorry, the list is empty </p>
}
if (!error && !isLoading && list.length > 0) {
return <div>
{
list.map(item => <ListItem item={item} />)
}
</div>
}
})()
}
</div>
)
}
这样一来就可以使用console.log进行简单调试了,也可以使用if...else 进行条件渲染。
再回到问题的本源:“为什么不能直接在JSX中使用if...else, 只能借用函数逻辑实现呢” ?
实际上,我们都知道JSX会被编译为React.createElement。 直白来说, React.createElement 的底层逻辑是无法运行JavaScript代码的,而它只能渲染一个结果。因此JSX中除了JS表达式,不能直接写JavaScript语法。准确的来说,JSX 只是函数调用和表达式的语法糖。
React程序员天天都在使用JSX, 但并不是所有人都明白其背后原理的。
JSX 的强大和灵活
虽然JSX只是函数调用和表达式的语法糖,但是JSX 仍然具有强大而灵活的能力。React 组件复用最流行的方式都是在JSX能力基础之上的,比如 HoC, 比如 render prop 模式:
class WindowWidth extends React.Component {
constructor() {
super()
this.state = {
width: 0
}
}
componentDidMount() {
this.setState(
{
width: window.innerWidth
},
window.addEventListener('resize', ({target}) => {
this.setState({
width: target.innerWidth
})
})
)
}
render() {
return this.props.children(this.state.width)
}
}
<WindowWidth>
{
width => (width > 800 ? <div>show</div> : null)
}
<WindowWidth>
甚至,我们还可以让 JSX 具有 Vue template 的能力:
render() {
const visible = true
return (
<div>
<div v-if={visible}>
content
</div>
</div>
)
}
render() {
const list = [1, 2, 3, 4]
return (
<div>
<div v-for={item in list}>
{item}
</div>
</div>
)
}
因为 JSX 总要进行一步编译, 在这个编译过程中我们借助AST(抽象语法树) 对 v-if 、v-for 进行处理即可。
你真的了解异步的 this.setSate吗?
绝大多数React开发者都知道 this.setState 是异步执行的, 但是我会说“你这个结论是错误的!”,那么 this.setState 到底是异步执行还是同步执行?这是一个问题......
this.setState 全是异步执行吗?
this.setSate 这个API, 官方描述为:
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall.
setState()并不总是立即更新组件。它可能会批处理或延迟更新,直到稍后。这使得在调用setState()之后立即读取This.state成为一个潜在的陷阱。
批处理 延迟更新,
实际上,React 控制的事件处理过程, setSate 一定不全是异步执行,也不全是同步执行。所谓的“延迟更新”并不是针对所有情况。
实际上, React 控制的事件处理过程, setState 不会同步更新 this.state。而在React 控制之外的情况,setSate 会同步更新 this.state。
但是什么是React控制内外呢?举个例子:
onClick() {
this.setState({
count: this.state.count + 1
})
}
componentDidMount() {
document.querySelectorAll('#btn-raw')
.addEventListener('click', this.onClick)
}
render() {
return (
<React.Fragment>
<button id="btn-raw">
click out React
</button>
<button onClick={this.onClick}>
click in React
</button>
</React.Fragment>
)
}
id 为 btn-raw 的 button 上绑定的事件,是在 componentDidMount 方法中通过 addEventListener 完成的,这是脱离于React 事件之外的,因为它是同步更新的。反之,代码中第二个 button 所绑定的事件处理函数对应的 setState 是异步更新的。 这样的设计也不难理解,通过“延迟更新”,可以达到更好的性能。
this.setState promise 化
官方提供了这种处理异步更新的方法。 其中之一就是 setState 接受第二个参数, 作为状态跟新后的回调。 但这无疑又带来了我们熟悉的
callback hell 问题。
举一个场景,我们在开发一个tabel, 这个table类似excel,当用户敲下回车键时,需要将光标移动到下一行,这是一个 setState 操作,然后马上聚焦,这又是一个 setState 操作。如果当前行就是最后一行,那用户敲下回车时,需要创建一个新行,这是第一个 setSate 操作,同时将光标移动到新的“最后一行”, 这是第二个 setState 操作; 在这个新行中进行聚焦,这是第三个 setState 操作。 这些 setState 操作依赖于前一个 setState 的完成。
面对这种场景,如果我们不想出现回调地狱的场景。常见的处理方式是利用生命周期方法, 在 componentDidUpdate 中进行相关操作。第一次 setState 进行完后, 在其触发的 componentDidUpdate中进行第二次 setState,依此类推。
但是这样做也有缺点:逻辑过于分散,存储面向生命周期编程的情况。
解决回调地狱其实是我们前端工程师的的拿手菜了,最直接的方案就是将 setState Promise 化:
const setStatePromise = (me, state) => {
new Promise(resolve => {
me.setState(state, () => {
resolve()
})
})
}
这只是 patch 做法,如果修改 React 源码的话,也不困难:

原生事件 VS React 合成事件
对 React 熟悉的可能知道:
React 中的事件机制并不是原生的那一套,事件没有绑定到在原生DOM上,大多数事件绑定在 document 上(除了少数不会冒泡到document的事件,如 video 等)
同时,触发的事件也是对原生事件的包装,并不是原生event
出于性能因素考虑,合成事件(syntheiticEvent)是被池化的。这意味着合成事件对象将会被重用,在调用事件回调之后所有属性将会被废弃。这样做可以大大节省内存,而不会频繁的创建和销毁事件对象。
这样的事件系统设计,无疑性能更加友好,但同时也带来了几个潜在现象。
现象1:异步访问事件对象
我们不能以异步的方式访问事件合成对象:
function handleClick(e) {
console.log(e)
setTimeout(() => {
console.log(e)
}, 0)
}
上述代码第二个 console.log 总将会输出 undefined
为此 React 也贴心的为我们准备了持久化合成事件的方法:
function handleClick(e) {
console.log(e)
e.persist()
setTimeout(() => {
console.log(e)
})
}
现象2:如何阻止冒泡
在 React 中,直接使用 e.stopPropagation 不能阻止原生事件冒泡,因为事件早已经冒泡 到了 document 上, React 此时才能够处理事件句柄。
如代码:
componentDidMount() {
document.addEventListener('click', () => {
console.log('document click')
})
}
handleClick = e => {
console.log('div click')
e.stopPropagation()
}
render() {
return (
<div onClick={this.handleClick}>
click
</div>
)
}
执行后会打印出 div click, 之后是 document click。 e.stopPropagation 是没有用的。
但是 React 的合成事件还给使用原生事件留了一个口子,通过合成事件上的nativeEvent 属性,我们还是可以访问原生事件。原生事件上的 stoplmmediatePropagation 方法: 除了能做到像 stopPropagation 一样阻止事件向父级冒泡之外,也能阻止当前元素剩余的、同类型事件的执行(第一个 click 触发时,调用 e.stoplmmediatePropagtion 阻止当前元素第二个click 事件的触发)。因此这一段代码:
componentDidMount() {
document.addEventListener('click', () => {
console.log('document click')
})
}
handleClick = e => {
console.log('div click')
e.nativeEvent.stopImmediatePropagation()
}
render() {
return (
<div onClick={this.handleClick}>
click
</div>
)
}
只会打印出 div click
请不要再背诵 Diff 算法了
很多开发者在面试中能“背诵” 出 React DOM diff 算法的方式,熟悉那著名的“三个假设”(不了解的读者可先自行学习),可是你真的懂 Dif 算法吗?
我们通过一个侧面来剖析 Diff 算法的细节。
Element diff 的那些事儿
我们都知道 React 把对比俩个树的时间复杂度从On立方降低到大On,三个假设也是老生常谈了。但是关于兄弟列表的diff细节,React 叫做 element diff,我们可以展开一下。
React 三个假设在对比element时,存在短板,于是需要开发者给每一个 element 通过提供key,这样react可以准确地发现新旧集合中的节点中相同的节点,对于相同节点无需进行节点删除和创建,只需要将旧集合中节点的位置进行移动,更新为新集合中节点的位置。

组件 1234,变为 2134,此时 React 给出的 diff 结果为2, 4不做任何操作;1, 3进行移动操作即可。
也就是元素在旧集合中的位置,相比新集合中的位置更靠后的话,那么它就不需要移动,当然这种diff听上去就并非完美无缺的。
我们来看这么一种情况:

实际只需对4执行移动操作,然而由于4在旧集合中的位置是最大的,导致其他节点全部移动,移动到4节点后面。
这无疑是愚蠢的,性能较差。针对这种情况,官方建议:
在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作
实际上很多类React类库(Inferno.js, Preact.js)都有了更优的element diff 移动策略。
有 key 就一定“性能最优”吗?
刚才提到,在进行 element diff时:由于 key 的存在,react 可以精确地判断出该节点在新集合中是否存在,这极大地提高了 element diff 效率。
但是加了 key 一定要比没加 key的性能更高吗?
我们来看这个场景,集合 [1,2,3,4] 渲染成 4 组数字, 注意仅仅是数字这么简单
<div id="1">1</div>
<div id="2">2</div>
<div id="3">3</div>
<div id="4">4</div>
当它变为[2, 1, 4, 5]: 删除了 3, 增加了 5,按照之前的算法,我们把1 放在 2后面, 删除 3, 再新增 5。整个操作移动了一次dom节点,删除和新增一共2处节点。
由于 dom 节点的移动操作开销是比较昂贵的,其实对于这种简单的 node text 更改情况,我们不需要再进行类似的 element diff 过程,只需要更改 dom.textContent 即可。
const startTime = performance.now()
$('#1').textContent = 2
$('#2').textContent = 1
$('#3').textContent = 4
$('#4').textContent = 5
console.log('time consumed:' performance.now() - startTime)
这么看,也许没有key的情况下要比有 key 的性能更好。
文章采用 知识共享署名 4.0 国际许可协议 进行许可,转载时请注明原文链接。