内容简介:由于有裁员消息流出+被打C的双重冲击,只好尽量在被裁之前恶补一波自己的未知领域,随时做好准备本文是自己在阅读完react fiber的主流程及部分功能模块的一篇个人总结,有些措辞、理解上难免有误,若各位读到这篇文章,请不要吝啬你的评论,请提出任何质疑、批评,可以逐行提出任何问题阅读之前需要了解react和fiber的基本知识,如fiber的链表结构,fiber的遍历,react的基本用法,react渲染两个阶段(reconcile和commit),fiber的副作用(effectTag)
由于有裁员消息流出+被打C的双重冲击,只好尽量在被裁之前恶补一波自己的未知领域,随时做好准备
本文是自己在阅读完react fiber的主流程及部分功能模块的一篇个人总结,有些措辞、理解上难免有误,若各位读到这篇文章,请不要吝啬你的评论,请提出任何质疑、批评,可以逐行提出任何问题
阅读之前需要了解react和fiber的基本知识,如fiber的链表结构,fiber的遍历,react的基本用法,react渲染两个阶段(reconcile和commit),fiber的副作用(effectTag)
个人推荐的fiber阅读方法:
- facebook/react
- 初级入门最佳文章: 司徒正美:React Fiber架构
正美大哥的其它fiber文章,需要边看源码边看文章,只看文章会十分费解,不是他讲的不好,而是react细节太多又重要,讲太细更加云里雾里 - 排除ref、context等细节,排除suspense、error boundary等模块,reconcile+commit在react中我认为是比较简单的,最好自己看看源码,不懂再参考: onefinis:React Fiber 源码理解
- reconcile+commit嫌源码太长太杂,直接参考: Luminqi/learn-react ,带讲解,且代码中影响主流程的源码已经被删光,需要注意其代码设定是expirationTime越小优先级越高,与最新版本的react相反
react调度器任务流程概括
react为实现并发模式(异步渲染)设计了一个带优先级的任务系统,如果我们不知道这个任务系统的运作方式,永远也不会真正了解react,接下来的讲解默认开启react并发模式
首先并发模式的功能:
- 时间分片(time slice),保证动画流畅,保证交互响应,提升用户体验
- 任务的优先级渲染
先从2说起,因为我们要对react有一个全局的理解
首先,要知道调度算法包含一切,每一个调度任务,都需要完成reconcile和commit的流程,因此ReactFiberScheduler.js即为react最核心的模块,完成任务调度全靠他;任务调度需要有一个调度器,细节请移步下文中的Scheduler.js;这个调度器按优先级顺序保存着多个任务,firstCallbackNode为当前任务,从最高优先级任务开始,如图所示:
大家想象一下,调度器要实现任务的优先级调度,当高优先级任务来临时,当前运行的任务(firstCallbackNode)需要打断,让位给高优先级任务,这个过程必须在macrotask中完成,为什么?首先 requestIdleCallback
为macrotask,而且这样的打断才是我们需要的,因为如果在主线程来调度,用户的交互会被js运行卡住,你想打断都打断不了
我们一般会在哪调用setState?
-
componentWillMount
中调用setState;该生命周期的执行会运行在reconcile阶段,不加入任务调度器 -
componentDidMount
中调用setState;该生命周期的执行会运行在commit阶段,react中有一个isRendering
标志,true表示reconcile+commit正在进行,任务加入调度器需要isRendering
为false,不加入任务调度器 - onClick、onChange事件的事件回调函数中调用setState;react将这些事件触发的更新视为批量更新,调度任务不会加入调度器,而是收集所有的setState,再批量同步更新
又由于初始化渲染不开启并发模式,因此调度器中只会有三种来源的任务:
- 在非主线程(setTimeout、promise等)中使用setState而创建的调度任务
- 手动使用
unstable_scheduleCallback
以调用setState而创建的任务 - 在2中,调度器开始执行setState任务,发起的调度任务
在1和3中,setState就真的成了异步更新了;对于1和3,react也会做一个合并的处理,将所有setState合并,如:
setTimeout(() => { this.setState({ nums: this.state.nums + 1 }) this.setState({ nums: this.state.nums + 1 }) }, 0) 复制代码
若state.nums初始值为0,在非并发模式下,最终会更新到2,因为setState是同步的;而在并发模式下,nums最终仍然为1,因为第二个setState任务无法加入调度器; 来源1和来源3都是调度任务,在react调度器中,调度任务不能同时出现两个或以上;为什么有这个规则,我们下文再谈 。源码如下:
function scheduleCallbackWithExpirationTime( root: FiberRoot, expirationTime: ExpirationTime, ) { if (callbackExpirationTime !== NoWork) { // A callback is already scheduled. Check its expiration time (timeout). // 低级别的任务直接 return if (expirationTime < callbackExpirationTime) { // Existing callback has sufficient timeout. Exit. return; } else { if (callbackID !== null) { // Existing callback has insufficient timeout. Cancel and schedule a // new one. // 否则会取消当前,再用新的替代 cancelDeferredCallback(callbackID); } } // The request callback timer is already running. Don't start a new one. } else { startRequestCallbackTimer(); } ... callbackID = scheduleDeferredCallback(performAsyncWork, {timeout}); } 复制代码
调度器已经简单介绍完毕,实现细节请移步下文的Scheduler.js;我们可以把调度器想象成一个黑盒,当我们每调用一次并发模式的setState时,就向调度器加入一个任务,它会自动帮我们按优先级运行
每一个并发模式调用的setState都会产生一个调度任务,而每一个调度任务都会完成一次渲染过程,因此我预测在并发模式正式推出后,会有大量文章针对并发模式下的setState的优化文章,至少我们现在可以知道并发模式下的setState可不能滥用;搞清楚了调度器和任务框架我们再来深入一下调度器中的每个任务
调度任务细节
有了上文的整体框架,我觉得这个时候大家可以自己去看看源码了,我只要告诉你 performAsyncWork
代表向调度器加入一个异步调度任务,而 performSyncWork
代表了主线程开始运行一个同步任务,源码阅读就不困难了
任务大流程
先简单思考一下,一个调度任务需要完成什么工作:
requestIdleCallback
先验知识:
- 调度任务从根节点(root)开始,按照深度优先遍历执行,根节点可以理解为
ReactDOM.render(<App/>, document.getElementById('root'))
所创建,正常来说一个项目应该只有一个 - 一个调度任务可能会包含多个root,执行完高优先级root,才能执行下一个root,高优先级root也是可以插队的
-
fiber.expirationTime
属性代表该fiber的优先级,数值越大,优先级越高,通过当前时间减超时时间获得,同步任务优先级默认为最高 - react当前时间是倒着走的,当前时间初始为一个极大值,随着时间流逝,当前时间越来越小;任务(fiber)的优先级是根据当前时间减超时时间计算,如当前时间10000,任务超时时间500,当前任务优先级算法10000-500=9500;新任务来临,时间流逝,当前时间变为9500,超时时间不变,新任务优先级算法9500-500=9000;新任务优先级低于原任务;
注意:当时间来到9500时,老任务超时,自动获得最高优先级,因为所有新任务除同步任务外优先级永远不会超过老任务,react以这套时间规则来防止低优先级任务一直被插队 -
fiber.childExpirationTime
属性代表该fiber的子节点优先级,该属性可以用来判断fiber的子节点还有没有任务或比较优先级,更新时若没有任务(fiber.childExpirationTime===0
)或者本次渲染的优先级大于子节点优先级,那么不必再往下遍历 当某组件(fiber)触发了任务时,会往上遍历,将fiber.return.expirationTime
和fiber.return.childExpirationTime
全部更新为该组件的expirationTime,一直到root,可以理解为在root上收集更新 - fiber有个alternate属性,实际上是fiber的复制体,同时也指向本体,用于react error boundary踩错误,随时回滚,执行完毕且无误后本体与复制体同步;在初始化时,react并不创建alternate,而在更新时创建
先看看调度任务的总流程:
从setState开始,区分同步或异步,同步则直接运行 performWork
,异步则将 performWork
加入到macrotask运行(调度器);再根据 isYieldy
(是否能打断,同步则不能打断,为false) 来调用不同的 performRoot
循环体;图中绿线代表异步任务,红框表示该过程可被打断;任务未执行完毕的话(被打断),这里会重复向调度器加入任务
注意:这里的打断代表macrotask中该任务已运行完毕,会把js运行交还给主线程,也是用户交互能得到喘息的唯一机会
再看看performRoot循环体:
循环判断是否还有任务以及 !(didYield && currentRendererTime > nextFlushedExpirationTime)
, didYield
表示是否已经被调度器叫停; currentRendererTime
可以理解为任务运行的当前时间,通过 recomputeCurrentRendererTime()
得到,上文说过,随着时间流逝,该值越来越小; nextFlushedExpirationTime
表示将要渲染的任务的时间(root.expirationTime);当两个表达式都为true时,循环才退出, didYield
为true说明任务被调度器叫停,需要被打断, currentRendererTime > nextFlushedExpirationTime
为true表明任务未超时
这里我认为判断有些重复,因为调度器已经为我们判断了是否超时,超时则不会打断,我认为react在这里是一个双保险机制,具体原因未知
进入循环,执行 performWorkOnRoot()
,这个稍后再讲;接下来是 findHighestPriorityRoot()
,其实就是找最高优先级的root,并得到root的expirationTime,root的expirationTime即为将要执行的任务的时间即这里的 nextFlushedExpirationTime
;最后是算当前时间
再看看 performWorkOnRoot()
:
我已合并同步异步的情况,绿线表示异步多出来的部分;代码很简单,就是判断 finishedWork
是否为空,为空则 renderRoot()
,不为空则 completeRoot()
,这里 renderRoot
即为reconcile过程, completeRoot
即为commit过程,接下来看reconcile过程
reconcile
renderRoot
其实只做两件事:
- 执行一个
workLoop
循环体 - 判断
nextUnitOfWork
是否为空,若不为空则任务未完成,下次再继续,为空则表明reconcile完成,赋值root.finishedWork
,这时候才能commit
workLoop循环体:
看的出来 workLoop
即为循环求 nextUnitOfWork
的过程,直到 nextUnitOfWork
为空或者被打断; nextUnitOfWork
是一个全局变量,就是遍历所在的fiber,那么 workLoop
就是不断地遍历,求出下一个fiber;先执行 beginWork
, beginWork
做了什么?
- 如果是初次渲染,需要把fiber的儿子们求出来
- 如果是更新,需要将新的儿子们与原来的儿子们做对比(diff 算法)
- 如果遍历到的是ClassComponent类型(组件)fiber,则要初始化组件,求出它的实例以及调用
processUpdateQueue
(参考后文)得到state,调用render
渲染函数以得到JSX表示的虚拟节点,标记componentDidMount等副作用 - 如果遍历到的是HostComponent类型(div等)fiber,标记Placement副作用
如果 beginWork
的结果为空,说明这个节点已经没有儿子了,接下来就该轮到 completeUnitOfWork
出场了, completeUnitOfWork
需要做到:
- 既然向下遍历已经到头了,需要向右遍历,向右遍历到头了,需要向上回朔
- 将有effectTag标记的fiber给连接起来,加速commit过程
- 做好appendChild工作
- 若有事件,需要绑定事件系统,参考后文
commit
reconcile完成以后,接下来再回到 performWorkOnRoot
中的 commitRoot
,主要工作如下:
- 按照reconcile连接的effect顺序来遍历fiber
- 处理被标记的fiber的effectTag,如:Placement、Deletion、Snapshot等
react调度任务细节总结完毕,我并没有说太多reconcile和commit的细节,因为我认为这部分写多了就不叫总结了,远不如自己读来的清楚
一个不错的比喻
将整个react项目比作一个大型矿场,用户是老板,调度器是包工头,调度任务是矿工,不同的矿场代表着不同的root;一个项目只能有一个矿坑( nextUnitOfWork
),虚线底部代表矿已挖完,reconcile结束
- 老板发出让矿工挖矿的指令,由包工头来调度矿工,包工头会按矿工的优先级顺序来挖矿,最高级的矿工先挖矿,若来了更高级的矿工,当前矿工停止工作,让位给他
- 矿工开始挖矿时,会选择优先级高的矿场来挖矿,正如调度任务从高优先级的root开始调度
- 矿工需要定时休息(任务超过1帧时间),休息好了会接着原来的矿坑来挖矿
- 所有矿场只会有一个矿坑,如果中途替换了不同矿工挖矿,矿工会新开一个矿坑来挖矿
疑问
- 为什么调度器内只能有一个调度任务?
因为一个矿坑只产出一种矿,不同矿工来挖同一个矿坑,有的矿工挖的是金矿,有的矿工挖的是煤矿,不允许;这里可能也有react15中setState合并的考虑
- 为什么commit阶段不能打断?
commit阶段要执行componentDidMount这种react完全失控的副作用,以及其它生命周期,当然不能打断,不然打断再运行,岂不是会重复调用多次? 在reconcile阶段,react同样避免调用任何失控的代码,如componentWillReceiveProps,componentWillReceiveProps,用户在这些生命周期里面调用setState,reconcile被打断后重新开始岂不是要调用多次setState?
- 为什么调度任务要从root开始调度?
如果从目标fiber开始更新,如这里的fiber2,那么我们的矿坑就可以从fiber2开始挖,节省了时间;但是你没有想过,root的优先级是会更新的,如果这时候fiber3拥有了更高优先级,那么会从fiber3开始遍历,由于遍历只能向下或向右,我们会忽视fiber2的更新;所以不如把所有更新提到root,这样唯一的坏处就是被打断之后要从root开始遍历,但是至少不会漏掉更新
思考
- react有个致命缺点,到了react16依然没有改进,那就是如果我们有1万个节点,只变动其中的一个,那么react会在reconcile过程遍历1万次以上,即使最后commit只做一次,以及dom变更只做一次,但是reconcile的开销太没必要了,vue在这点上完爆了react
- 浏览器的改进会导致现有的react fiber架构崩溃,只需要做一个改进:JS执行不阻塞用户交互,动画,重排与重绘
- react是否能使用web worker来改进调度器,调度器始终是个单线程任务执行器,如果我们用web worker来调度任务更能使浏览器的性能发挥到极致,当然第一个前提就是我们的矿坑(nextUnitOfWork)不能是个全局变量
- react并发模式有一个说不上是bug,但是对于用户体验来说是bug的问题
xilixjd/xjd-react-study 这个页面,当用户在输入框中输入123时,输入框最终显示结果为3,或13,原因是reconcile + commit过程太慢,用户在输入1时,页面上输入框的1都没刷出来,用户又输了2,刚刷出1时,用户又输入了3,所以setState接收到的可能是单独的1,单独的2,单独的3或13 - 再有,手动调用
unstable_scheduleCallback
时的timeout值非常有讲究,当用户以滚键盘的极快速度输入1-9时,timeout值设得过低,中间很多数字将不会被渲染! 举例来说,当输入1时,以unstable_scheduleCallback
来调用setState,调度器中存在的任务是setState任务,然后setState任务又创建了一个调度任务,这个调度任务不断地打断重连,我们的交互得到喘息,输入了2,3,4...调度器中又放入了多个setState任务,因为按得太快,这些任务在调度器中被连接到了一起;在第一个任务打断重连完毕后,接下来的几个setState任务全部执行并转成了调度任务,由于这几个调度任务expirationTime相等,执行的却是不同的setState任务,因此调度任务被合并,只会剩下最后一个执行的调度任务; 不过,当timeout值设置得够大时,问题将得到解决,因为这时候加入调度器的setState任务的expirationTime会非常大,它们的执行会非常靠后,在它们创建的每一个调度任务执行完之后,因此输入框的数字将渲染得很完整,不过依然无法摆脱4的问题
TODO
- react hooks
- react suspense,error boundary
- context,ref
react事件系统
react在这部分的内容很多很杂,但是我认为对主流程而言没必要讲的太细,况且我也没看太仔细,这里更多细节只需要参考这篇 文章React事件系统和源码浅析 - 掘金
简单来说,react实现了一套事件系统;在更新props阶段,就为所有拥有事件回调的fiber绑定好事件(react事件系统),事件绑定在document上;触发事件时,进入事件系统,事件系统创建一个 SyntheticEvent
用来代替原生的e对象;接着,以冒泡/捕获的顺序收集所有fiber和其中的事件回调;再按冒泡/捕获顺序触发绑定在fiber上的回调函数
这里需要注意几点:
interactiveUpdates()
if ( !isBatchingUpdates && !isRendering && lowestPriorityPendingInteractiveExpirationTime !== NoWork ) { // Synchronously flush pending interactive updates. performWork(lowestPriorityPendingInteractiveExpirationTime, false); lowestPriorityPendingInteractiveExpirationTime = NoWork; } 复制代码
Scheduler.js —— 实现了requestIdleCallback的polyfill + 优先级任务的功能
关键词:requestAnimationFrame、frameDeadline、activeFrameTime、timeout、unstable_scheduleCallback、unstable_cancelCallback、unstable_shouldYield
简介
核心模块。react fiber的任务调度全靠它,我认为搞懂这个模块才能搞懂react schedule的过程, unstable_scheduleCallback
、 unstable_cancelCallback
、 unstable_shouldYield
,三个api能够分别实现将任务加入任务列表,将任务从任务列表中删除,以及判断任务是否应该被打断
背景
主要实现方法是运用 requestAnimationFrame +MessageChannel + 双向链表的插入排序,最后暴露出 unstable_scheduleCallback
和 unstable_shouldYield
两个api。
对第一位的理解需要看一下这篇文章,简单来说是屏幕显示是显示器在不断地刷新图像,如60Hz的屏幕,每16ms刷新一次,而1帧代表的是一个静止的画面,若一个dom元素从左到右移动,而我们需要这个dom每一帧向右移动1px,60Hz的屏幕,我们需要在16ms以内,完成向右移动的js运行和dom绘制,这样在第二帧(17ms时)开始的时候,dom已经右移了1px,并且被屏幕给刷了出来,我们的眼睛才会感觉到动画的连续性,也就是常说的不掉帧。 requestAnimationFrame
则给了我们十分精确且可靠的服务。
requestAnimationFrame如何模拟requestIdleCallback?
requestIdleCallback
的功能是在每一帧的空闲时间(完成dom绘制、动画等之后)来运行js,若这一帧的空闲时间不足,则分配到下一帧执行,再不足,分配到下下帧完成,直到超过规定的timeout时间,则直接运行js。 requestAnimationFrame
能尽量保证回调函数在一帧内运行一次且dom绘制一次,这样也保证了动画等效果的流畅度,然而却没有超时运行机制,react polyfill的主要是超时功能。
requestAnimationFrame
通常的用法:
function callback(currentTime) { // 动画操作 ... window.requestAnimationFrame(callback) } window.requestAnimationFrame(callback) 复制代码
其代表的是每一帧都尽量运行一次callback,并完成动画绘制,若运行不完,也没办法,就掉帧。
Scheduler.js使用 animationTick
作为 requestAnimationFrame
的callback,用以计算 frameDeadline
和调用传入的回调函数,在react中即为调度函数; frameDeadline
表示的是运行到当前帧的帧过期时间,计算方法是当前时间 + activeFrameTime
, activeFrameTime
表示的是一帧的时间,默认为33ms,但是会根据设备动态调整,比如在刷新频率更高的设备上,连续运行两帧的当前时间比运行到该帧的过期时间 frameDeadline
都小,说明我们一帧中的js任务耗时也小,一帧时间充足且 requestAnimationFrame
调用比预设的33ms频繁,那么 activeFrameTime
会降低以达到最佳性能
有了 frameDeadline
与用户自定义的过期时间 timeoutTime
,那么我们很容易得到polyfill requestIdleCallback的原理:用户定义的callback在这一帧有空就去运行,超过帧过期时间 frameDeadline
就到下一帧去运行,你可以超过帧过期时间,但是你不能超过用户定义的 timeoutTime
,一旦超过,我啥也不管,直接运行callback。
如何实现优先级任务?
Scheduler.js将每一次 unstable_scheduleCallback
的调用根据用户定义的timeout来为任务分配优先级,timeout越小,优先级越高。具体实现为:用双向链表结构来表示任务列表,且按优先级从高到低的顺序进行排列,当某个任务插入时,从头结点开始循环遍历,若遇到某个任务结点node的expirationTime > 插入任务的expirationTime,说明插入任务比node优先级高,则退出循环,并在node前插入,expirationTime = 当前时间 + timeout;这样就实现了按优先级 排序 的任务插入功能, animationTick
会循环调用这些任务链表。
重难点
function unstable_shouldYield() { return ( !currentDidTimeout && ((firstCallbackNode !== null && firstCallbackNode.expirationTime < currentExpirationTime) || shouldYieldToHost()) ); } shouldYieldToHost = function() { return frameDeadline <= getCurrentTime(); }; 复制代码
unstable_shouldYield
被用来判断在任务列表中是否有更高级的任务,在react中用来判断是否能打断当前任务,是schedule中的一个核心api。
首先判断 currentDidTimeout
, currentDidTimeout
为false说明任务没有过期,大家要知道过期任务拥有最高优先级,那么即使有更高级的任务依然无法打断,直接return false; 再判断 firstCallbackNode.expirationTime < currentExpirationTime
,这里实际上是照顾一种特殊的情况,那就是一个最高优先级的任务插入之后,低优先级的任务还在运行中,这种情况是仍然需要打断的;这里 firstCallbackNode
其实是那个插入的高优先级任务,而 currentExpirationTime
其实是上一个任务的expirationTime,只是还没结算
最后是一个 shouldYieldToHost()
,很简单,就是看任务在帧内是否过期,注意到这边任务帧内过期的话是return true,代表直接就能被打断;
ReactUpdateQueue.js —— 用来更新state的模块
关键词:enqueueUpdate、processUpdateQueue
react15中,所有经交互事件触发的setState更新都会被收集到dirtyComponents,收集好了再批量更新;react16由于加入了优先级策略,在调度时连setState操作都被赋予不同的优先级,在同一组件针对带优先级的调度任务及setState操作,是该模块的核心功能
首先贴两个数据结构(已删去部分不关注的属性):
export type Update<State> = { expirationTime: ExpirationTime, payload: any, callback: (() => mixed) | null, next: Update<State> | null, nextEffect: Update<State> | null, }; export type UpdateQueue<State> = { baseState: State, // 头节点 firstUpdate: Update<State> | null, // 尾节点 lastUpdate: Update<State> | null, // callback 处理 firstEffect: Update<State> | null, lastEffect: Update<State> | null, }; 复制代码
fiber上有个 updateQueue
属性,就是来自上述数据结构。每次调用setState的时候,会新建一个 updateQueue
,queue中存储了 baseState
,用于记录state,该属性服务于优先级调度,后面会说;另外记录头节点、尾节点及用于callback的effect头尾指针;还有以链表形式连接的update,如图所示:
每当调用一次setState,会调用 enqueueUpdate
,就会在链表之后插入一个update,这个插入是无序的,然而不同的update是带优先级的,用一个属性expirationTime来表示,payload即为调用setState的第一个参数。
当调度任务依次执行时,会调用 processUpdateQueue
计算最终的state,我们不要忘了调度任务是带有优先级的任务,执行的时候有先后顺序,对应的是 processUpdateQueue
的先后执行顺序;而update也是优先级任务的一部分,当我们按链表顺序从头到尾执行时,需要优先执行高优先级的update,跳过低优先级的update;react的注释为我们阐明了这一过程:
假设有一updateQueue为A1 - B2 - C1 - D2;
A1、B2等代表一个update,其中字母代表state,数字大小代表优先级,1为高优先级;
调度任务按高低优先级依次执行,第一次调度是高优先级任务,从头结点firstUpdate开始处理,processUpdateQueue会跳过低优先级的update;
则执行的update为A1 - C1,本次调度得到的最终state为AC,baseState为A,queue的firstUpdate指针指向B2,以供下次调度使用;
第二次调度是低优先级任务,此时firstUpdate指向B2,则从B2开始,执行的update为 B2 - C1 - D2,最终state将与baseState:A合并,得到ABCD
以上即为 processUpdateQueue
的处理过程,我们需要注意几点:
-
processUpdateQueue
从头结点firstUpdate
开始遍历update,并对state进行合并 对于低优先级的update,遍历时会跳过 - 当遇到有被跳过的update时,
baseState
会定格在被跳过的update之前的resultState -
baseState
主要作用在于记录好被跳过的update之前的state,以便在下一次更加低优先级的调度任务时合并state - 所有调度任务完成后,
firstUpdate
和lastUpdate
指向null,updateQueue
完成使命
再看一个例子:
A1-B1-C2-D3-E2-F1
第一次调度:baseState:AB,resultState:ABF,firstUpdate:C2
第二次调度:baseState:ABC,resultState:ABCEF,firstUpdate:D3
第三次调度:baseState:ABC,resultState:ABCDEF,firstUpdate:null
以上所述就是小编给大家介绍的《react fiber 主流程及功能模块梳理》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 小白学习大数据测试:6个主流程和3个关键步骤归纳整理
- RDIFramework.NET V3.3 Web版新增报表管理功能模块-重量级实用功能
- 一种在 Library 模块中调用 Application 模块功能的方法
- 详解全链路监控架构--目标、功能模块、Dapper和方案比较
- RDIFramework.NET V3.3 WinForm版新增日程管理功能模块
- RDIFramework.NET V3.3 Web版新增日程管理功能模块
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
代码里的世界观——通往架构师之路
余叶 / 人民邮电出版社 / 2018-11 / 59.00元
本书分为两大部分,第一部分讲述程序员在编写程序和组织代码时遇到的很多通用概念和共同问题,比如程序里的基本元素,如何面向对象,如何面向抽象编程,什么是耦合,如何进行单元测试等。第二部分讲述程序员在编写代码时都会遇到的思考和选择,比如程序员的两种工作模式,如何坚持技术成长,程序员的组织生产方法,程序员的职业生涯规划等。一起来看看 《代码里的世界观——通往架构师之路》 这本书的介绍吧!