STAY HUNGRY , STAY FOOLISH.

求知若饥,虚心若愚。

       浏览:

React常见问题及解答(下)

上一章,主要介react中的css和部分js知识点,这章将继续 react 相关问题的解答。

  • 1.react事件处理函数为什么使用this.xxx.bind(this)或xxx() = () => {}?
  • 2.react中通过props传递到纯函数有哪些坑?
  • 3.react中map组件之后为什么需要声明key关键字?
  • 4.上面提到diff,那什么是diff算法?传统diff是怎样的?React diff?Vue diff?
  • 5.上面提到的Virtual DOM,什么是 Virtual DOM?为什么 Virtual DOM 比原生 DOM 快?
  • 6.Vue 和 React 之间的区别?
  • 7.React Fiber是什么?
  • 8.React Hook是什么?
  • 9.React Hook会取代Redux吗?
  • 10.{…this.props}是什么?
  • 11.Fragment是什么?
  • 12.dangerouslySetInnerHTML是什么?
  • 13.React项目中应不应该使用ts呢?

1.react事件处理函数为什么使用this.xxx.bind(this)或xxx() = () => {}?

在react中定义函数的方式有:
方式一:
this.handleClick.bind(this)

1
2
3
4
5
6
7
8
9
10
import React, { PureComponent } from 'react';
class Comp extends PureComponent {
handleClick() { ... }
render() {
return (
<button onClick={this.handleClick.bind(this)} />
)
}
}
export default Comp

方式二:
this.handleClick + this.handleClick = this.handleClick.bind(this)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { PureComponent } from 'react';
class Comp extends PureComponent {
constructor(props){
super(props);
this.handleClick = this.handleClick.bind(this)
}
handleClick() { ... }
render() {
return (
<button onClick={this.handleClick} />
)
}
}
export default Comp

方式三:
this.handleClick + handleClick = (e)=> {}

1
2
3
4
5
6
7
8
9
10
import React, { PureComponent } from 'react';
class Comp extends PureComponent {
handleClick = (e)=> { ... }
render() {
return (
<button onClick={this.handleClick} />
)
}
}
export default Comp

推荐写法,当然是方式三。但是我们要明白,为什么这样写?


首先要明确,在react中定义的所有组件都会被JSX语法转义成一个Object对象
1.如果只有onClick={this.handleClick},会被转义成以下形式:

1
2
3
4
5
6
7
const Comp = {
onClick:function(){
console.log(this)
}
}
const handleClick = Comp.onClick; // 注意这里
handleClick(); // { parent:Window. opener: null,... }

如果写法是onClick={this.handleClick},此时handleClick是中间变量,处理函数中的this指向会丢失。
在浏览器执行的话,结果在控制台输出的this是window对象,而非Comp对象

那么我们想将this指向当前Comp对象的话,该怎样做呢?
解决这个问题的办法是给声明函数时填加bind(this),从而使得无论事件处理函数如何传递,this指向都是当前实例化对象

2.如果是onClick={this.handleClick.bind(this)},会被转义成以下形式:

1
2
3
4
5
6
7
const Comp = {
onClick:function(){
console.log(this)
}
}
const handleClick = Comp.onClick.bind(Comp); // 注意这里
handleClick(); // { onClick:f }

bind理解上可能比较困难,还是想用onClick={this.handleClick}实现,该怎么做呢?于是乎诞生出方式三的写法。
3.如果是onClick={this.handleClick} + handleClick = (e)=> { ... },会被转义成以下形式:

1
2
3
4
5
6
7
8
9
const Comp = {
onClick:function(){
console.log(this)
}
}
const handleClick = () => { // 注意这里
Comp.onClick()
};
handleClick(); // { onClick:f }

是不是很神奇?么在外面包了一个() => {},this指向就从window变成Comp了。


2.react中通过props传递到纯函数有哪些坑?

在react中纯函数组件props传递,可以是一个基本类型、对象、数组、也可以是函数

1
2
3
4
5
6
7
8
9
const Lesson = (props) => (
<div>
<p>{ props.user.name }</p>
<Checkbox
checked={props.checked}
onChange={props.checkAllChange}
/>
</div>
)

上面demo中声明了一个Lesson组件,且从props中传来user、checked、checkAllChange。
如何使用这个Lesson组件呢?

1
2
3
4
5
6
7
8
...
checkAllChange = (e) => {}
...
<Lesson
user={{name:'ww', age: 26}}
checked={false}
checkAllChange={(e) => { this.checkAllChange(e) }}
/>

注意的地方是checkAllChange,该方法能获取到Lesson组件中Checkbox的内置event
如果换成:

1
checkAllChange={ this.checkAllChange(e) }

是没办法获取到DOM的event,及event下的target的。


如果是纯函数组件嵌套纯函数组件呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const Section = (props) => (
<div>
<p>{ props.name }</p>
<Checkbox
checked={props.checked}
onChange={(e) => {props.checkChange(e)}}
/>
</div>
)
const Lesson = (props) => (
<div>
<p>{ props.user.name }</p>
<Checkbox
checked={props.checked}
onChange={props.checkAllChange}
/>
{props.sections.map((item, index) => (
<Section
{...props}
key={index}
name={item.name}
checked={item.checked}
/>
))}
</div>
)

如何使用这个Lesson组件呢?

1
2
3
4
5
6
7
8
9
10
...
checkAllChange = (e) => {}
checkChange = (e) => {}
...
<Lesson
user={{name:'ww', age: 26}}
checked={false}
checkAllChange={(e) => { this.checkAllChange(e) }}
checkChange={this.checkChange}
/>

可以发现想获取纯函数组件中子组件的event时,需要在子组件中声明() => {}

1
onChange={(e) => {props.checkChange(e)}}

而想获取纯函数父组件中的event时,只需要在使用该组件的地方声明() => {}

1
checkAllChange={(e) => { this.checkAllChange(e) }}

注意区别。


3.react中map组件之后为什么需要声明key关键字?

先说结论吧,通过设置唯一 key,对 element diff 进行算法优化
说白话,就是有了key属性后,就可以与组件建立一种对应关系,react 根据key来决定是销毁重新创建组件还是更新组件


4.上面提到diff,那什么是diff算法?传统diff是怎样的?React diff?Vue diff?

说diff之前,先说说什么是Virtual DOM?(可以先看下一节,再看回该节)
那什么是diff算法呢?
diff算法是一种优化手段,当状态发生改变的时候,重新构造一个新的Virtual DOM,然后根据与老的Virtual DOM对比,生成patches补丁,打到对应的需要修改的地方。

1.传统Diff?

计算两颗树形结构差异并进行转换,传统diff算法是这样做的:循环递归每一个节点

diff_real

比如左侧树a节点依次进行如下对比,左侧树节点b、c、d、e亦是与右侧树每个节点对比 算法复杂度能达到O(n^2),n代表节点的个数

1
a->e、a->d、a->b、a->c、a->a

查找完差异后还需计算最小转换方式,最终达到的算法复杂度是O(n^3)
这里引用司徒正美的话:

最开始经典的深度优先遍历DFS算法,其复杂度为O(n^3),存在高昂的diff成本,然后是cito.js的横空出世,它对今后所有虚拟DOM的算法都有重大影响。它采用两端同时进行比较的算法,将diff速度拉高到几个层次。紧随其后的是kivi.js,在cito.js的基出提出两项优化方案,使用key实现移动追踪及基于key的编辑长度距离算法应用(算法复杂度 为O(n^2))。但这样的diff算法太过复杂了,于是后来者snabbdom将kivi.js进行简化,去掉编辑长度距离算法,调整两端比较算法。速度略有损失,但可读性大大提高。再之后,就是著名的vue2.0 把snabbdom整个库整合掉了。


2.React diff?

传统diff算法复杂度达到O(n^3)这意味着1000个节点就要进行数10亿次的比较,这是非常消耗性能的。react大胆的将diff的复杂度从O(n^3)降到了O(n),它是如何做到的呢?
a.diff 策略

  • react实现的diff是同层级比较
  • 拥有相同类型的两个组件产生的DOM结构也是相似的,不同类型的两个组件产生的DOM结构则不近相同
  • 对于同一层级的一组子节点,通过分配唯一id进行区分(key值) 基于如上,React分别对tree diff、component diff 、element diff 进行了算法优化。

b.tree diff
基于策略一,React的diff非常简单明了:只会对同一层次的节点进行比较

diff_react


c.component diff
由于React是基于组件开发的,所以组件的dom diff其实也非常简单,如果组件是同一类型,则进行tree diff比较。如果不是,则直接放入到patches中。即使是子组件结构类型都相同,只要父组件类型不同,都会被重新渲染。

如下图,当 component D 改变为 component G 时,即使这两个 component 结构相似,一旦 React 判断 D 和 G 是不同类型的组件,就不会比较二者的结构,而是直接删除 component D,重新创建 component G 以及其子节点。

deff_react_component


d.element diff
当节点处于同一层级时,React diff 提供三种节点操作,分别为:INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)

如下图,老集合中包含节点:A、B、C、D,更新后的新集合中包含节点:B、A、D、C,此时新老集合进行 diff 差异化对比,发现 B != A,则创建并插入 B 至新集合,删除老集合 A;以此类推,创建并插入 A、D和 C,删除 B、C 和 D。

react_diff_element

最后总结下react diff:

  • react通过定制大胆的diff 策略,将diff复杂度从O(n^3)降到O(n)
  • 通过同层级比较tree diff进行算法优化
  • 通过相同类生成相似树形结构,不同类生成不同树形结构component diff进行算法优化
  • 通过设置唯一 key,对element diff进行算法优化

3.Vue diff

记得之前说过vue很多模式其实借鉴react,因此vue diff和react diff没啥区别,只能说实现方式稍有不同,源码不同,但大致的思想和策略是一样的。 实在要说区别的话,react的diff是facebook自己实现的,vue的diff是通过整合snabbdom库实现的。 vue diff简单例子: 比如下图存在这两棵 需要比较的新旧节点树 和 一棵需要修改的页面 DOM树。
vue_dom

a.第一轮同级比较
因为父节点都是 1,所以开始比较他们的子节点; 按照我们上面的比较逻辑,所以先找相同 && 不需移动的点; 毫无疑问,找到 2;

vue_compare


b.第二轮相同保留节点、不同移动节点
没有相同 && 不需移动的节点;只能第二个方案,开始找相同的点; 找到 节点5,相同但是位置不同,所以需要移动。

vue_move

结果,页面 DOM 树需要移动DOM ,不修改,原样移动。

vue_move2


c.第三轮创建、删除节点
继续,相同节点没了,只能创建了; 所以要根据 新Vnode 中没找到的节点去创建并且插入; 然后旧Vnode 中有些节点不存在 新VNode 中,所以要删除。

vue_add

于是开始创建节点 6 和 9,并且删除节点 4 和 5;

vue_remove

然后页面就完成更新。


5.上面提到的Virtual DOM,什么是 Virtual DOM?为什么 Virtual DOM 比原生 DOM 快?

相较于 DOM 来说,操作 JS 对象会快很多,并且我们也可以通过 JS 来模拟 DOM:

1
2
3
4
5
6
7
8
9
10
const ul = {
tag: 'ul',
props: {
class: 'list'
},
children: {
tag: 'li',
children: '1'
}
}

上述代码对应的 DOM 是:

1
2
3
<ul class='list'>
<li>1</li>
</ul>

那么既然 DOM 可以通过 JS 对象来模拟,反之也可以通过 JS 对象来渲染出对应的 DOM。当然了,通过 JS 来模拟 DOM 并且渲染对应的 DOM 只是第一步,难点在于如何判断新旧两个 JS 对象的最小差异并且实现局部更新 DOM

首先 DOM 是一个多叉树的结构,如果需要完整的对比两颗树的差异,那么需要的时间复杂度会是 O(n ^ 3),这个复杂度肯定是不能接受的。于是 React 团队优化算法,实现 O(n) 的复杂度来对比差异。 实现 O(n) 复杂度的关键就是只对比同层的节点,而不是跨层对比,这也是考虑到在实际业务中很少会去跨层的移动 DOM 元素。 所以判断差异的算法就分为两步:

  • 首先从上至下,从左往右遍历对象,也就是树的深度遍历,这一步中会给每个节点添加索引,便于最后渲染差异
  • 一旦节点有子元素,就去判断子元素是否有不同

在第一步算法中我们需要判断新旧节点的 tagName 是否相同,如果不相同的话就代表节点被替换。如果没有更改 tagName 的话,就需要判断是否有子元素,有的话就进行第二步算法。 在第二步算法中,我们需要判断原本的列表中是否有节点被移除,在新的列表中需要判断是否有新的节点加入,还需要判断节点是否有移动

举个例子来说,假设页面中只有一个列表,我们对列表中的元素进行了变更:

1
2
3
4
// 假设这里模拟一个 ul,其中包含了 5 个 li
[1, 2, 3, 4, 5]
// 这里替换上面的 li
[1, 2, 5, 4]

从上述例子中,我们一眼就可以看出先前的 ul 中的第三个 li 被移除了,四五替换了位置。
那么在实际的算法中,我们如何去识别改动的是哪个节点呢?这就引入了 key 这个属性,想必大家在 Vue 或者 React 的列表中都用过这个属性。这个属性是用来给每一个节点打标志的,用于判断是否是同一个节点。 当然在判断以上差异的过程中,我们还需要判断节点的属性是否有变化等等。 当我们判断出以上的差异后,就可以把这些差异记录下来。当对比完两棵树以后,就可以通过差异去局部更新 DOM,实现性能的最优化。 当然 Virtual DOM 提高性能是其中一个优势,其实最大的优势还是在于:
1.将 Virtual DOM 作为一个兼容层,让我们还能对接非 Web 端的系统,实现跨端开发,比如:react nativeweexreact vr等。
2.通过 Virtual DOM 我们可以渲染到其他的平台,实现 SSR、同构渲染等,比如:next.jsnuxt.js
3.实现组件的高度抽象化



5.上面提到的Virtual DOM,什么是 Virtual DOM?为什么 Virtual DOM 比原生 DOM 快?

相较于 DOM 来说,操作 JS 对象会快很多,并且我们也可以通过 JS 来模拟 DOM:

1
2
3
4
5
6
7
8
9
10
const ul = {
tag: 'ul',
props: {
class: 'list'
},
children: {
tag: 'li',
children: '1'
}
}

上述代码对应的 DOM 是:

1
2
3
<ul class='list'>
<li>1</li>
</ul>

那么既然 DOM 可以通过 JS 对象来模拟,反之也可以通过 JS 对象来渲染出对应的 DOM。当然了,通过 JS 来模拟 DOM 并且渲染对应的 DOM 只是第一步,难点在于如何判断新旧两个 JS 对象的最小差异并且实现局部更新 DOM

首先 DOM 是一个多叉树的结构,如果需要完整的对比两颗树的差异,那么需要的时间复杂度会是 O(n ^ 3),这个复杂度肯定是不能接受的。于是 React 团队优化算法,实现 O(n) 的复杂度来对比差异。 实现 O(n) 复杂度的关键就是只对比同层的节点,而不是跨层对比,这也是考虑到在实际业务中很少会去跨层的移动 DOM 元素。 所以判断差异的算法就分为两步:

  • 首先从上至下,从左往右遍历对象,也就是树的深度遍历,这一步中会给每个节点添加索引,便于最后渲染差异
  • 一旦节点有子元素,就去判断子元素是否有不同

在第一步算法中我们需要判断新旧节点的 tagName 是否相同,如果不相同的话就代表节点被替换。如果没有更改 tagName 的话,就需要判断是否有子元素,有的话就进行第二步算法。 在第二步算法中,我们需要判断原本的列表中是否有节点被移除,在新的列表中需要判断是否有新的节点加入,还需要判断节点是否有移动

举个例子来说,假设页面中只有一个列表,我们对列表中的元素进行了变更:

1
2
3
4
// 假设这里模拟一个 ul,其中包含了 5 个 li
[1, 2, 3, 4, 5]
// 这里替换上面的 li
[1, 2, 5, 4]

从上述例子中,我们一眼就可以看出先前的 ul 中的第三个 li 被移除了,四五替换了位置。
那么在实际的算法中,我们如何去识别改动的是哪个节点呢?这就引入了 key 这个属性,想必大家在 Vue 或者 React 的列表中都用过这个属性。这个属性是用来给每一个节点打标志的,用于判断是否是同一个节点。 当然在判断以上差异的过程中,我们还需要判断节点的属性是否有变化等等。 当我们判断出以上的差异后,就可以把这些差异记录下来。当对比完两棵树以后,就可以通过差异去局部更新 DOM,实现性能的最优化。 当然 Virtual DOM 提高性能是其中一个优势,其实最大的优势还是在于:
1.将 Virtual DOM 作为一个兼容层,让我们还能对接非 Web 端的系统,实现跨端开发,比如:react nativeweexreact vr等。
2.通过 Virtual DOM 我们可以渲染到其他的平台,实现 SSR、同构渲染等,比如:next.jsnuxt.js
3.实现组件的高度抽象化


6.Vue 和 React 之间的区别?

1.Vue支持双向绑定,React不支持。 Vue 的表单可以使用 v-model 支持双向绑定,相比于 React 来说开发上更加方便,当然了 v-model 其实就是个语法糖,本质上和 React 写表单的方式没什么区别。

2.改变数据方式不同,Vue是this.datax,React是setState。 改变数据方式不同,Vue 修改状态相比来说要简单许多,React 需要使用 setState 来改变状态,并且使用这个 API 也有一些坑点。并且 Vue 的底层使用了依赖追踪,页面更新渲染已经是最优的,但是 React 还是需要用户手动去优化这方面的问题。 React 16以后,有些钩子函数会执行多次,这是因为引入Fiber的原因。

3.页面实现方式不同,Vue是HTML,React是JSX。 React 需要使用 JSX,有一定的上手成本,并且需要一整套的工具链支持,但是完全可以通过 JS 来控制页面,更加的灵活。Vue 使用模板语法,相比于 JSX 来说没有那么灵活,但是完全可以脱离工具链,通过直接编写 render 函数就能在浏览器中运行。

在生态上来说,两者其实没多大的差距,当然 React 的用户是远远高于 Vue 的。 在上手成本上来说,Vue 一开始的定位就是尽可能的降低前端开发的门槛,然而 React 更多的是去改变用户去接受它的概念和思想,相较于 Vue 来说上手成本略高

React 和 Vue 虽然是两个不同的框架,但是他们的底层原理都是很相似的,无非在上层堆砌了自己的概念上去。所以我们无需去对比到底哪个框架牛逼,引用尤雨溪的一句话:

说到底,就算你证明了 A 比 B 牛逼,也不意味着你或者你的项目就牛逼了… 比起争这个,不如多想想怎么让自己变得更牛逼吧。


7.React Fiber是什么?

React Fiber是个什么东西呢?
官方的一句话解释是“React Fiber是对核心算法的一次重新实现”。这么说似乎太虚无缥缈,简单理解就是react官方对diff进行了再次优化

React Fiber是React v16发布的,因此,我们知道这个概念就行。
对于我们开发者来说,用React v16之前,和用React v16之后感觉到网页性能更高了一些,仅此而已。

想深入理解React Fiber,看具体怎么优化的,可参考以下链接:


8.React Hook是什么?

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性
简单理解就是react提供的一系列API,关于纯函数组件如何使用state

基础Hook API有:useStateuseEffectuseContext
额外Hook API有:useReduceruseCallbackuseMemouseRefuseImperativeHandleuseLayoutEffectuseDebugValue

具体用法可参考react官方文档:
https://zh-hans.reactjs.org/docs/hooks-overview.html


9.React Hook会取代Redux吗?

我认为:不会。因为Redux 是一种架构,而 React Hook 是针对纯函数组件准备的状态管理库,两者并没啥太大的关系。
看关注知乎疑问:
React Hooks是否能取代Redux?
https://www.zhihu.com/question/324199539

React Hooks 越来越火了,它会取代传统的 Redux 吗?
https://segmentfault.com/a/1190000019913694


10.{…this.props}是什么?

1
<Lesson {...this.props} />

...是ES6扩展运算符,这个写法表示当前组件props属性里的值全部传给了子组件Lesson


11.Fragment是什么?

在JSX语法中,render 里是必须有一个根DOM的,如果出现两个DOM的情况,又不想额外增加一个无效的 DOM 元素,<div></div>的话,就用Fragment:

1
2
3
4
5
6
7
8
9
10
11
import React, { PureComponent, Fragment } from 'react';
...
render() {
return (
<Fragment>
<div></div>
<p></p>
</Fragment>
)
}
...

Fragment组件相当于<></>,既然官方提供了Fragment,那就使用Fragment来声明空节点,规范规范。


12.dangerouslySetInnerHTML是什么?

dangerouslySetInnerHTML 是React标签的一个属性,用于有<p>、<span>、<br/>等DOM元素样式的富文本

比如后台提供的字符串为以下内容:

1
text: "瓢虫在哪儿呢?她能找到自己的家吗?<br>Where is the ladybird? Can she find her home?↵<div style='color: red;font-size: 18px;'>红色18字体测试</div>"

如果写成这样,会全部当成字符串处理掉;

1
<div>{ props.text }</div>

想保留原有的标签样式的话,dangerouslySetInnerHTML上场:

1
2
3
<div dangerouslySetInnerHTML={{
__html: `${props.item.section}`,
}}></div>

13.React项目中应不应该使用ts呢?

个人喜好啦,我是不会的,虽然说能提高代码的可读性,强类型能减少bug的产生。但用了ts后导致项目代码量增大,还得去深入学习TypeScript,真没什么必要,不适合快速迭代
况且ts普及率还没那么高,JavaScript都够我学习一辈子的了,等以后普及了再说吧。