上一章,主要介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)
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)
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)=> {}
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}
,会被转义成以下形式:
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)}
,会被转义成以下形式:
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)=> { ... }
,会被转义成以下形式:
const Comp = {
onClick:function(){
console.log(this)
}
}
const handleClick = () => { // 注意这里
Comp.onClick()
};
handleClick(); // { onClick:f }
是不是很神奇?么在外面包了一个() => {}
,this指向就从window变成Comp了。
2.react中通过props传递到纯函数有哪些坑?
在react中纯函数组件props传递,可以是一个基本类型、对象、数组、也可以是函数
。
const Lesson = (props) => (
<div>
<p>{ props.user.name }</p>
<Checkbox
checked={props.checked}
onChange={props.checkAllChange}
/>
</div>
)
上面demo中声明了一个Lesson组件,且从props中传来user、checked、checkAllChange。 如何使用这个Lesson组件呢?
...
checkAllChange = (e) => {}
...
<Lesson
user={{name:'ww', age: 26}}
checked={false}
checkAllChange={(e) => { this.checkAllChange(e) }}
/>
注意的地方是checkAllChange
,该方法能获取到Lesson组件中Checkbox的内置event
。
如果换成:
checkAllChange={ this.checkAllChange(e) }
是没办法获取到DOM的event,及event下的target的。
如果是纯函数组件嵌套纯函数组件呢?
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组件呢?
...
checkAllChange = (e) => {}
checkChange = (e) => {}
...
<Lesson
user={{name:'ww', age: 26}}
checked={false}
checkAllChange={(e) => { this.checkAllChange(e) }}
checkChange={this.checkChange}
/>
可以发现想获取纯函数组件中子组件的event时,需要在子组件中声明() => {}
onChange={(e) => {props.checkChange(e)}}
而想获取纯函数父组件中的event时,只需要在使用该组件的地方声明() => {}
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算法是这样做的:循环递归每一个节点
比如左侧树a节点依次进行如下对比,左侧树节点b、c、d、e亦是与右侧树每个节点对比 算法复杂度能达到O(n^2)
,n代表节点的个数
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非常简单明了:只会对同一层次的节点进行比较
。
c.component diff
由于React是基于组件开发的,所以组件的dom diff其实也非常简单,如果组件是同一类型,则进行tree diff比较
。如果不是,则直接放入到patches中
。即使是子组件结构类型都相同,只要父组件类型不同,都会被重新渲染。
如下图,当 component D 改变为 component G 时,即使这两个 component 结构相似,一旦 React 判断 D 和 G 是不同类型的组件,就不会比较二者的结构,而是直接删除 component D,重新创建 component G 以及其子节点。
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:
- 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树。
a.第一轮同级比较
因为父节点都是 1,所以开始比较他们的子节点; 按照我们上面的比较逻辑,所以先找相同 && 不需移动
的点; 毫无疑问,找到 2;
b.第二轮相同保留节点、不同移动节点
没有相同 && 不需移动
的节点;只能第二个方案,开始找相同的点; 找到 节点5,相同但是位置不同,所以需要移动。
结果,页面 DOM 树需要移动DOM ,不修改,原样移动。
c.第三轮创建、删除节点 继续,相同节点没了,只能创建了; 所以要根据 新Vnode 中没找到的节点去创建并且插入; 然后旧Vnode 中有些节点不存在 新VNode 中,所以要删除。
于是开始创建节点 6 和 9,并且删除节点 4 和 5;
然后页面就完成更新。
5.上面提到的Virtual DOM,什么是 Virtual DOM?为什么 Virtual DOM 比原生 DOM 快?
相较于 DOM 来说,操作 JS 对象会快很多,并且我们也可以通过 JS 来模拟 DOM:
const ul = {
tag: 'ul',
props: {
class: 'list'
},
children: {
tag: 'li',
children: '1'
}
}
上述代码对应的 DOM 是:
<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 的话,就需要判断是否有子元素
,有的话就进行第二步算法。 在第二步算法中,我们需要判断原本的列表中是否有节点被移除,在新的列表中需要判断是否有新的节点加入,还需要判断节点是否有移动
。
举个例子来说,假设页面中只有一个列表,我们对列表中的元素进行了变更:
// 假设这里模拟一个 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 native
、weex
、react vr
等。
2.通过 Virtual DOM 我们可以渲染到其他的平台,实现 SSR、同构渲染等,比如:next.js
、nuxt.js
。
3.实现组件的高度抽象化
5.上面提到的Virtual DOM,什么是 Virtual DOM?为什么 Virtual DOM 比原生 DOM 快?
相较于 DOM 来说,操作 JS 对象会快很多,并且我们也可以通过 JS 来模拟 DOM:
const ul = {
tag: 'ul',
props: {
class: 'list'
},
children: {
tag: 'li',
children: '1'
}
}
上述代码对应的 DOM 是:
<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 的话,就需要判断是否有子元素
,有的话就进行第二步算法。 在第二步算法中,我们需要判断原本的列表中是否有节点被移除,在新的列表中需要判断是否有新的节点加入,还需要判断节点是否有移动
。
举个例子来说,假设页面中只有一个列表,我们对列表中的元素进行了变更:
// 假设这里模拟一个 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 native
、weex
、react vr
等。
2.通过 Virtual DOM 我们可以渲染到其他的平台,实现 SSR、同构渲染等,比如:next.js
、nuxt.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有:useState
、useEffect
、useContext
;
额外Hook API有:useReducer
、useCallback
、useMemo
、useRef
、useImperativeHandle
、useLayoutEffect
、useDebugValue
。
具体用法可参考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}是什么?
<Lesson {...this.props} />
...
是ES6扩展运算符,这个写法表示当前组件props属性里的值全部传给了子组件Lesson
。
11.Fragment是什么?
在JSX语法中,render 里是必须有一个根DOM
的,如果出现两个DOM的情况,又不想额外增加一个无效的 DOM 元素,<div></div>
的话,就用Fragment:
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元素样式的富文本
。
比如后台提供的字符串为以下内容:
text: "瓢虫在哪儿呢?她能找到自己的家吗?<br>Where is the ladybird? Can she find her home?↵<div style='color: red;font-size: 18px;'>红色18字体测试</div>"
如果写成这样,会全部当成字符串处理掉;
<div>{ props.text }</div>
想保留原有的标签样式的话,dangerouslySetInnerHTML上场:
<div dangerouslySetInnerHTML={{
__html: `${props.item.section}`,
}}></div>
13.React项目中应不应该使用ts呢?
个人喜好啦,我是不会的,虽然说能提高代码的可读性,强类型能减少bug的产生
。但用了ts后导致项目代码量增大
,还得去深入学习TypeScript
,真没什么必要,不适合快速迭代
。
况且ts普及率还没那么高,JavaScript
都够我学习一辈子的了,等以后普及了再说吧。