俗话说得好, 知其然更要知其所以然, 我相信现在大部人都用过react
, 但是对其中的原理又了解多少?今天这篇文章就是为了介绍react
的原理。文中不会涉及过多的源码, 但是里面的内容都是依据源码(v16.13.1)解析而来, 大家可以把这篇文章当作一篇科普类文章来阅读。如果发现错误也欢迎在评论区指正。
react 是什么
在react
的官网上, 它介绍自己是用于构建用户界面的 JavaScript 库, 那么它干的事情其实非常简单, 就是在DOM
结点和你之间搭了一座桥, 帮你屏蔽了一些底层的操作, 让你能够用更简单的方式去改变你的用户界面。
那这座桥是怎么搭起来的?其实也很简单, 即使它再复杂, 也是js
的产物。所以只要你了解js
的语法, 你也能写出一个react
。但是重点就在于你如何组织你的代码、如何实现这些功能。打个比方, 我们平时开的车, 很复杂。 你根本不会去关注它的内部结构是什么, 又是怎么组织起来的。但是把车的各个零件拆开摆在你面前, 你会知道, 哦这个是铁, 那个是橡胶, 这些零件我都认识。同时,了解了车的内部原理才能更好的开车,遇见什么小毛病自己就给修了。
那么今天, 我们就把react
拆开来看一看, 看看它到底是怎么把我们写的代码最终渲染到浏览器上的。
直捣黄龙
我们直接跳过最开始那些花里胡哨的步骤, 来看看react
最后做的操作, removeAttribute
熟悉吗? removeChild
熟悉吗?整到最后还是会回归到这些基础的api
,所以,不要对它怀着一颗恐惧的心态, 其实,也就是那么回事。
if (value === null) {
node.removeAttribute(_attributeName);
} else {
node.setAttribute(_attributeName, '' + value);
}
while (node.firstChild) {
node.removeChild(node.firstChild);
}
while (svgNode.firstChild) {
node.appendChild(svgNode.firstChild);
}
干货开始
好了下面就开始我们今天的正文, react
到底干了什么。
从 JSX 到 用户界面
我们在react
里面写的最多的东西就是JSX
了,这是个什么玩意?啥也不是,你可以把它理解为react
的一种规定,你按着它的规定写,它就能理解你写的东西,你不按它的规定写,它就不能理解你写的东西。
JSX 到 js 对象
react
会怎么处理我们写的JSX
?它会通过一个函数叫做createElement
来把我们写的东西变成一个固定的结构
var element = {
// This tag allows us to uniquely identify this as a React Element
$$typeof: REACT_ELEMENT_TYPE,
// Built-in properties that belong on the element
type: type,
key: key,
ref: ref,
props: props,
// Record the component responsible for creating this element.
_owner: owner
};
这玩意在react
内部就相当于一个DOM
结点。那我们为什么要写JSX
?
其实你完全可以手动调用createElement
来创建结点, 或者react
官方直接只接受上文的element
对象这种写法来形容一个组件。那react
就凉了,这么难用的东西谁会用,所以写JSX
只是为了方便你理解,而createElement
是把方便你理解的东西转换为方便react
理解的东西。
js 对象到 fiber 对象
fiber
对象是什么?本质上就是一个js
对象。但是它更加的复杂。
function FiberNode(tag, pendingProps, key, mode) {
// Instance
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null; // Fiber
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode; // Effects
this.effectTag = NoEffect;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
this.expirationTime = NoWork;
this.childExpirationTime = NoWork;
this.alternate = null;
{
// Note: The following is done to avoid a v8 performance cliff.
//
// Initializing the fields below to smis and later updating them with
// double values will cause Fibers to end up having separate shapes.
// This behavior/bug has something to do with Object.preventExtension().
// Fortunately this only impacts DEV builds.
// Unfortunately it makes React unusably slow for some applications.
// To work around this, initialize the fields below with doubles.
//
// Learn more about this here:
// https://github.com/facebook/react/issues/14365
// https://bugs.chromium.org/p/v8/issues/detail?id=8538
this.actualDuration = Number.NaN;
this.actualStartTime = Number.NaN;
this.selfBaseDuration = Number.NaN;
this.treeBaseDuration = Number.NaN; // It's okay to replace the initial doubles with smis after initialization.
// This won't trigger the performance cliff mentioned above,
// and it simplifies other profiler code (including DevTools).
this.actualDuration = 0;
this.actualStartTime = -1;
this.selfBaseDuration = 0;
this.treeBaseDuration = 0;
} // This is normally DEV-only except www when it adds listeners.
// TODO: remove the User Timing integration in favor of Root Events.
{
this._debugID = debugCounter++;
this._debugIsCurrentlyTiming = false;
}
{
this._debugSource = null;
this._debugOwner = null;
this._debugNeedsRemount = false;
this._debugHookTypes = null;
if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') {
Object.preventExtensions(this);
}
}
}
每一个组件就是一个fiber
对象,各个组件之间连接起来就是一颗fiber
树,和DOM
树很像,可以说fiber
树和DOM
树是对应起来的。为什么要搞这样一个fiber
树?你可曾听闻过diff
算法。fiber
树就是js
对象,所以操纵它会比操纵DOM
结点要快不少。我们把diff
的操作放在fiber
树上,最后再得出真正需要更新的结点映射到真实DOM
,优化性能。
其实最重要的一个原因就是这是react
为了避免在计算Virtual Dom
的时候一直占用js
线程导致无法响应其他事件特意推出的新架构,今天我们不在这里谈关于这个的内容,我们首先把react
的主流程搞明白。
fiber 对象的 diff 算法
diff
算法的目的是什么?找出可以复用的fiber
结点,节约创建新结点的时间,找到真正需要更新的部分。我们用图说话
这里有五个组件, 他们一起组成了一颗Fiber
树, 在一次setState
之后,组件D
变成了组件E
, 变成了下面这样
那第二颗fiber
树会如何生成?react
通过diff
,会知道App
、A
、B
、C
四个组件都没有改变,进而复用之前的fiber
结点。而D
组件变成了E
组件,react
就会帮它重新生成一个fiber
结点。
那么diff
的具体过程是什么?
简单来说,如果是单个结点,只要key
和type
(结点类型)相同,react
就会进行复用。不给key
的情况下默认是null
。在内部比较时null === null
,所以此时在type
相同的情况下也会进行复用。
如果是多个结点,也是比较key
和type
,此时不给key
的情况下会默认将index
作为key
。在这种情况下,除了按顺序比较key
和type
之外,还针对结点更换位置的情况做了优化,会根据key
值去列表中寻找对应的结点,尝试复用。
可以看出,react
会尽可能的去复用之前的结点, 避免创建新的结点。而这里的复用指的是复用之前的fiber
结点,并不意味着不会去更新真实DOM
。
想了解更多内容的可以参考我之前的文章
新 fiber 树到更新链表
我们把后面这颗fiber
树称为新fiber
树。
对于react
来说,一切都是props
,比如DOM
结点上的style
、src
、data-props
等等,里面包裹的内容也是props.children
。所以react
在生成新的fiber
后,会去对比新旧props
的区别,如果有改变就会把它加到更新链表中去。更新链表是什么?我们再次用图说话
fiber
结点上有一个属性叫做nextEffect
,它会把各个需要更新的结点连接起来,所以当react
发现props
有改变的时候,就会将其加入到这个链路中来。
最终渲染
最后react
会走到commit
阶段,在这里它会去遍历之前生成的更新链表,然后把这些内容真正的更新到用户界面上,至此,一次render
流程就结束了,我们看到的界面也更新了。
对于我们上面这张图来说,react
最终就会去更新A
、B
、D
三个组件。
也就是在这一步, react
会去调用js操纵DOM
的api
来更新用户界面。
渲染过程的优化
避免重复渲染
我们上面把react
的整个渲染流程梳理了一遍, 那么在我们更新state
的时候不可能把整个fiber
树都重新更新一遍,什么时候才会触发react
的更新流程呢?
只要props
改变了,就会触发react
的更新流程
这句话看似简单,实则另有玄机,我们来看看这个例子:
class App extends Component {
state = {data: "hello world", src: logo};
data = {data: 'test'}
render() {
return (
<div>
<Item data={{data: this.data}}/>
<button onClick={() => this.setState({data: "new data", src: logo2})}>setState</button>
</div>
);
}
}
export default App;
如果我点击setState
,传给Item
的data
会不会更新?我们这样想,data
是挂在this
上的一个对象,它其实是不会改变的,那这样想起来我们两次给的props
其实是没有改变的。
但是真实情况却是Item
组件每次都会重新经历一遍diff
流程,为什么呢?还记得我们之前说JSX
会调用一个createElement
吗,这个函数会对props
再包装一层,生成了一个新的props
。所以即使我们传给Item
的值没有改变,传到React
那里还是变了。
这个问题和Context
的重复渲染问题很像,我们再来看下面的代码:
//app.js
class App extends React.Component {
state = {
data: 'old data'
}
changeState = data => {
this.setState({
data
})
}
render() {
return (
<CustomContext.Provider value={{data: this.state, setData: this.changeState}}>
<div className="App">
<ContextContent />
<OtherContent />
</div>
</CustomContext.Provider>
);
}
}
export default App;
//ContextContent.js
export default class ContextContent extends Component {
render() {
console.log("Context render")
return <CustomContext.Consumer>
{value => (
<div>
<div>{value.data.data}</div>
<button onClick={() => value.setData(Math.random())}>change</button>
</div>
)}
</CustomContext.Consumer>
}
}
//OtherContent.js
export default class OtherContent extends Component {
render() {
console.log("otherContent render")
return (
<>
<div>other content</div>
</>
)
}
}
我们通过Context
给里面的组件传值,当我们改变state
的时候,当然只希望用到了这个值的组件重新渲染,但是目前这种写法会导致什么结果呢?
可以看到,我们每次改变state
的时候,另一个无关的组件也更新了。为什么?因为我们每次都调用了createElement
,每次都生成了一个新的props
(即使这里我们没有给任何props
,react
最后拿到的props
其实是createElement
内部生成的)。
那么问题如何解决?我们想办法让props
不要改变,也就是想办法不要每次都让无关组件调用createElement
,具体解决方法我这里就不给出了,让大家思考一下。
这也是为什么Context
使用不当会有性能问题的原因。想想在组件非常多的情况下,如果你在最外层给了一个Provider
,又去频繁的改变它的Value
,那么结果可想而知,每次都会重新diff
一遍整个fiber
树。
注意我这里说的是重新diff
而不是重新渲染。react
走到最后会发现其实任何东西都没有改变,所以它并不会重新渲染这个组件,也就是不会去更新用户界面上的内容。但是react
耗时的部分其实恰恰就在diff
,你可以理解为你做了一大堆工作来判断这个组件什么地方需要更新,最后得出的结论是这个组件并不需要更新。
给组件加 key 值
其实我们之前有提到过, react
会尽可能的去复用fiber
结点。所以即使你没有特意加key
,只要你不随意改变DOM
结构,react
还是会去复用各个结点。
而在渲染数组结构时,react
默认会拿index
作为key
,如果此时你随意改变各个结点的位置,可能就会导致react
的优化失效。在这种情况下我们需要给每个结点一个真正能代表它本身的key
,这样才能保证在顺序改变时react
还能认出他们。
那么加了key
之后的用处是什么?如果key
不变的情况下还会重新渲染吗?
加了key
之后react
在构建fiber
树的时候就会尝试去复用之前的结点。所以它带来的优化是diff
过程构建结点的时候更快了,后面也会照常的对比props
然后加入到更新链表中。
那么有没有一种方式是可以告诉react
,我这个组件啥都没改,你别给我动它,啥也别管。shouldComponentUpdate
就起到了一个这样的作用。
shouldComponentUpdate
通过给组件加key
的方法确实有效,但是react
最终还是走了diff
流程。
而生命周期中有一个叫做shouldComponentUpdate
的,从名字就可以看出来:组件是否应该更新,如果我们返回false
的话,那么react
是不会去进行diff
流程,也不会去对比props
,更不会去更新用户界面上的组件了。
我们平时可以通过继承PurComponent
组件来达到一个简单的优化效果,继承了PurComponent之后会对props
进行一层浅比较,在props
没有改变的情况下不会去重新走diff
更新流程。
而我们在使用function
组件的时候也可以通过React.memo
达到相同的效果。
比如我在上文的Context
例子中稍微修改一下, 把Component
改为PurComponent
//otherContent.js
export default class OtherContent extends PureComponent {
render() {
console.log("otherContent render")
return (
<>
<div>other content</div>
</>
)
}
}
我们再来看效果
这样每次改变的时候otherContent
组件就不会再走一遍diff
流程了。
react 和 原生比谁更快?
不会吧不会吧,不会现在还有人认为react
会快过原生操作吧。我们之前说了这么多,从JSX
到fiber
到渲染到界面上,react
绕了前面那么一大圈才到了最后的调用js
更新用户界面的流程,怎么可能比你直接调用js
去更新用户界面来的快。
我们之所以用react
开发而不是用原生,并不是因为它快,而是因为它方便。我们能用react
更快速高效的开发出一种新产品,而react
虚拟DOM
的快指的是它在给我们提供了这种便利的前提下,还能给我们保持一定的性能。但是一定是会比你直接写原生慢的。
那当然,你直接用原生写的代码可维护性和可阅读性以及开发效率上也是一定比不上react
的
结语
好了,这就是今天的全部内容了,本文主要介绍了react
是如何把我们的代码渲染到浏览器上的,其实这里面还有很多学问和细节没有在文中列出。我们再用一张图来总结react
的整个流程
其实第一次渲染和后续的渲染略有不同,我这里没有太区别开来,但是大体是这样一个流程。
同时如果文中有什么错误的地方,还请大家在评论区中指出
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!