本文会讲react生态中三类、四个主要library的运行原理和必要的源码验证:
- react
- redux
- mobx
- react-router
react参考版本为v17.0.2
继续阅读之前,如果没看过这篇讲框架的文章,建议先去瞄几眼,后面的讨论中涉及到其中的概念便不再重复。
本次发布主要包括前两章,其余部分后续会陆续补齐。
1 react相关概念
react是一个ui library,使用组件构建可交互ui,并会在数据发生变化时及时更新。
1.1 react mental modal
我们先看一下react最初的设计思路,感受一下递进式的设计过程。
以下前四步在这里有完整代码示例,其余步骤的伪代码可点上面链接查看。
- 转换
react将ui简化为data到另外一些data的映射,相同的输入应返回相同的输出。
- 抽象和组合
一个复杂ui当然不能用一个简单的函数表示,因此可以将ui抽象为多个可复用的片段,然后可以组合起来,即使用一个函数调用另一个函数。
- 状态
一个ui并不只是服务端数据或业务数据的副本,还有很多大量数据表示应用中特定场景的状态快照,比如在输入框中键入、滚动条位置。
因此我们倾向于将数据模型设计为不可变的,并可以通过一个顶层的原子操作就刻意更新状态,代码示例中通过回调来触发重新渲染,这和react实际的数据更新检测模型不太一致,这种主动通知更新的称为push,实际为pull。
- 缓存
一次次调用同一个纯函数(没有副作用的函数,且结果只受参数影响)是很浪费性能的,我们应创建一个可缓存的版本,通过保存参数和结果,只要参数不变,就不用重新执行。
- 逻辑和数据分离
实际的开发中有很多地方需要管理这些状态,为了复用,我们可以使用柯里化,封装业务逻辑后再输入状态。
- 列表
很多ui中会产生很多列表,为了保存这些列表项,可以维护一个map进行保存,其中key可为各项的唯一id。
当我们需要保存很多列表项时,就需要找一个好的缓存策略,来平衡使用频率和占用内存,好在ui列表一般不会太改变,因此可以借助ui中的树来保存(比如dom树或者虚拟dom树等数据结构)
- Algebraic Effects
有时候我们不希望穿过一层层抽象(这里可以理解为函数调用,延伸为抽象树的一个层级)传递每一份数据,即我们需要一个功能可以透过至少两层进行直传,这在react中称为context。
我们可以通过Algebraic Effects的思想实现这个功能,即将做什么和怎么做解耦,如果在当前层级获取不到数据,就将控制权交给另一个获取该数据的逻辑,然后将数据返回后归还控制权继续执行原来的逻辑,可以参考什么是 Algebraic Effects(代数效应)?和Algebraic Effects,以及它在React中的应用。
1.2 深入react编程模型
这里参考将 React 作为 UI 运行时,会对react中的一些概念做进一步的了解。
- 宿主树
react最终会输出一棵随时间变化的树,这被称为宿主树,比如dom树、json对象等,它们是所在宿主环境的一部分。
宿主树的各个节点被称为宿主实例,比如一个dom节点;这些宿主实例有自己的属性,比如domNode.className;宿主环境会提供相关api操作这些宿主实例,比如appendChild。
- 渲染器
渲染器被react用来管理宿主实例,比如react dom,会将react在内存中管理的virtual输出到对应宿主环境。
每个渲染器都有一个入口,指定将react元素树输出到最终的宿主环境中。
- react元素
在宿主环境中宿主实例是最小的构建单位,在react中对应的是react元素,是一个普通的js对象,用来描述宿主对象,react元素之间也组成了一棵树用来表示整个ui,比如
// JSX 是用来描述这些对象的语法糖。
// <dialog>
// <button className="blue" />
// <button className="red" />
// </dialog>
//以下只包含部分属性
{
type: 'dialog',
props: {
children: [{
type: 'button',
props: { className: 'blue' }
}, {
type: 'button',
props: { className: 'red' }
}]
}
}
- 协调
即reconciliation,当调用render首次渲染时,react会维护一个react元素组成的树,当下一次状态更新时,render就会返回另一棵最新的树,react需要基于这两棵树之间的差别有效率的更新ui以保证状态和ui同步,这个过程就被称为协调,这是本文的重点,会在后文详细介绍。
- 组件
上一节我们提到react将ui简化为data到另外一些data的映射
,当映射的目标,也就是函数的输出为react元素时我们称这些函数为组件,另外返回组件的函数也是组件,除了函数组件,还有class组件。
- 控制反转
我们的组件可以是函数,比如Form
,但是实际使用时用的是<Form />
,而不是Form()
// ? React 并不知道 Layout 和 Article 的存在。
// 因为你在调用它们。
ReactDOM.render(
Layout({ children: Article() }),
domContainer
)
// ✅ React知道 Layout 和 Article 的存在。
// React 来调用它们。
ReactDOM.render(
<Layout><Article /></Layout>,
domContainer
)
这种写法可以将控制权交给react,完成后面的工作。
- 缓存
当react中组件状态发生变化时,会默认协调触发setState的整个子树,有些方法可以保存之前的协调结果,比如通过React.memo或PureComponent,当props通过浅对比先后没变化时就会跳过该组件的协调。
- 批量更新
react中的数据更新是异步的,当执行setState等更新数据后,不会立刻触发协调, 而是会根据内部的调度机制批量更新
- 副作用
副作用是指函数做了与本身返回值无关的事,比如数据获取,手动更新dom。
2 fiber架构
本章参考
- React Fiber Architecture
- Inside Fiber: in-depth overview of the new reconciliation algorithm in React
- In-depth explanation of state and props update in React
- The how and why on React’s usage of linked list in Fiber to walk the component’s tree
- 协调
2.1 reconciliation
协调分为render和commit两个阶段,前者用来diff两个virtual dom的区别,后者将diff出来的变化渲染到宿主环境。
下面讲一下在react中的diff算法,这个算法基于以下两个假设
- 两个不同类型的元素产生不同的树
- 子元素上的key属性会在不同渲染下保持稳定
具体为
- 对于根节点
- 当根节点不同时,react会拆卸原来的树并且建立新的树,原来的状态被销毁。
- 当根节点相同时,react保留节点,对比更新的属性,然后对子树递归diff
- 对于子结点
- 如果是列表,即子元素相同,则应添加key属性以复用,就会按顺序对比
- 对于组件元素,如果类型相同,则实例复用更新状态,否则不复用。
2.2 为什么引入新架构
前面我们讲了前端框架中的变化侦测,当应用中状态发生变化,对前后两个virtual dom时有各种diff算法,这个算法的优化效果是有限的,且由于是仍然会有造成长时间阻塞,单帧处理超过16ms,造成卡顿的风险。
在传统的实现中,virtual dom的diff被称为stack reconciler,即以调用栈的形式同步执行,中间不能有停顿,因此可能会影响主线程的其他工作造成卡顿。
因此这里引入fiber架构,对diff流程进行优化,即将render阶段设置为增量的,如有必要分为多个帧执行,以给一些优先级高的任务,比如动画等,充足的的时间。
2.3 什么是fiber
fiber是一种新数据结构的virtual dom,以链表实现。
新的架构使用自己设计的新数据结构,新的数据结构利用window.requestIdleCallback()和window.requestAnimationFrame()将不同优先级的工作分别调度以实现目的。fiber架构中以fiber节点为单位,每个单位是一个执行单元。
2.4 fiber reconciler的目的
这里对fiber的目的做一下汇总
- 将可中断的任务分片,并可重置或复用
- 能够对不同任务调整优先级
- 能够在父元素和子元素之间交错处理,以支持react中的布局(这个场景暂时没想到)
- 能够在render中返回多个元素
- 更好的支持error boundaries
2.5 fiber reconciler相关概念
我们用一个例子来说明整个过程,可以在这里在线演示。
class ClickCounter extends React.Component {
constructor(props) {
super(props);
this.state = {count: 0};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState((state) => {
return {count: state.count + 1};
});
}
render() {
return [
<button key="1" onClick={this.handleClick}>Update counter</button>,
<span key="2">{this.state.count}</span>
]
}
}
- 从react元素到fiber节点
前面我们介绍过react元素,每个react元素描述了一个宿主实例。每个react元素又对应一个fiber节点,其中保存着组件和dom的一些状态。每次组件的render方法执行时,react元素都会重新生成,而fiber节点可能会复用。
前面一直提工作这个词,那么工作具体指的是什么呢,比如调用生命周期函数,或者更新refs,不同类型的react元素的工作是不一样的,在这个简单例子中,ClickCounter组件调用生命周期方法,span host组件同步dom,每个fiber节点上都保存着一些需要处理的工作信息,因此可以看成是一个工作单元。这些工作的执行可以被跟踪、调度、暂停和取消。
当由react元素生成fiber节点时,除了把并在的react元素对应的fiber节点删除和复用旧节点(如有必要会修改属性),还会根据key属性在同一层级移动。
- fiber tree
fiber节点构成一棵树,即之前提到的virtual dom,本例中的树是这样的
树中的节点使用child, sibling and return属性连接,其中child表示子结点,sibling表示同级节点,return表示父节点。
在一个react应用中维护了两棵fiber树,当首次渲染中,react会生成一个fiber树来对应当前ui,这棵树一般被称为current,当react修改状态引起更新,会创建另一棵树来表示下一次渲染,这棵树被称为workInProgress,两棵树使用alternate互相引用。
所有工作都在workInProgress树上进行,这棵树可以认为是current树的草稿,react会在这个草稿上处理完所有组件,然后一口气渲染到屏幕。当其被渲染到屏幕上以后就会成为新的current树。
我们从图上可以看出fiber树的根节点是hostRoot,其作为一个current属性挂载到fiberRoot上,而后者可以在容器的_reactRootContainer._internalRoot属性上获取,比如
const fiberRoot = query('#container')._reactRootContainer._internalRoot
const hostRootFiberNode = fiberRoot.current
- fiber节点
fiber树上的各个节点都是FiberNode类型,除了前面提的alternate, effectTag and nextEffect还有以下属性
- stateNode 表示当前节点表示的组件实例、dom节点或react element,用来存储相关状态
- type 节点相关的函数或class定义,如果对应dom元素,值是html标签,如果是其他则是对应组件实例或函数
- tag 定义fiber节点的类型,决定这种节点包含什么工作
- updateQueue 状态更新队列
- memorizedState 表示上次渲染到屏幕上的state
- memorizedProps 表示上次渲染到屏幕上的props
- key 在前面介绍的用于处理列表内的子元素
- side-effects
前面提过副作用相关概念,react中的,我们可以把一个组件看成是使用state和props作为参数,返回ui的函数,因此与此无关的都可看作副作用,比如同步dom或者调用生命周期函数,执行副作用本身也是一种工作,在每个fiber上有个effectTag字段保存这些副作用,各个节点上的副作用用firstEffect和nextEffect连成一个链表,被称为effects list,如fiber树中
副作用生成的链表为
2.6 fiber reconciler详情
这里是上一节(2.5)的延续。
reconciler分为render和commit两步,第一步的结果是一个标记了side-effects的fiber tree,这个步骤是fiber主要作用的阶段,异步处理一些和ui没关的方面,当首次渲染时同步执行;第二步总是同步的,因为整个渲染过程应该是不间断的,而不能让用户看到渲染的中间过程,主要用于执行更新dom在内的副作用,这个过程结束。
两个阶段还会调用对应的生命周期函数,这些函数就是标记整个过程进行到哪些阶段的hook,类似的比如git hook。
协调的具体细节会在源码阶段解读,现有的资料都比较旧了,和实际的实现不符,因此这里不过多赘述。
2.7 优先级
react系统中优先级分为五种,默认第三种
export const unstable_ImmediatePriority = 1;
export const unstable_UserBlockingPriority = 2;
export const unstable_NormalPriority = 3;
export const unstable_IdlePriority = 5;
export const unstable_LowPriority = 4;
3 fiber相关源码
这里会包含最新版本整个协调过程,包括首次渲染和更新在内的源码。
先推荐几个现成的
- React@16.8.6原理浅析
- React源码系列
4 hooks
hook是react16 除fiber以外的另一个重要更新,利用hook的方式使函数组件有了状态。
参考
- 深入 React Hooks 原理
- React hooks 的基础概念:hooks链表
- Under the hood of React’s hooks system
- useEffect 完整指南
- 函数式组件与类组件有何不同?
5 事件系统
react为了浏览器兼容和性能原因,在原生事件基础上封装了自己的事件系统。
参考
- React 事件系统工作原理
- 动画浅析REACT事件系统和源码
- 谈谈React事件机制和未来(react-events)
- React v17.0 RC 版本发布:没有新特性
6 状态管理
本部分会分别介绍redux和mobx原理及其对比
参考
- Mobx总结以及mobx和redux区别
- Redux or MobX: An attempt to dissolve the Confusion
7 react-router
本部分对react-router相关原理做讨论
参考
- Understanding The Fundamentals of Routing in Reac
- reactjs routing
- React Router源码浅析
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!