如今的前端,框架横行,出去面试问到框架是常有的事。

我比较常用React, 这里就写了一篇 React 基础原理的内容, 面试基本上也就问这些, 分享给大家。

React 是什么

React是一个专注于构建用户界面的 Javascript Library.

一、React做了什么?

  • Virtual Dom模型

  • 生命周期管理

  • setState机制

  • Diff算法

  • React patch、事件系统

  • React的 Virtual Dom模型

virtual dom 实际上是对实际Dom的一个抽象,是一个js对象。react所有的表层操作实际上是在操作Virtual dom。

经过 Diff 算法会计算出 Virtual DOM 的差异,然后将这些差异进行实际的DOM操作更新页面。

二、React  总体架构

三、几点要了解的知识

  • JSX 如何生成Element

  • Element 如何生成DOM

1JSX 如何生成Element

先看一个例子, Counter :

App.js 就做了一件事情,就是把 Counter 组件挂在 #root 上.

Counter 组件里面定义了自己的 state, 这是个默认的 property ,还有一个 handleclick 事件和 一个 render 函数。

看到 render 这个函数里,竟然在 JS 里面写了 html !

这是一种 JSX 语法。React 为了方便 View 层组件化,承载了构建 html 结构化页面的职责。

这里也简单的举个例子:

将 html 语法直接加入到 javascript 代码中,再通过翻译器转换到纯 javascript 后由浏览器执行。

这里调用了 React 和 createElement 方法,这个方法就是用于创建虚拟元素 Virtual Dom 的。

React 把真实的 DOM 树转换成 Javascript 对象树,也就是 Virtual Dom。

每次数据更新后,重新计算 Virtual Dom ,并和上一次生成的 virtual dom 做对比,对发生变化的部分做批量更新。

而 React 是通过创建与更新虚拟元素 Virtual Element 来管理整个Virtual Dom 的。

虚拟元素可以理解为真实元素的对应,它的构建与更新都是在内存中完成的,并不会真正渲染到 dom 中去。

回到我们的计数器 counter 组件:

注意下 a 标签 createElement 的返回结果, 这里 CreateElement 只是做了简单的参数修正,返回一个 ReactElemet 实例对象。

Virtual element 彼此嵌套和混合,就得到了一颗 virtual dom 的树:

2Element 如何生成DOM

现在我们有了由 ReactElement 组成的 Virtual Dom 树,接下来我们要怎么我们构建好的 Virtual dom tree 渲染到真正的 DOM 里面呢?

这时可以利用 ReactDOM.render 方法,传入一个 reactElement 和一个 作为容器的 DOM 节点。

看进去 ReactDOM.render 的源码,里面有两个比较关键的步骤:

第一步是 instantiateReactComponent。

这个函数创建一个 ReactComponent 的实例并返回,也可以看到 ReactDOM.render 最后返回的也是这个实例。

instantiateReactComponent 方法是初始化组件的入口函数,它通过判断 node 的类型来区分不同组件的入口。

  1. 当 node 为空的时候,初始化空组件。

  2. 当 node 为对象,类型 type 字段标记为是字符串,初始化 DOM 标签。否则初始化自定义组件。

  3. 当 node 为字符串或者数字时,初始化文本组件。

虽然 Component 有多种类型,但是它们具有基本的数据结构:ReactComponent 类。

注意到这里的 setState, 这也是重点之一。

创建了 Component 实例后,调用 component 的 mountComponent 方法,注意到这里是会被批量 mount 的,这样组件就开始进入渲染到 DOM 的流程了。

四、React生命周期

React 组件基本由三个部分组成,

  1. 属性 props

  2. 状态 state

  3. 生命周期方法

React 组件可以接受参数props, 也有自身状态 state。
一旦接受到的参数 props 或自身状态 state 有所改变,React 组件就会执行相应的生命周期方法。React 生命周期的全局图

首次挂载组件时,按顺序执行

  1. componentWillMount、

  2. render

  3. componentDidMount

卸载组件时,执行 componentDidUnmount

当组件接收到更新状态,重新渲染组件时,执行

  1. componentWillReceiveProps

  2. shouldComponentUpdate

  3. componentWillUpdate

  4. render

  5. componentDidUpdate

更新策略

通过 updateComponent 更新组件,首先判读上下文是否改变,前后元素是否一致,如果不一致且组件的 componentWillReceiveProps 存在,则执行。然后进行 state 的合并。

调用 shouldComponentUpdate 判断是否需要进行组件更新,如果存在 componentWillUpdate 则执行。

后面的流程跟 mountComponent 相似,这里就不赘述了。

五、setState机制

为避免篇幅过长,这部分可移步我的专题文章:

六、Diff算法

Diff算法用于计算出两个virtual dom的差异,是React中开销最大的地方。

传统diff算法通过循环递归对比差异,算法复杂度为 O(n3)。

React diff算法制定了三条策略,将算法复杂度从 O(n3)降低到O(n)。

  • 1. UI中的DOM节点跨节点的操作特别少,可以忽略不计。

  • 2. 拥有相同类的组件会拥有相似的DOM结构。拥有不同类的组件会生成不同的DOM结构。

  • 3. 同一层级的子节点,可以根据唯一的ID来区分。

1. Tree Diff

对于策略一,React 对树进行了分层比较,两棵树只会对同一层次的节点进行比较。

只会对相同层级的 DOM 节点进行比较,当发现节点已经不存在时,则该节点及其子节点会被完全删除,不会用于进一步的比较。

如果出现了 DOM 节点跨层级的移动操作。

如上图这样,A节点就会被直接销毁了。

Diif 的执行情况是:create A -> create C -> create D -> delete A

2. Element Diff

  1. 当节点处于同一层级时,diff 提供了 3 种节点操作:插入、移动和删除。

  2. 对于同一层的同组子节点添加唯一 key 进行区分。

通过 diff 对比后,发现新旧集合的节点都是相同的节点,因此无需进行节点删除和创建,只需要将旧集合中节点的位置更新为新集合中节点的位置.

七、原理解析

几个概念

  • 对新集合中的节点进行循环遍历,新旧集合中是否存在相同节点

  • nextIndex: 新集合中当前节点的位置

  • lastIndex: 访问过的节点在旧集合中最右的位置(最大位置)

  • If (child._mountIndex < lastIndex)

对新集合中的节点进行循环遍历,通过 key 值判断,新旧集合中是否存在相同节点,如果存在,则进行移动操作。

在移动操作的过程中,有两个指针需要注意,

一个是 nextIndex,表示新集合中当前节点的位置,也就是遍历新集合时当前节点的坐标。

另一个是 lastIndex,表示访问过的节点在旧集合中最右的位置,

更新流程:

1

( 如果新集合中当前访问的节点比 lastIndex 大,证明当前访问节点在旧集合中比上一个节点的位置靠后,则该节点不会影响其他节点的位置,即不进行移动操作。只有当前访问节点比 lastIndex 小的时候,才需要进行移动操作。)

首先,我们开遍历新集合中的节点, 当前 lastIndex = 0, nextIndex = 0,拿到了 B.

此时在旧集合中也发现了 B,B 在旧集合中的 mountIndex 为 1 , 比当前 lastIndex 0 要大,不满足 child._mountIndex < lastIndex,对 B 不进行移动操作,更新 lastIndex = 1, 访问过的节点在旧集合中最右的位置,也就是 B 在旧集合中的位置,nextIndex++ 进入下一步。

2

当前 lastIndex = 1, nextIndex = 1,拿到了 A,在旧集合中也发现了 A,A 在旧集合中的 mountIndex 为 0 , 比当前 lastIndex 1 要小,满足 child._mountIndex < lastIndex,对 A 进行移动操作,此时 lastIndex 依然 = 1, A 的 _mountIndex 更新为 nextIndex = 1, nextIndex++, 进入下一步.

3

这里,A 变成了蓝色,表示对 A 进行了移动操作。

当前 lastIndex = 1, nextIndex = 2,拿到了 D,在旧集合中也发现了 D,D 在旧集合中的 mountIndex 为 3 , 比当前 lastIndex 1 要大,不满足 child._mountIndex < lastIndex,不进行移动操作,此时 lastIndex = 3, D 的 _mountIndex 更新为 nextIndex = 2, nextIndex++, 进入下一步.

4

当前 lastIndex = 3, nextIndex = 3,拿到了 C,在旧集合中也发现了 C,C 在旧集合中的 mountIndex 为 2 , 比当前 lastIndex 3 要小,满足 child._mountIndex < lastIndex,要进行移动,此时 lastIndex不变,为 3, C 的 _mountIndex 更新为 nextIndex = 3.

5

由于 C 已经是最后一个节点,因此 diff 操作完成.

这样最后,要进行移动操作的只有 A C。

另一种情况

刚刚说的例子是新旧集合中都是相同节点但是位置不同。

那如果新集合中有新加入的节点且旧集合存在需要删除的节点,

那 diff 又是怎么进行的呢?比如:

1

首先,依旧,我们开遍历新集合中的节点, 当前 lastIndex = 0, nextIndex = 0,拿到了 B,此时在旧集合中也发现了 B,B 在旧集合中的 mountIndex 为 1 , 比当前 lastIndex 0 要大,不满足 child._mountIndex < lastIndex,对 B 不进行移动操作,更新 lastIndex = 1, 访问过的节点在旧集合中最右的位置,也就是 B 在旧集合中的位置,nextIndex++ 进入下一步。

2

当前 lastIndex = 1, nextIndex = 1,拿到了 E,发现旧集合中并不存在 E,此时创建新节点 E,nextIndex++,进入下一步

3

当前 lastIndex = 1, nextIndex = 2,拿到了 C,在旧集合中也发现了 C,C 在旧集合中的 mountIndex 为 2 , 比当前 lastIndex 1 要大,不满足 child._mountIndex < lastIndex,不进行移动,此时 lastIndex 更新为 2, nextIndex++ ,进入下一步

4

当前 lastIndex = 2, nextIndex = 3,拿到了 A,在旧集合中也发现了 A,A 在旧集合中的 mountIndex 为 0 , 比当前 lastIndex 2 要小,不满足 child._mountIndex < lastIndex,进行移动,此时 lastIndex 不变, nextIndex++ ,进入下一步

5

当完成新集合中所有节点的差异化对比后,还需要对旧集合进行循环遍历,判断是否勋在新集合中没有但旧集合中存在的节点。

此时发现了 D 满足这样的情况,因此删除 D。

Diff 操作完成。

整个过程还是很繁琐的, 明白过程即可。

二、性能优化

由于react中性能主要耗费在于update阶段的diff算法,因此性能优化也主要针对diff算法。

1减少diff算法触发次数

减少diff算法触发次数实际上就是减少update流程的次数。

正常进入update流程有三种方式:

1、setState

setState机制在正常运行时,由于批更新策略,已经降低了update过程的触发次数。

因此,setState优化主要在于非批更新阶段中(timeout/Promise回调),减少setState的触发次数。

常见的业务场景即处理接口回调时,无论数据处理多么复杂,保证最后只调用一次setState。

2、父组件render

父组件的render必然会触发子组件进入update阶段(无论props是否更新)。此时最常用的优化方案即为shouldComponentUpdate方法。

最常见的方式为进行this.props和this.state的浅比较来判断组件是否需要更新。或者直接使用PureComponent,原理一致。

需要注意的是,父组件的render函数如果写的不规范,将会导致上述的策略失效。

// Bad case// 每次父组件触发render 将导致传入的handleClick参数都是一个全新的匿名函数引用。// 如果this.list 一直都是undefined,每次传入的默认值[]都是一个全新的Array。// hitSlop的属性值每次render都会生成一个新对象class Father extends Component {    onClick() {}    render() {return <Child handleClick={() => this.onClick()} list={this.list || []} hitSlop={{ top: 10, left: 10}}/>    }}// Good case// 在构造函数中绑定函数,给变量赋值// render中用到的常量提取成模块变量或静态成员const hitSlop = {top: 10, left: 10};class Father extends Component {    constructor(props) {super(props);this.onClick = this.onClick.bind(this);this.list = [];    }    onClick() {}    render() {return <Child handleClick={this.onClick} list={this.list} hitSlop={hitSlop} />    }}

3、forceUpdate

forceUpdate方法调用后将会直接进入componentWillUpdate阶段,无法拦截,因此在实际项目中应该弃用。

其他优化策略

1.  shouldComponentUpdate

使用shouldComponentUpdate钩子,根据具体的业务状态,减少不必要的props变化导致的渲染。如一个不用于渲染的props导致的update。
另外, 也要尽量避免在shouldComponentUpdate 中做一些比较复杂的操作, 比如超大数据的pick操作等。

2. 合理设计state,不需要渲染的state,尽量使用实例成员变量。

不需要渲染的 props,合理使用 context机制,或公共模块(比如一个单例服务)变量来替换。

2正确使用 diff算法

  • 不使用跨层级移动节点的操作。

  • 对于条件渲染多个节点时,尽量采用隐藏等方式切换节点,而不是替换节点。

  • 尽量避免将后面的子节点移动到前面的操作,当节点数量较多时,会产生一定的性能问题。

看个具体的例子

这时一个 List 组件,里面有标题,包含 ListItem 子组件的members列表,和一个按钮,绑定了一个 onclick 事件.

然后我加了一个插件,可以显示出各个组件的渲染情况。

现在我们来点击改变标题, 看看会发生些什么。

奇怪的事情发生了,为什么我只改了标题,  为什么不相关的 ListItem 组件也会重新渲染呢?

我们可以回到组件生命周期看看为什么。

还记得这个组件更新的生命周期流程图嘛,这里的重点在于这个 shouldComponentUpdate。

只有这个方法返回 true 的时候,才会进行更新组件的操作。我们进步一来看看源码。

可以看到这里,原来如果组件没有定义 shouldComponentUpdate 方法,也是默认认为需要更新的。

当然,我们的 ListItem 组件是没有定义这个 shouldComponentUpdate 方法的。

然后我们使用PureComponent :

其原理为重新实现了 shouldComponentUpdate 生命周期方法,让当前传入的 props 和 state 之前做浅比较,如果返回 false ,那么组件就不会更新了。

这里也放上一张官网的例图:

根据渲染流程,首先会判断shouldComponentUpdate(SCU)是否需要更新。

如果需要更新,则调用组件的render生成新的虚拟DOM,然后再与旧的虚拟DOM对比(vDOMEq)。

如果对比一致就不更新,如果对比不同,则根据最小粒度改变去更新DOM;

如果SCU不需要更新,则直接保持不变,同时其子元素也保持不变。

相似的APi还有React.memo:

回到组件

再次回到我们的组件中, 这次点击按钮, 把第二条数据换掉:

奇怪的事情发生了,为什么我只改了第二个 listItem, 还是全部 10 个都重新渲染了呢?

原因在于 shallow compare , 浅比较。

前面说到,我们不能直接修改 this.state 的值,所以我们把

this.state.members 拷贝出来再修改第二个人的信息。

很明显,因为对象的比较是引用地址,显然是不相等的。

因此 shoudComponentUpdate 方法都返回了 false, 组件就进行了更新。

那么我们怎么能避免这种情况的发生呢?

其中一个方法是做深比较,但是如果对象或数组层级比较深和复制,那么这个代价就太昂贵了。

我们就可以用到 Immutable.js 来解决这个问题,进一步提高组件的渲染性能。

Immutable Data 就是一旦被创建,就是不能再更改的数据。

首先,我们定义了一个 Immutable 的 List 对象,List 对应于原生 JS 的 Array,对 Immutable 对象进行修改、添加或删除操作,都会返回一个新的 Immutable 对象,所以这里 bbb 不等于 aaa。

但是同时为了避免深拷贝吧所有节点都复制一遍带来的性能消耗,Immutable 使用了结构共享,即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其他节点则进行共享。

结果也是我们预期的那样。

性能分析

用好火焰图, 该优化的时候再优化。

八、Hooks  及其后续更新

为避免篇幅过长,这部分可移步我的专题文章:

最后

react如何遍历并比较_[前端进阶] 这可能是最通俗易懂的React 渲染原理及性能优化...相关推荐

  1. 【前端性能优化】浏览器渲染原理与性能优化

    目录 1. 浏览器渲染基本步骤 2. 构建DOM树.CSSOM树 3. 构建渲染树 4. 计算渲染树的布局 5. 将布局渲染到屏幕上 6. 渲染优化 1. 浏览器渲染基本步骤 浏览器主要有以下步骤: ...

  2. 进阶Java架构师必看书:大型架构+框架+性能优化+中间件+分布式

    进阶Java架构师必看书:大型架构+框架+性能优化+中间件+分布式 优知学院 2018-09-05 07:21:00 1.大型网站技术架构:核心原理与案例分析 本书通过梳理大型网站技术发展历程,剖析大 ...

  3. fifo算法_前端进阶算法6:一看就懂的队列及配套算法题

    引言 队列这种数据结构,据瓶子君了解,前端需要了解的队列结构主要有:双端队列.滑动窗口,它们都是算法中是比较常用的数据结构. 因此,本节主要内容为: 数据结构:队列(Queue) 双端队列(Deque ...

  4. 前端为什么有的接口明明是成功回调却执行了.catch失败回调_前端进阶高薪必看-手写源码篇(高频技术点)...

    前言 此系列作为笔者之前发过的前端高频面试整理的补充 会比较偏向中高前端面试问题 当然大家都是从新手一路走过来的 感兴趣的朋友们都可以看哈 初衷 我相信不少同学面试的时候最怕的一个环节就是手写代码 大 ...

  5. web 折线图大数据量拉取展示方案_【第2010期】QQ音乐Android客户端Web页面通用性能优化实践...

    前言 今日早读文章由QQ音乐客户端开发工程师@关岳分享,公号:云加社区(ID:QcloudCommunity,腾讯云官方开发者社区)授权分享. 正文从这开始~~ QQ音乐 Android 客户端的 W ...

  6. react如何遍历并比较_在React中遍历JSON

    小编典典 除了@Dan的答案,我认为其他答案对您没有帮助/有用,因为它们不会遍历您的JSON对象. 为了正确执行此操作,您需要遍历JSON对象中的每个键.您可以通过几种方法来执行此操作,其中之一是使用 ...

  7. java前端div浮动靠左_前端进阶第5周打卡题目汇总

    第一天 1.用css实现单行文本溢出省略以及多行文本溢出省略 2.用css实现一个音乐抖动条 css实现扇形可以用伪类矩形旋转一个角度,来遮挡父容器的圆形,父容器溢出隐藏 第二天 1. 写出3种js数 ...

  8. react网页适配不同分辨率_前端页面适应不同分辨率

    前端开发要考虑到不同分辨率电脑的页面展示问题,在开发者电脑上的界面在用户电脑上打开可能出现很大变形. 解决方案主要有: 针对不同分辨率用户设置不同的css 使用JS/jQuery动态调整 使用前端框架 ...

  9. react骨架屏自动生成_前端骨架屏方案小结

    骨架屏 最近在项目不时有用到骨架屏的需求,所以抽时间对骨架屏的方案作了一下调研,骨架屏的实践已经有很多了,也有很多人对自己的方案作了介绍.在这里按照个人的理解做了一个汇总和分类,分享给大家. 关于骨架 ...

最新文章

  1. 企业管理难题:团队协作
  2. 国内人工智能专利布局存隐忧
  3. (七)nodejs循序渐进-模块系统(进阶篇)
  4. (转)基于libRTMP的流媒体直播之 AAC、H264 推送
  5. java joda 获取utc时间_java – JodaTime – 如何获取UTC的当前时间
  6. python 函数中的面试题
  7. pytorch VGG
  8. java 中的随机数
  9. Shell 神技:掩盖 Linux 服务器上的操作痕迹
  10. 协同过滤Collaborative Filtering
  11. 专业的售后服务管理系统
  12. MySQL批量插入性能优化
  13. C++开发斗地主(QT)第五篇之牌型权重
  14. RGMII以太网测试方案
  15. 硬核干货 | 基于Impala的网易有数BI查询优化总结
  16. 人工智能已经成为全球新一轮科技革命和产业变革的核心驱动力
  17. 算法复习 - 蛮力法
  18. JavaScript 无缝连接滚动特效
  19. apache benchmark(ab) 进行压测
  20. 2019年12月1号总结

热门文章

  1. 软件测试个人心得总结
  2. vs2010编写的net3.5用vs2008打开
  3. Android中设置输入法为数字输入
  4. 内存大对象dump linux_在 Linux 上创建并调试转储文件 | Linux 中国
  5. (JAVA)格式化输出日期
  6. php上操作redis,PHP操作redis
  7. maya python 创建求_如何使用python在Maya中创建列表
  8. 常用内存分配函数的说明
  9. 【C++深度剖析教程1】C++中的经典问题解析-c++中的对象的构造顺序与析构顺序
  10. Golang tcp转发 remoteAddr错误