当前位置: 首页 > news >正文

广州市建设工程造价管理站网站建设 wordpress

广州市建设工程造价管理站,网站建设 wordpress,最有创意促销活动方案,网站开发描述转前端一年半了#xff0c;平时接触最多的框架就是React。在熟悉了其用法之后#xff0c;避免不了想深入了解其实现原理#xff0c;网上相关源码分析的文章挺多的#xff0c;但是总感觉不如自己阅读理解来得深刻。于是话了几个周末去了解了一下常用的流程。也是通过这篇文章…转前端一年半了平时接触最多的框架就是React。在熟悉了其用法之后避免不了想深入了解其实现原理网上相关源码分析的文章挺多的但是总感觉不如自己阅读理解来得深刻。于是话了几个周末去了解了一下常用的流程。也是通过这篇文章将自己的个人理解分享出来。 在具体的源码流程分析之前根据个人理解结合网上比较好的文章先来分析一些概念性的东西。后续再分析具体的流程逻辑。 React 15 架构分层 React 15版本(Fiber以前)整个更新渲染流程分为两个部分 Reconciler(协调器); 负责找出变化的组件Renderer(渲染器); 负责将变化的组件渲染到页面上 Reconciler 在React中可以通过setState、forceUpdate、ReactDOM.render来触发更新。每当有更新发生时Reconciler会做如下工作 调用组件的render方法将返回的JSX转化为虚拟DOM将虚拟DOM和上次更新时的虚拟DOM对比通过对比找出本次更新中变化的虚拟DOM通知Renderer将变化的虚拟DOM渲染到页面上 Renderer 在对某个更新节点执行玩Reconciler之后会通知Renderer根据不同的宿主环境进行相应的节点渲染/更新。 React 15的缺陷 React 15的diff过程是 递归执行更新 的。由于是递归一旦开始就无法中断 。当层级太深或者diff逻辑(钩子函数里的逻辑)太复杂导致递归更新的时间过长Js线程一直卡主那么用户交互和渲染就会产生卡顿。看个例子: count-demo button click button li1li - li2li li2li - li4li li3li - li6li当点击button后列表从左边的1、2、3变为右边的2、4、6。每个节点的更新过程对用户来说基本是同步但实际上他们是顺序遍历的。具体步骤如下 点击button触发更新Reconciler检测到li1需要变更为li2则立刻通知Renderer更新DOM。列表变成2、2、3Reconciler检测到li2需要变更为li4通知Renderer更新DOM。列表变成2、4、3Reconciler检测到li3需要变更为li6则立刻通知Renderer更新DOM。列表变成2、4、6 从此可见 Reconciler和Renderer是交替工作 的当第一个节点在页面上已经变化后第二个节点再进入Reconciler。由于整个过程都是同步的所以在用户看来所有节点是同时更新的。如果中断更新则会在页面上看见更新不完全的新的节点树 假如当进行到第2步的时候突然因为其他任务而中断当前任务导致第3、4步无法进行那么用户就会看到: button click button li1li - li2li li2li - li2li li3li - li3li这种情况是React绝对不希望出现的。但是这种应用场景又是十分必须的。想象一下用户在某个时间点进行了输入事件此时应该更新input内的内容但是因为一个不在当前可视区域的列表的更新导致用户的输入更新被滞后那么给用户的体验就是卡顿的。因此React团队需要寻找一个办法来解决这个缺陷。 React 16 架构分层 React15架构不能支撑异步更新以至于需要重构于是React16架构改成分为三层结构 Scheduler(调度器);调度任务的优先级高优任务优先进入ReconcilerReconciler(协调器);负责找出变化的组件Renderer(渲染器);负责将变化的组件渲染到页面上 Scheduler React 15对React 16提出的需求是Diff更新应为可中断的那么此时又出现了两个新的两个问题:中断方式和判断标准; React团队采用的是 合作式调度即主动中断和控制器出让。判断标准为超时检测。同时还需要一种机制来告知中断的任务在何时恢复/重新执行。 React 借鉴了浏览器的requestIdleCallback接口当浏览器有剩余时间时通知执行。 由于一些原因React放弃使用rIdc而是自己实现了功能更完备的polyfill即Scheduler。除了在空闲时触发回调的功能外Scheduler还提供了多种调度优先级供任务设置。 Reconciler 在React 15中Reconciler是递归处理Virtual DOM的。而React16使用了一种新的数据结构Fiber。Virtual DOM树由之前的从上往下的树形结构变化为基于多向链表的图。 更新流程从递归变成了可以中断的循环过程。每次循环都会调用shouldYield()判断当前是否有剩余时间。源码地址。 function workLoopConcurrent() {// Perform work until Scheduler asks us to yieldwhile (workInProgress ! null !shouldYield()) {workInProgress performUnitOfWork(workInProgress);} }前面有分析到React 15中断执行会导致页面更新不完全原因是因为Reconciler和Renderer是交替工作的因此在React 16中Reconciler与Renderer不再是交替工作。当Scheduler将任务交给Reconciler后Reconciler只是会为变化的Virtual DOM打上代表增/删/更新的标记而不会发生通知Renderer去渲染。类似这样 export const Placement /* */ 0b0000000000010; export const Update /* */ 0b0000000000100; export const PlacementAndUpdate /* */ 0b0000000000110; export const Deletion /* */ 0b0000000001000;只有当所有组件都完成Reconciler的工作才会统一交给Renderer进行渲染更新。 Renderer(Commit) Renderer根据Reconciler为Virtual DOM打的标记同步执行对应的渲染操作。 对于我们在上一节使用过的例子在React 16架构中整个更新流程为 setState产生一个更新更新内容为state.count从1变为2更新被交给SchedulerScheduler发现没有其他更高优先任务就将该任务交给ReconcilerReconciler接到任务开始遍历Virtual DOM判断哪些Virtual DOM需要更新为需要更新的Virtual DOM打上标记Reconciler遍历完所有Virtual DOM通知RendererRenderer根据Virtual DOM的标记执行对应节点操作 其中步骤2、3、4随时可能由于如下原因被中断 有其他更高优先任务需要先更新当前帧没有剩余时间 由于Scheduler和Reconciler的工作都在内存中进行不会更新页面上的节点所以用户不会看见更新不完全的页面。 Diff原则 React的Diff是有一定的 前提假设 的主要分为三点: DOM跨层级移动的情况少对 Virtual DOM 树进行分层比较两棵树只会对同一层次的节点进行比较。不同类型的组件树形结构不一样。相同类型的组件树形结构相似同一层级的一组子节点操作无外乎 更新、移除、新增 ,可以通过 唯一ID 区分节点 无论是JSX格式还是React.createElement创建的React组件最终都会转化为Virtual DOM最终会根据层级生成相应的Virtual DOM树形结构。React 15 每次更新会成新的Virtual DOM然后通 递归 的方式对比新旧Virtual DOM的差异得到对比后的更新补丁最后映射到真实的DOM上。React 16 的具体流程后续会分析到 源码分析 React源码非常多而且16以后的源码一直在调整目前Github上最新源码都是保留xxx.new.js与xxx.old.js两份代码。react源码 是采用Monorepo结构来进行管理的不同的功能分在不同的package里唯一的坏处可能就是方法地址索引起来不是很方便如果不是对源码比较熟悉的话某个功能点可能需要通过关键字全局查询然后去一个个排查。开始之前可以先阅读下官方的这份阅读指南 相关参考视频讲解进入学习 因为源码实在是太多太复杂了所有我这里尽可能的最大到小从面到点的一个个分析。大致的流程如下 首先得知道通过JSX或者createElement编码的代码到底会转成啥然后分析应用的入口ReactDOM.render接着进一步分析setState更新的流程最后再具体分析Scheduler、Reconciler、Renderer的大致流程 触发渲染更新的操作除了ReactDOM.render、setState外还有forceUpdate。但是其实是差不多的最大差异在于forceUpdate不会走shouldComponentUpdate钩子函数。 数据结构 Fiber 开始正式流程分析之前希望你对Fiber有过一定的了解。如果没有建议你先看看这则视频。然后先来熟悉下ReactFiber的大概结构。 export type Fiber {// 任务类型信息// 比如ClassComponent、FunctionComponent、ContextProvidertag: WorkTag,key: null | string,// reactElement.type的值用于reconciliation期间的保留标识。elementType: any,// fiber关联的function/classtype: any,// any类型!! 一般是指Fiber所对应的真实DOM节点或对应组件的实例stateNode: any,// 父节点/父组件return: Fiber | null,// 第一个子节点child: Fiber | null,// 下一个兄弟节点sibling: Fiber | null,// 变更状态比如删除移动effectTag: SideEffectTag,// 用于链接新树和旧树旧-新新-旧alternate: Fiber | null,// 开发模式mode: TypeOfMode,// ...};FiberRoot 每一次通过ReactDom.render渲染的一棵树或者一个应用都会初始化一个对应的FiberRoot对象作为应用的起点。其数据结构如下ReactFiberRoot。 type BaseFiberRootProperties {// The type of root (legacy, batched, concurrent, etc.)tag: RootTag,// root节点ReactDOM.render()的第二个参数containerInfo: any,// 持久更新会用到。react-dom是整个应用更新用不到这个pendingChildren: any,// 当前应用root节点对应的Fiber对象current: Fiber,// 当前更新对应的过期时间finishedExpirationTime: ExpirationTime,// 已经完成任务的FiberRoot对象在commit(提交)阶段只会处理该值对应的任务finishedWork: Fiber | null,// 树中存在的最旧的未到期时间firstPendingTime: ExpirationTime,// 挂起任务中的下一个已知到期时间nextKnownPendingLevel: ExpirationTime,// 树中存在的最新的未到期时间lastPingedTime: ExpirationTime,// 最新的过期时间lastExpiredTime: ExpirationTime,// ... };Fiber 类型 export const FunctionComponent 0; export const ClassComponent 1; export const IndeterminateComponent 2; // 不确定类型可能是class或function export const HostRoot 3; // 树的根 export const HostPortal 4; // 一颗子树 export const HostComponent 5; // 原生节点根据环境而定浏览器环境就是div等 export const HostText 6; // 纯文本节点 export const Fragment 7;模式 到React 16.13.1版本为止内置的开发模式有如下几种 export type TypeOfMode number; // 普通模式|Legacy模式同步渲染React15-16的生产环境用 export const NoMode 0b0000; // 严格模式用来检测是否存在废弃API(会多次调用渲染阶段生命周期)React16-17开发环境使用 export const StrictMode 0b0001; // ConcurrentMode 模式的过渡版本 export const BlockingMode 0b0010; // 并发模式异步渲染React17的生产环境用 export const ConcurrentMode 0b0100; // 性能测试模式用来检测哪里存在性能问题React16-17开发环境使用 export const ProfileMode 0b1000;本文只分析 ConcurrentMode 模式 JSX与React.createElement 先来看一个最简单的JSX格式编码的组件这里借助babel进行代码转换代码看这 // JSX class App extends React.Component {render() {return div /} }// babel var App /*#__PURE__*/function (_React$Component) {_inherits(App, _React$Component);var _super _createSuper(App);function App() {_classCallCheck(this, App);return _super.apply(this, arguments);}_createClass(App, [{key: render,value: function render() {return /*#__PURE__*/React.createElement(div, null);}}]);return App; }(React.Component);关键点在于render方法实际上是调用了React.createElement方法。那么接下来我们只需要分析createElement做了啥即可。我们先看看ReactElement的结构: let REACT_ELEMENT_TYPE 0xeac7; if (typeof Symbol function Symbol.for) {REACT_ELEMENT_TYPE Symbol.for(react.element); }const ReactElement function (type, key, ref, props) {const element {// 唯一地标识为React Element防止XSSJSON里不能存Symbol?typeof: REACT_ELEMENT_TYPE,type: type,key: key,ref: ref,props: props,}return element; }很简单的一个数据结构每个属性的作用都一目了然就不一一解释了。然后分析React.createElement源码。 防XSS攻击 如果你不清楚XSS攻击建议先读这篇文章如何防止XSS攻击。 首先我们编码的组件都会转化为ReactElement的对象。DOM的操作和产生都是有Js脚本产生的。从根本上杜绝了三种XSS攻击(你思品)。 但是React提供了dangerouslySetInnerHTML来作为innerHTML的替代方案。假如某种场景下接口给了我JSON格式的数据。我需要展示在一个div中。如果被攻击者拦截到了并将JSON替换为一段ReactElement格式的结构。那么会发生什么呢? 我这里写了一个demo当去掉?typeof会发现会报错。而Symbol无法JSON化的因此外部也是无法利用dangerouslySetInnerHTML进行攻击的。具体检测的源码看这里 const hasOwnProperty Object.prototype.hasOwnProperty; const RESERVED_PROPS {key: true,ref: true,__self: true,__source: true, };function createElement(type, config, children) {let propName;// Reserved names are extractedconst props {};let key null;let ref null;if (config ! null) {if (hasValidRef(config)) {ref config.ref;}if (hasValidKey(config)) {key config.key;}}// 过滤React保留的关键字for (propName in config) {if (hasOwnProperty.call(config, propName) !RESERVED_PROPS.hasOwnProperty(propName)) {props[propName] config[propName];}}// 遍历childrenconst childrenLength arguments.length - 2;if (childrenLength 1) {props.children children;} else if (childrenLength 1) {const childArray Array(childrenLength);for (let i 0; i childrenLength; i) {childArray[i] arguments[i 2];}props.children childArray;}// 设置默认propsif (type type.defaultProps) {const defaultProps type.defaultProps;for (propName in defaultProps) {if (props[propName] undefined) {props[propName] defaultProps[propName];}}}return ReactElement(type, key, ref, props); }注释应该已经够清楚了哈。总结下来就是根据参数来生成一个ReactElement对象并绑定对应的props、key、ref等 render流程 ReactDOM.render使用参考这里 一般来说使用React编写应用ReactDOM.render是我们触发的第一个函数。那么我们先从ReactDOM.render这个入口函数开始分析render的整个流程。 源码中会频繁出现针对hydrate的逻辑判断和处理。这个是跟SSR结合客户端渲染相关不会做过多分析。源码部分我都会进行省略 ReactDOM.render实际上对ReactDOMLegacy里的render方法的引用精简后的逻辑如下 export function render(// React.creatElement的产物element: React$Elementany, container: Container, callback: ?Function, ) {return legacyRenderSubtreeIntoContainer(null,element,container,false,callback,); }实际上调用的是legacyRenderSubtreeIntoContainer方法再来看看这个咯 function legacyRenderSubtreeIntoContainer(parentComponent: ?React$Componentany, any, // 一般为nullchildren: ReactNodeList, container: Container, forceHydrate: boolean, callback: ?Function, ) {let root: RootType (container._reactRootContainer: any);let fiberRoot;if (!root) {// [Q]: 初始化容器。清空容器内的节点并创建FiberRootroot container._reactRootContainer legacyCreateRootFromDOMContainer(container,forceHydrate,);// FiberRoot; 应用的起点fiberRoot root._internalRoot;if (typeof callback function) {const originalCallback callback;callback function () {const instance getPublicRootInstance(fiberRoot);originalCallback.call(instance);};}// [Q]: 初始化不能批量处理,即同步更新unbatchedUpdates(() {updateContainer(children, fiberRoot, parentComponent, callback);});} else {// 省略... 跟上面类似差别是无需初始化容器和可批处理// [Q]咦? unbatchedUpdates 有啥奥秘呢updateContainer(children, fiberRoot, parentComponent, callback);}return getPublicRootInstance(fiberRoot); }根据官网的使用文档可知在这一步会先清空容器里现有的节点如果有异步回调callback会先保存起来并绑定对应FiberRoot引用关系以用于后续传递正确的根节点。注释里我标注了两个[Q]代表两个问题。我们先来仔细分析这两个问题 初始化 从命名上看legacyCreateRootFromDOMContainer是用来初始化根节点的。 将legacyCreateRootFromDOMContainer的返回结果赋值给container._reactRootContainer而_reactRootContainer从代码上看是作为是否已经初始化的依据也验证了这一点。不信的话打开你的React应用查看下容器元素的_reactRootContainer属性 function legacyCreateRootFromDOMContainer(container: Container, forceHydrate: boolean, ): RootType {// 省略 hydrate ...return createLegacyRoot(container, undefined); }export function createLegacyRoot(container: Container, options?: RootOptions, ): RootType {return new ReactDOMBlockingRoot(container, LegacyRoot, options); }function ReactDOMBlockingRoot(container: Container, tag: RootTag, options: void | RootOptions, ) {// !!! look herethis._internalRoot createRootImpl(container, tag, options); }一连串的函数调用其实就是还回了一个ReactDOMBlockingRoot实例。其中重点在于属性_internalRoot是通过createRootImpl创建的产物。 function createRootImpl(container: Container, tag: RootTag, options: void | RootOptions, ) {// 省略 hydrate ...const root createContainer(container, tag, hydrate, hydrationCallbacks);// 省略 hydrate ...return root; }export function createContainer(containerInfo: Container, tag: RootTag, hydrate: boolean, hydrationCallbacks: null | SuspenseHydrationCallbacks, ): OpaqueRoot {return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks); }export function createFiberRoot(containerInfo: any, tag: RootTag, hydrate: boolean, hydrationCallbacks: null | SuspenseHydrationCallbacks, ): FiberRoot {// 生成 FiberRootconst root: FiberRoot (new FiberRootNode(containerInfo, tag, hydrate): any);if (enableSuspenseCallback) {root.hydrationCallbacks hydrationCallbacks;}// 为Root生成Fiber对象const uninitializedFiber createHostRootFiber(tag);// 绑定 FiberRoot 与 Fiber root.current uninitializedFiber;uninitializedFiber.stateNode root;// 生成更新队列initializeUpdateQueue(uninitializedFiber);return root; }export function initializeUpdateQueueState(fiber: Fiber): void {const queue: UpdateQueueState {baseState: fiber.memoizedState,baseQueue: null,shared: {pending: null,},effects: null,};fiber.updateQueue queue; }大致逻辑就是生成了一个FiberRoot对象root。并生成了root对应的Fiber对象同时生成了该fiber的更新队列。从这里清楚的知道了FiberRoot是在何时初始化的我们得先记住这个FiberRoot可以认为他是整个React应用的起点。 unbatchedUpdates 源码中的英文注释说明这里是无需批处理应该立即执行。其传入参数是一个执行updateContainer的包装函数。 但是在else判断中实际上也执行了updateContainer。那么unbatchedUpdates有啥奥秘呢 export function unbatchedUpdatesA, R(fn: (a: A) R, a: A): R {const prevExecutionContext executionContext;executionContext ~BatchedContext;executionContext | LegacyUnbatchedContext;try {return fn(a);} finally {// !!! look hereexecutionContext prevExecutionContext;if (executionContext NoContext) {flushSyncCallbackQueue();}} }export function flushSyncCallbackQueue() {// 省略...flushSyncCallbackQueueImpl(); }// 清空同步任务队列 function flushSyncCallbackQueueImpl() {if (!isFlushingSyncQueue syncQueue ! null) {isFlushingSyncQueue true;let i 0;try {const isSync true;const queue syncQueue;// 以最高优先级来清空队列里的任务runWithPriority(ImmediatePriority, () {for (; i queue.length; i) {let callback queue[i];do {callback callback(isSync);} while (callback ! null);}});syncQueue null;} catch (error) {// 移除错误的任务if (syncQueue ! null) {syncQueue syncQueue.slice(i 1);}// 在下一个执行单元恢复执行Scheduler_scheduleCallback(Scheduler_ImmediatePriority,flushSyncCallbackQueue,);throw error;} finally {isFlushingSyncQueue false;}} }在unbatchedUpdates中其实就是多了一段finally中的逻辑。其中的逻辑主要是刷新同步任务队列。想一想为啥呢那么说明在fn(a)的执行过程中肯定产生了同步任务呗那么接下来继续跟进到updateContainer中瞧一瞧。 updateContainer 注意这里updateContainer已经是属于Reconciler流程了哦。继续跟进 export function updateContainer(element: ReactNodeList, // 要渲染的组件container: OpaqueRoot, // OpaqueRoot就是FiberRootparentComponent: ?React$Componentany, any, callback: ?Function, ): ExpirationTimeOpaque {// 根节点Fiberconst current container.current;const eventTime requestEventTime();const suspenseConfig requestCurrentSuspenseConfig();// [Q]:计算此次任务的过期时间const expirationTime computeExpirationForFiber(currentTime,current,suspenseConfig,);const context getContextForSubtree(parentComponent);if (container.context null) {container.context context;} else {container.pendingContext context;}// 创建一个更新任务const update createUpdate(eventTime, expirationTime, suspenseConfig);update.payload { element };callback callback undefined ? null : callback;if (callback ! null) {update.callback callback;}// 将任务插入Fiber的更新队列enqueueUpdate(current, update);// 调度任务 scheduleWork为scheduleUpdateOnFiberscheduleWork(current, expirationTime);return expirationTime; }这一步看上去代码贼多其实就是先计算出当前更新的过期时间expirationTime然后通过createUpdate创建了一个update更新任务接着通过enqueueUpdate插入 循环任务队列最后使用scheduleUpdateOnFiber来调度任务。 expirationTime 计算 expirationTime是一个非常重要的概念。 React中为防止某个 update 因为优先级的原因一直被打断而未能执行。React 会设置一个 expirationTime当时间到了 expirationTime 的时候如果某个 update 还未执行的话React 将会强制执行该 update这就是 expirationTime的作用。 这是我们第一次遇到其计算逻辑。我们来具体分析分析。 第一步是需要计算出currentTime其实就是根据当前的时间戳来转换成内置的ExpirationTime。看看 // Max 31 bit integer. The max integer size in V8 for 32-bit systems. // Math.pow(2, 30) - 1 // 0b111111111111111111111111111111 const MAX_SIGNED_31_BIT_INT 1073741823; export const Sync MAX_SIGNED_31_BIT_INT; export const Batched Sync - 1;const UNIT_SIZE 10; const MAGIC_NUMBER_OFFSET Batched - 1;export function msToExpirationTime(ms: number): ExpirationTime {return MAGIC_NUMBER_OFFSET - ((ms / UNIT_SIZE) | 0); }export function requestCurrentTimeForUpdate() {// 省略...return msToExpirationTime(now()); }再来看看computeExpirationForFiber具体的计算逻辑 export function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber, suspenseConfig: null | SuspenseConfig, ): ExpirationTime {const mode fiber.mode;// 同步模式if ((mode BlockingMode) NoMode) {return Sync;}// 从Scheduler取得当前优先级const priorityLevel getCurrentPriorityLevel();if ((mode ConcurrentMode) NoMode) {return priorityLevel ImmediatePriority ? Sync : Batched;}// ...let expirationTime;switch (priorityLevel) {case ImmediatePriority:expirationTime Sync;break;case UserBlockingPriority:// 跟 computeAsyncExpiration 一样。区别在于 expirationInMs 参数值更小。// 因此得到的expirationTime越小优先级越高expirationTime computeInteractiveExpiration(currentTime);break;case NormalPriority:case LowPriority: // TODO: Handle LowPriority// TODO: Rename this to... something better.expirationTime computeAsyncExpiration(currentTime);break;case IdlePriority:expirationTime Idle;break;default:invariant(false, Expected a valid priority level);} }export const LOW_PRIORITY_EXPIRATION 5000; // 这个 BATCH 是那个意思吗 export const LOW_PRIORITY_BATCH_SIZE 250;export function computeAsyncExpiration(currentTime: ExpirationTime, ): ExpirationTime {return computeExpirationBucket(currentTime,LOW_PRIORITY_EXPIRATION,LOW_PRIORITY_BATCH_SIZE,); }function ceiling(num: number, precision: number): number {return (((num / precision) | 0) 1) * precision; }function computeExpirationBucket(currentTime, expirationInMs, // 5000bucketSizeMs, // 250 ): ExpirationTime {return (MAGIC_NUMBER_OFFSET -ceiling(MAGIC_NUMBER_OFFSET - currentTime expirationInMs / UNIT_SIZE,bucketSizeMs / UNIT_SIZE,)); }汇总起来计算公式如下: // current MAGIC_NUMBER_OFFSET - ((now() / UNIT_SIZE) | 0); // expirationTime MAGIC_NUMBER_OFFSET - ((((MAGIC_NUMBER_OFFSET - currentTime 500) / 25) | 0) 1) * 25 // MAGIC_NUMBER_OFFSET - ((((((now() / UNIT_SIZE) | 0) 500) / 25) | 0) 1) * 25其中| 0是用于取整的。注意到 1这个操作说明了啥说明了两个不同的expirationTime之间的差距为25的倍数即25ms内的任务都是同一个expirationTime。那么连续25ms内的更新操作会合并成一个任务咯 正如官网介绍legacy模式在合成事件中有自动批处理的功能但仅限于一个浏览器任务。非React事件想使用这个功能必须使用unstable_batchedUpdates。在blocking模式和concurrent模式下所有的setState在默认情况下都是批处理的。这里写了两个例子便于理解 非 concurrent 模式 setStateconcurrent 模式 setState 分析完expirationTime的计算继续来看看scheduleUpdateOnFiber的逻辑。 从这里开始源码中有同步和异步两种处理方式同步任务是不会经过Scheduer进行调度的。为了分析的完整性我们只分析异步过程。后续频繁提到的expirationTime可以暂且认为其为任务的过期时间节点是具体的时间点而不是时间长度。但是在不同的阶段其意义是不一样的。可以确定的是组件的更新与否或者说更新的时间节点是由其来决定的。 export function scheduleUpdateOnFiber(fiber: Fiber, expirationTime: ExpirationTimeOpaque, ) {// 获取FiberRootconst root markUpdateTimeFromFiberToRoot(fiber, expirationTime);if (root null) {return null;}if (expirationTime Sync) {// 同步任务调度} else {ensureRootIsScheduled(root);schedulePendingInteractions(root, expirationTime);}// 省略... }scheduleUpdateOnFiber只是用于 更新以当前节点为Root的整个树的过期时间。 其中重点在ensureRootIsScheduled这个方法 // 此函数用于调度任务。 一个root(fiber节点)只能有一个任务在执行 // 如果已经有任务在调度中将检查已有任务的到期时间与下一级别任务的到期时间相同。 // 每次更新和任务退出前都会调用此函数 // 注意root是FiberRoot function ensureRootIsScheduled(root: FiberRoot) {// lastExpiredTime代表过期时间const lastExpiredTime root.lastExpiredTime;if (lastExpiredTime ! NoWork) {// 特殊情况过期的工作应同步刷新root.callbackExpirationTime Sync;root.callbackPriority ImmediatePriority;root.callbackNode scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root),);return;}// 下一个最近的到期时间const expirationTime getNextRootExpirationTimeToWorkOn(root);// root有正在处理的调度任务const existingCallbackNode root.callbackNode;if (expirationTime NoWork) {if (existingCallbackNode ! null) {root.callbackNode null;root.callbackExpirationTime NoWork;root.callbackPriority NoPriority;}return;}// 获取当前任务的过期时间; 同一事件中发生的所有优先级相同的更新都收到相同的到期时间const currentTime requestCurrentTimeForUpdate();// 根据下一次调度任务的过期时间与当前任务的过期时间计算出当前任务的优先级// 即currentTime小于expirationTime那么其优先级更高const priorityLevel inferPriorityFromExpirationTime(currentTime,expirationTime,);// 如果当前正在处理的任务优先级基于此次任务取消正在处理的任务!if (existingCallbackNode ! null) {const existingCallbackPriority root.callbackPriority;const existingCallbackExpirationTime root.callbackExpirationTime;if (// 任务必须具有完全相同的到期时间。existingCallbackExpirationTime expirationTime // 比较两次任务的优先级existingCallbackPriority priorityLevel) {return;}// 取消调度任务cancelCallback(existingCallbackNode);}// 更新到期时间与优先级root.callbackExpirationTime expirationTime;root.callbackPriority priorityLevel;let callbackNode;if (expirationTime Sync) {// 省略...// 这里会将任务推入同步任务队列前面分析到 flushSyncCallbackQueueImpl 清空的任务就是从这里推入} else {// 将任务推入Scheduler调度队列callbackNode scheduleCallback(priorityLevel,// 绑定performConcurrentWorkOnRoot.bind(null, root),// 计算超时时间{ timeout: expirationTimeToMs(expirationTime) - now() },);}// 更新Fiber的当前回调节点root.callbackNode callbackNode; }ensureRootIsScheduled中的主要逻辑分三步 计算此次任务的过期时间和优先级。如果当前节点已有任务在调度中。如果到期时间相同且已有任务的的优先级更高则取消此次调度。否则取消已有任务。将任务推入Scheduler中的调度队列并设置其优先级与任务过期时间 这段代码每一段都是可以去延伸开分析的。但是我这里主要是分析大致流程所以主要分析scheduleCallback相关的逻辑。其他部分以后有时间在进一步分析。 scheduleCallback是将任务的执行函数交由Scheduler来处理。那么后续的流程需要等待Scheduler来触发具体的执行函数performConcurrentWorkOnRoot。关于render的流程就先暂时分析到这里为止。 render流程总结 render会调用legacyRenderSubtreeIntoContainer方法legacyRenderSubtreeIntoContainer中如果是第一次渲染会先初始化FiberRoot其为应用的起点。同时生成根节点的Fiber实例。这里 FiberRoot.current Fiber; Fiber.stateNode FiberRoot。调用updateContainer会计算出此次更新的过期时间。并生成任务对象update将其插入Fiber中的更新队列然后调用scheduleUpdateOnFiber触发任务调度scheduleUpdateOnFiber会更新以该Fiber节点为根节点的整棵Fiber树的过期时间。然后调用ensureRootIsScheduled进行调度ensureRootIsScheduled中会绑定任务与具体执行函数。然后交由Scheduler处理 setState流程 在继续分析后续的Reconciler和Renderer细节之前咋们趁热打铁来熟悉下setState的流程。既然调用的时候是通过this.setState来调动的那么就从Component里面去找咯。来look一下ReactBaseClasses const emptyObject {}; function Component(props, context, updater) {this.props props;this.context context;this.refs emptyObject;// ReactNoopUpdateQueue 是一个没啥意义的空对象this.updater updater || ReactNoopUpdateQueue; }Component.prototype.setState function (partialState, callback) {this.updater.enqueueSetState(this, partialState, callback, setState); };Component的初始结构很简单。我们看到其setState方法就是调用了this.updater.enqueueSetState方法但是update默认是空的无用对象我们一般也没有在构造方法里传入一个update参数那么说明这个方法肯定是后续注入的咯。与是我找啊找找到了一个差不多的东西classComponentUpdater const classComponentUpdater {isMounted,enqueueSetState(inst, payload, callback) {const fiber getInstance(inst);const currentTime requestCurrentTimeForUpdate();const suspenseConfig requestCurrentSuspenseConfig();const expirationTime computeExpirationForFiber(currentTime,fiber,suspenseConfig,);// 生成此次setState的更新对象const update createUpdate(expirationTime, suspenseConfig);update.payload payload;if (callback ! undefined callback ! null) {update.callback callback;}// 更新任务入队enqueueUpdate(fiber, update);scheduleWork(fiber, expirationTime);},enqueueReplaceState(inst, payload, callback) {// 同上类似},enqueueForceUpdate(inst, callback) {// 同上类似}, };嘿嘿是不是发现了enqueueSetState里的逻辑有点似曾相识。其实就是我们之前分析render流程中遇到的updateContainer的流程是一样的啦。不记得的话回头再看看咯。那么接下来我们只要分析下classComponentUpdater是怎么注入为Component的update属性即可了。 前面分析render流程的时候我们还只分析到了生成任务分片并推入调度队列还没有对组件的初始化有过分析。从Component的构造函数中猜测是不是在初始化Component的时候React帮我们注入的呢 顺着这个思路进行下一步的分析。首先我们先来看beginWork方法中的一段代码beginWork方法在后面会具体分析。这里先知道他是用于创建子组件的Fiber对象即可。 function beginWork(current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null {// 尝试复用 current 节点if (current ! null) {// 省略...}// 不能复用则 update 或者 mountswitch (workInProgress.tag) {// 省略...case ClassComponent: {const Component workInProgress.type;const unresolvedProps workInProgress.pendingProps;const resolvedProps workInProgress.elementType Component? unresolvedProps: resolveDefaultProps(Component, unresolvedProps);return updateClassComponent(current,workInProgress,Component,resolvedProps,renderExpirationTime,);}// 省略...} }beginWork中的代码分为两部分。分别用于处理mount和update的逻辑。我们分析的流程是第一次初始化那么走的是mount流程。beginWork会根据不同的tag调用不同的方法这里我们先来看看updateClassComponent function updateClassComponent(current: Fiber | null, workInProgress: Fiber, Component: any, nextProps, renderExpirationTime: ExpirationTime, ) {// 省略 context 的处理...// 组件的实例const instance workInProgress.stateNode;let shouldUpdate;// instance为null 说明组件第一次渲染if (instance null) {if (current ! null) {// 重置current与wip的依赖(备份)current.alternate null;workInProgress.alternate null;// 标记为新增节点workInProgress.effectTag | Placement;}// 初始化组件实例constructClassInstance(workInProgress, Component, nextProps);// 挂载 并调用相应的生命周期mountClassInstance(workInProgress,Component,nextProps,renderExpirationTime,);shouldUpdate true;} else {// 省略更新逻辑...}// TODO执行 render 新建子Fiber。const nextUnitOfWork finishClassComponent(current,workInProgress,Component,shouldUpdate,hasContext,renderExpirationTime,);return nextUnitOfWork; }function constructClassInstance(workInProgress: Fiber, ctor: any, props: any, ): any {let context emptyContextObject;// 省略 context 相关逻辑...const instance new ctor(props, context);const state (workInProgress.memoizedState instance.state ! null instance.state ! undefined? instance.state: null);adoptClassInstance(workInProgress, instance);// 省略 context 相关逻辑...return instance; }function adoptClassInstance(workInProgress: Fiber, instance: any): void {instance.updater classComponentUpdater;workInProgress.stateNode instance;// 绑定实例与Fiber方便后续更新使用setInstance(instance, workInProgress); }可以看到当instance为null的时候会执行以下几个流程 并标记当前的effectTag为Placement代表为新增节点初始化生成实例然后绑定到Fiber(workInProgress)上并绑定update属性最后会调用mountClassInstance来挂载节点并调用相关的生命周期。 至此后续的更新流程就跟render流程一致的了就不做重复分析啦~ Scheduler Scheduler是React团队针对任务调度单独实现的一个rIdc的polyfill。React团队其意图不仅仅局限于React这一个应用场景更想服务与更多的业务成为更广泛应用的一个工具。 最小优先队列 既然任务具有不同的过期时间和优先级那么就需要一个数据结构来管理优先级任务。React中expirationTime越小的任务应该更优先处理那么这个数据结构显然就是一个最小优先队列啦。而React是基于小顶堆来实现的最小优先队列。还是直接看代码吧。SchedulerMinHeap type Heap ArrayNode; type Node {|id: number,sortIndex: number, |};// 插入到堆末尾 export function push(heap: Heap, node: Node): void {const index heap.length;heap.push(node);siftUp(heap, node, index); }// 获取堆顶任务sortIndex/id 最小的任务 export function peek(heap: Heap): Node | null {const first heap[0];return first undefined ? null : first; }// 删除堆顶任务 export function pop(heap: Heap): Node | null {const first heap[0];if (first ! undefined) {const last heap.pop();if (last ! first) {heap[0] last;siftDown(heap, last, 0);}return first;} else {return null;} }// 向上维持小顶堆 function siftUp(heap, node, i) {let index i;while (true) {// 位运算对应根据节点求其父节点- i / 2 - 1const parentIndex (index - 1) 1;const parent heap[parentIndex];if (parent ! undefined compare(parent, node) 0) {// parent 更大交换位置heap[parentIndex] node;heap[index] parent;index parentIndex;} else {return;}} }// 向下维持小顶堆 function siftDown(heap, node, i) {let index i;const length heap.length;while (index length) {const leftIndex (index 1) * 2 - 1;const left heap[leftIndex];const rightIndex leftIndex 1;const right heap[rightIndex];// // 如果左子节点或右子节点小于目标节点(父节点)则交换if (left ! undefined compare(left, node) 0) {if (right ! undefined compare(right, left) 0) {heap[index] right;heap[rightIndex] node;index rightIndex;} else {heap[index] left;heap[leftIndex] node;index leftIndex;}} else if (right ! undefined compare(right, node) 0) {heap[index] right;heap[rightIndex] node;index rightIndex;} else {return;}} }function compare(a, b) {// Compare sort index first, then task id.// 先比较sort index再比较 task idconst diff a.sortIndex - b.sortIndex;return diff ! 0 ? diff : a.id - b.id; }具体实现就是用数组模拟了一个最小堆的结构。可以看到每次任务的插入或者移除都会重新回复最小堆结构排序规则以sortIndextaskId为辅。在React中sortIndex对应的其实就是过期时间taskId则为递增任务序列。这一点后续会分析到。 开启任务调度 前面有分析到在ensureRootIsScheduled中会生成一个任务节点然后通过scheduleCallback将任务推入Scheduler中。那么我们先从这个任务进队的方法来逐步分析 var taskIdCounter 1;// 目前Scheduler对外的api都是unstate_级别的表示不是稳定版本 function unstable_scheduleCallback(priorityLevel, callback, options) {// 实际是调用performance.now() 或者 Date.now() 前者更精确var currentTime getCurrentTime();var startTime;var timeout;// 根据是否有延迟来确定开始时间if (typeof options object options ! null) {var delay options.delay;if (typeof delay number delay 0) {startTime currentTime delay;} else {startTime currentTime;}// [Q1]:有超时配置直接用。否则根据优先级计算timeout typeof options.timeout number? options.timeout: timeoutForPriorityLevel(priorityLevel);} else {timeout timeoutForPriorityLevel(priorityLevel);startTime currentTime;}// 过期时间等于开始时间超时时间var expirationTime startTime timeout;// 一个task的数据结构就是这样啦。var newTask {// 相同超时时间的任务会对比id那就是先到先得咯id: taskIdCounter,callback,priorityLevel,startTime,expirationTime,sortIndex: -1,};if (enableProfiling) {newTask.isQueued false;}// [Q2]下面出现了一个延迟队列timerQueue和一个任务队列(taskQueue)if (startTime currentTime) {// This is a delayed task.// 说明这是一个延迟任务即options.delay存在嘛newTask.sortIndex startTime;// 如果开始时间大于当前时间就将它 push 进这个定时器队列说明这个是一个等待队列push(timerQueue, newTask);// 如果任务队列为空说明所有任务都被延迟且newTask是最早的延迟任务。if (peek(taskQueue) null newTask peek(timerQueue)) {// All tasks are delayed, and this is the task with the earliest delay.// 如果正在进行超时处理先取消后续再重新开始if (isHostTimeoutScheduled) {cancelHostTimeout();} else {isHostTimeoutScheduled true;}// 发起一个超时处理requestHostTimeout(handleTimeout, startTime - currentTime);}} else {newTask.sortIndex expirationTime;// 非延迟任务丢入任务队列push(taskQueue, newTask);if (enableProfiling) {markTaskStart(newTask, currentTime);newTask.isQueued true;}// 如果没在调度中则开启调度if (!isHostCallbackScheduled !isPerformingWork) {isHostCallbackScheduled true;// [Q]开启调度requestHostCallback(flushWork);}}// [A]还回这个task的引用return newTask; }从这段代码可以看到一个调度任务的数据结构是怎样的以及任务的排序依据sortIndex其实就是任务的过期时间expirationTime而id则是一个递增序列。注释中标注了几个问题下面一一具体分析 timeout计算 // 立即执行 var IMMEDIATE_PRIORITY_TIMEOUT -1; // 用户行为阻塞 var USER_BLOCKING_PRIORITY 250; // 默认五秒过期时间 var NORMAL_PRIORITY_TIMEOUT 5000; var LOW_PRIORITY_TIMEOUT 10000; // 永不过期, maxSigned31BitInt为v8 32为系统最大有效数值 var IDLE_PRIORITY maxSigned31BitInt;function timeoutForPriorityLevel(priorityLevel) {switch (priorityLevel) {case ImmediatePriority:return IMMEDIATE_PRIORITY_TIMEOUT;case UserBlockingPriority:return USER_BLOCKING_PRIORITY;case IdlePriority:return IDLE_PRIORITY;case LowPriority:return LOW_PRIORITY_TIMEOUT;case NormalPriority:default:return NORMAL_PRIORITY_TIMEOUT;} }可以看到这里将优先级转换成了常量级的具体时间优先级越高的timeout时间越低。 taskQueue timerQueue 在startTime currentTime的条件分支中分别将任务推入了taskQueue和timerQueue。而这两个队列其实就是我们前面分析到的一个最小堆的结构。taskQueue代表当前正在调度的任务而timerQueue代表延迟任务队列。在任务调度的过程中会不停的将timerQueue中的任务转移到taskQueue中这一步后续会分析到。 调度的具体过程 我们看到当任务插入调度队列时如果此时不在调度中会调用requestHostCallback方法开启调度并传入了一个flushwork作为入参函数。 requestHostCallback function(callback) {// 这里将传入的callback缓存起来了scheduledHostCallback callback;// 是否在消息循环中if (!isMessageLoopRunning) {isMessageLoopRunning true;port.postMessage(null);} };从代码看似乎rHC的作用只是缓存了callback即flushwork这个入参函数。并发送了一个空的message。那么重点就在与这个port是为何物了。其实这里就是React如何模拟requestIdleCallback的地方了。 MessageChannel 模拟 rIC 实现循环调度 不熟悉MessageChannel的可以先了解一下。先来看看Scheduler中是如何用的。 const channel new MessageChannel(); const port channel.port2; channel.port1.onmessage performWorkUntilDeadline;可以得知当使用port.postMessage发生消息的时候实际处理消息的函数为performWorkUntilDeadline。 let isMessageLoopRunning false; let scheduledHostCallback null;const performWorkUntilDeadline () {// scheduledHostCallback 具体是由 scheduledHostCallback 赋值的if (scheduledHostCallback ! null) {const currentTime getCurrentTime();// [Q]:截止时间 当前时间 yieldIntervaldeadline currentTime yieldInterval;const hasTimeRemaining true;try {// 是否还有剩余任务。scheduledHostCallback 可能是 flushworkconst hasMoreWork scheduledHostCallback(hasTimeRemaining,currentTime,);if (!hasMoreWork) {// 没有更多任务 停止循环并清楚scheduledHostCallback引用isMessageLoopRunning false;scheduledHostCallback null;} else {// 如果还有任务则继续发消息。类似一个递归的操作port.postMessage(null);}} catch (error) {// 如果一个任务出错了。直接跳过执行下一个任务并抛出错误port.postMessage(null);throw error;}} else {// 重置循环状态isMessageLoopRunning false;}// [Q]: 目前不知道这是啥needsPaint false; };老样子这里有几个问题需要仔细分析下。 yieldInterval 从名字和使用方法上来看我觉着应该是代表任务的执行时间。 // 默认是5 let yieldInterval 5;forceFrameRate function (fps) {// 看不起我144hzif (fps 0 || fps 125) {console[error](forceFrameRate takes a positive int between 0 and 125, forcing framerates higher than 125 fps is not unsupported,);return;}if (fps 0) {yieldInterval Math.floor(1000 / fps);} else {yieldInterval 5;} };forceFrameRate是一个对外提供的api接口用于动态配置调度任务的执行周期。 deadline needsPaint let deadline 0; let maxYieldInterval 300; let needsPaint false;if (enableIsInputPending navigator ! undefined navigator.scheduling ! undefined navigator.scheduling.isInputPending ! undefined ) {const scheduling navigator.scheduling;shouldYieldToHost function () {const currentTime getCurrentTime();if (currentTime deadline) {// 没有时间了。可能希望让主线程让出控制权以便浏览器可以执行高优先级任务主要是绘制和用户输入// 因此如果有绘制或者用户输入行为,则应该让出放回true// 如果两者都不存在那么可以在保持响应能力的同时降低产量// 但是存在非requestPaint发起的绘制状态更新或其他主线程任务(如网络事件)// 因此最终在某个临界点还是得让出控制权if (needsPaint || scheduling.isInputPending()) {// 有待处理的绘制或用户输入return true;}// 没有待处理的绘制或输入。但在达到最大产量间隔时也需要释放控制权return currentTime maxYieldInterval;} else {return false;}};requestPaint function () {needsPaint true;}; } else {shouldYieldToHost function () {return getCurrentTime() deadline;};requestPaint function () { }; }首先需要明确的是shouldYieldToHost与requestPaint是Scheduler对外提供的接口函数。具体的使用后续会分析到位。 从代码可知deadline的用途是用于在shouldYieldToHost中 检测调度是否超时。默认清空下是直接对比当前时间currentTime与deadline的值。但是在支持navigator.scheduling的环境下React会有更多的考虑也就是浏览器绘制与用户输入要有限响应否则可以适当的延长调度时间。 到这里先总结下调度启动的过程免得脑子糊了。 requestHostCallback准备好要执行的任务scheduledHostCallbackrequestHostCallback开启任务调度循环MessageChannel接收消息并调用performWorkUntilDeadline执行任务performWorkUntilDeadline中先计算此次调度的deadline。然后执行任务在执行完一个任务后会根据返回值来判断是否有下一个任务。如果有则通过消息循环来达到递归执行performWorkUntilDeadline。否则结束消息循环 前面还只是分析了任务调度循环执行的逻辑。具体执行的任务是scheduledHostCallback的引用函数flushWork。 任务执行 function flushWork(hasTimeRemaining, initialTime) {if (enableProfiling) {markSchedulerUnsuspended(initialTime);}// Well need a host callback the next time work is scheduled.isHostCallbackScheduled false;if (isHostTimeoutScheduled) {// We scheduled a timeout but its no longer needed. Cancel it.isHostTimeoutScheduled false;cancelHostTimeout();}isPerformingWork true;const previousPriorityLevel currentPriorityLevel;try {if (enableProfiling) {try {return workLoop(hasTimeRemaining, initialTime);} catch (error) {if (currentTask ! null) {const currentTime getCurrentTime();markTaskErrored(currentTask, currentTime);currentTask.isQueued false;}throw error;}} else {// No catch in prod codepath.// 官方注释说生成环境不会去catch workLoop抛出的错误return workLoop(hasTimeRemaining, initialTime);}} finally {currentTask null;currentPriorityLevel previousPriorityLevel;isPerformingWork false;if (enableProfiling) {const currentTime getCurrentTime();markSchedulerSuspended(currentTime);}} }flushWork的工作比较简单。只是重置了一些标志符最终返回了workLoop的执行结果。那么重点肯定在这个函数了。 function workLoop(hasTimeRemaining, initialTime) {let currentTime initialTime;// [Q]: 这是作甚advanceTimers(currentTime);// 取出顶端任务。即最优先的任务currentTask peek(taskQueue);while (currentTask ! null // debug 用的不管!(enableSchedulerDebugging isSchedulerPaused)) {if (// 任务未过期并且当前调度的deadline到了将任务放到下次调度周期进行; shouldYieldToHost currentTask.expirationTime currentTime // 这两个前面分析过了 hasTimeRemaining一直为true那还判断有啥意义???(!hasTimeRemaining || shouldYieldToHost())) {break;}const callback currentTask.callback;if (callback ! null) {currentTask.callback null;currentPriorityLevel currentTask.priorityLevel;// 计算当前任务是否已经超时const didUserCallbackTimeout currentTask.expirationTime currentTime;markTaskRun(currentTask, currentTime);// [Q]: 执行callback比如前面render流程分析到的 performConcurrentWorkOnRootconst continuationCallback callback(didUserCallbackTimeout);currentTime getCurrentTime();if (typeof continuationCallback function) {// continuationCallback 成立则取代当前任务的callbackcurrentTask.callback continuationCallback;markTaskYield(currentTask, currentTime);} else {if (enableProfiling) {markTaskCompleted(currentTask, currentTime);currentTask.isQueued false;}// continuationCallback 不成立从任务队列弹出// 防止任务被其他地方取出得判断一下if (currentTask peek(taskQueue)) {pop(taskQueue);}}// em.... 又是它advanceTimers(currentTime);} else {// 任务被取消了弹出任务// 回顾下ensureRootIsScheduled 中调用 cancelCallback 的情况pop(taskQueue);}// 再次从顶端取任务// 注意如果 continuationCallback 成立的话是没有pop当前任务的。此次取到的还是当前任务currentTask peek(taskQueue);}// performWorkUntilDeadline 中判断 hasMoreWork 的逻辑就是这里啦!if (currentTask ! null) {return true;} else {// [Q]检测延迟队列中的任务是不是过期let firstTimer peek(timerQueue);if (firstTimer ! null) {requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);}return false;} }大致流程注释已经很详细了。老规矩分析标注的几个问题。 advanceTimers function advanceTimers(currentTime) {// 遍历 timerQueue 中的任务将超时的任务转移到 taskQueue 中去let timer peek(timerQueue);while (timer ! null) {if (timer.callback null) {// 任务被取消pop(timerQueue);} else if (timer.startTime currentTime) {// 超时任务转移pop(timerQueue);timer.sortIndex timer.expirationTime;push(taskQueue, timer);if (enableProfiling) {markTaskStart(timer, currentTime);timer.isQueued true;}} else {// 未过时的继续挂起return;}timer peek(timerQueue);} }wookLoop函数入口第一次调用advanceTimers是将任务重新梳理一下刷新任务队列。而之后每次在while调用是 因为任务的执行是需要消耗一定的时间的所有在执行完后需要重新刷新任务队列。 continuationCallback 首先continuationCallback的产生是有callback决定的。callback的返回值可能是一个函数这代表着当前任务应该被重新处理一次。这里先留个问题后续在分析callback的具体实现的时候我们再进一步分析 requestHostTimeout handleTimeout 在wookLoop的结尾当currentTask null的时候会去检测延迟队列中的任务是否已经过期。 requestHostTimeout function (callback, ms) {taskTimeoutID setTimeout(() {callback(getCurrentTime());}, ms); };function handleTimeout(currentTime) {isHostTimeoutScheduled false;// 重新梳理任务队列advanceTimers(currentTime);// isHostCallbackScheduled 为true。说明有新任务进来了if (!isHostCallbackScheduled) {// 如果上面的 advanceTimers 梳理了过期的延迟任务到任务队列中则执行if (peek(taskQueue) ! null) {isHostCallbackScheduled true;requestHostCallback(flushWork);} else {// 否则递归调用该方法const firstTimer peek(timerQueue);if (firstTimer ! null) {requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);}}} }可以看出其实就是在任务队列中的任务执行完成后。通过递归的方法从延迟队列中查询是否有过期任务有的话则转移到任务队列中并执行。 到这里Scheduler从任务入列到循环调度到任务执行的完整过程就已经分析完成了。做个简单的流程总结: unstable_scheduleCallback创建任务如果任务是延迟的则推入延迟队列timerQueue否则推入任务队列taskQueue如果创建的任务是延迟任务则调用requestHostTimeout方法使用setTimeout来 递归检测任务是否过期。否则直接发起任务调度requestHostCallbackrequestHostCallback通过MessageChannel的port2发送消息给port1具体的处理函数为performWorkUntilDeadlineperformWorkUntilDeadline会计算此次调度的deadline同时使用 消息循环 来递归执行任务任务具体处理是由wookLoop执行。其将任务从任务队列taskQueue堆顶依次取出执行。如果任务队列清空则调用requestHostTimeout开启递归检测。 Reconciler 分析完Scheduler的逻辑后接下来接着分析Reconciler的逻辑。我们老生常谈的Diff更新的逻辑大部分就是发生在Reconciler阶段其中包含了大量的组件更新计算与优化。 上面分析了Scheduler的调度过程。而具体在Scheduler中的执行的callback是performConcurrentWorkOnRoot。我们来看一看 // 被Scheduler调用的入口函数 function performConcurrentWorkOnRoot(root, didTimeout) {// 重置currentEventTime NoWork;if (didTimeout) {// 任务已经超时const currentTime requestCurrentTimeForUpdate();// 将过期时间标记为当前以在单个批处理中同步处理已过期的工作。markRootExpiredAtTime(root, currentTime);// 调度一个同步任务ensureRootIsScheduled(root);return null;}// 获取下一个到期(更新)时间. 将以此作为本次渲染的执行必要性判断const expirationTime getNextRootExpirationTimeToWorkOn(root);if (expirationTime ! NoWork) {const originalCallbackNode root.callbackNode;// TODO:刷新被动的HooksflushPassiveEffects();// 如果根或到期时间已更改则丢弃现有堆栈并准备新的堆栈。 否则我们将从中断的地方继续。if (root ! workInProgressRoot ||expirationTime ! renderExpirationTime) {// [Q]: 重置数据;// 设置 renderExpirationTime 为expirationTime// 复制 root.current 为 workInProgress等prepareFreshStack(root, expirationTime);startWorkOnPendingInteractions(root, expirationTime);}if (workInProgress ! null) {// 省略...do {try {workLoopConcurrent();break;} catch (thrownValue) {handleError(root, thrownValue);}} while (true);// 省略...}if (workInProgress ! null) {// 仍然有任务要做。说明是超时了退出而不提交。stopInterruptedWorkLoopTimer();} else {stopFinishedWorkLoopTimer();const finishedWork: Fiber ((root.finishedWork root.current.alternate): any);root.finishedExpirationTime expirationTime;// commit开始 Renderer 流程finishConcurrentRender(root,finishedWork,workInProgressRootExitStatus,expirationTime,);}}return null; }首先会判断任务是否超时如果超时则以同步的方式执行该任务防止任务被中断。如果没有超时则先在prepareFreshStack中做一些初始化的工作。然后进入了workLoopConcurrent循环。 prepareFreshStack // 本次渲染的到期时间 let renderExpirationTime: ExpirationTime NoWork;function prepareFreshStack(root, expirationTime) {// 省略...if (workInProgress ! null) {// workInProgress 不为空说明之前有中断的任务。放弃let interruptedWork workInProgress.return;while (interruptedWork ! null) {unwindInterruptedWork(interruptedWork);interruptedWork interruptedWork.return;}}workInProgressRoot root;// 从current 复制 wip; 并重置effectListworkInProgress createWorkInProgress(root.current, null);// 设置renderExpirationTime为下一个到期时间renderExpirationTime expirationTime;// 省略... }如果当前wip不为空说明上次有中断的任务通过不停向上回溯直到root节点来取消中断的任务。然后从 同时将前面从FiberRoot中获取下一个任务的到期时间赋值给renderExpirationTime作为本次渲染的到期时间。 workLoopConcurrent workLoopConcurrent的代码在本文开头就贴出来过这里重新看下 function workLoopConcurrent() {while (workInProgress ! null !shouldYield()) {// 第一次入参workInProgress为FiberRoot的Fiber// 后续将上一次返回值(子Fiber)作为入参workInProgress performUnitOfWork(workInProgress);} }workLoopConcurrent的工作主要是循环对比current和workInProgress两颗Fiber树。在wip中为变化的Fiber打上effectTag。同时会从下往上更新/创建DOM节点构成一颗离屏DOM树最后交由Renderer处理。 基于循环的递归 在熟悉流程之前先贴出一个删减版的代码流程。这里不按套路出牌先根据个人理解做个总结。这样带着大致的思路结构可能会更好的去理解后续的源码。 function performUnitOfWork(unitOfWork: Fiber): Fiber | null {// 旧的 Fiber, 用于对比const current unitOfWork.alternate;// 省略...// [Q]: 处理当前Fiber节点还回下一个子节点Fiberlet next beginWork(current, unitOfWork, renderExpirationTime);unitOfWork.memoizedProps unitOfWork.pendingProps;// 没有子节点if (next null) {next completeUnitOfWork(unitOfWork);}ReactCurrentOwner.current null;return next; }// 尝试完成当前的Fiber然后移至下一个同级。如果没有更多的同级返回父fiber。 function completeUnitOfWork(unitOfWork: Fiber): Fiber | null {workInProgress unitOfWork;do {// 旧的 Fiber, 用于对比const current workInProgress.alternate;const returnFiber workInProgress.return;// Check if the work completed or if something threw.if ((workInProgress.effectTag Incomplete) NoEffect) {// [Q]: 创建/更新当前Fiber对应的节点实例let next completeWork(current, workInProgress, renderExpirationTime);stopWorkTimer(workInProgress);resetChildExpirationTime(workInProgress);if (next ! null) {// 产生了新的子节点return next;}// [Q]:后面是在构建 effectList 的单向链表// 先省略...} else {// 有异常抛出。根据是否是boundary来决策是捕获还是抛出异常// 省略...}const siblingFiber workInProgress.sibling;// 是否存在兄弟节点if (siblingFiber ! null) {return siblingFiber;}workInProgress returnFiber;} while (workInProgress ! null);if (workInProgressRootExitStatus RootIncomplete) {workInProgressRootExitStatus RootCompleted;}return null; }首先执行beginWork进行节点操作以及创建子节点子节点会返回成为next如果有next就返回。返回到workLoopConcurrent之后workLoopConcurrent会判断是否过期之类的如果没过期则再次调用该方法。 如果next不存在说明当前节点向下遍历子节点已经到底了说明这个子树侧枝已经遍历完可以完成这部分工作了。执行completeUnitOfWork主要分一下几个步骤 completeUnitOfWork首先调用completeWork创建/更新当前Fiber对应的节点实例(如原生DOM节点)instance同时将已经更新的子Fiber的实例插入到instance构成一颗离屏渲染树。存在当前Fiber节点存在effectTag则将其追加到effectList中查找是否有sibling兄弟节点有则返回该兄弟节点因为这个节点可能也会存在子节点需要通过beginWork进行操作。如果不存在兄弟节点。一直往上返回直到root节点或者在某一个节点发现有sibling兄弟节点。如果到了root那么其返回也是null代表整棵树的遍历已经结束了可以commit了。如果中间遇到兄弟节点则同于第3步 文字表达可能不是很清楚直接看一个例子: 执行顺序为: 文本节点“你好” 不会执行beginWork/completeWork因为React针对只有单一文本子节点的Fiber会特殊处理 1. App beginWork 2. div Fiber beginWork 3. span Fiber beginWork 4. span Fiber completeWork 5. div Fiber completeWork 6. p Fiber beginWork 7. p Fiber completeWork 8. App Fiber completeWorkbeginWork beginWork在前面分析setState的时候已经分析过其中mount阶段对应的逻辑了。那么这里就只分析update的逻辑了。先来看下beginWork的大致工作。 /** * param {*} current 旧的Fiber * param {*} workInProgress 新的Fiber * param {*} renderExpirationTime 下一次到期时间即本次渲染有效时间 * returns 子组件 Fiber */ function beginWork(current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null {const updateExpirationTime workInProgress.expirationTime;// 尝试复用 current 节点if (current ! null) {// 省略...// 复用 currentreturn bailoutOnAlreadyFinishedWork(current,workInProgress,renderExpirationTime,);}workInProgress.expirationTime NoWork;// 不能复用则 update 或者 mountswitch (workInProgress.tag) {// 省略...case ClassComponent: {const Component workInProgress.type;const unresolvedProps workInProgress.pendingProps;const resolvedProps workInProgress.elementType Component? unresolvedProps: resolveDefaultProps(Component, unresolvedProps);return updateClassComponent(current,workInProgress,Component,resolvedProps,renderExpirationTime,);}case HostRoot:return updateHostRoot(current, workInProgress, renderExpirationTime);case HostComponent:return updateHostComponent(current, workInProgress, renderExpirationTime);case HostText:return updateHostText(current, workInProgress);// 省略... } }我们接着之前分析过的updateClassComponent来分析update的流程。 function updateClassComponent(current: Fiber | null, workInProgress: Fiber, Component: any, nextProps, renderExpirationTime: ExpirationTime, ) {// 提前处理context逻辑。省略....// 组件的实例const instance workInProgress.stateNode;let shouldUpdate;if (instance null) {// mount. wip.effectTag Placement// 省略...} else {// update. wip.effectTag Update | Snapshot// 调用 render 之前的生命周期getDerivedStateFromProps | UNSAFE_componentWillReceiveProps(可能两次)// 接着调用shouldComponentUpdate判断是否需要更新// 最后更新props 和 stateshouldUpdate updateClassInstance(current,workInProgress,Component,nextProps,renderExpirationTime,);}// 执行 render 新建子Fiber。const nextUnitOfWork finishClassComponent(current,workInProgress,Component,shouldUpdate,hasContext,renderExpirationTime,);return nextUnitOfWork; }function finishClassComponent(current: Fiber | null, workInProgress: Fiber, Component: any, shouldUpdate: boolean, hasContext: boolean, renderExpirationTime: ExpirationTime, ) {// 引用应该更新即使shouldComponentUpdate返回falsemarkRef(current, workInProgress);const didCaptureError (workInProgress.effectTag DidCapture) ! NoEffect;// 无需更新且没有发送错误则直接复用currentif (!shouldUpdate !didCaptureError) {if (hasContext) {invalidateContextProvider(workInProgress, Component, false);}// 复用currentreturn bailoutOnAlreadyFinishedWork(current,workInProgress,renderExpirationTime,);}const instance workInProgress.stateNode;// RerenderReactCurrentOwner.current workInProgress;let nextChildren instance.render();// PerformedWork 提供给 React DevTools 读取workInProgress.effectTag | PerformedWork;if (current ! null didCaptureError) {// 出错了。// 省略...} else {reconcileChildren(current,workInProgress,nextChildren,renderExpirationTime,);}workInProgress.memoizedState instance.state;if (hasContext) {invalidateContextProvider(workInProgress, Component, true);}return workInProgress.child; }export function reconcileChildren(current: Fiber | null, workInProgress: Fiber, nextChildren: any, renderExpirationTime: ExpirationTime, ) {if (current null) {// mount的组件workInProgress.child mountChildFibers(workInProgress,null,nextChildren,renderExpirationTime,);} else {// update的组件workInProgress.child reconcileChildFibers(workInProgress,current.child,nextChildren,renderExpirationTime,);} }最后还回的就是workInProgress.child跟beginWork一样根据current null来区分mount和update。 实际上mountChildFibers和reconcileChildFibers均指向同一个函数reconcileChildFibers。差别在于第二个参数currentFirstChild。如果为null则会去创建一个新的Fiber对象否则复用并更新props。比如reconcileSingleElement用于处理只有单个节点的情况。 completeWork function completeWork(current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null {const newProps workInProgress.pendingProps;switch (workInProgress.tag) {//省略...case HostComponent: {popHostContext(workInProgress);const rootContainerInstance getRootHostContainer();const type workInProgress.type;// fiber节点对应的DOM节点是否存在// updateif (current ! null workInProgress.stateNode ! null) {// 为 wip 计算出新的 updateQueue// updateQueue 是一个奇数索引的值为变化的prop key偶数索引的值为变化的prop value 的数组updateHostComponent(current,workInProgress,type,newProps,rootContainerInstance,);if (current.ref ! workInProgress.ref) {markRef(workInProgress);}} else {// mountif (!newProps) {return null;}const currentHostContext getHostContext();// 是不是服务端渲染let wasHydrated popHydrationState(workInProgress);if (wasHydrated) {// 省略...} else {// 生成真实DOMlet instance createInstance(type,newProps,rootContainerInstance,currentHostContext,workInProgress,);// 将子孙DOM节点插入刚生成的DOM节点中从下往上构成一颗离屏DOM树appendAllChildren(instance, workInProgress, false, false);workInProgress.stateNode instance;// 与updateHostComponent类似的处理 propsif (finalizeInitialChildren(instance,type,newProps,rootContainerInstance,currentHostContext,)) {markUpdate(workInProgress);}}if (workInProgress.ref ! null) {markRef(workInProgress);}}return null;}//省略...}}首先和beginWork一样根据current null判断是mount还是update。 update时主要做了如下几件事情具体源码diffProperties 计算新的STYLE prop计算新的DANGEROUSLY_SET_INNER_HTML prop计算新的CHILDREN prop 每次计算出新的prop都将其propKey与nextProp成对的保存在数组updatePayload中。最后将updatePayload赋值给wip.updateQueue。 mount时处理的事情比较多大致如下: createInstance: 为Fiber节点生成对应的真实DOM节点appendAllChildren: 将子孙DOM节点插入刚生成的DOM节点中。以此从下往上构成完整的DOM树finalizeInitialChildren: 在setInitialProperties中处理事件注册。在setInitialDOMProperties根据props初始化DOM属性 值的注意的是appendAllChildren方法。由于completeWork属于向上回溯的过程每次调用appendAllChildren时都会将已生成的子孙DOM节点插入当前生成的DOM节点下。那么当回溯到根root节点时整个DOM树就都已经更新好了。 effectList 在每次completeWork后代表某个节点已经处理完成。前面说过Reconciler会为发生改变的节点打上effectTag用于在Renderer根据节点的effectTag的执行具体更新。 因此在completeWork的上层函数completeUnitOfWork中(也就是之前省略的代码)每执行完completeWork会去维护一个effectList的单向链表。如果当前Fiber存在effectTag则插入链表。 // 构建 effectList 的单向链表 if (returnFiber ! null (returnFiber.effectTag Incomplete) NoEffect ) {// firstEffect 为链表头结点if (returnFiber.firstEffect null) {returnFiber.firstEffect workInProgress.firstEffect;}// lastEffect 为链表尾节点if (workInProgress.lastEffect ! null) {if (returnFiber.lastEffect ! null) {returnFiber.lastEffect.nextEffect workInProgress.firstEffect;}returnFiber.lastEffect workInProgress.lastEffect;}const effectTag workInProgress.effectTag;// 跳过NoWork和PerformedWork tag。后者是提供给React Tools读取if (effectTag PerformedWork) {if (returnFiber.lastEffect ! null) {returnFiber.lastEffect.nextEffect workInProgress;} else {returnFiber.firstEffect workInProgress;}returnFiber.lastEffect workInProgress;} }至此Reconciler流程结束。回头再看看开头的总结是不是清楚一些了呢~ Renderer(Commit) Commit阶段的代码相对另外两个来说是较为简单的。其入口在前面分析过的任务调度入口函数performConcurrentWorkOnRoot中的结尾finishConcurrentRender。最终调用的函数为commitRootImpl。看看代码: let nextEffect: Fiber | null null;function commitRootImpl(root, renderPriorityLevel) {// 省略...const finishedWork root.finishedWork;const expirationTime root.finishedExpirationTime;if (finishedWork null) {return null;}root.finishedWork null;root.finishedExpirationTime NoWork;// commit不可中断。 总是同步完成。// 因此现在可以清除这些内容以允许安排新的回调。root.callbackNode null;root.callbackExpirationTime NoWork;root.callbackPriority NoPriority;root.nextKnownPendingLevel NoWork;// 省略...// 获取effectListlet firstEffect;if (finishedWork.effectTag PerformedWork) {if (finishedWork.lastEffect ! null) {finishedWork.lastEffect.nextEffect finishedWork;firstEffect finishedWork.firstEffect;} else {firstEffect finishedWork;}} else {firstEffect finishedWork.firstEffect;}if (firstEffect ! null) {// 省略...nextEffect firstEffect;do {// [Q]: 执行 snapshot getSnapshotBeforeUpdate()// 结果赋值为 Fiber.stateNode.instance.__reactInternalSnapshotBeforeUpdate snapshotcommitBeforeMutationEffects();} while (nextEffect ! null);// 省略...nextEffect firstEffect;do {// [Q]: 根据Fiber.effectTag 执行具体的增删改DOM操作// 如果是卸载组件还会调用 componentWillUnmount()commitMutationEffects(root, renderPriorityLevel);} while (nextEffect ! null);// 省略...nextEffect firstEffect;do {// [Q]: 调用 render 后的生命周期// current null ? componentDidMount : componentDidUpdatecommitLayoutEffects(root, expirationTime);} while (nextEffect ! null);stopCommitLifeCyclesTimer();nextEffect null;// 告诉Scheduler在帧末尾停止调度这样浏览器就有机会绘制。requestPaint();// 省略...} else {// 省略...}// 省略...return null; }省略了许多的代码留下主要的内容。主要逻辑就是拿到Reconciler维护的effectList链表后三次遍历该链表分别做的是 获取Snapsshot用于componentDidUpdate的第三个参数根据Fiber.effectTag对组件或DOM执行具体操作调用所有组件的生命周期函数 commitBeforeMutationEffects 完整代码看commitBeforeMutationLifeCycles其中tai为ClassComponent的组件主要逻辑如下 const current nextEffect.alternate; finishedWork nextEffect; if (finishedWork.effectTag Snapshot) {if (current ! null) {const prevProps current.memoizedProps;const prevState current.memoizedState;const instance finishedWork.stateNode;const snapshot instance.getSnapshotBeforeUpdate(finishedWork.elementType finishedWork.type? prevProps: resolveDefaultProps(finishedWork.type, prevProps),prevState,);instance.__reactInternalSnapshotBeforeUpdate snapshot;} }commitMutationEffects function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {while (nextEffect ! null) {const effectTag nextEffect.effectTag;if (effectTag ContentReset) {// 把节点的文字内容设置为空字符串commitResetTextContent(nextEffect);}if (effectTag Ref) {const current nextEffect.alternate;if (current ! null) {// 把ref置空后续会设置ref所以之前ref上的值需要先清空commitDetachRef(current);}}let primaryEffectTag effectTag (Placement | Update | Deletion | Hydrating);switch (primaryEffectTag) {case Placement: {commitPlacement(nextEffect);// 从effectTag中清除Placement标记nextEffect.effectTag ~Placement;break;}case PlacementAndUpdate: {// PlacementcommitPlacement(nextEffect);nextEffect.effectTag ~Placement;// Updateconst current nextEffect.alternate;commitWork(current, nextEffect);break;}case Update: {const current nextEffect.alternate;commitWork(current, nextEffect);break;}case Deletion: {// componentWillUnmountcommitDeletion(root, nextEffect, renderPriorityLevel);break;}// 省略...}nextEffect nextEffect.nextEffect;} }好像也没啥好说的。值得注意的是开始前会先调用commitDetachRef将ref的引用清除。然后针对不同的effectTag执行不同的DOM操作。 commitPlacement; 新增节点。其中节点插入位置的计算算法可以看下commitWork; 根据Reconciler在diffProperties计算出来的updateQueue数组进行DOM更新commitDeletion; 这一步会从上往下依次调用该子树下每个组件的componentWillUnmount函数 commitLayoutEffects function commitLayoutEffects(root: FiberRoot, committedExpirationTime: ExpirationTime, ) {while (nextEffect ! null) {setCurrentDebugFiberInDEV(nextEffect);const effectTag nextEffect.effectTag;if (effectTag (Update | Callback)) {recordEffect();const current nextEffect.alternate;commitLayoutEffectOnFiber(root,current,nextEffect,committedExpirationTime,);}if (effectTag Ref) {recordEffect();commitAttachRef(nextEffect);}resetCurrentDebugFiberInDEV();nextEffect nextEffect.nextEffect;} }function commitLifeCycles(finishedRoot: FiberRoot, current: Fiber | null, finishedWork: Fiber, committedExpirationTime: ExpirationTime, ): void {switch (finishedWork.tag) {// ...case ClassComponent: {const instance finishedWork.stateNode;if (finishedWork.effectTag Update) {if (current null) {instance.componentDidMount();} else {const prevProps finishedWork.elementType finishedWork.type? current.memoizedProps: resolveDefaultProps(finishedWork.type, current.memoizedProps);const prevState current.memoizedState;instance.componentDidUpdate(prevProps,prevState,instance.__reactInternalSnapshotBeforeUpdate,);}}const updateQueue finishedWork.updateQueue;if (updateQueue ! null) {// 调用setState注册的回调函数commitUpdateQueue(finishedWork, updateQueue, instance);}return;}// ...} }还是遍历每个Fiber节点。如果是ClassComponent需要调用生命周期方法。同时对于更新的ClassComponent需要判断调用的setState是否有回调函数如果有的话需要在这里一起调用。最后会调用commitAttachRef更新ref引用。 Commit阶段的流程到这里也就结束了。 说实话React的源码是在是真的多。想完完全全细节分析到每一个点需要大量的时间和精力。本文也只是分析了一个大致的流程很多细节之处没有分析到位。后续会再花点时间针对一些细节问题做下探索。说到底目前也只从面到面而没有达到从面到点分析的效果。许多观点是个人的理解写出来是以供学习交流有不妥之处还请提提意见。
文章转载自:
http://www.morning.qxnns.cn.gov.cn.qxnns.cn
http://www.morning.nckzt.cn.gov.cn.nckzt.cn
http://www.morning.rjtmg.cn.gov.cn.rjtmg.cn
http://www.morning.dwhnb.cn.gov.cn.dwhnb.cn
http://www.morning.yldgw.cn.gov.cn.yldgw.cn
http://www.morning.ryztl.cn.gov.cn.ryztl.cn
http://www.morning.bfnbn.cn.gov.cn.bfnbn.cn
http://www.morning.qhydkj.com.gov.cn.qhydkj.com
http://www.morning.jxdhc.cn.gov.cn.jxdhc.cn
http://www.morning.wjhqd.cn.gov.cn.wjhqd.cn
http://www.morning.hnkkm.cn.gov.cn.hnkkm.cn
http://www.morning.gwsll.cn.gov.cn.gwsll.cn
http://www.morning.ctqbc.cn.gov.cn.ctqbc.cn
http://www.morning.ohmyjiu.com.gov.cn.ohmyjiu.com
http://www.morning.jpwkn.cn.gov.cn.jpwkn.cn
http://www.morning.zdxss.cn.gov.cn.zdxss.cn
http://www.morning.ffcsr.cn.gov.cn.ffcsr.cn
http://www.morning.zshuhd015.cn.gov.cn.zshuhd015.cn
http://www.morning.gmnmh.cn.gov.cn.gmnmh.cn
http://www.morning.kfcz.cn.gov.cn.kfcz.cn
http://www.morning.dnydy.cn.gov.cn.dnydy.cn
http://www.morning.qyhcm.cn.gov.cn.qyhcm.cn
http://www.morning.nqrfd.cn.gov.cn.nqrfd.cn
http://www.morning.cnyqj.cn.gov.cn.cnyqj.cn
http://www.morning.mnyzz.cn.gov.cn.mnyzz.cn
http://www.morning.jbmsp.cn.gov.cn.jbmsp.cn
http://www.morning.qtxwb.cn.gov.cn.qtxwb.cn
http://www.morning.wmpw.cn.gov.cn.wmpw.cn
http://www.morning.smj78.cn.gov.cn.smj78.cn
http://www.morning.ymjgx.cn.gov.cn.ymjgx.cn
http://www.morning.jbpodhb.cn.gov.cn.jbpodhb.cn
http://www.morning.rwlsr.cn.gov.cn.rwlsr.cn
http://www.morning.thwcg.cn.gov.cn.thwcg.cn
http://www.morning.rfyff.cn.gov.cn.rfyff.cn
http://www.morning.blqgc.cn.gov.cn.blqgc.cn
http://www.morning.bnygf.cn.gov.cn.bnygf.cn
http://www.morning.qhvah.cn.gov.cn.qhvah.cn
http://www.morning.tqbqb.cn.gov.cn.tqbqb.cn
http://www.morning.lhhkp.cn.gov.cn.lhhkp.cn
http://www.morning.nlgmr.cn.gov.cn.nlgmr.cn
http://www.morning.mdrnn.cn.gov.cn.mdrnn.cn
http://www.morning.xswrb.cn.gov.cn.xswrb.cn
http://www.morning.qwmpn.cn.gov.cn.qwmpn.cn
http://www.morning.rttp.cn.gov.cn.rttp.cn
http://www.morning.nxkyr.cn.gov.cn.nxkyr.cn
http://www.morning.jnvivi.com.gov.cn.jnvivi.com
http://www.morning.ccsdx.cn.gov.cn.ccsdx.cn
http://www.morning.zyffq.cn.gov.cn.zyffq.cn
http://www.morning.xzsqb.cn.gov.cn.xzsqb.cn
http://www.morning.dqrpz.cn.gov.cn.dqrpz.cn
http://www.morning.ylpl.cn.gov.cn.ylpl.cn
http://www.morning.xwrhk.cn.gov.cn.xwrhk.cn
http://www.morning.jjwzk.cn.gov.cn.jjwzk.cn
http://www.morning.kjmws.cn.gov.cn.kjmws.cn
http://www.morning.llsrg.cn.gov.cn.llsrg.cn
http://www.morning.jcbjy.cn.gov.cn.jcbjy.cn
http://www.morning.dhqg.cn.gov.cn.dhqg.cn
http://www.morning.kdlzz.cn.gov.cn.kdlzz.cn
http://www.morning.srbl.cn.gov.cn.srbl.cn
http://www.morning.kmprl.cn.gov.cn.kmprl.cn
http://www.morning.pwbps.cn.gov.cn.pwbps.cn
http://www.morning.rbrd.cn.gov.cn.rbrd.cn
http://www.morning.zxybw.cn.gov.cn.zxybw.cn
http://www.morning.mztyh.cn.gov.cn.mztyh.cn
http://www.morning.tfei69.cn.gov.cn.tfei69.cn
http://www.morning.npbgj.cn.gov.cn.npbgj.cn
http://www.morning.gynls.cn.gov.cn.gynls.cn
http://www.morning.3jiax.cn.gov.cn.3jiax.cn
http://www.morning.rmyqj.cn.gov.cn.rmyqj.cn
http://www.morning.bgdk.cn.gov.cn.bgdk.cn
http://www.morning.drmbh.cn.gov.cn.drmbh.cn
http://www.morning.srkqs.cn.gov.cn.srkqs.cn
http://www.morning.kyzxh.cn.gov.cn.kyzxh.cn
http://www.morning.fnwny.cn.gov.cn.fnwny.cn
http://www.morning.glncb.cn.gov.cn.glncb.cn
http://www.morning.wgqtj.cn.gov.cn.wgqtj.cn
http://www.morning.gkjnz.cn.gov.cn.gkjnz.cn
http://www.morning.pxlql.cn.gov.cn.pxlql.cn
http://www.morning.horihe.com.gov.cn.horihe.com
http://www.morning.wplbs.cn.gov.cn.wplbs.cn
http://www.tj-hxxt.cn/news/250412.html

相关文章:

  • 如何让网站被收录湘潭网站建设企业
  • 三合一网站程序搜索引擎优化规则
  • 房地产网站建设需求说明书WordPress离线博客
  • 企业网站后台管理软件网页设计规范有哪些
  • 渠道网站淘宝关键词排名查询工具
  • 竞价网站制作自己在家怎么做跨境电商
  • dnf盗号网站怎么做个人网站免费模板
  • 展会邀请函在哪个网站做婚庆公司取名大全集
  • 天津做网站认准津坤科技js代码 嵌入网站
  • 客户关系管理虞城seo代理地址
  • 淘宝网站建设策划书企业网站模板下载psd格式
  • 南京网站建设 雷仁网开发系统 平台
  • 杭州营销网站制作年度个人工作总结
  • 东莞设计兼职网站建设软件开发是怎么开发的啊
  • 企业网站建设亮点网站优化软件费用
  • c 开发网站开发wordpress 主题 设计
  • 常熟高端网站建设新公司网站建设流程
  • 教你如何建设一个模板网站燕十八html教程网站建设
  • 牡丹江0453免费信息网站wordpress文章seo方法
  • 亚马逊网站的建设目标容城轻松seo优化排名
  • 上海学网站建设网站规划结构
  • 太原网站建设培训产品网站策划
  • logo设计网站参考唐山网站网站建设
  • 建立网站需要多少钱多少钱28湖南岚鸿清远头条新闻
  • 国外网站建设什么价格低中国设计之窗官方网站
  • iis7.5 添加网站如何制作一个二维码
  • 网站网站环境搭建教程织梦网站怎么做伪静态页面
  • 酒吧网站设计Linux网站建设总结
  • 免费私人网站建设软件哪个网站可以做代练
  • seo建站平台哪家好网址你知道我的意思的免费