React更进一步:在实践中理解React工作原理

Posted by mieruko on 2018-02-22

前言

最近读了程墨前辈的《深入浅出React和Redux》这本书,很庆幸自己是用RN写过些东西之后读的这本书,一边读一边回忆自己曾经实践的过程,对React的理解又加深了一层,特别开心。
这篇文章是想结合之前写过的项目,对React的运行机制,实践原则做一个自己的梳理。
驱使我了解React,学习React的项目,是”一图”。
(突然觉得我博客里给一图打的广告是不是太多了哈哈哈哈,既然如此那我就再打一次吧哈哈哈哈哈)
开源代码地址: 一图:图书馆的另一半
希望大家不要烦,我保证过段时间就不推一图了。。。。到时候你们会发现满屏幕的一图变成了满屏幕的双生。。。。
“双生”是当下我和小伙伴在着手的另一个RN的开源项目。但其实双生的项目结构和一图相比并没有特别大的改动,是对既有模式的又一次实践。
考虑到大部分的知识,原理以及实践模式都是在开发一图的过程中习得,所以本文需要结合实际代码实例的部分仍然会截取一图的代码来说事情。

React: 工作原理

React本质上是什么?
React细分的知识点可以写一本书,但是React最重要的特点是以下三点:

  • 数据驱动(f = UI(data))
  • 一切都是组件
  • 声明式编程

数据驱动

我们看到的界面,可以理解为是UI这个函数生成的一个结果。
所有的改动都是data的改动,通过data的改动去驱动界面的改变。
就是说在render函数确定的情况下,最后生成的界面是什么样子,只跟我们的数据有关。
这一点主要是区别于jQuery,在jQuery里,你除了操心data的改动,你还需要手动去操作DOM,但是用了React,你就不需要费这个力气了。

不需要我们操作DOM,React帮我们去修改DOM,那么修改的过程是什么样的呢?难道是大家一起整个组件重新渲染吗??不不不,React很聪明,它有自己的更新机制,即虚拟DOM+Diff算法:

虚拟DOM

大家都知道DOM是结构化文本(即HTML)文本的抽象,我们的虚拟DOM就是在DOM的基础上的进一步抽象,它不涉及和浏览器有关的那一部分,只是一个存在于js空间的一个树形结构。它和真实的DOM节点一一对应。当渲染发生时,会对比这一次产生的虚拟DOM树和之前那颗虚拟DOM树,看看到底哪里是真正需要更新的,再有选择地去更新。
“发现两棵树的差别”(这个过程叫做调和)这一点非常关键,如果是用传统的算法对两棵树做完全的比较,时间复杂度是O(n^3)。这是一个可怕的复杂度。。。还好React没有这么做,它内部使用了优化过的diff算法,保证了时间复杂度只有O(n)。

diff算法(调和过程)

React版本更迭很快,但是diff算法的大体思路都是:从根节点开始比对,然后以递归的方式逐个向下比对。
比较根节点时:
1.先看节点的类型是否相同,如果类型不同,那就直接认为这个树形结构没用了,然后就把这个节点给unmount掉然后重新mount。
2.如果类型相同,那么就认为只需要更新过程,就去走一个更新的生命周期
然后递归这个过程。
注意这里有个bug,就是如果我们移动了一个节点(我给您画个图~):
React移动节点
根据我们这个思路,C和E作比较,发现不一样,React这时候很蠢的,照样给你Unmount掉然后重新Mount,后面几个也跟着走这个过程,增加了不少不必要的消耗。
虽然代价很大。。。但是代价再大也比O(n^3)强呀是不是。。。。😂
并且这个Bug,其实是可以弥补的,只需要我们开发人员给它加个key!
说起来也是缘分,我之前就踩过这个key的坑
BCD以前的key我们给它设置个b,c,d,如此,我们如果在中间来个E,React也不会抓瞎,它会根据b,c,d来判断,就知道说你B,C,D三个组件其实还是原来的自己嘛!然后就不会傻傻地推倒重来了~
key这个东西,具体来说就是这么回事(下图截自我楼上key的坑那篇文章的总结部分):
key的总结
key最重要的两个特性!
稳定性!唯一性!
唯一性不说了,稳定性是说不管渲染多少次,一个节点对应的key一定是固定的!
所以不能设置为数组下标啊朋友们!(其实说的是曾经的我自己。。。/捂脸)
为啥?因为它不稳定,如果我们给数组做插入删除,那么就会引发同一个节点上key值的改变,我们想要通过key去标识一个组件的目的也就彻底达不到了。。。。。

一切都是组件

组件这块需要注意的是组件的拆分。
观察我们的React组件,我们会发现它主要是做两件事:
1.和数据打交道
2.渲染界面
本着”一个组件只做一件事”的原则,我们可以把组件划分为容器组件和展示组件。
容器组件一般位于外层,它负责和数据打交道,把数据以props的形式传递给展示组件。
展示组件一般是一个纯函数,即无状态组件,它根据props产生视图结果,负责视图渲染。

声明式编程

和声明式编程相对应的是命令式编程
声明式编程是,我们告诉框架,我们想做什么,它帮我们去做,不需要想过程是什么样的。
命令式编程是,我们需要去亲力亲为每一个具体的步骤。
jQuery是命令式编程,单单一个事件绑定,就经历了从live到delete到on的api演变。每改一次,我们写代码的人也要跟着改。
但是React不是这样,React基本不需要我们手动去调用系统级的api,而是我们去实现固定的api,然后React再去调用你写的api。
声明式编程的好处是,React的api可以变,可以更新,但是你写的code是不用跟着变的。

细节拾遗

props和state

在React中,数据分为两种,props和state。
对一个组件来说,props是外来的数据,state是组建内部的状态。
组件要想改变自己的状态,只能通过改变state来实现,一个组件永远不能去修改自己的props。

生命周期

还是来大体描述一下生命周期的过程:首先我们获取初始的数据,包括props和state,然后调用componentWillMount,render,componentDidMount。
注意componentDidMount,服务端渲染的时候服务端那边是不走这一步的。
着重说一下更新过程:

因state改变引发的update过程:
shouldComponentUpdate
componentWillUpdate
render
componentDidUpdate

因父组件想要render这个组件改变引发的update过程:
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
render
componentDidUpdate

以render函数为界,render前的函数调用之后,这个组件的state和props都是没有改变的,只是说把改变的值以参数的形式保留了,只有render后才会真正改变。

创建高阶组件

高阶组件的存在,是为了共享。
高阶组件是一个函数,它接收一个组件作为参数,然后返回一个全新的组件。
注意是返回全新的组件,而不是改造现有的组件
包裹的方式🌰:

harmony
1
2
3
4
5
6
7
8
const HoC = (WrappedComponent) => {
const WrappingComponent = (props) => (
<div className="foo">
<WrappedCompoent {...props} />
</div>
);
return WrappingComponent;
};

继承的方式🌰:

harmony
1
2
3
4
5
6
7
8
9
10
const HoC = (WrappedComponent) => {
class WrappingComponent extends WrappendComponent {
render() (
const {user, ...otherProps} = this.props;
this.props = otherProps;
return super.render();
}
}
return WrappingComponent;
};

能用第一种方式,就不用继承的方式。继承的方式会有一些共有的方法,父类子类会纠缠在一起。

高阶组件常见的应用场景

有一些功能可以用在不同的组件类里,而你不想反复去写,这时候就可以用HOC来解决。

组件间通信

父子组件通信

这是最直接的一种通信方式,我们通过props来传递。

子父组件通信

父组件传递一个作用域为父组件自身的函数给子组件,子组件调用该函数,把想要传递的信息传递到父组件的作用域里。

兄弟组件通信

方法一:
假设两个组件分别是A和B,父组件是C,我们可以把需要修改的数据放在父组件里,先用传递回调函数给A的方式,去修改父组件中的数据,然后再用props传递的方式,把数据传递给B组件的。
方法二:
发布-订阅模式:
假设要发送数据的是A组件,要接收数据的是B组件。
我们可以引入一个实现该模式的外部的库,然后A组件订阅事件,B组件发布事件,在发布的同时把数据放进回调函数里。