大神们能够写出“深刻浅出”系列,小白就写点"真·浅尝辄止"系列的吧,主要便于本身理解和巩固,毕竟一开始就上源码仍是会头大滴,因而就准备浅尝辄止的了解下"React是如何工做的?"javascript
React是怎么工做的? 你知道Diff算法吗 ---xx面试官html
小白的前端排坑指南:前端
1.VK的秋招前端奇遇记(一)java
2.VK的秋招前端奇遇记(二)react
4.番外篇:前端面试&笔试算法 Algorithmgithub
React除了是MVC框架,数据驱动页面的特色以外,核心的就是他很"快"。 按照广泛的说法:"由于直接操做DOM会带来重绘、回流等,带来巨大的性能损耗而致使渲染慢等问题。React使用了虚拟DOM,每次状态更新,React比较虚拟DOM的差别以后,再更改变化的内容,最后统一由React去修改真实DOM、完成页面的更新、渲染。"面试
上面这段话,是咱们都会说的,那么通常到这里,面试官就问了:"什么是虚拟DOM,React是怎么进行比较的?Diff算法了解吗?"。以前是有点崩溃的,因而决定浅尝一下:算法
DOM没什么好说的,主要说下虚拟DOM的一些特色:数据库
前几点没什么好说的,注意第四点,也就是你每个改动,每个动做都会让React去根据当前的状态建立一个全新的Virtual DOM。
这里每当Virtual DOM生成,都打印了出来,能够看到,它表明着真实DOM,而每次生成全新的,也是为了可以比较old dom和new dom以前的差异。
刚才提到了,React会抓取每一个状态下的内容,生成一个全新的Virtual DOM,而后经过和前一个的比较,找出不一样和差别。React的Diff算法有两个约定:
第二点着重说一下,举个例子:好比真实DOM的ul标签下,有一系列的<li>
标签,然而当你想要从新排列这个标签时,若是你给了每一个标签一个key
值,React在比较差别的时候,就可以知道"你仍是你,只不过位置变化了"。 React除了要最快的找到差别外,还但愿变化是最小的。若是加了key
,react就会保留实例,而不像以前同样,彻底创造一个全新的DOM。
来个更具体的:
1234
下一个状态后,序列变为
1243
对于咱们来说,其实就是调换了4和3的顺序。但是怎么让React知道,原来的那个3
跑到了原来的4
后面了呢? 就是这个惟一的key
起了做用。
相关面试题:为何React中列表模板中要加入key
Diff在进行比较的时候,首先会比较两个根元素,当差别是类型的改变的时候,可能就要花更多的“功夫”了
好比如今状态有这样的一个改变:
<div>
<Counter />
</div>
-----------
<span>
<Counter />
</span>
复制代码
能够看到,从<div>
变成了<span>
,这种类型的改变,带来的是直接对old tree的总体摧毁,包括子元素Counter
。 因此旧的实例Counter
会被彻底摧毁后,建立一个新的实例来,显然这种效率是低下的
当比较后发现两个是同类型的,那好办了,React会查看其属性的变化,而后直接修改属性,原来的实例都得意保留,从而使得渲染高效,好比:
<div className="before" title="stuff" />
<div className="after" title="stuff" />
复制代码
除了className
,包括style
也是同样,增长、删除、修改都不会对整个 dom tree进行摧毁,而是属性的修改,保留其下面元素和节点
与上面相似,相同类型的组件元素,子元素的实力会保持,不会摧毁。 当组件更新时,实例保持不变,以便在渲染之间保持状态。React更新底层组件实例的props以匹配新元素,并在底层实例上调用componentWillReceiveProps()
和componentWillUpdate()
。
接下来,调用render()
方法,diff算法对前一个结果和新结果进行递归
若是前面对key
的解释,还不够清除,这里用一个真正的实例来讲明key
的重要性吧。
<ul>
<li>first</li>
<li>second</li>
</ul>
------
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
复制代码
能够看到,在这种状况下,React只须要在最后insert
一个新元素便可,其余的都不须要变化,这个时候React是高效的,可是若是在场景二下:
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
---
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
复制代码
这对React可能就是灾难性的,由于React只知道前两个元素不一样,所以会彻底创新一个新的元素,最后致使三个元素都是从新建立的,这大大下降了效率。这个时候,key
就排上用场了。当子元素有key
时,React使用key
将原始树中的子元素与后续树中的子元素相匹配。例如,在上面的低效示例中添加一个key
可使树转换更高效:
<ul>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
------
<ul>
<li key="2014">Connecticut</li>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
复制代码
这样,只有key
值为2014的是新建立的,而2015
和2016
仅仅是移动了位置而已。
React是用什么策略来比较两颗tree之间的差别呢?这个策略是最核心的部分:
两个树的彻底的 diff 算法是一个时间复杂度为 O(n^3) 的问题。可是在前端当中,你不多会跨越层级地移动DOM元素。因此 Virtual DOM 只会对同一个层级的元素进行对比:
上面的div
只会和同一层级的div
对比,第二层级的只会跟第二层级对比。这样算法复杂度就能够达到 O(n)。
在实际代码中,会对新旧两棵树进行一个深度优先的遍历,这样每一个节点都会有一个惟一的标记,而后记录差别
在深度优先遍历的时候,每遍历到一个节点就把该节点和新的的树进行对比。若是有差别的话就记录到一个对象里面。
好比第上图中的1号节点p,有了变化,这样子就记录以下:
patches[1] = [{difference}, {difference}...]//用数组存储新旧节点的差别
复制代码
ok,那么差别类型呢,在上一节中已经说了,包括根元素的类型的不一样分为两大类,而后根据不一样的状况采起不一样的更换策略。
最后,就是在真实DOM进行操做,apply这些差别,更新和渲染了。
这又是一个很厉害的问题了,使用Redux的都知道,reducers会接收上一个state
和action
做为参数,而后返回一个新的state
,这个新的state
不能是在原来state
基础上的修改。因此常常能够看到如下的写法:
return Object.assign(...)
//或者----------
return {...state,xx:xxx}
复制代码
其做用,都是为了返回一个全新的对象。
为何reducers要求是纯函数(返回全新的对象,不影响原对象)? --某面试官
从本质上讲,纯函数的定义以下:不修改函数的输入值,依赖于外部状态(好比数据库,DOM和全局变量),同时对于任何相同的输入有着相同的输出结果。
举个例子,下面的add函数不修改变量a或b,同时不依赖外部状态,对于相同的输入始终返回相同的结果。
const add = (a,b) => {a + b};
复制代码
这就是一个纯函数,结果对a、b没有任何影响,回头去看reducer,它符合纯函数的全部特征,因此就是一个纯函数
先告诉你结果吧,若是在reducer中,在原来的state
上进行操做,并返回的话,并不会让React从新渲染。 彻底不会有任何变化!
接下来看下Redux的源码:
Redux接收一个给定的state(对象),而后经过循环将state的每一部分传递给每一个对应的reducer。若是有发生任何改变,reducer将返回一个新的对象。若是不发生任何变化,reducer将返回旧的state。
Redux只经过比较新旧两个对象的存储位置来比较新旧两个对象是否相同。若是你在reducer内部直接修改旧的state对象的属性值,那么新的state和旧的state将都指向同一个对象。所以Redux认为没有任何改变,返回的state将为旧的state。
好了,也就是说,从源码的角度来说,redux要求开发者必须让新的state
是全新的对象。那么为何非要这么麻烦开发者呢?
请看下面的例子:尝试比较a和b是否相同
var a = {
name: 'jack',
friend: ['sam','xiaoming','cunsi'],
years: 12,
...//省略n项目
}
var b = {
name: 'jack',
friend: ['sam','xiaoming','cunsi'],
years: 13,
...//省略n项目
}
复制代码
思路是怎样的?咱们须要遍历对象,若是对象的属性是数组,还须要进行递归遍历,去看内容是否一致、是否发生了变化。 这带来的性能损耗是很是巨大的。 有没有更好的办法?
有!
//接上面的例子
a === b //false
复制代码
我不要进行深度比较,只是浅比较,引用值不同(不是同一个对象),那就是不同的。 这就是redux
的reducer
如此设计的缘由了