关键词:
点击进入React源码调试仓库。
概述
一旦用户的交互产生了更新,那么就会产生一个update对象去承载新的状态。多个update会连接成一个环装链表:updateQueue,挂载fiber上,然后在该fiber的beginWork阶段会循环该updateQueue,依次处理其中的update,这是处理更新的大致过程,也就是计算组件新状态的本质。在React中,类组件与根组件使用一类update对象,函数组件则使用另一类update对象,但是都遵循一套类似的处理机制。暂且先以类组件的update对象为主进行讲解。
相关概念
更新是如何产生的呢?在类组件中,可以通过调用setState产生一个更新:
this.setState(val: 6);
而setState实际上会调用enqueueSetState
,生成一个update对象,并调用enqueueUpdate
将它放入updateQueue。
const classComponentUpdater =
enqueueSetState(inst, payload, callback)
...
// 依据事件优先级创建update的优先级
const lane = requestUpdateLane(fiber, suspenseConfig);
const update = createUpdate(eventTime, lane, suspenseConfig);
update.payload = payload;
enqueueUpdate(fiber, update);
// 开始调度
scheduleUpdateOnFiber(fiber, lane, eventTime);
...
,
;
假设B节点产生了更新,那么B节点的updateQueue最终会是是如下的形态:
A
/
/
B ----- updateQueue.shared.pending = update————
/ ^ |
/ |_______|
C -----> D
updateQueue.shared.pending中存储着一个个的update。下面我们讲解以一下update和updateQueue的结构。
update的结构
update对象作为更新的载体,必然要存储更新的信息
const update: Update<*> =
eventTime,
lane,
suspenseConfig,
tag: UpdateState,
payload: null,
callback: null,
next: null,
;
- eventTime:update的产生时间,若该update一直因为优先级不够而得不到执行,那么它会超时,会被立刻执行
- lane:update的优先级,即更新优先级
- suspenseConfig:任务挂起相关
- tag:表示更新是哪种类型(UpdateState,ReplaceState,ForceUpdate,CaptureUpdate)
payload:更新所携带的状态。
- 类组件中:有两种可能,对象(),和函数((prevState, nextProps):newState => )
- 根组件中:是React.element,即ReactDOM.render的第一个参数
- callback:可理解为setState的回调
- next:指向下一个update的指针
updateQueue的结构
在组件上有可能产生多个update,所以对于fiber来说,需要一个链表来存储这些update,这就是updateQueue,它的结构如下:
const queue: UpdateQueue<State> =
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared:
pending: null,
,
effects: null,
;
我们假设现在产生了一个更新,那么以处理这个更新的时刻为基准,来看一下这些字段的含义:
- baseState:前一次更新计算得出的状态,它是第一个被跳过的update之前的那些update计算得出的state。会以它为基础计算本次的state
- firstBaseUpdate:前一次更新时updateQueue中第一个被跳过的update对象
- lastBaseUpdate:前一次更新中,updateQueue中以第一个被跳过的update为起点一直到的最后一个update截取的队列中的最后一个update。
- shared.pending:存储着本次更新的update队列,是实际的updateQueue。shared的意思是current节点与workInProgress节点共享一条更新队列。
- effects:数组。保存update.callback !== null的Update
有几点需要解释一下:
关于产生多个update对象的场景,多次调用setState即可
this.setState(val: 2); this.setState(val: 6);
产生的updateQueue结构如下:
可以看出它是个单向的环装链表u1 ---> u2 ^ | |________|
- 关于更新队列为什么是环状。
结论是:这是因为方便定位到链表的第一个元素。updateQueue指向它的最后一个update,updateQueue.next指向它的第一个update。
试想一下,若不使用环状链表,updateQueue指向最后一个元素,需要遍历才能获取链表首部。即使将updateQueue指向第一个元素,那么新增update时仍然要遍历到尾部才能将新增的接入链表。而环状链表,只需记住尾部,无需遍历操作就可以找到首部。理解概念是重中之重,下面再来看一下实现:
function enqueueUpdate<State>(fiber: Fiber, update: Update<State>)
const updateQueue = fiber.updateQueue;
if (updateQueue === null)
return;
const sharedQueue: SharedQueue<State> = (updateQueue: any).shared; // ppending是真正的updateQueue,存储update
const pending = sharedQueue.pending;
if (pending === null) // 若链表中没有元素,则创建单向环状链表,next指向它自己
update.next = update;
else
// 有元素,现有队列(pending)指向的是链表的尾部update,
// pending.next就是头部update,新update会放到现有队列的最后
// 并首尾相连
// 将新队列的尾部(新插入的update)的next指向队列的首部,实现
// 首位相连
update.next = pending.next; // 现有队列的最后一个元素的next指向新来的update,实现把新update
// 接到现有队列上
pending.next = update;
// 现有队列的指针总是指向最后一个update,可以通过最后一个寻找出整条链表
sharedQueue.pending = update;
关于firstBaseUpdate 和 lastBaseUpdate,它们两个其实组成的也是一个链表:baseUpdate,以当前这次更新为基准,这个链表存储的是上次updateQueue中第一个被跳过的低优先级的update,到队列中最后一个update之间的所有update。关于baseState,它是第一个被跳过的update之前的那些update计算的state。
这两点稍微不好理解,下面用例子来说明:比如有如下的updateQueue:A1 -> B1 -> C2 -> D1 - E2
字母表示update携带的状态,数字表示update携带的优先级。Lanes模型中,可理解为数越小,优先级越高,所以 1 > 2
第一次以1的渲染优先级处理队列,遇到C2时,它的优先级不为1,跳过。那么直到这次处理完updateQueue时,此时的baseUpdate链表为
C2 -> D1 - E2
本次更新完成后,firstBaseUpdate 为 C2
,lastBaseUpdate 为 E2
,baseState为ABD
。
用firstBaseUpdate 和 lastBaseUpdate记录下被跳过的update到最后一个update的所有update,用baseState记录下被跳过的update之前那些update所计算出的状态。这样做的目的是保证最终updateQueue中所有优先级的update全部处理完时候的结果与预期结果保持一致。也就是说,尽管A1 -> B1 -> C2 -> D1 - E2
这个链表在第一次以优先级为1去计算的结果为ABD(因为优先级为2的都被跳过了),但最终的结果一定是ABCDE,因为这是队列中的所有update对象被全部处理的结果,下边来详细剖析updateQueue的处理机制。
更新的处理机制
处理更新分为三个阶段:准备阶段、处理阶段、完成阶段。前两个阶段主要是处理updateQueue,最后一个阶段来将新计算的state赋值到fiber上。
准备阶段
整理updateQueue。由于优先级的原因,会使得低优先级更新被跳过等待下次执行,这个过程中,又有可能产生新的update。所以当处理某次更新的时候,有可能会有两条update队列:上次遗留的和本次新增的。上次遗留的就是从firstBaseUpdate 到 lastBaseUpdate 之间的所有update;本次新增的就是新产生的那些的update。
准备阶段阶段主要是将两条队列合并起来,并且合并之后的队列不再是环状的,目的方便从头到尾遍历处理。另外,由于以上的操作都是处理的workInProgress节点的updateQueue,所以还需要在current节点也操作一遍,保持同步,目的在渲染被高优先级的任务打断后,再次以current节点为原型新建workInProgress节点时,不会丢失之前尚未处理的update。
处理阶段
循环处理上一步整理好的更新队列。这里有两个重点:
- 本次更新是否处理update取决于它的优先级(update.lane)和渲染优先级(renderLanes)。
本次更新的计算结果基于baseState。
优先级不足
优先级不足的update会被跳过,它除了跳过之外,还做了三件事:
- 将被跳过的update放到firstBaseUpdate 和 lastBaseUpdate组成的链表中,(就是baseUpdate),等待下次处理低优先级更新的时候再处理。
- 记录baseState,此时的baseState为该低优先级update之前所有已被处理的更新的结果,并且只在第一次跳过时记录,因为低优先级任务重做时,要从第一个被跳过的更新开始处理。
将被跳过的update的优先级记录下来,更新过程即将结束后放到workInProgress.lanes中,这点是调度得以再次发起,进而重做低优先级任务的关键。
关于第二点,ReactUpdateQueue.js文件头部的注释做了解释,为了便于理解,我再解释一下。第一次更新的baseState 是空字符串,更新队列如下,字母表示state,数字表示优先级。优先级是1 > 2的 A1 - B1 - C2 - D1 - E2 第一次的渲染优先级(renderLanes)为 1,Updates是本次会被处理的队列: Base state: \'\' Updates: [A1, B1, D1] <- 第一个被跳过的update为C2,此时的baseUpdate队列为[C2, D1, E2], 它之前所有被处理的update的结果是AB。此时记录下baseState = \'AB\' 注意!再次跳过低优先级的update(E2)时,则不会记录baseState Result state: \'ABD\'-------------------------------------------------------------------------------------------------- 第二次的渲染优先级(renderLanes)为 2,Updates是本次会被处理的队列: Base state: \'AB\' <- 再次发起调度时,取出上次更新遗留的baseUpdate队列,基于baseState 计算结果。 Updates: [C2, D1, E2] Result state: \'ABCDE\'
优先级足够
如果某个update优先级足够,主要是两件事:
- 判断若baseUpdate队列不为空(之前有被跳过的update),则将现在这个update放入baseUpdate队列。
处理更新,计算新状态。
将优先级足够的update放入baseUpdate这一点可以和上边低优先级update入队baseUpdate结合起来看。这实际上意味着一旦有update被跳过,就以它为起点,将后边直到最后的update无论优先级如何都截取下来。再用上边的例子来说明一下。A1 - B2 - C1 - D2 B2被跳过,baseUpdate队列为 B2 - C1 - D2
这样做是为了保证最终全部更新完成的结果和用户行为触发的那些更新全部完成的预期结果保持一致。比如,A1和C1虽然在第一次被优先执行,展现的结果为AC,但这只是为了及时响应用户交互产生的临时结果,实际上C1的结果需要依赖B2计算结果,当第二次render时,依据B2的前序update的处理结果(baseState为A)开始处理B2 - C1 - D2队列,最终的结果是ABCD。在提供的高优先级任务插队的例子中,可以证明这一点。
变化过程为 0 -> 2 -> 3,生命周期将state设置为1(任务A2),点击事件将state + 2(任务A1),正常情况下A2正常调度,但是未render完成,此时A1插队,更新队列A2 - A1,为了优先响应高优先级的更新,跳过A2先计算A1,数字由0变为2,baseUpdate为A2 - A1,baseState为0。然后再重做低优先级任务。处理baseUpdate A2 - A1,以baseState(0)为基础进行计算,最后结果是3。
完成阶段
主要是做一些赋值和优先级标记的工作。
- 赋值updateQueue.baseState。若此次render没有更新被跳过,那么赋值为新计算的state,否则赋值为第一个被跳过的更新之前的update。
- 赋值updateQueue 的 firstBaseUpdate 和 lastBaseUpdate,也就是如果本次有更新被跳过,则将被截取的队列赋值给updateQueue的baseUpdate链表。
- 更新workInProgress节点的lanes。更新策略为如果没有优先级被跳过,则意味着本次将update都处理完了,lanes清空。否则将低优先级update的优先级放入lanes。之前说过,
此处是再发起一次调度重做低优先级任务的关键。 更新workInProgress节点上的memoizedState。
源码实现
上面基本把处理更新的所有过程叙述了一遍,现在让我们看一下源码实现。这部分的代码在
processUpdateQueue
函数中,它里面涉及到了大量的链表操作,代码比较多,
我们先来看一下它的结构,我标注出了那三个阶段。function processUpdateQueue<State>(workInProgress: Fiber, props: any, instance: any, renderLanes: Lanes,): void // 准备阶段 const queue: UpdateQueue<State> = (workInProgress.updateQueue: any); let firstBaseUpdate = queue.firstBaseUpdate; let lastBaseUpdate = queue.lastBaseUpdate; let pendingQueue = queue.shared.pending; if (pendingQueue !== null) /* ... */ if (firstBaseUpdate !== null) // 处理阶段 do ... while (true); // 完成阶段 if (newLastBaseUpdate === null) newBaseState = newState; queue.baseState = ((newBaseState: any): State); queue.firstBaseUpdate = newFirstBaseUpdate; queue.lastBaseUpdate = newLastBaseUpdate; markSkippedUpdateLanes(newLanes); workInProgress.lanes = newLanes; workInProgress.memoizedState = newState;
对于上面的概念与源码的主体结构了解之后,放出完整代码,但删除了无关部分,我添加了注释,对照着那三个过程来看会更有助于理解,否则单看链表操作还是有些复杂。
function processUpdateQueue<State>( workInProgress: Fiber, props: any, instance: any, renderLanes: Lanes,): void // 准备阶段---------------------------------------- // 从workInProgress节点上取出updateQueue // 以下代码中的queue就是updateQueue const queue: UpdateQueue<State> = (workInProgress.updateQueue: any); // 取出queue上的baseUpdate队列(下面称遗留的队列),然后 // 准备接入本次新产生的更新队列(下面称新队列) let firstBaseUpdate = queue.firstBaseUpdate; let lastBaseUpdate = queue.lastBaseUpdate; // 取出新队列 let pendingQueue = queue.shared.pending; // 下面的操作,实际上就是将新队列连接到上次遗留的队列中。 if (pendingQueue !== null) queue.shared.pending = null; // 取到新队列 const lastPendingUpdate = pendingQueue; const firstPendingUpdate = lastPendingUpdate.next; // 将遗留的队列最后一个元素指向null,实现断开环状链表 // 然后在尾部接入新队列 lastPendingUpdate.next = null; if (lastBaseUpdate === null) firstBaseUpdate = firstPendingUpdate; else // 将遗留的队列中最后一个update的next指向新队列第一个update // 完成接入 lastBaseUpdate.next = firstPendingUpdate; // 修改遗留队列的尾部为新队列的尾部 lastBaseUpdate = lastPendingUpdate; // 用同样的方式更新current上的firstBaseUpdate 和 // lastBaseUpdate(baseUpdate队列)。 // 这样做相当于将本次合并完成的队列作为baseUpdate队列备份到current节 // 点上,因为如果本次的渲染被打断,那么下次再重新执行任务的时候,workInProgress节点复制 // 自current节点,它上面的baseUpdate队列会保有这次的update,保证update不丢失。 const current = workInProgress.alternate; if (current !== null) // This is always non-null on a ClassComponent or HostRoot const currentQueue:UpdateQueue<State> = (current.updateQueue: any); const currentLastBaseUpdate = currentQueue.lastBaseUpdate; if (currentLastBaseUpdate !== lastBaseUpdate) if (currentLastBaseUpdate === null) currentQueue.firstBaseUpdate = firstPendingUpdate; else currentLastBaseUpdate.next = firstPendingUpdate; currentQueue.lastBaseUpdate = lastPendingUpdate; // 至此,新队列已经合并到遗留队列上,firstBaseUpdate作为 // 这个新合并的队列,会被循环处理 // 处理阶段------------------------------------- if (firstBaseUpdate !== null) // 取到baseState let newState = queue.baseState; // 声明newLanes,它会作为本轮更新处理完成的 // 优先级,最终标记到WIP节点上 let newLanes = NoLanes; // 声明newBaseState,注意接下来它被赋值的时机,还有前置条件: // 1. 当有优先级被跳过,newBaseState赋值为newState, // 也就是queue.baseState // 2. 当都处理完成后没有优先级被跳过,newBaseState赋值为 // 本轮新计算的state,最后更新到queue.baseState上 let newBaseState = null; // 使用newFirstBaseUpdate 和 newLastBaseUpdate // 来表示本次更新产生的的baseUpdate队列,目的是截取现有队列中 // 第一个被跳过的低优先级update到最后的所有update,最后会被更新到 // updateQueue的firstBaseUpdate 和 lastBaseUpdate上 // 作为下次渲染的遗留队列(baseUpdate) let newFirstBaseUpdate = null; let newLastBaseUpdate = null; // 从头开始循环 let update = firstBaseUpdate; do const updateLane = update.lane; const updateEventTime = update.eventTime; // isSubsetOfLanes函数的意义是,判断当前更新的优先级(updateLane) // 是否在渲染优先级(renderLanes)中如果不在,那么就说明优先级不足 if (!isSubsetOfLanes(renderLanes, updateLane)) const clone: Update<State> = eventTime: updateEventTime, lane: updateLane, suspenseConfig: update.suspenseConfig, tag: update.tag, payload: update.payload, callback: update.callback, next: null, ; // 优先级不足,将update添加到本次的baseUpdate队列中 if (newLastBaseUpdate === null) newFirstBaseUpdate = newLastBaseUpdate = clone; // newBaseState 更新为前一个 update 任务的结果,下一轮 // 持有新优先级的渲染过程处理更新队列时,将会以它为基础进行计算。 newBaseState = newState; else // 如果baseUpdate队列中已经有了update,那么将当前的update // 追加到队列尾部 newLastBaseUpdate = newLastBaseUpdate.next = clone; /* * * newLanes会在最后被赋值到workInProgress.lanes上,而它又最终 * 会被收集到root.pendingLanes。 * 再次更新时会从root上的pendingLanes中找出渲染优先级(renderLanes), * renderLanes含有本次跳过的优先级,再次进入processUpdateQueue时, * update的优先级符合要求,被更新掉,低优先级任务因此被重做 * */ newLanes = mergeLanes(newLanes, updateLane); else if (newLastBaseUpdate !== null) // 进到这个判断说明现在处理的这个update在优先级不足的update之后, // 原因有二: // 第一,优先级足够; // 第二,newLastBaseUpdate不为null说明已经有优先级不足的update了 // 然后将这个高优先级放入本次的baseUpdate,实现之前提到的从updateQueue中 // 截取低优先级update到最后一个update const clone: Update<State> = eventTime: updateEventTime, lane: NoLane, suspenseConfig: update.suspenseConfig, tag: update.tag, payload: update.payload, callback: update.callback, next: null, ; newLastBaseUpdate = newLastBaseUpdate.next = clone; markRenderEventTimeAndConfig(updateEventTime, update.suspenseConfig); // 处理更新,计算出新结果 newState = getStateFromUpdate( workInProgress, queue, update, newState, props, instance, ); const callback = update.callback; // 这里的callback是setState的第二个参数,属于副作用, // 会被放入queue的副作用队列里 if (callback !== null) workInProgress.effectTag |= Callback; const effects = queue.effects; if (effects === null) queue.effects = [update]; else effects.push(update); // 移动指针实现遍历 update = update.next; if (update === null) // 已有的队列处理完了,检查一下有没有新进来的,有的话 // 接在已有队列后边继续处理 pendingQueue = queue.shared.pending; if (pendingQueue === null) // 如果没有等待处理的update,那么跳出循环 break; else // 如果此时又有了新的update进来,那么将它接入到之前合并好的队列中 const lastPendingUpdate = pendingQueue; const firstPendingUpdate = ((lastPendingUpdate.next: any): Update<State>); lastPendingUpdate.next = null; update = firstPendingUpdate; queue.lastBaseUpdate = lastPendingUpdate; queue.shared.pending = null; while (true); // 如果没有低优先级的更新,那么新的newBaseState就被赋值为 // 刚刚计算出来的state if (newLastBaseUpdate === null) newBaseState = newState; // 完成阶段------------------------------------ queue.baseState = ((newBaseState: any): State); queue.firstBaseUpdate = newFirstBaseUpdate; queue.lastBaseUpdate = newLastBaseUpdate; markSkippedUpdateLanes(newLanes); workInProgress.lanes = newLanes; workInProgress.memoizedState = newState;
hooks中useReducer处理更新计算状态的逻辑与此处基本一样。
总结
经过上面的梳理,可以看出来整个对更新的处理都是围绕优先级。整个processUpdateQueue函数要实现的目的是处理更新,但要保证更新按照优先级被处理的同时,不乱阵脚,这是因为它遵循一套固定的规则:优先级被跳过后,记住此时的状态和此优先级之后的更新队列,并将队列备份到current节点,这对于update对象按次序、完整地被处理至关重要,也保证了最终呈现的处理结果和用户的行为触发的交互的结果保持一致。
欢迎扫码关注公众号,发现更多技术文章
深入理解reactdiff算法(代码片段)
点击进入React源码调试仓库。上一篇扒一扒React计算状态的原理之后,我们来分析一下Diff的过程。fiber上的updateQueue经过React的一番计算之后,这个fiber已经有了新的状态,也就是state,对于类组件来说,state是在render函数里被使用... 查看详情
扒一扒eventserviceprovider源代码(代码片段)
Ajax用一句话来说就是无须刷新页面即可从服务器取得数据。注意,虽然Ajax翻译过来叫异步JavaScript与XML,但是获得的数据不一定是XML数据,现在服务器端返回的都是JSON格式的文件。完整的Ajax请求过程完整的Ajax请求过程创建XMLHtt... 查看详情
zuul1.3源码扒一扒(代码片段)
先开个头吧作为偶尔点进源码的时候看到东西,或是学到,或是不解,或是惊讶,之后的一些记录。从springcloud各个组件开始吧,计划文段保持间断,只道出核心点,不过各个文段保持连续。zuul作为springcloud推荐网关,搭建起来... 查看详情
扒一扒面向对象编程的另一面(代码片段)
摘要:尽管有很多小伙伴对面向对象的概念已经很很熟了,但是到底什么是面向对象编程?面向对象有哪些特性?面向对象编程能够为我们带来哪些便利?面向对象又有哪些不足呢?本文分享自华为云社区... 查看详情
扒一扒nodejsformidable的onpart
话说使用Nodejs实现一个文件上传,还是蛮简单的,基于Express4.x一般也就formidable用的多些吧;基本的不多说了,github一下都会的;接着《也说文件上传之兼容IE789的进度条---丢掉flash》,新版的大文件上传,最后就差断点续传了,... 查看详情
扒一扒万恶的导航网站
最近脑海里一直徘徊一句话,不知有没有道理:世界愈加复杂,越应该看清楚这个世界的套路,而不是被眼前的具象蒙蔽。是的,这个世界套路很深,雾里看花,大多不是因为雾遮住了花,而是因为自我蒙蔽。 比如说导... 查看详情
为什么会触发anr,从源码中扒一扒(代码片段)
在面试过程中,难免会遇到一道这样的问题,为什么Android程序会ANR?你得解释什么是ANR;ANR(ApplicationNotresponding),是指应用程序未响应,Android系统对于一些事件需要在一定的时间范围内完成,如果超过... 查看详情
[实践篇]13.11扒一扒qvm和vmm之间的那点儿事儿
【QNXHypervisor2.2用户手册】目录(完结) QVM和VMM时Q+LA方案中非常重要的两个概念。VMM通过startupmgr拉起,并根据设备树但配置来启动qvm进程;vmm用于监听qvm服务的状态事件,并及时分发给所需要配合处理的客户端,就像广播一样... 查看详情
[实践篇]13.11扒一扒qvm和vmm之间的那点儿事儿
【QNXHypervisor2.2用户手册】目录(完结) QVM和VMM时Q+LA方案中非常重要的两个概念。VMM通过startupmgr拉起,并根据设备树但配置来启动qvm进程;vmm用于监听qvm服务的状态事件,并及时分发给所需要配合处理的客户端,就像广播一样... 查看详情
扒一扒抖音是如何做线程优化的(代码片段)
背景最近在对一些大厂App进行研究学习,在对某音App进行研究时,发现其在线程方面做了一些优化工作,并且其解决的问题也是之前我在做线上卡顿优化时遇到的,因此对其具体实现方案做了深入分析。本文是对... 查看详情
扒一扒前端包管理器
好久没有写文章了,前端时间一直瞎忙,最近总算有空闲时间可以好好学习下了,今天和大家分享一下有关前端包管理器的东西,主要把工作中常用的bower与最近一直在研究的browserify、duo以及快过时的component进行... 查看详情
扒一扒最近爆火的sdt-luckclub究竟是什么鬼?
...晚上线到现在十分火爆,大批量的玩家进场!今天我们来扒一扒这款区块链去中心化的游戏究竟是什么鬼?SuperSingleDog(超级单身狗)是全球知名游戏开发公司LUCKCLUB(幸运俱乐部)旗下首款区块链去中心化游戏。SDT是区块链发... 查看详情
扒一扒那些被视为“硅谷宠儿”的隐身俱乐部
如果说好点子是“天时”,市场机会是“地利”,那么人脉是让创业更容易更快实现的“人和”因素。对于不少年轻创业者来说,剀洛社团就扮演了“人和”的角色。650)this.width=650;"src="https://img.mp.itc.cn/upload/20170507/79a21e9289bb42a0a2... 查看详情
从零开始的python生活①手撕爬虫扒一扒力扣的用户刷题数据(代码片段)
☘前言☘读完这篇博客,你可以学到什么?python的基础语法(适合c转python)excel的读取和写入方式基本的爬虫定位方法python的安装(这个很容易的)基本的环境配置(这个基本上不用配置)这篇博客里,我将带领大家手撕第一... 查看详情
扒一扒那些说起来简单做着难的技术需求
原文链接 总有一些产品经理在跟技术沟通的时候,说这个不就是调用一个什么方法,那个按钮不就是添加个监听事件吗,有什么难的。但实际开发中要比产品经理想的困难、麻烦得多,老夫来分享一些人僧惊艳。对于产品和... 查看详情
程序员英语语法学习扒一扒名词与形容词的关系
扒一扒形容词与名词的关系热门电视剧最常出现你发现了吗?一个小小的形容词,就让画风突变。上一秒还是后宫嫔妃间的撕[—哔—],下一秒就是霸道总裁式的宠溺。小身材,大能量。没错,今天我们要开始... 查看详情
深入扒一扒安卓中的adb命令
...了。但是,这样不是正确的治学态度,为此孔祥子决定去扒一扒!如下,是我在发现Androidstudio连接不上手机的时候,尝试去启动一个adbserver,但是失败了。<pre>根据问题提示,5037端口被占用了。下面就来看看到底是哪一个... 查看详情
springboot实战:优雅的使用枚举参数(原理篇)(代码片段)
...枚举参数中聊了怎么优雅的使用枚举参数,本文就来扒一扒Spring是如何找到对应转换器Converter的。找入口对Spring有一定基础的同学一定知道,请求入口是DispatcherServlet,所有的请求最终都会落到doDis 查看详情