nero

nero 查看完整档案

北京编辑  |  填写毕业院校云原生相关  |  前端工程师 编辑 www.neroht.com 编辑
编辑

前端工程师

个人网站: https://www.neroht.com

微信公众号: 一口一个前端

个人动态

nero 赞了回答 · 今天 17:04

解决SF的创建团队和背后的故事?

segmentfault-team.jpg
目前SegmentFault这个团队

  1. joyqi]: 1985年,华中科技大学电信系毕业, 联合创始人、技术负责人。 前雅虎口碑员工,前MagnetJoy游戏架构师,资深PHP、iOS工程师!开源项目Typecho.org发起人

  2. sunny: 1990年,退学生,联合创始人、产品运营、市场商务负责人;黑客马拉松活动主要推进者。 MagnetJoy多平台游戏运营,全球3000万用户;前Tech2IPO作者、AngelCrunch分析师;独立博客,多年社会化营销、品牌营销经验。

  3. fen: 1987年,联合创始人、首席设计师。个人介绍如同他的风格一样简洁:一个互联网的裁缝。

子品牌

  1. Typecho:Typecho是一款开源博客程序,是由SF创始人@joyqi早在2007年发起,它在GPL Version 2许可证下发行,基于PHP(需要PHP5以上版本)构建,可以运行在各种平台上,支持多种数据库(Mysql,PostgreSQL,SQLite)。

  2. SegmentFault黑客马拉松:由SegmentFault主办并发起的黑客马拉松活动组织,以社会企业的形式独立运作!

团队相册:http://xiangce.baidu.com/suns...

Snip20130127_10.png
Snip20130127_11.png

关注 12 回答 29

nero 发布了文章 · 今天 09:52

扒一扒React计算状态的原理

点击进入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

有几点需要解释一下:

  1. 关于产生多个update对象的场景,多次调用setState即可
this.setState({val: 2});
this.setState({val: 6});

产生的updateQueue结构如下:

可以看出它是个单向的环装链表

 u1 ---> u2
 ^        |
 |________|
  1. 关于更新队列为什么是环状。

结论是:这是因为方便定位到链表的第一个元素。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;
}
  1. 关于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会被跳过,它除了跳过之外,还做了三件事:

  1. 将被跳过的update放到firstBaseUpdate 和 lastBaseUpdate组成的链表中,(就是baseUpdate),等待下次处理低优先级更新的时候再处理。
  2. 记录baseState,此时的baseState为该低优先级update之前所有已被处理的更新的结果,并且只在第一次跳过时记录,因为低优先级任务重做时,要从第一个被跳过的更新开始处理。
  3. 将被跳过的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对象按次序、完整地被处理至关重要,也保证了最终呈现的处理结果和用户的行为触发的交互的结果保持一致。

欢迎扫码关注公众号,发现更多技术文章

查看原文

赞 3 收藏 0 评论 1

nero 发布了文章 · 1月15日

ReactFiber节点的更新入口:beginWork

React的更新任务主要是调用一个叫做workLoop的工作循环去构建workInProgress树,构建过程分为两个阶段:向下遍历和向上回溯,向下和向上的过程中会对途径的每个节点进行beginWork和completeWork。

本文即将提到的beginWork是处理节点更新的入口,它会依据fiber节点的类型去调用不同的处理函数。

React对每个节点进行beginWork操作,进入beginWork后,首先判断节点及其子树是否有更新,若有更新,则会在计算新状态和diff之后生成新的Fiber,然后在新的fiber上标记flags(effectTag),最后return它的子节点,以便继续针对子节点进行beginWork。若它没有子节点,则返回null,这样说明这个节点是末端节点,可以进行向上回溯,进入completeWork阶段。

点击进入React源码调试仓库。

beginWork的工作流程如下图,图中简化了流程,只对App节点进行了beginWork处理,其余节点流程相似

beginWork流程

职责

通过概述可知beginWork阶段的整体工作是去更新节点,并返回子树,但真正的beginWork函数只是节点更新的入口,不会直接进行更新操作。作为入口,它的职责很明显,拦截无需更新的节点。同时,它还会将context信息入到栈中(beginWork入栈,completeWork出栈),暂时先不关注。

function beginWork(
    current: Fiber | null,
    workInProgress: Fiber,
    renderLanes: Lanes
): Fiber | null {
  // 获取workInProgress.lanes,可通过判断它是否为空去判断该节点是否需要更新
  const updateLanes = workInProgress.lanes;

  // 依据current是否存在判断当前是首次挂载还是后续的更新
  // 如果是更新,先看优先级够不够,不够的话就能调用bailoutOnAlreadyFinishedWork
  // 复用fiber节点来跳出对当前这个节点的处理了。
  if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    if (
        oldProps !== newProps ||
        hasLegacyContextChanged()
    ) {
      didReceiveUpdate = true;
    } else if (!includesSomeLane(renderLanes, updateLanes)) {
      // 此时无需更新
      didReceiveUpdate = false;
      switch (workInProgress.tag) {
        case HostRoot:
          ...
        case HostComponent:
          ...
        case ClassComponent:
          ...
        case HostPortal:
          ...
      }

      // 拦截无需更新的节点
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    }
  } else {
    didReceiveUpdate = false;
  }

  // 代码走到这里说明确实要去处理节点了,此时会根据不同fiber的类型
  // 去调用它们各自的处理函数

  // 先清空workInProgress节点上的lanes,因为更新过程中用不到,
  // 在处理完updateQueue之后会重新赋值
  workInProgress.lanes = NoLanes;

  // 依据不同的节点类型来处理节点的更新
  switch (workInProgress.tag) {
    case IndeterminateComponent:
      ...
    case LazyComponent:
      ...
    case FunctionComponent:
      ...
      return updateFunctionComponent(
          current,
          workInProgress,
          Component,
          resolvedProps,
          renderLanes,
      );
    }
    case ClassComponent:
      ...
      return updateClassComponent(
          current,
          workInProgress,
          Component,
          resolvedProps,
          renderLanes,
      );
    }
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderLanes);
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);
    case HostText:
      return updateHostText(current, workInProgress);

    ......
  }
}

可以看出,一旦节点进入beginWork,会先去识别该节点是否需要处理,若无需处理,则调用bailoutOnAlreadyFinishedWork复用节点,否则才真正去更新。

如何区分更新与初始化过程

判断current是否存在。

这首先要理解current是什么,基于双缓冲的规则,调度更新时有两棵树,展示在屏幕上的current Tree和正在后台基于current树构建的
workInProgress Tree。那么,current和workInProgress可以理解为镜像的关系。workLoop循环当前遍历到的workInProgress节点来自于它对应的current节点父级fiber的子节点(即current节点),所以workInProgress节点和current节点也是镜像的关系。

如果是首次渲染,对具体的workInProgress节点来说,它是没有current节点的,如果是在更新过程,由于current节点已经在首次渲染时产生了,所以workInProgress节点有对应的current节点存在。

最终会根据节点是首次渲染还是更新来决定是创建fiber还是diff fiber。只不过更新时,如果节点的优先级不够会直接复用已有节点,即走跳出(bailout)的逻辑,而不是去走下面的更新逻辑。

复用节点过程

节点可复用表示它无需更新。在上面beginWork的代码中可以看到,若节点的优先级不满足要求,说明它不用更新,会调用bailoutOnAlreadyFinishedWork函数,去复用current节点作为新的workInProgress树的节点。

beginWork函数中拦截无需更新节点的逻辑

if (!includesSomeLane(renderLanes, updateLanes)) {
  ...

  // 此时无需更新,拦截无需更新的节点
  return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}

beginWork它的返回值有两种情况:

  • 返回当前节点的子节点,然后会以该子节点作为下一个工作单元继续beginWork,不断往下生成fiber节点,构建workInProgress树。
  • 返回null,当前fiber子树的遍历就此终止,从当前fiber节点开始往回进行completeWork。

bailoutOnAlreadyFinishedWork函数的返回值也是如此。

  • 返回当前节点的子节点,前置条件是当前节点的子节点有更新,此时当前节点未经处理,是可以直接复用的,复用的过程就是复制一份current节点的子节点,并把它return出去。
  • 返回null,前提是当前子节点没有更新,当前子树的遍历过程就此终止。开始completeWork。

从这个函数中,我们也可以意识到,识别当前fiber节点的子树有无更新显得尤为重要,这可以决定是否终止当前Fiber子树的遍历,将复杂度直接降低。实际上可以通过fiber.childLanes去识别,childLanes如果不为空,表明子树中有需要更新的节点,那么需要继续往下走。

标记fiber.childLanes的过程是在开始调度时发生的,在markUpdateLaneFromFiberToRoot 函数中

带着上边的认知,来看一下源码了解具体的复用过程:

function bailoutOnAlreadyFinishedWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null {

  if (current !== null) {
    workInProgress.dependencies = current.dependencies;
  }

  // 标记有跳过的更新
  markSkippedUpdateLanes(workInProgress.lanes);

  // 如果子节点没有更新,返回null,终止遍历
  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
    return null;
  } else {
    // 子节点有更新,那么从current上复制子节点,并return出去
    cloneChildFibers(current, workInProgress);
    return workInProgress.child;
  }
}

总结

beginWork的主要功能就是处理当前遍历到的fiber,经过一番处理之后返回它的子fiber,一个一个地往外吐出fiber节点,那么workInProgress树也就会被一点一点地构建出来。

这是beginWork的大致流程,但实际上,核心更新的工作都是在各个更新函数中,这些函数会安排fiber节点依次进入两大处理流程:计算新状态和Diff算法,限于篇幅,这两个内容会分两篇文章详细讲解,可以持续关注。

欢迎扫码关注公众号,发现更多技术文章

查看原文

赞 2 收藏 1 评论 0

nero 发布了文章 · 1月12日

React的秘密-原理解析第一篇:核心概念

作为一个构建用户界面的库,React的核心始终围绕着更新这一个重要的目标,将更新和极致的用户体验结合起来是React团队一直在努力的事情。为什么React可以将用户体验做到这么好?我想这是基于以下两点原因:

  • Fiber架构和Scheduler出色的调度模式可以实现异步可中断的更新行为。
  • 优先级机制贯穿更新的整个周期

本文是对React原理解读系列的第一篇文章,在正式开始之前,我们先基于这两点展开介绍,以便对一些概念可以先有个基础认知。

配合的源码调试环境在这里 ,会跟随React主要版本进行更新,欢迎随意下载调试。

Fiber是什么

Fiber是什么?它是React的最小工作单元,在React的世界中,一切都可以是组件。在普通的HTML页面上,人为地将多个DOM元素整合在一起可以组成一个组件,HTML标签可以是组件(HostComponent),普通的文本节点也可以是组件(HostText)。每一个组件就对应着一个fiber节点,许多个fiber节点互相嵌套、关联,就组成了fiber树,正如下面表示的Fiber树和DOM的关系一样:

    Fiber树                    DOM树

   div#root                  div#root
      |                         |
    <App/>                     div
      |                       /   \
     div                     p     a
    /   ↖
   /      ↖
  p ----> <Child/>
             |
             a

一个DOM节点一定对应着一个Fiber节点,但一个Fiber节点却不一定有对应的DOM节点。

fiber 作为工作单元它的结构如下:

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {

  // Fiber元素的静态属性相关
  this.tag = tag;
  this.key = key; // fiber的key
  this.elementType = null;
  this.type = null; // fiber对应的DOM元素的标签类型,div、p...
  this.stateNode = null; // fiber的实例,类组件场景下,是组件的类,HostComponent场景,是dom元素

  // Fiber 链表相关
  this.return = null; // 指向父级fiber
  this.child = null; // 指向子fiber
  this.sibling = null; // 同级兄弟fiber
  this.index = 0;

  this.ref = null; // ref相关

  // Fiber更新相关
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null; // 存储update的链表
  this.memoizedState = null; // 类组件存储fiber的状态,函数组件存储hooks链表
  this.dependencies = null;

  this.mode = mode;

  // Effects
  // flags原为effectTag,表示当前这个fiber节点变化的类型:增、删、改
  this.flags = NoFlags;
  this.nextEffect = null;

  // effect链相关,也就是那些需要更新的fiber节点
  this.firstEffect = null;
  this.lastEffect = null;

  this.lanes = NoLanes; // 该fiber中的优先级,它可以判断当前节点是否需要更新
  this.childLanes = NoLanes;// 子树中的优先级,它可以判断当前节点的子树是否需要更新

  /*
  * 可以看成是workInProgress(或current)树中的和它一样的节点,
  * 可以通过这个字段是否为null判断当前这个fiber处在更新还是创建过程
  * */
  this.alternate = null;

}

fiber架构下的React是如何更新的

首先要明白,React要完成一次更新分为两个阶段: render阶段和commit阶段,两个阶段的工作可分别概括为新fiber树的构建和更新最终效果的应用。

render阶段

render阶段实际上是在内存中构建一棵新的fiber树(称为workInProgress树),构建过程是依照现有fiber树(current树)从root开始深度优先遍历再回溯到root的过程,这个过程中每个fiber节点都会经历两个阶段:beginWork和completeWork。组件的状态计算、diff的操作以及render函数的执行,发生在beginWork阶段,effect链表的收集、被跳过的优先级的收集,发生在completeWork阶段。构建workInProgress树的过程中会有一个workInProgress的指针记录下当前构建到哪个fiber节点,这是React更新任务可恢复的重要原因之一。

如下面的动图,就是render阶段的简要过程:

fiberTask

commit阶段

在render阶段结束后,会进入commit阶段,该阶段不可中断,主要是去依据workInProgress树中有变化的那些节点(render阶段的completeWork过程收集到的effect链表),去完成DOM操作,将更新应用到页面上,除此之外,还会异步调度useEffect以及同步执行useLayoutEffect。

这两个阶段都是独立的React任务,最后会进入Scheduler被调度。render阶段采取的调度优先级是依据本次更新的优先级来决定的,以便高优先级任务的介入可以打断低优先级任务的工作;commit阶段的调度优先级采用的是最高优先级,以保证commit阶段同步执行不可被打断。

Scheduler 的作用

Scheduler用来调度执行上面提到的React任务。

何为调度?依据任务优先级来决定哪个任务先被执行。调度的目标是保证高优先级任务最先被执行。

何为执行?Scheduler执行任务具备一个特点:即根据时间片去终止任务,并判断任务是否完成,若未完成则继续调用任务函数。它只是去做任务的中断和恢复,而任务是否已经完成则要依赖React告诉它。Scheduler和React相互配合的模式可以让React的任务执行具备异步可中断的特点。

优先级机制

为了区分任务的轻重缓急,React内部有一个从事件到调度的优先级机制。事件本身自带优先级属性,它导致的更新会基于事件的优先级计算出更新自己的优先级,更新会产生更新任务,更新任务的优先级由更新优先级计算而来,更新任务被调度,所以需要调度优先级去协调调度过程,调度优先级由更新任务优先级计算得出,就这样一步一步,React将优先级的概念贯穿整个更新的生命周期。

React优先级相关的更多介绍请移步 React中的优先级

双缓冲机制

双缓冲机制是React管理更新工作的一种手段,也是提升用户体验的重要机制。

当React开始更新工作之后,会有两个fiber树,一个current树,是当前显示在页面上内容对应的fiber树。另一个是workInProgress树,它是依据current树深度优先遍历构建出来的新的fiber树,所有的更新最终都会体现在workInProgress树上。当更新未完成的时候,页面上始终展示current树对应的内容,当更新结束时(commit阶段的最后),页面内容对应的fiber树会由current树切换到workInProgress树,此时workInProgress树即成为新的current树。

function commitRootImpl(root, renderPriorityLevel) {
    ...

    // finishedWork即为workInProgress树的根节点,
    // root.current指向它来完成树的切换
    root.current = finishedWork;

    ...
}

两棵树在进入commit阶段时候的关系如下图,最终commit阶段完成时,两棵树会进行切换。
current树和workInProgress树

在未更新完成时依旧展示旧内容,保持交互,当更新完成立即切换到新内容,这样可以做到新内容和旧内容无缝切换。

总结

本文基本概括了React大致的工作流程以及角色,本系列文章会以更新过程为主线,从render阶段开始,一直到commit阶段,讲解React工作的原理。除此之外,会对其他的重点内容进行大篇幅分析,如事件机制、Scheduler原理、重点Hooks以及context原理。

本系列文章耗时较长,落笔撰写时,17版本还未发布,所以参照的源码版本为16.13.1、17.0.0-alpha.0以及17共三个版本,我曾经对文章中涉及到的三个版本的代码进行过核对,逻辑基本无差别,可放心阅读。

查看原文

赞 6 收藏 4 评论 0

nero 发布了文章 · 1月12日

React中的优先级

UI产生交互的根本原因是各种事件,这也就意味着事件与更新有着直接关系。不同事件产生的更新,它们的优先级是有差异的,所以更新优先级的根源在于事件的优先级。一个更新的产生可直接导致React生成一个更新任务,最终这个任务被Scheduler调度。

所以在React中,人为地将事件划分了等级,最终目的是决定调度任务的轻重缓急,因此,React有一套从事件到调度的优先级机制。

本文将围绕事件优先级、更新优先级、任务优先级、调度优先级,重点梳理它们之间的转化关系。

  • 事件优先级:按照用户事件的交互紧急程度,划分的优先级
  • 更新优先级:事件导致React产生的更新对象(update)的优先级(update.lane)
  • 任务优先级:产生更新对象之后,React去执行一个更新任务,这个任务所持有的优先级
  • 调度优先级:Scheduler依据React更新任务生成一个调度任务,这个调度任务所持有的优先级

前三者属于React的优先级机制,第四个属于Scheduler的优先级机制,Scheduler内部有自己的优先级机制,虽然与React有所区别,但等级的划分基本一致。下面我们从事件优先级开始说起。

优先级的起点:事件优先级

React按照事件的紧急程度,把它们划分成三个等级:

  • 离散事件(DiscreteEvent):click、keydown、focusin等,这些事件的触发不是连续的,优先级为0。
  • 用户阻塞事件(UserBlockingEvent):drag、scroll、mouseover等,特点是连续触发,阻塞渲染,优先级为1。
  • 连续事件(ContinuousEvent):canplay、error、audio标签的timeupdate和canplay,优先级最高,为2。

事件优先级的Map

派发事件优先级

事件优先级是在注册阶段被确定的,在向root上注册事件时,会根据事件的类别,创建不同优先级的事件监听(listener),最终将它绑定到root上去。

let listener = createEventListenerWrapperWithPriority(
    targetContainer,
    domEventName,
    eventSystemFlags,
    listenerPriority,
  );

createEventListenerWrapperWithPriority函数的名字已经把它做的事情交代得八九不离十了。它会首先根据事件的名称去找对应的事件优先级,然后依据优先级返回不同的事件监听函数。

export function createEventListenerWrapperWithPriority(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  priority?: EventPriority,
): Function {
  const eventPriority =
    priority === undefined
      ? getEventPriorityForPluginSystem(domEventName)
      : priority;
  let listenerWrapper;
  switch (eventPriority) {
    case DiscreteEvent:
      listenerWrapper = dispatchDiscreteEvent;
      break;
    case UserBlockingEvent:
      listenerWrapper = dispatchUserBlockingUpdate;
      break;
    case ContinuousEvent:
    default:
      listenerWrapper = dispatchEvent;
      break;
  }
  return listenerWrapper.bind(
    null,
    domEventName,
    eventSystemFlags,
    targetContainer,
  );
}

最终绑定到root上的事件监听其实是dispatchDiscreteEventdispatchUserBlockingUpdatedispatchEvent这三个中的一个。它们做的事情都是一样的,以各自的事件优先级去执行真正的事件处理函数。

比如:dispatchDiscreteEventdispatchUserBlockingUpdate最终都会以UserBlockingEvent的事件级别去执行事件处理函数。

以某种优先级去执行事件处理函数其实要借助Scheduler中提供的runWithPriority函数来实现:

function dispatchUserBlockingUpdate(
  domEventName,
  eventSystemFlags,
  container,
  nativeEvent,
) {

  ...

  runWithPriority(
    UserBlockingPriority,
    dispatchEvent.bind(
      null,
      domEventName,
      eventSystemFlags,
      container,
      nativeEvent,
    ),
  );

  ...

}

这么做可以将事件优先级记录到Scheduler中,相当于告诉Scheduler:你帮我记录一下当前事件派发的优先级,等React那边创建更新对象(即update)计算更新优先级时直接从你这拿就好了。

function unstable_runWithPriority(priorityLevel, eventHandler) {
  switch (priorityLevel) {
    case ImmediatePriority:
    case UserBlockingPriority:
    case NormalPriority:
    case LowPriority:
    case IdlePriority:
      break;
    default:
      priorityLevel = NormalPriority;
  }

  var previousPriorityLevel = currentPriorityLevel;
  // 记录优先级到Scheduler内部的变量里
  currentPriorityLevel = priorityLevel;

  try {
    return eventHandler();
  } finally {
    currentPriorityLevel = previousPriorityLevel;
  }
}

更新优先级

以setState为例,事件的执行会导致setState执行,而setState本质上是调用enqueueSetState,生成一个update对象,这时候会计算它的更新优先级,即update.lane:

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);
    ...
  },
};

重点关注requestUpdateLane,它首先找出Scheduler中记录的优先级:schedulerPriority,然后计算更新优先级:lane,具体的计算过程在findUpdateLane函数中,计算过程是一个从高到低依次占用空闲位的操作,具体的代码在这里 ,这里就先不详细展开。

export function requestUpdateLane(
  fiber: Fiber,
  suspenseConfig: SuspenseConfig | null,
): Lane {

  ...
  // 根据记录下的事件优先级,获取任务调度优先级
  const schedulerPriority = getCurrentPriorityLevel();

  let lane;
  if (
    (executionContext & DiscreteEventContext) !== NoContext &&
    schedulerPriority === UserBlockingSchedulerPriority
  ) {
    // 如果事件优先级是用户阻塞级别,则直接用InputDiscreteLanePriority去计算更新优先级
    lane = findUpdateLane(InputDiscreteLanePriority, currentEventWipLanes);
  } else {
    // 依据事件的优先级去计算schedulerLanePriority
    const schedulerLanePriority = schedulerPriorityToLanePriority(
      schedulerPriority,
    );
    ...
    // 根据事件优先级计算得来的schedulerLanePriority,去计算更新优先级
    lane = findUpdateLane(schedulerLanePriority, currentEventWipLanes);
  }
  return lane;
}

getCurrentPriorityLevel负责读取记录在Scheduler中的优先级:

function unstable_getCurrentPriorityLevel() {
  return currentPriorityLevel;
}

update对象创建完成后意味着需要对页面进行更新,会调用scheduleUpdateOnFiber进入调度,而真正开始调度之前会计算本次产生的更新任务的任务优先级,目的是与已有任务的任务优先级去做比较,便于做出多任务的调度决策。

调度决策的逻辑在ensureRootIsScheduled 函数中,这是一个非常重要的函数,控制着React任务进入Scheduler的大门。

任务优先级

一个update会被一个React的更新任务执行掉,任务优先级被用来区分多个更新任务的紧急程度,它由更新优先级计算而来,举例来说:

假设产生一前一后两个update,它们持有各自的更新优先级,也会被各自的更新任务执行。经过优先级计算,如果后者的任务优先级高于前者的任务优先级,那么会让Scheduler取消前者的任务调度;如果后者的任务优先级等于前者的任务优先级,后者不会导致前者被取消,而是会复用前者的更新任务,将两个同等优先级的更新收敛到一次任务中;如果后者的任务优先级低于前者的任务优先级,同样不会导致前者的任务被取消,而是在前者更新完成后,再次用Scheduler对后者发起一次任务调度。

这是任务优先级存在的意义,保证高优先级任务及时响应,收敛同等优先级的任务调度。

任务优先级在即将调度的时候去计算,代码在ensureRootIsScheduled函数中:

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {

  ...

  // 获取nextLanes,顺便计算任务优先级
  const nextLanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
  );

  // 获取上面计算得出的任务优先级
  const newCallbackPriority = returnNextLanesPriority();

  ...

}

通过调用getNextLanes去计算在本次更新中应该处理的这批lanes(nextLanes),getNextLanes会调用getHighestPriorityLanes去计算任务优先级。任务优先级计算的原理是这样:更新优先级(update的lane),
它会被并入root.pendingLanes,root.pendingLanes经过getNextLanes处理后,挑出那些应该处理的lanes,传入getHighestPriorityLanes,根据nextLanes找出这些lanes的优先级作为任务优先级。

function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
  ...
  // 都是这种比较赋值的过程,这里只保留两个以做简要说明
  const inputDiscreteLanes = InputDiscreteLanes & lanes;
  if (inputDiscreteLanes !== NoLanes) {
    return_highestLanePriority = InputDiscreteLanePriority;
    return inputDiscreteLanes;
  }
  if ((lanes & InputContinuousHydrationLane) !== NoLanes) {
    return_highestLanePriority = InputContinuousHydrationLanePriority;
    return InputContinuousHydrationLane;
  }
  ...
  return lanes;
}

getHighestPriorityLanes的源码在这里,getNextLanes的源码在这里

return_highestLanePriority就是任务优先级,它有如下这些值,值越大,优先级越高,暂时只理解任务优先级的作用即可。

export const SyncLanePriority: LanePriority = 17;
export const SyncBatchedLanePriority: LanePriority = 16;

const InputDiscreteHydrationLanePriority: LanePriority = 15;
export const InputDiscreteLanePriority: LanePriority = 14;

const InputContinuousHydrationLanePriority: LanePriority = 13;
export const InputContinuousLanePriority: LanePriority = 12;

const DefaultHydrationLanePriority: LanePriority = 11;
export const DefaultLanePriority: LanePriority = 10;

const TransitionShortHydrationLanePriority: LanePriority = 9;
export const TransitionShortLanePriority: LanePriority = 8;

const TransitionLongHydrationLanePriority: LanePriority = 7;
export const TransitionLongLanePriority: LanePriority = 6;

const RetryLanePriority: LanePriority = 5;

const SelectiveHydrationLanePriority: LanePriority = 4;

const IdleHydrationLanePriority: LanePriority = 3;
const IdleLanePriority: LanePriority = 2;

const OffscreenLanePriority: LanePriority = 1;

export const NoLanePriority: LanePriority = 0;

如果已经存在一个更新任务,ensureRootIsScheduled会在获取到新任务的任务优先级之后,去和旧任务的任务优先级去比较,从而做出是否需要重新发起调度的决定,若需要发起调度,那么会去计算调度优先级。

调度优先级

一旦任务被调度,那么它就会进入Scheduler,在Scheduler中,这个任务会被包装一下,生成一个属于Scheduler自己的task,这个task持有的优先级就是调度优先级。

它有什么作用呢?在Scheduler中,分别用过期任务队列和未过期任务的队列去管理它内部的task,过期任务的队列中的task根据过期时间去排序,最早过期的排在前面,便于被最先处理。而过期时间是由调度优先级计算的出的,不同的调度优先级对应的过期时间不同。

调度优先级由任务优先级计算得出,在ensureRootIsScheduled更新真正让Scheduler发起调度的时候,会去计算调度优先级。

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {

    ...

    // 根据任务优先级获取Scheduler的调度优先级
    const schedulerPriorityLevel = lanePriorityToSchedulerPriority(
      newCallbackPriority,
    );

    // 计算出调度优先级之后,开始让Scheduler调度React的更新任务
    newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );

    ...
}

lanePriorityToSchedulerPriority计算调度优先级的过程是根据任务优先级找出对应的调度优先级。

export function lanePriorityToSchedulerPriority(
  lanePriority: LanePriority,
): ReactPriorityLevel {
  switch (lanePriority) {
    case SyncLanePriority:
    case SyncBatchedLanePriority:
      return ImmediateSchedulerPriority;
    case InputDiscreteHydrationLanePriority:
    case InputDiscreteLanePriority:
    case InputContinuousHydrationLanePriority:
    case InputContinuousLanePriority:
      return UserBlockingSchedulerPriority;
    case DefaultHydrationLanePriority:
    case DefaultLanePriority:
    case TransitionShortHydrationLanePriority:
    case TransitionShortLanePriority:
    case TransitionLongHydrationLanePriority:
    case TransitionLongLanePriority:
    case SelectiveHydrationLanePriority:
    case RetryLanePriority:
      return NormalSchedulerPriority;
    case IdleHydrationLanePriority:
    case IdleLanePriority:
    case OffscreenLanePriority:
      return IdleSchedulerPriority;
    case NoLanePriority:
      return NoSchedulerPriority;
    default:
      invariant(
        false,
        'Invalid update priority: %s. This is a bug in React.',
        lanePriority,
      );
  }
}

总结

本文一共提到了4种优先级:事件优先级、更新优先级、任务优先级、调度优先级,它们之间是递进的关系。事件优先级由事件本身决定,更新优先级由事件计算得出,然后放到root.pendingLanes,任务优先级来自root.pendingLanes中最紧急的那些lanes对应的优先级,调度优先级根据任务优先级获取。几种优先级环环相扣,保证了高优任务的优先执行。

查看原文

赞 4 收藏 3 评论 0

nero 赞了文章 · 1月11日

探索 Vue.js 响应式原理

提到“响应式”三个字,大家立刻想到啥?响应式布局?响应式编程?

响应式关键词.png

从字面意思可以看出,具有“响应式”特征的事物会根据条件变化,使得目标自动作出对应变化。比如在“响应式布局”中,页面根据不同设备尺寸自动显示不同样式。

Vue.js 中的响应式也是一样,当数据发生变化后,使用到该数据的视图也会相应进行自动更新。

接下来我根据个人理解,和大家一起探索下 Vue.js 中的响应式原理,如有错误,欢迎指点😺~~

一、Vue.js 响应式的使用

现在有个很简单的需求,点击页面中 “leo” 文本后,文本内容修改为“你好,前端自习课”。

我们可以直接操作 DOM,来完成这个需求:

<span id="name">leo</span>
const node = document.querySelector('#name')
node.innerText = '你好,前端自习课';

实现起来比较简单,当我们需要修改的数据有很多时(比如相同数据被多处引用),这样的操作将变得复杂。

既然说到 Vue.js,我们就来看看 Vue.js 怎么实现上面需求:

<template>
  <div id="app">
    <span @click="setName">{{ name }}</span>
  </div>
</template>

<script>
export default {
  name: "App",
  data() {
    return {
      name: "leo",
    };
  },
  methods: {
    setName() {
      this.name = "你好,前端自习课";
    },
  },
};
</script>

观察上面代码,我们通过改变数据,来自动更新视图。当我们有多个地方引用这个 name 时,视图都会自动更新。

<template>
  <div id="app">
    <span @click="setName">{{ name }}</span>
    <span>{{ name }}</span>
    <span>{{ name }}</span>
    <span>{{ name }}</span>
  </div>
</template>

当我们使用目前主流的前端框架 Vue.js 和 React 开发业务时,只需关注页面数据如何变化,因为数据变化后,视图也会自动更新,这让我们从繁杂的 DOM 操作中解脱出来,提高开发效率。

二、回顾观察者模式

前面反复提到“通过改变数据,来自动更新视图”,换个说法就是“数据改变后,使用该数据的地方被动发生响应,更新视图”。

是不是有种熟悉的感觉?数据无需关注自身被多少对象引用,只需在数据变化时,通知到引用的对象即可,引用的对象作出响应。恩,有种观察者模式的味道?

关于观察者模式,可阅读我之前写的《图解设计模式之观察者模式(TypeScript)》

1. 观察者模式流程

观察者模式表示一种“一对多”的关系,n 个观察者关注 1 个被观察者,被观察者可以主动通知所有观察者。接下图:

observer.png
在这张图中,粉丝想及时收到“前端自习课”最新文章,只需关注即可,“前端自习课”有新文章,会主动推送给每个粉丝。该过程中,“前端自习课”是被观察者,每位“粉丝”是观察者。

2. 观察者模式核心

观察者模式核心组成包括:n 个观察者和 1 个被观察者。这里实现一个简单观察者模式:

2.1 定义接口

// 观察目标接口
interface ISubject {
    addObserver: (observer: Observer) => void; // 添加观察者
    removeObserver: (observer: Observer) => void; // 移除观察者
    notify: () => void; // 通知观察者
}

// 观察者接口
interface IObserver {
    update: () => void;
}

2.2 实现被观察者类

// 实现被观察者类
class Subject implements ISubject {
    private observers: IObserver[] = [];

    public addObserver(observer: IObserver): void {
        this.observers.push(observer);
    }

    public removeObserver(observer: IObserver): void {
        const idx: number = this.observers.indexOf(observer);
        ~idx && this.observers.splice(idx, 1);
    }

    public notify(): void {
        this.observers.forEach(observer => {
            observer.update();
        });
    }
}

2.3 实现观察者类

// 实现观察者类
class Observer implements IObserver {
    constructor(private name: string) { }

    update(): void {
        console.log(`${this.name} has been notified.`);
    }
}

2.4 测试代码

function useObserver(){
    const subject: ISubject = new Subject();
    const Leo = new Observer("Leo");
    const Robin = new Observer("Robin");
    const Pual = new Observer("Pual");

    subject.addObserver(Leo);
    subject.addObserver(Robin);
    subject.addObserver(Pual);
    subject.notify();

    subject.removeObserver(Pual);
    subject.notify();
}

useObserver();
// [LOG]: "Leo has been notified." 
// [LOG]: "Robin has been notified." 
// [LOG]: "Pual has been notified." 
// [LOG]: "Leo has been notified." 
// [LOG]: "Robin has been notified." 

三、回顾 Object.defineProperty()

Vue.js 的数据响应式原理是基于 JS 标准内置对象方法 Object.defineProperty()方法来实现,该方法不兼容 IE8 和 FF22 及以下版本浏览器,这也是为什么 Vue.js 只能在这些版本之上的浏览器中才能运行的原因。

理解 Object.defineProperty() 对我们理解 Vue.js 响应式原理非常重要

Vue.js 3 使用 proxy 方法实现响应式,两者类似,我们只需搞懂Object.defineProperty()proxy 也就差不多理解了。

1. 概念介绍

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
语法如下:

Object.defineProperty(obj, prop, descriptor)
  • 入参说明:

obj :要定义属性的源对象
prop :要定义或修改的属性名称Symbol
descriptor :要定义或修改的属性描述符,包括 configurableenumerablevaluewritablegetset,具体的可以去参阅文档

  • 出参说明:

修改后的源对象。

举个简单🌰例子:

const leo = {};
Object.defineProperty(leo, 'age', { 
    value: 18,
    writable: true
})
console.log(leo.age); // 18
leo.age = 22;
console.log(leo.age); // 22

2. 实现 getter/setter

我们知道 Object.defineProperty() 方法第三个参数是属性描述符(descriptor),支持设置 getset 描述符:

  • get 描述符:当访问该属性时,会调用此函数,默认值为 undefined ;
  • set 描述符:当修改该属性时,会调用此函数,默认值为 undefined
一旦对象拥有了 getter/setter 方法,我们可以简单将该对象称为响应式对象。

这两个操作符为我们提供拦截数据进行操作的可能性,修改前面示例,添加 getter/setter 方法:

let leo = {}, age = 18;
Object.defineProperty(leo, 'age', { 
    get(){
        // to do something
          console.log('监听到请求数据');
        return age;
    },
    set(newAge){
        // to do something
          console.log('监听到修改数据');
        age = newAge > age ? age : newAge
    }
})
leo.age = 20;  // 监听到修改数据
console.log(leo.age); // 监听到请求数据  // 18

leo.age = 10;  // 监听到修改数据
console.log(leo.age); // 监听到请求数据  // 10

访问 leo 对象的 age 属性,会通过 get 描述符处理,而修改 age 属性,则会通过 set 描述符处理。

四、实现简单的数据响应式

通过前面两个小节,我们复习了“观察者模式”和“Object.defineProperty()” 方法,这两个知识点在 Vue.js 响应式原理中非常重要。

接下来我们来实现一个很简单的数据响应式变化,需求如下:点击“更新数据”按钮,文本更新。

data-change.png

接下来我们将实现三个类:

  • Dep 被观察者类,用来生成被观察者;
  • Watcher 观察者类,用来生成观察者;
  • Observer 类,将普通数据转换为响应式数据,从而实现响应式对象

用一张图来描述三者之间关系,现在看不懂没关系,这小节看完可以再回顾这张图:
observer-watcher-dep.png

1. 实现精简观察者模式

这里参照前面复习“观察者模式”的示例,做下精简:

// 实现被观察者类
class Dep {
    constructor() {
        this.subs = [];
    }
    addSub(watcher) {
        this.subs.push(watcher);
    }
    notify(data) {
        this.subs.forEach(sub => sub.update(data));
    }
}
// 实现观察者类
class Watcher {
    constructor(cb) {
        this.cb = cb;
    }
    update(data) {
        this.cb(data);
    }
}

Vue.js 响应式原理中,观察者模式起到非常重要的作用。其中:

  • Dep 被观察者类,提供用来收集观察者( addSub )方法和通知观察者( notify )方法;
  • Watcher 观察者类,实例化时支持传入回调( cb )方法,并提供更新( update )方法;

2. 实现生成响应式的类

这一步需要实现 Observer 类,核心是通过 Object.defineProperty() 方法为对象的每个属性设置 getter/setter,目的是将普通数据转换为响应式数据,从而实现响应式对象

reactive-data.png

这里以最简单的单层对象为例(下一节会介绍深层对象),如:

let initData = {
    text: '你好,前端自习课',
    desc: '每日清晨,享受一篇前端优秀文章。'
};

接下来实现 Observer 类:

// 实现响应式类(最简单单层的对象,暂不考虑深层对象)
class Observer {
    constructor (node, data) {
        this.defineReactive(node, data)
    }

    // 实现数据劫持(核心方法)
    // 遍历 data 中所有的数据,都添加上 getter 和 setter 方法
    defineReactive(vm, obj) {
        //每一个属性都重新定义get、set
        for(let key in obj){
            let value = obj[key], dep = new Dep();
            Object.defineProperty(obj, key, {
                enumerable: true,
                configurable: true,
                get() {
                    // 创建观察者
                    let watcher = new Watcher(v => vm.innerText = v);
                    dep.addSub(watcher);
                    return value;
                },
                set(newValue) {
                    value = newValue;
                    // 通知所有观察者
                    dep.notify(newValue);
                }
            })
        }
    }
}

上面代码的核心是 defineReactive 方法,它遍历原始对象中每个属性,为每个属性实例化一个被观察者(Dep),然后分别调用 Object.defineProperty() 方法,为每个属性添加 getter/setter。

  • 访问数据时,getter 执行依赖收集(即添加观察者),通过实例化 Watcher 创建一个观察者,并执行被观察者的 addSub() 方法添加一个观察者;
  • 修改数据时,setter 执行派发更新(即通知观察者),通过调用被观察者的 notify() 方法通知所有观察者,执行观察者 update() 方法。

3. 测试代码

为了方便观察数据变化,我们为“更新数据”按钮绑定点击事件来修改数据:

<div id="app"></div>
<button id="update">更新数据</button>

测试代码如下:

// 初始化测试数据
let initData = {
    text: '你好,前端自习课',
    desc: '每日清晨,享受一篇前端优秀文章。'
};

const app = document.querySelector('#app');

// 步骤1:为测试数据转换为响应式对象
new Observer(app, initData);

// 步骤2:初始化页面文本内容
app.innerText = initData.text;

// 步骤3:绑定按钮事件,点击触发测试
document.querySelector('#update').addEventListener('click', function(){
    initData.text = `我们必须经常保持旧的记忆和新的希望。`;
    console.log(`当前时间:${new Date().toLocaleString()}`)
})

测试代码中,核心在于通过实例化 Observer,将测试数据转换为响应式数据,然后模拟数据变化,来观察视图变化。
每次点击“更新数据”按钮,在控制台中都能看到“数据发生变化!”的提示,说明我们已经能通过 setter 观察到数据的变化情况。

当然,你还可以在控制台手动修改 initData 对象中的 text 属性,来体验响应式变化~~

到这里,我们实现了非常简单的数据响应式变化,当然 Vue.js 肯定没有这么简单,这个先理解,下一节看 Vue.js 响应式原理,思路就会清晰很多。

这部分代码,我已经放到我的 Github,地址:https://github.com/pingan8787/Leo-JavaScript/blob/master/Cute-Gist/Vue/Basics-Reactive-Demo.js

可以再回顾下这张图,对整个过程会更清晰:

observer-watcher-dep.png

五、Vue.js 响应式实现

本节代码:https://github.com/pingan8787/Leo-JavaScript/blob/master/Cute-Gist/Vue/leo-vue-reactive/

这里大家可以再回顾下下面这张官网经典的图,思考下前面讲的示例。

(图片来自:https://cn.vuejs.org/v2/guide/reactivity.html

上一节实现了简单的数据响应式,接下来继续通过完善该示例,实现一个简单的 Vue.js 响应式,测试代码如下:

// index.js
const vm = new Vue({
    el: '#app',
    data(){
        return {
            text: '你好,前端自习课',
            desc: '每日清晨,享受一篇前端优秀文章。'
        }
    }
});

是不是很有内味了,下面是我们最终实现后项目目录:

- mini-reactive
    / index.html   // 入口 HTML 文件
  / index.js     // 入口 JS 文件
  / observer.js  // 实现响应式,将数据转换为响应式对象
  / watcher.js   // 实现观察者和被观察者(依赖收集者)
  / vue.js       // 实现 Vue 类作为主入口类
  / compile.js   // 实现编译模版功能

知道每一个文件功能以后,接下来将每一步串联起来。

1. 实现入口文件

我们首先实现入口文件,包括 index.html / index.js  2 个简单文件,用来方便接下来的测试。

1.1 index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <script data-original="./vue.js"></script>
    <script data-original="./observer.js"></script>
    <script data-original="./compile.js"></script>
    <script data-original="./watcher.js"></script>
</head>
<body>
    <div id="app">{{text}}</div>
    <button id="update">更新数据</button>
    <script data-original="./index.js"></script>
</body>
</html>

1.2 index.js

"use strict";
const vm = new Vue({
    el: '#app',
    data(){
        return {
            text: '你好,前端自习课',
            desc: '每日清晨,享受一篇前端优秀文章。'
        }
    }
});

console.log(vm.$data.text)
vm.$data.text = '页面数据更新成功!'; // 模拟数据变化
console.log(vm.$data.text)

2. 实现核心入口 vue.js

vue.js 文件是我们实现的整个响应式的入口文件,暴露一个 Vue 类,并挂载全局。

class Vue {
    constructor (options = {}) {
        this.$el = options.el;
        this.$data = options.data();
        this.$methods = options.methods;

        // [核心流程]将普通 data 对象转换为响应式对象
        new Observer(this.$data);

        if (this.$el) {
            // [核心流程]将解析模板的内容
            new Compile(this.$el, this)
        }
    }
}
window.Vue = Vue;

Vue 类入参为一个配置项 option ,使用起来跟 Vue.js 一样,包括 $el 挂载点、 $data 数据对象和 $methods 方法列表(本文不详细介绍)。

通过实例化 Oberser 类,将普通 data 对象转换为响应式对象,然后判断是否传入 el 参数,存在时,则实例化 Compile 类,解析模版内容。

总结下 Vue 这个类工作流程 :
vue-class.png

3. 实现 observer.js

observer.js 文件实现了 Observer 类,用来将普通对象转换为响应式对象:

class Observer {
    constructor (data) {
        this.data = data;
        this.walk(data);
    }

    // [核心方法]将 data 对象转换为响应式对象,为每个 data 属性设置 getter 和 setter 方法
    walk (data) {
        if (typeof data !== 'object') return data;
        Object.keys(data).forEach( key => {
            this.defineReactive(data, key, data[key])
        })
    }

    // [核心方法]实现数据劫持
    defineReactive (obj, key, value) {
        this.walk(value);  // [核心过程]遍历 walk 方法,处理深层对象。
        const dep = new Dep();
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get () {
                console.log('[getter]方法执行')
                Dep.target &&  dep.addSub(Dep.target);
                return value
            },
            set (newValue) {
                console.log('[setter]方法执行')
                if (value === newValue) return;
                // [核心过程]当设置的新值 newValue 为对象,则继续通过 walk 方法将其转换为响应式对象
                if (typeof newValue === 'object') this.walk(newValue);
                value = newValue;
                dep.notify(); // [核心过程]执行被观察者通知方法,通知所有观察者执行 update 更新
            }
        })
    }
}

相比较第四节实现的 Observer 类,这里做了调整:

  • 增加 walk 核心方法,用来遍历对象每个属性,分别调用数据劫持方法( defineReactive() );
  • defineReactive() 的 getter 中,判断 Dep.target 存在才添加观察者,下一节会详细介绍 Dep.target
  • defineReactive() 的 setter 中,判断当前新值( newValue )是否为对象,如果是,则直接调用 this.walk() 方法将当前对象再次转为响应式对象,处理深层对象

通过改善后的 Observer 类,我们就可以实现将单层或深层嵌套的普通对象转换为响应式对象

4. 实现 watcher.js

这里实现了 Dep 被观察者类(依赖收集者)和 Watcher 观察者类。

class Dep {
    constructor() {
        this.subs = [];
    }
    addSub(watcher) {
        this.subs.push(watcher);
    }
    notify(data) {
        this.subs.forEach(sub => sub.update(data));
    }
}

class Watcher {
    constructor (vm, key, cb) {
        this.vm = vm;   // vm:表示当前实例
        this.key = key; // key:表示当前操作的数据名称
        this.cb = cb;   // cb:表示数据发生改变之后的回调

        Dep.target = this; // 全局唯一
        this.oldValue = this.vm.$data[key]; // 保存变化的数据作为旧值,后续作判断是否更新
        Dep.target = null;
    }
    
    update () {
        console.log(`数据发生变化!`);
        let oldValue = this.oldValue;
        let newValue = this.vm.$data[this.key];
        if (oldValue != newValue) {  // 比较新旧值,发生变化才执行回调
            this.cb(newValue, oldValue);
        };
    }
}

相比较第四节实现的 Watcher  类,这里做了调整:

  • 在构造函数中,增加 Dep.target 值操作;
  • 在构造函数中,增加 oldValue 变量,保存变化的数据作为旧值,后续作为判断是否更新的依据;
  • update() 方法中,增加当前操作对象 key 对应值的新旧值比较,如果不同,才执行回调。

Dep.target当前全局唯一的订阅者,因为同一时间只允许一个订阅者被处理。target当前正在处理的目标订阅者,当前订阅者处理完就赋值为 null 。这里 Dep.target 会在 defineReactive() 的 getter 中使用到。

通过改善后的 Watcher 类,我们操作当前操作对象 key 对应值的时候,可以在数据有变化的情况才执行回调,减少资源浪费。

4. 实现 compile.js

compile.js 实现了 Vue.js 的模版编译,如将 HTML 中的 {{text}} 模版转换为具体变量的值。

compile.js 介绍内容较多,考虑到篇幅问题,并且本文核心介绍响应式原理,所以这里就暂时不介绍 compile.js 的实现,在学习的朋友可以到我 Github 上下载该文件直接下载使用即可,地址:
https://github.com/pingan8787/Leo-JavaScript/blob/master/Cute-Gist/Vue/leo-vue-reactive/compile.js

5. 测试代码

到这里,我们已经将第四节的 demo 改造成简易版 Vue.js 响应式,接下来打开 index.html 看看效果:

当 index.js 中执行到:

vm.$data.text = '我们必须经常保持旧的记忆和新的希望。';

页面便发生更新,页面显示的文本内容从“你好,前端自习课”更新成“我们必须经常保持旧的记忆和新的希望。”。

到这里,我们的简易版 Vue.js 响应式原理实现好了,能跟着文章看到这里的朋友,给你点个大大的赞👍

六、总结

本文首先通过回顾观察者模式和 Object.defineProperty() 方法,介绍 Vue.js 响应式原理的核心知识点,然后带大家通过一个简单示例实现简单响应式,最后通过改造这个简单响应式的示例,实现一个简单 Vue.js 响应式原理的示例。

相信看完本文的朋友,对 Vue.js 的响应式原理的理解会更深刻,希望大家理清思路,再好好回味下~

参考资料

  1. 官方文档 - 深入响应式原理 
  2. 《浅谈Vue响应式原理》
  3. 《Vue的数据响应式原理》 
查看原文

赞 7 收藏 5 评论 0

nero 赞了文章 · 1月7日

两年经验面试阿里前端开发岗,已拿offer,这些知识点该放出来了

一般阿里社招都是招3-5年的P6+工程师,但在某boss上有个阿里的技术专家私聊了我要我简历,我说我只有两年经历,但是这boss说,没关系,他喜欢基础好的,让我试一试,于是我也抱着试一试的心态发了简历。

简历发过去之后,boss就给我打了电话,让我简单的介绍一下自己,我就噼里啪啦说了一些,还说了一些题外话。然后boss就开始问我问题。

由于面了四轮,所以最开始的面试记忆有点模糊了。

电话面试

1.说一下你了解CSS盒模型。
2.说一下box-sizing的应用场景。
3.说一下你了解的弹性FLEX布局.
4.说一下一个未知宽高元素怎么上下左右垂直居中。
5.说一下原型链,对象,构造函数之间的一些联系。
6.DOM事件的绑定的几种方式
7.说一下你项目中用到的技术栈,以及觉得得意和出色的点,以及让你头疼的点,怎么解决的。
8.有没有了解http2.0,websocket,https,说一下你的理解以及你所了解的特性。

第一轮电面大约面了50分钟,就记起来这么多,还有一些细节问题可能淡忘了,总体来说,面的都是以基础为主,然后boss说把我简历推荐给内部,进行正式的社招流程。

一轮技术面

这次就直接省略自我介绍了。

1.webpack的入口文件怎么配置,多个入口怎么分割啥的,我也没太听清楚。
2.我看到你的项目用到了Babel的一个插件:transform-runtime以及stage-2,你说一下他们的作用。
3.我看到你的webpack配置用到webpack.optimize.UglifyJsPlugin这个插件,有没有觉得压缩速度很慢,有什么办法提升速度。
4.简历上看见你了解http协议。说一下200和304的理解和区别
5.DOM事件中target和currentTarget的区别
6.说一下你平时怎么解决跨域的。以及后续JSONP的原理和实现以及cors怎么设置。
7.说一下深拷贝的实现原理。
8.说一下项目中觉得可以改进的地方以及做的很优秀的地方?

最后问了有什么需要问的地方,面试到这里基本就结束了,大约面了一个多钟头,还是蛮累的。总体来说,回答的广度和深度以及细节都还算OK,觉得这轮面试基本没什么悬念。

二轮技术面

过了几天,接到阿里另一个面试官的电话,上一轮面试通过了,这次是二轮技术面,说估计一个钟头。这次依然跳过自我介绍之类的,直奔主题。

1.有没有自己写过webpack的loader,他的原理以及啥的,记得也不太清楚。
2.有没有去研究webpack的一些原理和机制,怎么实现的。
3.babel把ES6转成ES5或者ES3之类的原理是什么,有没有去研究。
4.git大型项目的团队合作,以及持续集成啥的。
5.什么是函数柯里化?以及说一下JS的API有哪些应用到了函数柯里化的实现?
6.ES6的箭头函数this问题,以及拓展运算符。
7.JS模块化Commonjs,UMD,CMD规范的了解,以及ES6的模块化跟其他几种的区别,以及出现的意义。
8.说一下Vue实现双向数据绑定的原理,以及vue.js和react.js异同点,如果让你选框架,你怎么怎么权衡这两个框架,分析一下。
9.我看你也写博客,说一下草稿的交互细节以及实现原理。

最后面试官问我有什么想问的吗,面试到这里基本就结束了,差不多面了一个小时。

三轮技术面

上一轮发挥感觉没前两轮发挥好,所以还是有点不自信的,没想到第三天后,就来电话了,通知我去阿里园区面试。

第一个面试官

1.先自我介绍一下,说一下项目的技术栈,以及项目中遇到的一些问题啥的。
2.一个业务场景,面对产品不断迭代,以及需求的变动该怎么应对,具体技术方案实现。
具体业务场景,我就不一一描述,Boss在白板上画了一个大致的模块图,然后做了一些需求描述。
然后需求一层一层的改变,然后往下挖,主要是考察应对产品能力,以及对代码的可维护性和可拓展性这些考察,开放性问题,我觉得还考察一些沟通交流方面的能力,因为有些地方面试官故意说得很含糊,反正就是一个综合能力,以及对产品的理解,中间谈到怎么实现,也问到了一些具体的点,记得问到的有一下几个。

① 怎么获取一个元素到视图顶部的距离。
② getBoundingClientRect获取的top和offsetTop获取的top区别
③事件委托

第二个面试官

1.业务场景:比如说百度的一个服务不想让阿里使用,如果识别到是阿里的请求,然后跳转到404或者拒绝服务之类的?
2.二分查找的时间复杂度怎么求,是多少
3.XSS是什么,攻击原理,怎么预防。
4.线性顺序存储结构和链式存储结构有什么区别?以及优缺点。
5.分析一下移动端日历,PC端日历以及桌面日历的一些不同和需要注意的地方。
6.白板写代码,用最简洁的代码实现数组去重。
7.怎么实现草稿,多终端同步,以及冲突问题?

小结

一面的时候其实我自己感觉答得不是特别好,当时面下来感觉要凉了,很幸运的时候还是给我过了。

我准备面试之前对我自己的要求就是,我会的尽量不会很快就被问倒,所以我重点复习了我擅长的知识,并且到网上刷了很多阿里面试题,做了一个整理,现在分享给大家,算是一个感恩回馈吧。

React
  • REACT 全家桶
  • 介绍Redux,主要解决什么问题?数据流程是怎么样的?多个组件使用相同状态如何进行管理?
  • React-Redux到react组件的连接过
  • Redux中间件是什么东西,接受几个参数
  • redux请求中间件如何处理并发
  • Redux中异步的请求怎么处理
  • 如何配置React-Router
  • react-router怎么实现路由切换?BrowserRouter as Router

......

HTML和CSS
  • 你做的页面在哪些流览器测试过?这些浏览器的内核分别是什么?
  • 每个 HTML 文件里开头都有个很重要的东西,Doctype,知道这是干什么的吗?
  • Quirks 模式是什么?它和 Standards 模式有什么区别
  • div+css 的布局较 table 布局有什么优点?
  • img 的 alt 与 title 有何异同? strong 与 em 的异同?
  • 你能描述一下渐进增强和优雅降级之间的不同吗?
  • 为什么利用多个域名来存储网站资源会更有效?
  • 请谈一下你对网页标准和标准制定机构重要性的理解。
  • 请描述一下 cookies,sessionStorage 和 localStorage 的区别?

JavaScript
  • JQuery 一个对象可以同时绑定多个事件,这是如何实现的?
  • 知道什么是 webkit 么? 知道怎么用浏览器的各种工具来调试和 debug 代码么?
  • 如何测试前端代码么? 知道 BDD, TDD, Unit Test 么? 知道怎么测试你的前端工程么(mocha, sinon, jasmin, qUnit..)?
  • 前端 templating(Mustache, underscore, handlebars)是干嘛的, 怎么用?
  • 简述一下 Handlebars 的基本用法?
  • 简述一下 Handlerbars 的对模板的基本处理流程, 如何编译的?如何缓存的?
  • 用 js 实现千位分隔符?
  • 检测浏览器版本版本有哪些方式?
  • 我们给一个 dom 同时绑定两个点击事件,一个用捕获,一个用冒泡,你来说下会执行几次事件,然后会先执行冒泡还是捕获
  • 实现一个函数 clone,可以对 JavaScript 中的 5 种主要的数据类型(包括 Number、String、Object、Array、Boolean)进行值复制
  • 如何消除一个数组里面重复的元素?

HTML5和CSS3
  • CSS3 有哪些新特性?
  • html5 有哪些新特性、移除了那些元素?如何处理 HTML5 新标签的浏览器兼容问题?
  • 如何区分 HTML 和 HTML5?
  • 本地存储(Local Storage )和 cookies(储存在用户本地终端上的数据)之间的区别是什么?
  • 如何实现浏览器内多个标签页之间的通信?
  • 你如何对网站的文件和资源进行优化?
  • 什么是响应式设计?
  • 新的 HTML5 文档类型和字符集是?
  • HTML5 Canvas 元素有什么用?
  • HTML5 存储类型有什么区别?

查看原文

赞 23 收藏 18 评论 3

nero 赞了文章 · 2020-12-29

vue 源码解析(3-2-2,虚拟dom)

前面写过一个snabbdom的解析,vue2.0版本用的就是这个,然后在他的基础上添加了一些功能

推荐先去看下总结:vue源码总结

vue-clic生成项目中的render 中的 h 函数,就是createElement()
src/core/instance/render.js中创建h函数
image.png
Vue.init 的时候会调用initRender

image.png
初始化_render,
image.png

在 vm._render() 中调用了,用户传递的或者编译生成的 render 函数,这个时候传递了 createElement。

vm.c 和 vm.$createElement 内部都调用了 createElement,不同的是最后一个参数。vm.c 在编译生成的render 函数内部会调用,vm.$createElement 在用户传入的 render 函数内部调用。当用户传入render 函数的时候,要对用户传入的参数做处理

经过前一篇vm.$mount我们知道vue 内部通过这个挂载了元素,这个方法最后执行了mountComponent方法
image.png

这个方法定义了updateComponent,updateComponent中通过vm._render()(里面调用vm._c或者vm.$createElement)生成虚拟dom,然后vm._update对比 两次的虚拟dom进行更新.
创建渲染Watcher时会调用一次updateComponent来渲染dom.后续属性变化时 放入队列的渲染watcher,最后执行run()的时候,会调用updateComponent来进行对比更新,可以结合我上一篇一起看.

然后该看vm._c和vm.$createElement也就是h函数 内部调用createElement生成vNode并返回。

createElement 类似snabbdom的h函数
我们关注重点不同部分

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  if (isDef(data) && isDef((data: any).__ob__)) {
    process.env.NODE_ENV !== 'production' && warn(
      `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render!',
      context
    )
    return createEmptyVNode()
  }
  // <component v-bind:is="currentTabComponent"></component>
  // object syntax in v-bind
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // warn against non-primitive key
  if (process.env.NODE_ENV !== 'production' &&
    isDef(data) && isDef(data.key) && !isPrimitive(data.key)
  ) {
    if (!__WEEX__ || !('@binding' in data.key)) {
      warn(
        'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
        context
      )
    }
  }
  // support single function children as default scoped slot
  if (Array.isArray(children) &&
    typeof children[0] === 'function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }

  if (normalizationType === ALWAYS_NORMALIZE) {
    //返回一维数组,处理用户手写的 render
    //判断children的类型,如果是原始值的话转换成VNode的数组
    //如果是数组的话,继续处理数组中的元素
    //如果数组中的子元素又是数组(slottemplate),递归处理
    //如果连续两个节点都是字符串会合并文本节点
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    // 把二维数组,转换成一维数组
    //如果children中有函数组件的话,函数组件会返回数组形式
    //这时候children就是一个二维数组,只需要把二维数组转换为一维数组
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  //判断tag是字符串还是组件
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    // 是否是 html 的保留标签
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
        warn(
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
          context
        )
      }
      ////如果是浏览器的保留标签,创建对应的VNode
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    // 判断是否是 自定义组件
    } else if ((!data || !data.pre) && 
      isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // 查找自定义组件构造函数的声明
      // 根据 Ctor 创建组件的 VNode
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

这里虚拟dom 和snabbdom不太一样,vue中的vNode属性更多,我们目前只关注我们要的就可以了(关注和snabbdom一样)。
image.png

update方法内部调用 vm.__patch__() 把虚拟 DOM 转换成真实 DOM
vue构造函数定义时混入时创建_update

image.png

image.png

这样我们就知道了updateComponent中 _update(_render()),render生成虚拟dom,update对比更新,是怎样更新的.

patch 函数初始化
功能:对比两个 VNode 的差异,把差异更新到真实 DOM。如果是首次渲染的话,会把真实 DOM 先转换成VNode

src/platforms/web/runtime/index.js初始化 patch

import {patch} from' ./patch'
Vue.prototype.__patch__=inBrowser?patch:noop

src/platforms/web/runtime/patch.js
image.png

src/core/vdom/patch.js

export function createPatchFunction (backend) {
  let i, j
  const cbs = {}

  // modules 节点的属性/事件/样式的操作
  // nodeOps 节点操作
  const { modules, nodeOps } = backend

  for (i = 0; i < hooks.length; ++i) {
    // cbs['update'] = []
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        // cbs['update'] = [updateAttrs, updateClass, update...]
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }
  //........省略
   return function patch (oldVnode, vnode, hydrating, removeOnly) {
    // 新的 VNode 不存在
    if (isUndef(vnode)) {
      // 老的 VNode 存在,执行 Destroy 钩子函数
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    // 老的 VNode 不存在
    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      // 创建新的 VNode
      createElm(vnode, insertedVnodeQueue)
    } else {
      // 新的和老的 VNode 都存在,更新
      const isRealElement = isDef(oldVnode.nodeType)
      // 判断参数1是否是真实 DOM,不是真实 DOM
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // 更新操作,diff 算法
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        // 第一个参数是真实 DOM,创建 VNode
        // 初始化
        if (isRealElement) {
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            } else if (process.env.NODE_ENV !== 'production') {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
              )
            }
          }
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          oldVnode = emptyNodeAt(oldVnode)
        }

        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // create new node
        // 创建 DOM 节点
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

        // update parent placeholder node element, recursively
        if (isDef(vnode.parent)) {
          let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            ancestor.elm = vnode.elm
            if (patchable) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor)
              }
              // #6513
              // invoke insert hooks that may have been merged by create hooks.
              // e.g. for directives that uses the "inserted" hook.
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                // start at index 1 to avoid re-invoking component mounted hook
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }

        // destroy old node
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
}

createElm把 VNode 转换成真实 DOM,插入到 DOM 树上这个和snabbdom的类似就不再这里展开了,有兴趣可以下源码读下这里

patchVnode这里:

 function patchVnode (
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
  ) {
    if (oldVnode === vnode) {
      return
    }

    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // clone reused vnode
      vnode = ownerArray[index] = cloneVNode(vnode)
    }

    const elm = vnode.elm = oldVnode.elm

    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
      } else {
        vnode.isAsyncPlaceholder = true
      }
      return
    }

    // reuse element for static trees.
    // note we only do this if the vnode is cloned -
    // if the new node is not cloned it means the render functions have been
    // reset by the hot-reload-api and we need to do a proper re-render.
    // 如果新旧 VNode 都是静态的,那么只需要替换componentInstance
    if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
    }

    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }

    const oldCh = oldVnode.children
    const ch = vnode.children
    if (isDef(data) && isPatchable(vnode)) {
      // 调用 cbs 中的钩子函数,操作节点的属性/样式/事件....
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      // 用户的自定义钩子
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }

    // 新节点没有文本
    if (isUndef(vnode.text)) {
      // 老节点和老节点都有有子节点
      // 对子节点进行 diff 操作,调用 updateChildren
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        // 新的有子节点,老的没有子节点
        if (process.env.NODE_ENV !== 'production') {
          checkDuplicateKeys(ch)
        }
        // 先清空老节点 DOM 的文本内容,然后为当前 DOM 节点加入子节点
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        // 老节点有子节点,新的没有子节点
        // 删除老节点中的子节点
        removeVnodes(oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        // 老节点有文本,新节点没有文本
        // 清空老节点的文本内容
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      // 新老节点都有文本节点
      // 修改文本
      nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

updateChildren
updateChildren 和 Snabbdom 中的 updateChildren 整体算法一致,这里就不再展开了。我们再来看下它处理过程中 key 的作用,再 patch 函数中,调用 patchVnode 之前,会首先调用 sameVnode()判断当前的新老 VNode 是否是相同节点,sameVnode() 中会首先判断 key 是否相同。

image.png

当没有设置 key 的时候

  • 在 updateChildren 中比较子节点的时候,会做三次更新 DOM 操作和一次插入 DOM 的操作

当设置 key 的时候

  • 在 updateChildren 中比较子节点的时候,因为 oldVnode 的子节点的 b,c,d 和 newVnode 的 x,b,c 的key 相同,所以只做比较,没有更新 DOM 的操作,当遍历完毕后,会再把 x 插入到 DOM 上DOM 操作只有一次插入操作。
  • image.png

image.png

这是虚拟dom的更新,和snabbdom很像,就是添加了h函数中对组件部分的相关处理,根据组件编译出来的ast语法树,生成组件的vNode下篇我们再来了解一下vue模板的编译了解下组件相关.

查看原文

赞 1 收藏 0 评论 0

nero 赞了文章 · 2020-12-29

vue3+typeScript 多选标签(每周一个小组件)

每周一个小组件

前言

实现功能:单击标签可选中或取消,标签可多选。
每周分享一个vue3+typeScript的小组件,我只想分享下自己的实现思路,楼主是个菜鸡前端,记录下实现过程,说不定对你有帮助。

效果展示

预览地址

github地址

image

开发过程

其实,只要思路对了,实现这个标签多选功能非常简单,下面看代码结构

html部分

   <div class="menu-box">
        <!-- 循环每个标签 -->
        <duv class="menu-item" v-for="(vo,inx) in items" @click="itemFn(inx)" :class="vo.isTrue?'acitve':''" :key="inx">
            {{vo.name}}
        </duv>
    </div>

ts部分

<script lang="ts">
import {
    defineComponent,
    reactive,
    toRefs
} from 'vue'
export default defineComponent({
    setup() {
        const data = reactive({
            items: [{
                name: '推荐',
                isTrue: false
            }, {
                name: '图片',
                isTrue: false
            }, {
                name: '视频',
                isTrue: false
            }, {
                name: '段子',
                isTrue: false
            }, ],
            itemFn: (inx: number) => {
                data.items[inx].isTrue = !data.items[inx].isTrue
            }
        })
        const data_ = toRefs(data)
        return {
            ...data_
        }
    }
})
</script>

css部分

    .menu-box {
        width: 400px;
        height: 100px;
        background: #f5f5f5;
        display: flex;
        flex-wrap: wrap;
        justify-content: space-between;
        padding: 0 20px;
        padding-top: 20px;

        .menu-item {
            padding: 6px 15px;
            height: 40px;
            border: 1px solid #ddd;
            border-radius: 20px;
            cursor: pointer;
        }

        .acitve {
            border: 1px solid #409EFF;
            color: #409EFF;
        }
    }

vue3持续更新中...

查看原文

赞 1 收藏 0 评论 0

nero 赞了回答 · 2020-12-21

解决bind传参和直接传参的优先级如何?

不是,bind是返回一个函数,简单的实现了一下bind.你的runTest返回的函数,参数是写死的root,所以肯定不会打印false。

Function.prototype.myBind = function () {
    var _this = this
    var ctx = Array.prototype.shift.apply(arguments) 
    var argu = arguments
    return function() {
        return _this.apply(ctx, argu)
    }
}

关注 3 回答 2

nero 赞了回答 · 2020-12-21

解决bind传参和直接传参的优先级如何?

不是哦,false 会作为第二个参数传递给 test 函数。

function test(root, b) {
    console.log(root, b)
}
function runTest() {
    return test.bind(null, 'root')
}

runTest()(false)
// root false

关注 3 回答 2

nero 提出了问题 · 2020-12-21

解决bind传参和直接传参的优先级如何?

如下面的代码,test函数内打印入参root,在runTest里return 一个test的bind,bind时给test传参为字符串'root'。

然后我调用runTest(),它的调用结果是test函数的引用,但是是bind过后的。

此时我再给它传入false:runTest()(false)。

打印出来的是bind时传的参数:'root',而不是我后来传入的false,这是因为bind传参比调用时传参的优先级高吗?

function test(root) {
    console.log(root)
}
function runTest() {
    return test.bind(null, 'root')
}

runTest()(false)
// root

关注 3 回答 2

nero 赞了文章 · 2020-12-14

Vue3源码解析(computed-计算属性)

作者:秦志英

前言

上一篇文章中我们分析了Vue3响应式的整个流程,本篇文章我们将分析Vue3中的computed计算属性是如何实现的。

在Vue2中我们已经对计算属性了解的很清楚了,在Vue3中提供了一个computed的函数作为计算属性的API,下面我们来通过源码的角度去分析计算属性的运行流程。

computed

export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>
export function computed<T>(
  options: WritableComputedOptions<T>
): WritableComputedRef<T>
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>
  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }
  return new ComputedRefImpl(
    getter,
    setter,
    isFunction(getterOrOptions) || !getterOrOptions.set
  ) as any
}
  • 在最开始使用函数重载的方式允许computed函数接受两种类型的参数:第一种是一个getter函数, 第二种是一个带getset的对象。
  • 接下就是在函数内部根据传入的不同类型的参数初始化函数内部的gettersetter函数,如果传入的是一个函数类型的参数,那么getter就是这个函数,setter就是一个空的操作,如果传入的参数是一个对象,则getter就等于这个对象的get函数,setter就等于这个对象的set函数。
  • 在函数的结尾返回了一个new ComputedRefImpl,并将前面我们标准化后的参数传递给了这个构造函数。

下面我们就来分析一下ComputedRefImpl这个构造函数。

ComputedRefImpl

class ComputedRefImpl<T> {
  // 缓存结果
  private _value!: T
  // 重新计算开关
  private _dirty = true
  public readonly effect: ReactiveEffect<T>
  public readonly __v_isRef = true;
  public readonly [ReactiveFlags.IS_READONLY]: boolean
  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean
  ) {
    // 对传入的getter函数进行包装
    this.effect = effect(getter, {
      lazy: true,
      // 调度执行
      scheduler: () => {
        if (!this._dirty) {
          this._dirty = true
          // 派发通知
          trigger(toRaw(this), TriggerOpTypes.SET, 'value')
        }
      }
    })
  }
  // 访问计算属性的时候 默认调用此时的get函数
  get value() {
    // 是否需要重新计算
    if (this._dirty) {
      this._value = this.effect()
      this._dirty = false
    }
    // 访问的时候进行依赖收集 此时收集的是访问这个计算属性的副作用函数
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }

  set value(newValue: T) {
    this._setter(newValue)
  }
}

ComputedRefImpl类在内部维护了_value_dirty这两个非常重要的私有属性,其中_value使用用来缓存我们计算的结果,_dirty是用来控制是否需要重现计算。接下来我们来看一下这个函数的内部运行机制。

  • 首先构造函数在初始化的时候使用了effect函数对传入getter进行了一层包装(上一篇文章中我们分析过effect函数的作用就是将传入的函数变成可响应式的副作用函数),但是这里我们在effect中传入了一些配置参数,还记得前面我们分析trigger函数的时候有这一段代码:
const run = (effect: ReactiveEffect) => {
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }
effects.forEach(run)

当属性值发生改变之后,会触发trigger函数进行派发更新,将所有依赖这个属性的effect函数循环遍历,使用run函数执行effect,如果effect的参数中配置了scheduler,则就执行scheduler函数,而不是执行依赖的副作用函数。当计算属性依赖的属性发生变化的时候,回执行包装getter函数的effect, 但是因为配置了scheduler函数,所以真正执行的是scheduler函数,在scheduler函数中并没有执行计算属性的getter函数求取新值,而是将_dirty设置为false,然后通知依赖计算属性的副作用函数进行更新, 当依赖计算属性的副作用函数收到通知的时候就会访问计算属性的get函数,此时会根据_dirty值来确定是否需要重新计算。

回到我们的这个构造函数中,只需要记得我们在构造函数初始化三个重要的点:第一:对传入的getter函数使用effect函数进行包装。第二:在使用effect包装的过程中,我们会执行getter函数,此时getter函数执行过程中对于访问到的属性会将当前的这个计算属性收集到对应的依赖集合中, 第三:传入了配置参数lazyscheduler,这些配置参数在当前的这个计算属性所订阅的属性发生改变的时候,用来控制计算属性的调度时机。

  • 接着我们继续分析get value,当我们访问计算属性的值时候实际上访问的就是这个函数的返回值, 它会根据_dirty的值来判断是否需要重新计算getter函数,_dirty为true需要重新执行effect函数,并将effect的值置为false,否则就返回之前缓存的_value值。在访问计算属性值的阶段会调用track函数进行依赖收集,此时收集的是访问计算属性值的副作用函数, key始终是vlaue。
  • 最后就是当设置计算属性的值的时候会执行set函数,然后调用我们传入的_setter函数。

示例流程

至此计算属性的执行流程就分析完毕了,我们来结合一个示例来完整的过一遍整个流程:

<template>
    <div>
        <button @click="addNum">add</button>
        <p>计算属性:{{computedData}}</p>
    </div>
</template>

<script>
import { ref, watch,reactive, computed } from 'vue' 
import { effect } from '@vue/reactivity'
export default {
  name: 'App',
  setup(){
    const testData = ref(1)
    const computedData = computed(() => {
      return testData.value++
    })
    function addNum(){
      testData.value += 10
    }
    return {
      addNum,
      computedData
    }
  },
}

</script>

下面是一张流程图,当点击页面中的按钮改变testData的value值时,发生的变化流程就是下面的红线部分。

  • 首先初始化页面的时候,testData经过ref()之后变成响应式数据,会对访问testData.value的值进行依赖收集,当testData.value的值发生变化的话,会对依赖这个值的依赖集合进行派发更新
  • computed中传入了一个getter函数,getter函数内部有对testData.value的访问,此时当前的这个计算属性的副作用函数就订阅了testData.value的值,computed返回了一个值,而页面中的组件有对computed返回值的访问,页面的渲染副作用函数就订阅了computed的返回值,所以这个页面中有两个依赖集合。
  • 当我们点击页面中的按钮,会改变testData.value的值,此时会通知订阅计算属性的副作用函数进行更新操作,由于我们在生成计算属性副作用的时候配置了scheduler,所以执行的是scheduler函数,scheduler函数并没有立即执行getter函数进行重新计算,而是将ComputedRefImpl类内部的私有变量_dirty设置为true,然后通知订阅当前计算属性的副作用函数进行更新操作。
  • 组件中的渲染副作用函数执行更新操作的时候会访问到get value函数,函数内部会根据_dirty值来判断是否需要重新计算,由于前面的scheduler函数将_dirty设置为true所以此时会调用getter函数的副作用函数effect,这个时候才会重新计算并将结果返回,页面数据更新。

总结

计算属性两个最大的特点就是

  • 延时计算 计算属性所依赖的值发生改变的时候并不会立即执行getter函数去重新计算新的结果,而是打开重新计算的开关并通知订阅计算属性的副作用函数进行更新。如果当前的计算属性没有依赖集合就不执行重新计算逻辑,如果有依赖触发计算属性的get,这个时候才会调用this.effect()进行重新计算。
  • 缓存结果 当依赖的属性没有发生改变的,访问计算属性会返回之前缓存在_value中的值。

对 Electron 感兴趣?请关注我们的开源项目 Electron Playground,带你极速上手 Electron。

我们每周五会精选一些有意思的文章和消息和大家分享,来掘金关注我们的 晓前端周刊


我们是好未来 · 晓黑板前端技术团队。
我们会经常与大家分享最新最酷的行业技术知识。
欢迎来 知乎掘金SegmentfaultCSDN简书开源中国博客园 关注我们。
查看原文

赞 1 收藏 0 评论 0

nero 赞了文章 · 2020-12-11

二手房购买流程及注意事项,建议收藏!避免踩坑!

买房对于任何一个漂一族来说都是头等大事,很多时候,老有读者问我,年轻人到底要不要买房,我一直给的建议是:要,但是要量力而行。

这里就是必须要提到,买新房与买二手房,新房购买直接去开发商销售中心就可以了,一般的流程无非就是看房,付首付款,符合同,贷款还贷,然后就等新房交房。

今天,民工哥主要和大家聊一聊二手房买卖这中间的流程以及一些需要注意的事项。

因为,很多刚需群体可能对于新房(有时候又叫期房)时间上等待不了,比如:小孩要上学等情况,所以,也会直接考虑二手房。

二手房买卖流程

  • 1、按需量力选择购买房源
  • 2、联系中介看房
  • 3、沟通房价及相关事项
  • 4、签定存量房交易合同与房产经济服务合同
  • 5、付定金
  • 6、提交贷款资料(全款的请忽略)
  • 7、不动产登记中心开具证明(是否具备限购条件下购买资格)
  • 8、贷款银行办卡(后期还贷银行开卡)
  • 9、签定资金托管协议
  • 10、首付款托管办理
  • 11、银行贷款预审通知
  • 12、交税过户
  • 13、银行办理产权抵押
  • 14、等待银行放款(你贷款的部分款项)
  • 15、与原业主交接物业等相关的手续办理
  • 16、去放款银行办理结款(你的首付+贷款将一次性钱额支付给原业主)
  • 17、整个交易完成

1、按需量力选择购买房源

对于刚需购房者,这点必须首先要考虑的,否则后期还贷、生活、工作的压力会很大程度上影响你的生活质量。

所以,你手上的资金是决定你买房的关键,好的地区、学区,肯定是比其它的地点贵一点。在选择的时候,一定不能没有自己的心里打算,不然,这样很容易失去好的购房机会。

  • 手上的资金需要覆盖到以下几点:
  • 1、房子的总首付是多少,各地不同,有3成,4成,5成。。。
  • 2、各种税费是多少
  • 3、中介服务费是多少

这里给大家计算个例子,比如:某二手房80平总价200万,那么需要准备的资金总额如下:以上的问题解决之后,就可以在网上去先选择性看房,设定一定的条件,比如:总价在多少范围?学区是什么样的?楼层?面积?等等情况。

网上平台看房,建议还是选择大的平台,这里我就不推荐平台,否则会有广告嫌疑,大平台一般房源、房价等信息都相对比较真实可靠,会省去一些不必要的麻烦。

2、联系中介看房

网上平台看完后,心里有一定的预期了,就可以去实地联系中介公司的业务员实地看房。

这里,肯定有很多人会讲,干吗要找中介?花那钱。。。其实,不然,专业的事还是得交给专业的人做,有些钱可以省,有些钱不可以省。大家可以看看后面的流程,就知道这其中的原因了。

3、沟通房价及相关事项

如果看中某套房子,而且房价、户型、装修等比较符合心理预期,这时就会进入房价的沟通环节,这一环节也是比较重要的节点,毕竟关乎到钱。

一般来说,首先你可以让中介业务员先去和现业主沟通,探一下业主的心理预期,然后,你也可以参考一下此小区相同户型的价格,以及你对这套房子的心理预期。

但是,民工哥建议你买房不像买菜,总房价不太可能会下降很多(当然是在挂牌价格符合市场价格的前提下,如果现业主的挂牌价格远高于市场,只会有两种可能:1、房东不缺钱花,不着急着卖,2、房东本身可能是投资客或对房价期望过高)。这类房东估计也不好谈,建议可以换一家,别白白浪费时间,错过其它机会。

所以,一般总价的浮动预计在2-5个点左右,也就是说总价100万的房子,最终能谈下来2-5万元,这也是合理的,符合目前的市场趋势。但是也有可能不降反涨的情况,那就是你选择的地段好、学区好、户型好,购买者较多,产生了竞争的情况。

4、签定存量房交易合同与房产经济服务合同

4与5其实是同时进行的,谈好价格之后就会签定存量房(二手房的通用名称)交易合同,一式三份,你、房东、房产中心各一份。

存量房交易合同的注意事项:

  • 1、注意合同当中的总房价是否正确
  • 2、合同中关于房子的地址及楼层、房间号信息是否正确
  • 3、沟通的首付款约定的支付时间
  • 4、贷款金额及方式
  • 5、合同中对于房屋产权的描述信息
  • 6、合同中的一些违约条款,无论是对于房东,还是你的约束条款。这些注意事项都非常重要,一定要在签字之前看仔细了,特别是产权的信息,对于产权的信息准确无误,这点是中介服务其中的一项,这也是为什么要通过中介的原因。现实生活,付了钱之后,产权存在问题的情况是时有发生,真到这个时候会很麻烦。

房产经济合同:这是你与中介公司之间的合同。

同样,合同当中还是会有房屋的信息,这里注意要与之前的存量房交易合同一致,还有就是中介服务内容及费用(这里民工哥建议大家先谈好再签定合同),一般现在中介服务费如下:

  • 中小公司:2%,一般可以谈到1.5%,最终可以根据总价再打个折扣。
  • 大平台:3%,大平台一般可谈的区间不大,但是不会是一口价,一定要要求打折,这个钱必须省。

注:这个是房主总价的比例,如果房子谈下来价格空间大,中介服务,后面的税费就会相应的减少。

房产经济合同,一定要注明好如果此房屋存在产权纠纷问题,中介是需要提前告知买方或者承担违约责任,这就是为什么要请中介的目的之一,因为很多事项你可能无法查询或知道。如上图所示,产权是共有性质,那么存量房合同需要共同所有人共同签字,否则合同无法生效,或者后续会出现其它问题。

还有就是也得看一下其中的违约条款约定信息,以及你、中介公司所需要提供的条款约定信息。总之,签定合同一定谨慎,看好之后再签字。

一般还会签定一个补充协议,比如:房子有装修,房东送家具家电的等一些情况,可以在这个补充协议里注明。以你们三方沟通情况定。

上述步骤完之后,就需要支付一定金额的中介服务费了,切记!开具收条或发票备用。

5、付定金

房价谈好之后,就是预付定金给房东,一般情况付5万左右即可,不过也还看房子的总价以及与房东的沟通决定。记得让房东开具定金收条,后面资金托管需要使用。

6、提交贷款资料(全款的请忽略)

贷款材料,因各地的政策不同,需要的也不相同,这里民工哥以合肥家庭为例,大体列举一下。

  • 1、夫妻双方的身份证、户口本、结婚证原件及复印件
  • 2、提供近两年时间段内连续缴纳一年及以上社保明细(需提前去社保中心自助机打印)
  • 3、双方的收入证明及银行流水
  • 4、其它需要补充的材料
  • 5、双方征信明细(带上身份证去中国银行打印)

这些材料提交给中介公司,由中介公司贷款专员负责处理。

这里提一下,在合肥是需要支付6600元贷款服务费和3000过户费,这个和中介费是一起支付给中介公司的,所以中介服务合同有个补充协议上会标明。

7、不动产登记中心开具证明(是否具备限购条件下购买资格)

这个是贷款需要的材料,各地都在限购,所以需要不动产中心开具一个你的购房资质证明,中介会带你去开这个证明,各地限购条件不同,合肥为例,如下图:

8、贷款银行办卡(后期还贷银行开卡)

一般情况下,在一个月内,中介业务员会联系你去银行办卡(这个卡就是你以后还贷的卡),提示:如果你在此银行在此之前办理过I类卡,在办理此卡之前需要将之前的I类卡降级为II类卡,然后在为你放款的银行办理一上张I类卡。

办理完成之后,记得激活,开通大额转账功能(后期从此卡转账到托管账户需要)。

9、签定资金托管协议

你、房东、中介三方同去贷款银行提交相关材料,办理资金签定资金托管协议。

这个就是让你们去签一堆的字,按银行指定要求签字(签字按手印)。本协议一式三份,你、房东、银行各执一份。

10、首付款托管办理

完成资金托管协议签定之后,将在行办理好的卡激活,然后排队取号去专业窗口办理首付款托管,托管的意思就是:将你的首付款通过你的卡转账至不动产中心专门的一存量房资金托管账户,相当于这个钱是由国家暂时帮你、房东双方保管,在没有完成交易之前,这个钱是无法支取的,此举是为了保证买、卖双方的合法权益。

这个过程也很简单,中介贷款专业会提前给你填写一个像银行存钱转账的单子,你直接去窗口办理就可以,在办理此业务之前,你需要将你的首付款转到你第8步办理的银行卡内,然后,银行会从你这张卡上直接将钱转账至存量房托管专用账户进行冻结。

这里需要注意:如果原业主的房子存在抵押情况,可能就会存在,房东会需要拿着你的首付款去帮他还贷款,这样房子的产权才会被解压出来,才能正常交易,否则,在这之前房子的产权是抵押给银行的,房东是无权处置买卖的。

不过,这个过程也是在监管的情况下进行的,属于下当合法合规的行为,不必担心你的钱。

11、银行贷款预审通知

上面的1-10步操作完成之后,接下来就等给你贷款的银行对你的贷款资质的预审结果,如果中间需要补充材料的,可以同中介一同沟通补充材料,最终你会得预审通过的结果。

基本上不会出现不通过的现象,如果有,那就需要重新找其它银行办理了,所以,这里提示大家个人征信一定不要出现问题,这个是现在是一个很重要的资质,特别是买房、上学等情况下。

12、交税过户

贷款银行预审通过,就意味可以给你贷款了。

这个时个,中介会通知你、房东一起去办理过户,将房屋产权过户至你的名下。在过户之前,你需要去税务中心交纳个税、契税、增值税,以合肥为例:

  • 个税:房屋总价*1%
  • 契税:房屋总价*1%-3%,
  • 房屋面积<=90平 首套及二套1%,三套及以上3%
  • 房屋面积>90平 首套1.5%,二套2%,三套及以上3%
  • 增值税:这个每个地方的政策不是很了解,合肥是5.3%

这里是有一个概念:商品房是否满五年?是否是房东唯一住房,也就是中介公司常说的此房是满五唯一。如果满五年就不需要交纳增值税,如果再符合唯一住房的条件,也就是如果买的房子唯五唯一,那么个税与增值税就不需要交纳了。

交完税之后,你们就可以去产权登记中心办理过户了,房东会将他的产权过到你的名下,到时候不动产中心会下发新的不动产登记证书给你,上面就是你的信息了,也可以说房子现在一半是属于你了。

这里有个小提示:注意在办理过户之前,一定要注意原业主是否将他的户口迁移走,也就是说不是在你这个产权所属的房屋名下,否则后面如果涉及户口迁移的问题可能会有麻烦。切记!!!如果可以,将此项条款写进房产经济合同之中,这样中介会帮你搞定。

13、银行办理产权抵押

过户完成,不动产登记中心下发新的不动产登记证书给你。

然后,你拿着你的不动产登记证书去银行办理抵押贷款手续,签定贷款合同。贷款合同无非就是你将你的房屋产权抵押给银行,然后银行给你放款,将剩余款项打到你之前办理的账户当中。

这里就涉及到一个利率、年限与还款方式的问题,利率一般国家都有规定基准利率,当然各地银行有不同的政策,首套合肥是4.9%,二套上浮20-30%。

还款方式,全国统一:

  • 等额本金
  • 首月还款相对于下面的还款方式来言会多不少,然后每个月以某个既定的数值递减,比如首月还款7000,每月递减30,那么,第2月还款6970,第3月还6940,。。。。以次类推。
  • 这种适合收入比较稳定,且提升空间稳定的人群,可以选择这类方式,说明就是利息会少很多,相对等额本息的方式
  • 等额本息
  • 每月还款额度一样
  • 这种方式比较适合收入比较固定,收入提升空间不大的人群,相对等额本金,每月款的金额利息占了大部分,也就是说可能利息支付的较多。

至于年限,我个人建议能贷多久贷多久,这也是普通人向银行借钱的不多的方式中的一种。

14、等待银行放款(你贷款的部分款项)

这个过程,就是银行内部流程审批时间了,等等即可。

15、与原业主交接物业等手续办理

接到银行放款通知之后,中介还会联系你、房东,你们三方一起去办理房屋交接事项。比如:水、电、燃气过户,所在的物业费、水、电、燃气费等费结算办理。

然后,按着当时签定的协议或补充协议,去房屋实地交接,比如:家具、家电、钥匙等。

16、去放款银行办理结款(你的首付+贷款将一次性钱额支付给原业主)

交接完成之后,那么你就得去银行签定确认,然后银行会将这些钱(去除定金之外的款项)一次性支付给原业主。

17、整个交易完成

房东收到钱,你收到房子,整个交易过程完成,现在这套房子才真正属于你了,你爱怎么折腾就怎么折腾,想怎么装修就怎么装修。

总结

整个二手房的交易流程大体如此,不过各地政策不同,可能有所区别,但大体流程差不多。

需要注意和考虑的事项如下:

  • 1、手中的资金覆盖问题
  • 2、你购房的需求是什么
  • 3、中介看房与选房的预期
  • 4、房屋的产权问题(这是重点)
  • 5、合同条款及付款收条
  • 6、配合提供办理贷款的材料及后续过程
  • 7、房屋交接及所有费清算问题

买房是人生中的大事,在慎重的同时也不要太过纠结,以防错过购买的最佳时机,在量力而行的前提,看准了就买,不要犹豫。

以上就是民工哥给大家分享的二手房买卖过程,不一定符合所有的市场,有不正之处欢迎指正,欢迎大家在看与转发朋友圈给有需要的朋友,也欢迎大家留言分享你的购房经验或者遇到坑。

image

查看原文

赞 42 收藏 26 评论 4

nero 赞了文章 · 2020-12-10

OBKoro1的2020年年终总结-人生是一场马拉松

前言

一晃眼2020年马上就要过去了,今年感觉过的特别快。

工作已经三年了,之前都没有写过年终总结,结果造成了下面这个现象:

回首过去的几年,记忆已经很模糊了,需要很用力才能想起过去一部分往事

人生百年,好像也没有多少年终总结可以写呢~

这么激励一下,一下子就有动力写年终总结了 😝

工作

在家办公

年初的疫情,是2020年过不去的记忆~

待到山花烂漫时,我们再相见

当时疫情比较严重,全国封闭,公司很快就宣布在家办公,并且特地说明了不用担心隔离14天赶不上上班~

对于当时还惶惶不安的朋友来说,着实感受到了公司所给予的温暖❤️

在家办公

在家办公的感受

从大年初九开始在家远程办公,疫情对线上教育行业也是一个机会,周末也没有休息,总共在家办公两个月。

后面直到疫情已经很稳定了,在四月一号那天才回的上海,然后隔离一周多,就恢复正常的上班了。

在家办公的优势

效率高:

在家办公的时候,家里人都不会来打扰我,可以很专心,效率非常高。

三餐都不用 操心

一日三餐我老妈都弄好了,都不用操心,我妈也都不会唠叨我,非常幸福^_^~

空间大

家里是自己盖的房子, 拿一间来做书房,空间比较大, 不会感到压抑,累了就在家里走走跳跳,活动一下身体。

可以陪伴家人

偶尔在晚上的时候会跟家人一起打打牌,输赢几十块钱的那种,也很开心~

过年的时候买了一个羽毛球网,下午或者晚上(开灯),也可以跟家里人在院子里打羽毛球。

又可以锻炼身体又可以陪伴家人,非常nice!

弊端: 工作和生活没有界限

因为全体人员都在家里办公,每个人的作息时间、生活习惯都不一样。

有些人很早就起床开始办公,有些人很晚才起床,但是晚上很晚才休息。

这时候就会出现,别人在他的办公时间来找你语音,影响了你的休息时间。

你可能会感到任何时间都有可能有人来找你,没什么事不敢离开工作岗位~

总的来说,在家里办公还是非常愉快的,只要完成你的工作内容就可以了,其他时间可以自己调节。

疫情封闭期间科比意外逝世

退役的时候你曾经说过,如果再过18年你人生的成就还只是篮球的话,那么你该有多失败?但是我们永远猜不到明天和意外谁先来💔

科比逝世对我个人来说是一件非常重大的事情

科比一直是我的精神偶像,他身上的专注与不服输的精神深深的影响我,激励我前行

一觉醒来同学突然在群里艾特我,说科比去世了,还以为是在开玩笑,好端端的,没灾没病的怎么会呢?

当我打开微博求证之后,情绪一下子崩溃了,泪奔, 花了很长时间才接受这个事实😭

写到这里,联想到多灾多难的2020,属实有点难过~

实不相瞒,我从来不信鬼神之说,但是如果有上帝的话

希望他2021年能对这个世界能够温柔一点,谢谢❤️

electron教程开源项目

今年做了几个关于electron的项目,公司恰好有开源的计划,后面就拨出几个人员来做开源项目。

经过几个版本的推到重来与迭代,最后沉淀出了一个关于electron的教程项目-electron-playground

这是一个类似于 store book的项目,通过尝试electron各种特性,使electron的各项特性所见即所得, 来达到我们快速上手和学习electron的目的。

将来如果需要学习electron,或者通过electron做项目,一定要看一下这个项目,可以通过这个项目来入门electron、搭建electron、做一些工程化方面的内容。

前两天写了一篇推广文章,感兴趣的朋友可以看一下这篇博客了解一下这个项目。

公司被好未来收购

今年公司有点动荡,公司因为对赌失败被好未来收购了,创始人出局。

一整年都在融合好未来的制度,我意外的成为了“大厂”人 😝

PPT能力

被收购之后,过了一段时间,要求所有人员进行一次定级,需要所有人讲20分钟左右的PPT。

大厂在晋升和述职的时候都会要求演讲者准备PPT,PPT能力特别重要,如何将自己的东西讲的高大上一点!

没有写过PPT的同学,建议可以自己准备一个晋升PPT模拟一下~

PPT经验

  • PPT字不要太多,准备一些关键词提示一下思路即可。

    字写得密密麻麻非常减分,PPT应该是一个类似大纲的东西。

  • 写完PPT之后,自己对着镜子演练几遍,
    这样会提高熟练度,讲起来不会磕磕巴巴的,并且能找出一些问题。
  • 平日做业务多思考,多承担职责这样在写PPT的时候就不会没东西写了。

    可以对标大公司,大公司有哪些东西我们没有哪些东西,哪些东西能给公司带来什么东西。

蠢事

第一次讲PPT没有经验,我干了一件特别蠢的事情,在这里跟大家分享一下,希望大家不要踩到这个坑:

因为平时做的业务比较杂,项目比较多,不知道要讲哪一块

后面把自己在开源社区上的成就,当成PPT的主要内容。

当时因为是自己熟悉和喜欢的内容,讲的特别流畅和自信,还以为自己肯定没问题。

结果后面评委因为我主要讲开源上的东西,工作上的内容不足,认为我本末倒置了 o(╥﹏╥)o,结果不太理想

融合与动荡

裁员

经历了所有人员定级之后,公司也根据定级表现优化了一部分同事。

因为赔偿N+1给够了,大家也没有什么怨言,好聚好散。

后面听说有的同事是自己申请的名额,工作的累了,自己想休息下,这招学到了~

实线与虚线管理

团队由实线管理,前端leader直属管理,要转变为虚线管理。

简单的说现在就是跟项目,一个人从头到尾做一个项目,以前一个人做很多个项目,这边做一期那边做一期。

跟项目的好处在于:做项目的人对项目熟悉,项目也不会太乱,职责也比较清晰。

技术

博客

有一些粉丝在微信上,以及一些朋友都会说我今年博客写的少了。

今年前中期的时候,在工作、学习上有点迷茫、懈怠。

后面因为投入大量时间在跑步、健身上。

在学习、维护开源项目、写博客、生活、lol等事项上的时间管理做的不是很好。

突然培养了一个耗时间的习惯是这样的。

现在我在时间管理上已经调整好了,学习目标也已经找到了,每天很忙但很充实。

还有一点是我写博客比较磨蹭慢,一篇博客需要三五天的时间才能完成。

因为以上诸多借口原因,今年的输出少了一些。

此情此景,我只能说下次一定😝

要写的东西是很多,我都记在笔记中,明年争取多输出一些篇高质量的博客 💪

深入学习webpack

近期在深入学习webpack,包括各种环境的配置,plugin、loader、项目优化。

也了解了webpack的编译流程,tapable如何通过发布订阅动态生成代码,

从webpack入口配置开始是如何运行的,模块的编译过程、chunk的生成过程,

下面准备自己手写一个webpack,以及再深入一下webpack的tapable机制以及其他细节。

学习方法:

这里分享一下我朋友(易全文)跟我说的学习方法,我觉得特别有效:

学习的时候不要分散精力,集中所有精力攻破一个方向的所有内容,彻底学会、学精

webpack我就是采取这种方式学习,正在进行中, 感觉真的掌握了这个知识。

autoCommit

autoCommit是我年初的时候开源的一款插件,它是用来刷首页Github commit记录的。

它可以刷过去几年以及未来的commit, 一键帮你把github首页的绿色格子填满

有兴趣的同学可以点击autoCommit来了解它,觉得项目还不错的话,就给我点个Star⭐️)吧~

技术无罪

当时以为会大火,结果反响也就一般,还有些争议,有人质疑我为了commit而commit~

我的观点:技术无罪,每个人用工具的方式和目的都不一样

  1. 坚持了很久的commit,不小心断更的commit记录, 可以用autoCommit补一下记录。
  2. 规划一下github首页commit记录的图案,通过autoCommit在绿色格子里面画出有创意的图形~

如果github什么有价值的东西都没有,就算把绿色格子都刷满了,那也不能代表什么

不喜欢这个工具的人可以不用,没人强迫你必须要用。

emmmm, 千金难买我乐意,写到这里越想越气,我把Github的commit记录刷了一波

koroFileHeader

这是我开源的另外一款插件,目前插件已经维护两年半了, 更新了50+版本,关闭200多个issue~

今年比较开心的一件事是koroFileHeader头部注释插件Github仓库Star数量突破了2000,啦啦啦~

今年更新了两个比较重要的功能:

  1. 一键添加注释图案的功能

  1. 函数参数自动提取功能

关于生活

健身

这块是我今年来最大的收获,记录了一下我减肥和健身的心理路程,写的比较长~

被肥胖速度刺激到了

过年回到家里,不出意料,所有人都说我胖了。

去上海后,呆了一个月,就又回去了一下。家里所有人都说我胖了,那时候163斤应该是有的。

我胖起来的原因其实自己也知道就是天天吃夜宵+很少运动,

这个事情一下子就给我刺激到了, 以前一直不以为然,我的天,我才出去一个月就又胖了😱

而且胖的那么明显,再这样下去可怎么办?当时在老家我就开始跑步减肥之旅了..

跑步

不适应-痛苦

今年五月份的时候我只能跑两三公里,并且一公里才配速8分半。

因为太久没有运动了,身体不适应,跑完一次浑身酸痛的要死。

走路都要慢慢走,走一步痛一次,上下楼梯更是痛的要死。

每次需要休息大概四天才缓过来,这个阶段持续了一个月跑了8次。

跟大佬一起锻炼

六月下旬公司健身五六年的大佬(全文)带我一起跑步锻炼,有人一起锻炼,积极性也会提高很多。

每天到下午六点钟的时候就会互相提醒去锻炼,如果不去就是一顿嘲讽,哈哈哈

我用了一个月时间跑了把配速练到七分钟,距离也可以跑到五公里了,跑了20次。

又花了一个月把配速练到6分10秒左右(20次),花了三个月终于达到正常的配速。

办健身卡

在我跑步两个月后(8月份), 大佬带我去体验了一周的健身房。

体验过后经过一番讨价还价,以三年6K的价格办了威尔士的全国通用卡。

哼 说实话过了四个月,我还是觉得他肯定吃了我办卡的回扣,奈何没有抓到证据😕

如果有想锻炼的同学可以先办一个季卡,不要办太久 万一不能坚持就浪费了~

现在我一个月大概去健身房15次到22次之间,每次锻炼一个半小时左右。

今年室外和室内总共跑了六个月105次,800公里✌️

有氧运动超级解压

我最喜欢每次跑完步大汗淋漓的感觉,特别爽,在健身房洗过澡。

走在回家的路上,即使你刚刚加过班,也会感觉整个人非常轻松。

赶走身上所有疲惫,超级超级超级解压!

跑步经验

我现在跑步机10公里能跑46分钟,最后三四公里都是4分半左右的配速 ✌️

设备: 运动手表

一开始跑步我是把手机拿在手上,后面买了腰包,但是跑步的时候都不舒服。

大佬教我买个设备检测一下自己的心率,可以根据心率判断一下自己的状态。

后来我买的是华为手表,跑步的时候不用带手机,跑完再同步跑步数据到keep。

根据心率判断自己的状态是否极限,以及查看目前的配速,距离。

如何提高跑步配速

我个人建议就是不断的打破自己的舒适区,不断挑战更高配速!

可以采取大佬教我的变速跑,比如12公里配速跑400米再10公里配速跑200米,这样交替进行跑8组。

这种形式锻炼心肺能力非常好,心率一直会保持比较高的速度。

前面几次会非常辛苦,到后面就适应了。

我提高配速都是以这种形式,现在我可以用13公里的配速跑三四公里不休息

健身餐

三分练,七分吃,我感觉减肥就是消耗卡路里大于摄入卡路里就自然会瘦下来。

不要以为锻炼了就自然会瘦下来,我一开始锻炼吃的没有控制,减肥也是没有效果的。

我看到其他大佬有在吃健身餐,我也跟着吃了三个月(7.15-10.16)

最疯狂的时期经常:早上不吃,一天只吃中午一顿健身餐,晚上回家吃脱脂面包

现在再也不想吃健身餐了😭, 我减肥很野蛮不够科学,经常被大佬骂~

效果

锻炼两个月之后,在八月份中旬我从163已经减重到150以下了。

目前稳定在143左右,但是减不到140以下。

增加肌肉含量,增加消耗

大佬说我肌肉含量不够,每天身体自然消耗不够大。

下面我可能要吃一下蛋白粉,练练肌肉,增加消耗,不然减不下去。

增加肌肉之后,以后也不会轻易反弹,因为每天不运动消耗也比较大。

健身习惯一辈子的事情

健身感受

在我看来我今年最大的收获就是培养了健身的习惯

无论我以后走到哪里我都会找一个健身房定期的去锻炼身体

真心特别感谢大佬全文带我锻炼身体, 我也培养了一辈子的好习惯 ❤️

一开始身体不适应运动,很痛苦,很勉强,为了减肥后面还是撑了过来

但现在我在健身和跑步中我收获了快乐,一点都不勉强

只要有空都会积极的去健身,因为我知道对我的身体好,而且很也很快乐

通过健身我感觉我的身体也充满了活力,精神状态也好了很多,整个人积极向上多了。

健身例子对比

我们这个行业每天久坐,普遍缺乏锻炼,很多人还有熬夜或者其他不良的习惯。

95年的同事

有一个跟我玩的非常好的朋友,跟我一样95年的

平时基本不运动,人也比较胖,25岁都有脂肪肝和高血压了

肩颈和背部都不太好(职业病 我也是) 等其他问题

32岁的大佬

反观带我健身的大佬,今年32岁,以前一开始见的时候还以为他只是比我大两三岁的样子

健身四五年后,他现在身体状态和精神状态都非常好,活力满满。

大佬说他的同学,一个个的都是中年大叔,跟他们站在一起好像两个时代的人一样。

有一个说法是:什么时候开始锻炼,你的年龄就停留在什么时候。

坚持运动健身,稳赚不赔

上面通过两个例子的对比,大家可以感受一下健身带来的好处,希望大家也可以尽快的行动起来~

旅行散心

今年跟我基友yeyan1996一样,出去玩了几次~

可能工作久了,总想散散心,一有机会就想在周边城市玩一下

没有机会,也要请一两天假凑成一个小长假出去转转,下周飞重庆😝

今年自驾游去了这几个地方:安徽黄山、福建平潭、南京、杭州

墙裂推荐自驾游,同学开车,到地方下车玩就可以了什么都不用操心😝

福建平潭

基金理财

今年我也入了理财的坑,钱放着是行不通的

起码也要做点投资吧,不然怎么跑得赢通货膨胀,要让钱生钱

后面请教了一些同事,开始玩支付宝的基金,今年的收益率24.8% ✌️

为了缓冲一下风险,买了一个银行定期,定期是稳赚的,不过3.5%-4%实在是太低了

后面嫌赚的太慢了,但是银行定期不让我取出来 😭

看书

今年看了几本书:图解http、系统之美、老人与海、三体、瓦尔登湖

这里推荐一下三体,这本小说是国内第一科幻,还获得了雨果奖,经常在知乎、各种场合看到别人在讨论这本书。

里面的设定、脑洞、对人性的深刻描写,超级精彩,总之,看了就是赚!

后记

年终总结写的比较琐碎、比较细,全文5000多字,感谢耐心看完。

写完总结,顿感今年要结束了

来年的计划已经做好了,明年应该会是收获的一年。

2021冲冲冲,最后送一段鸡汤给大家。

人生是一场马拉松

这一段给大家分享一下我的人生观,希望对诸位有所启发。

种一棵树最好的时间是十年前,其次就是在现在了

这是我的人生格言。

人生是一场马拉松,没有终点,不要在意自己能跑多远。

只要一直种树,一直在路上,就不会太差。

在路上的过程中,也要记得欣赏

生活还是要快乐最重要,我一向不主张给自己上太多的限制。

有什么事情是必须做的? 房子?车子?

佛系一点,享受生活不一定需要这些。

培养好学习习惯,锻炼习惯,自然而然的向前跑就好了。

最后衷心祝愿大家过得开心,自在!❤️

查看原文

赞 6 收藏 0 评论 1

nero 赞了回答 · 2020-12-09

svg 的 path元素有办法调整大小和位置吗?

这个你没有具体的代码可能别人没有办法说。
不过很明确的是path本身仅仅是信息的描述,即其本身就是和位置相关的,所谓的大小是需要改变path来使得绘画的图像大小变化的。

<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" 
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">

<svg width="100%" height="100%" version="1.1"
xmlns="http://www.w3.org/2000/svg">

<path d="M250 150 L150 350 L350 350 Z" />

</svg>

<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" 
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">

<svg width="100%" height="100%" version="1.1"
xmlns="http://www.w3.org/2000/svg">

<path d="M0 0 L1920 920 L350 450 Z" />

</svg>

就是完全的2个不同大小位置的图形啊。

关注 3 回答 2

nero 赞了回答 · 2020-12-09

svg 的 path元素有办法调整大小和位置吗?

如下面是svg绘图的方法,创建一个圆并圆圈中填充文字,通过r来控制圆的大小,其他的也应该有svg开发的一套书写格式吧,如果有需要,可能得了解关于svg更多的东西。
image.png
image.png

关注 3 回答 2

nero 提出了问题 · 2020-12-09

svg 的 path元素有办法调整大小和位置吗?

如题,想要调整svg path元素的大小和位置,但却发现怎么都不起作用

关注 3 回答 2

nero 赞了文章 · 2020-12-07

探索 React 的内在 —— postMessage & Scheduler

postMessage & Scheduler

写在前面

  • 本文包含了一定量的源码讲解,其中笔者写入了一些内容来替代官方注释(就是写了差不多等于没写那种),若读者更青睐于原始的代码,

可移步官方仓库,结合起来阅读。也正因为这个原因,横屏或 PC 的阅读体验也许会更佳(代码可能需要左右滑动)

  • 本文没有显式的涉及 React Fiber Reconciler 和 Algebraic Effects(代数效应)的内容,但其实它们是息息相关的,可以理解为本文的内容就是实现前两者的基石。

有兴趣的读者可移步《Fiber & Algebraic Effects》做一些前置阅读。

开始

在去年 2019 年 9 月 27 日的 release 中,React 在 Scheduler 中开启了新的调度任务方案试验:

  • 旧方案:通过 requestAnimationFrame(以下统称 cAF,相关的 requestIdleCallback 则简称 rIC)使任务调度与帧对齐
  • 新方案:通过高频(短间隔)的调用 postMessage 来调度任务

Emm x1... 突然有了好多问题
那么本文就来探索一下,在这次“小小的” release 中都发生了什么

契机

通过对这次 release 的 commit-message 的查看,我们总结出以下几点:

  1. 由于 rAF 仰仗显示器的刷新频率,因此使用 rAF 需要看 vsync cycle(指硬件设备的频率)的脸色
  2. 那么为了在每帧执行尽可能多的任务,采用了 5ms 间隔的消息事件 来发起调度,也就是 postMessage 的方式
  3. 这个方案的主要风险是:更加频繁的调度任务会加剧主线程与其他浏览器任务的资源争夺
  4. 相较于 rAF 和 setTimeout,浏览器在后台标签下对消息事件进行了什么程度的节流还需要进一步确定,该试验是假设它与定时器有相同的优先级

简单来说,就是放弃了由 rAF 和 rIC 两个 API 构成的帧对齐策略,转而人为的控制调度频率,提升任务处理速度,优化 React 运行时的性能

postMessage


那么,postMessage 又是什么呢?是指 iframe 通信机制中的 postMessage 吗?

不对,也对

Emm x2... 好吧,有点谜语了,那解谜吧

不对

说不对呢,是因为 postMessage 本身是使用的 MessageChannel 这个接口创建的对象发起的

Channel Message API 的 MessageChannel 接口允许我们创建一个新的消息通道,并通过该通道的两个 MessagePort 进行通信

这个通道同样适用于 Web Worker —— 所以,它挺有用的...
我们看看它到底是怎样通信的:

const ch = new MessageChannel()

ch.port1.onmessage = function(msgEvent) {
  console.log('port1 got ' + msgEvent.data)
  ch.port1.postMessage('Ok, r.i.p Floyd')
}

ch.port2.onmessage = function(msgEvent) {
  console.log(msgEvent.data)
}

ch.port2.postMessage('port2!')

// 输出:
// port1 got port2!
// Ok, r.i.p Floyd.

很简单,没什么特别的...
Emm x3...
啊... 平常很少直接用它,它的兼容性怎么样呢?
image.png


唔!尽管是 10,但 IE 竟然也可以全绿!

也对

害,兼容性这么好,其实就是因为现代浏览器中 iframe 与父文档之间的通信,就是使用的这个消息通道,你甚至可以:

// 假设 <iframe id="childFrame" data-original="XXX" />

const ch = new MessageChannel()
const childFrame = document.querySelector('#childFrame')

ch.port2.onmessage = function(msgEvent) {
  console.log(msgEvent.data)
  console.log('There\'s no father exists ever')
}

childFrame.contentWindow.postMessage('Father I can\'t breathe!', '*', [ch.port2])

// 输出:
// Father I can't breathe
// There's no father exists ever

好了,我们已经知道这个 postMessage 是个什么东西了,那接着看看它是怎么运作的吧

做事

在谈到 postMessage 的运作方式之前,先提一下 Scheduler

Scheduler

Scheduler 是 React 团队开发的一个用于事务调度的包,内置于 React 项目中。其团队的愿景是孵化完成后,使这个包独立于 React,成为一个能有更广泛使用的工具
我们接下来要探索的相关内容,都是在这个包的范畴之内

找到 MessageChannel

在 Scheduler 的源码中,通过搜索 postMessage 字眼,我们很容易的就将目光定位到了 SchedulerHostConfig.default.js 文件,我们截取部分内容:

在完整源码中,有一个 if-else 分支来实现了两套不同的 API。对于非 DOM 或是没有 MessageChannel 的 JavaScript 环境(如 JavaScriptCore),以下内容是采用 setTimeout 实现的,有兴趣的同学可以去看一下,相当简单的一段 Hack,本文不作赘述,仅专注于 else 分支下的源码。
以上也是为什么这个文件会叫 xxxConfig 的原因,它确实是带有配置性的逻辑的
const performWorkUntilDeadline = () => {
    if (scheduledHostCallback !== null) {
      const currentTime = getCurrentTime();
      // Yield after `yieldInterval` ms, regardless of where we are in the vsync
      // cycle. This means there's always time remaining at the beginning of
      // the message event.
      deadline = currentTime + yieldInterval;
      const hasTimeRemaining = true;
      try {
        const hasMoreWork = scheduledHostCallback(
          hasTimeRemaining,
          currentTime,
        );
        if (!hasMoreWork) {
          isMessageLoopRunning = false;
          scheduledHostCallback = null;
        } else {
          // If there's more work, schedule the next message event at the end
          // of the preceding one.
          port.postMessage(null);
        }
      } catch (error) {
        // If a scheduler task throws, exit the current browser task so the
        // error can be observed.
        port.postMessage(null);
        throw error;
      }
    } else {
      isMessageLoopRunning = false;
    }
    // Yielding to the browser will give it a chance to paint, so we can
    // reset this.
    needsPaint = false;
  };

  const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;

  requestHostCallback = function(callback) {
    scheduledHostCallback = callback;
    if (!isMessageLoopRunning) {
      isMessageLoopRunning = true;
      port.postMessage(null);
    }
  };

这行代码的逻辑其实很简单:

  1. 定义一个名为 channel 的 MessageChannel,并定义一个 port 变量指向其 port2 端口
  2. 将预先定义好的 performWorkUntilDeadline 方法作为 channel 的 port1 端口的消息事件处理函数
  3. 在 requestHostCallback 中调用前面定义的 port 变量 —— 也就是 channel 的 port2 端口 —— 上的 postMessage 方法发送消息
  4. performWorkUntilDeadline 方法开始运作

好了,我们现在就来剖析一下这一小段代码中的各个元素

requestHostCallback(以下简称 rHC)

还记得 rAF 和 rIC 吗?他们前任调度机制的核心 API,那么既然 rHC 和他们长这么像,一定就是现在值班那位咯
确实,我们直接进入代码身体内部尝尝:

requestHostCallback = function(callback) {
    // 将传入的 callback 赋值给 scheduledHostCallback
    // 类比 `requestAnimationFrame(() => { /* doSomething */ })` 这样的使用方法,
    // 我们可以推断 scheduledHostCallback 就是当前要执行的任务(scheduled嘛)
    scheduledHostCallback = callback;
  
      // isMessageLoopRunning 标志当前消息循环是否开启
    // 消息循环干嘛用的呢?就是不断的检查有没有新的消息——即新的任务——嘛
    if (!isMessageLoopRunning) {
      // 如果当前消息循环是关闭的,则 rHC 有权力打开它
      isMessageLoopRunning = true;
      // 打开以后,channel 的 port2 端口将受到消息,也就是开始 performWorkUntilDeadline 了
      port.postMessage(null);
    } // else 会发生什么?
  };

好了,我们现在知道,rHC 的作用就是:

  • 准备好当前要执行的任务(scheduledHostCallback)
  • 开启消息循环调度
  • 调用 performWorkUntilDeadline

performWorkUntilDeadline

现在看来,rHC 是搞事的,performWorkUntilDealine 就是做事的咯
确实,我们又直接进入代码身体内部尝尝:

const performWorkUntilDeadline = () => {
      // [A]:先检查当前的 scheduledHostCallback 是否存在
    // 换句话说就是当前有没有事需要做
    if (scheduledHostCallback !== null) {
      const currentTime = getCurrentTime();
      // 啊,截止时间!
      // 看来就是截止到 yieldInterval 之后,是多少呢?
      // 按前文的内容,应该是 5ms 吧,我们之后再验证
      deadline = currentTime + yieldInterval;
      // 唔,新鲜的截止时间,换句话说就是还有多少时间呗
      // 有了显示的剩余时间定义,无论我们处于 vsync cycle 的什么节点,在收到消息(任务)的时候都有时间了
      const hasTimeRemaining = true; // timeRemaining 这个字眼让人想起了 rIC
      try {
        // 嗯,看来这个 scheduledHostCallback 中不简单,稍后研究它
        const hasMoreWork = scheduledHostCallback(
          hasTimeRemaining,
          currentTime,
        );
        if (!hasMoreWork) {
            // 如果完成了最后一个任务,就关闭消息循环,并清洗掉 scheduledHostCallback 的引用
          isMessageLoopRunning = false;
          scheduledHostCallback = null;
        } else {
          // [C]:如果还有任务要做,就用 port 继续向 channel 的 port2 端口发消息
          // 显然,这是一个类似于递归的操作
          // 那么,如果没有任务了,显然不会走到这儿,为什么还要判断 scheduledHostCallback 呢?往后看
          port.postMessage(null);
        }
      } catch (error) {
        // 如果当前的任务执行除了故障,则进入下一个任务,并抛出错误
        port.postMessage(null);
        throw error;
      }
    } else {
      // [B]:没事儿做了,那么就不用循环的检查消息了呗
      isMessageLoopRunning = false;
    }
    // Yielding to the browser will give it a chance to paint, so we can
    // reset this.
    needsPaint = false;
  };

现在就明朗许多了,我们用一个示意图进行表示:
how_postMessage_work.png
两个虚线箭头表示引用关系,那么根据代码中的分析现在可以知道,所有的任务调度,都是由 port —— 也就是 channel 的 port2 端口 —— 通过调用 postMessage 方法发起的,而这个任务是否要被执行,似乎与 yieldInterval 和 hasTimeRemaning 有关,来看看它们:

  • yieldInterval: 在完整源码中,有这两么两处:
// 直接定义为 5ms,根本没商量的
const yieldInterval = 5

// 但是
// 这个方法其实是 Scheduler 包提供给开发者的公共 API,
// 允许开发者根据不同的设备刷新率设置调度间隔
// 其实就是因地制宜的考虑

forceFrameRate = function(fps) {
      // 最高到 125 fps
    // 我的(假装有)144hz 电竞屏有被冒犯到
    if (fps < 0 || fps > 125) {
      // Using console['error'] to evade Babel and ESLint
      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 {
      // 显然,如果没传或者传了个负的,就重置为 5ms,提升了一些鲁棒性
      // reset the framerate
      yieldInterval = 5;
    }
  };
  • hasTimeRemaning:参考 rIC 通常的使用方式:
function doWorks() {
  // todo
}

function doMoreWorks() {
     // todo more 
}

function todo() {
      requestIdleCallback(() => {
      // 做事嘛,最重要的就是还有没有时间
           if (e.timeRemaining()) {
        doMoreWorks()
      }
   })
   doWorks()
}

Emm x4... 上图中还有两处标红的疑问:

  • what happened?: 其实这个地方呢,就是为 performWorkUntilDeadline 提供新的 scheduledHostCallback。这样一来,performWorkUntilDeadline 就“一直有事做”,直到不再有任务通过 rHC 注册进来
  • But How?: 接下来,我们就来解答这个问题的答案,一切都要从 Scheduler 说起

Scheduler

啊哈,这次我们给 Scheduler 了一个更大的标题来表明它的主角身份 🐶...
我们这次直接从入口开始,一步一步地回归到 But How? 这个问题上去

又写在前面

  • 根据 Scheduler 的 README 文件可知,其当前的 API 尚非最终方案,因此其入口文件 Scheduler.js 所暴露出来的接口都带上了 unstable_ 前缀,为使篇幅简单,以下对接口名称的描述都省去该前缀
  • 源码中还包含了一些 profiling 相关的逻辑,它们主要是用于辅助调试和审计,与运作方式没有太大的关系,因此下文会忽略这些内容,专注于核心逻辑的阐释

scheduleCallback —— 把任务交给 Scheduler

我们旅程的起点就从这个接口开始,它是开启 Scheduler 魔法的钥匙🔑~
该接口用于将一个回调函数——也就是我们要执行的任务——按给定的优先级额外设置注册进 Scheduler 的任务队列中,并启动任务调度:

function unstable_scheduleCallback(priorityLevel, callback, options) {
  var currentTime = getCurrentTime(); // [A]:getCurrentTime 是怎样获取当前时间的?

  var startTime; // 给定回调函数一个开始时间,并根据 options 中定义的 delay 来延迟
  // 给定回调函数一个定时器,并根据 options 中的 timeout 定义来确定是直接使用自定义的还是用 timeoutForPriorityLevel 方法来产出定时时间
  // [B]:那么 timeoutForPriorityLevel 是怎么做的呢?
  var timeout;
  if (typeof options === 'object' && options !== null) {
    var delay = options.delay;
    if (typeof delay === 'number' && delay > 0) {
      startTime = currentTime + delay;
    } else {
      startTime = currentTime;
    }
    timeout =
      typeof options.timeout === 'number'
        ? options.timeout
        : timeoutForPriorityLevel(priorityLevel); // [C] 这个 priorityLevel 哪来的?
  } else {
    timeout = timeoutForPriorityLevel(priorityLevel);
    startTime = currentTime;
  }
  
  // 定义一个过期时间,之后还会遇到它
  var expirationTime = startTime + timeout;

  // 啊,从这里我们可以看到,在 Scheduler 中一个 task 到底长什么样了
  var newTask = {
    id: taskIdCounter++, // Scheduler.js 中全局定义了一个 taskIdCounter 作为 taskId 的生产器
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1,  // [D]:前面的都见过了,这个 sortIndex 是排序用的吗?
  };
  if (enableProfiling) {
    newTask.isQueued = false;
  }

  if (startTime > currentTime) {
    // 还记得 options 中的 delay 属性吗,这就给予了该任务开始时间大于当前时间的可能
    // 唔,前面定义 sortIndex 又出现了,在这种情况下被赋值为了 startTime,
    newTask.sortIndex = startTime;
    // [E]:这里出现了一个定时器队列(timerQueue)
    // 如果开始时间大于当前时间,就将它 push 进这个定时器队列
    // 显然,对于要将来执行的任务,势必得将它放在一个“待激活”的队列中
    push(timerQueue, newTask);
    // 这里的逻辑稍后讨论,先进入 else 分支
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // All tasks are delayed, and this is the task with the earliest delay.
      if (isHostTimeoutScheduled) {
        // Cancel an existing timeout.
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // Schedule a timeout.
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    // expirationTime 作为了 sortIndex 的值,从逻辑上基本可以确认 sortIndex 就是用于排序了
    newTask.sortIndex = expirationTime;
    // [F]: 这里又出现了 push 方法,这次是将任务 push 进任务队列(taskQueue),看来定时器队列和任务队列是同构的咯?
    push(taskQueue, newTask);
    if (enableProfiling) {
      markTaskStart(newTask, currentTime);
      newTask.isQueued = true;
    }
    // 从逻辑上看,这里就是判断当前是否正处于流程,即 performWorkUntilDeadline 是否正处于一个递归的执行状态中中,如果不在的话,就开启这个调度
    // [G]:Emm x5... 那这个 flushWork 是干什么的呢?
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    }
  }

  return newTask;
}

ok,我们现在来分解一下上述注释中标记了 [X] 的几个问题,使函数作用更加立体一点:

  • A: getCurrentTime 是如何获取当前时间的呢?

    • 解:在之前提到的 schedulerHostConfig.default.js 文件中,根据 performance 对象及 performance.now 方法是否存在,区分了是用 Date.now 还是用 performance.now 来获取当前时间,原因是后者比前者更加精确切绝对,详情可参考这里
  • B C: 我们直接来看看 Scheduler.js 中 timeoutForPriorityLevel 方法的相关内容便知:
// ...other code
var maxSigned31BitInt = 1073741823;

/**
 * 以下几个变量是全局定义的,相当于系统常量(环境变量)
 */
// 立即执行
// 显然,如果不定义 deley,根据 [B] 注释处紧接的逻辑,expirationTime 就等于 currentTime - 1 了
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// 再往后就一定会进入 else 分支,并 push 到任务队列立即进入 performWorkUntilDealine
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// 最低的优先级看起来是永远不会被 timeout 到的,稍后看看它会在什么时候执行
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;

// ...other code

// 可以看到,priorityLevel 显然也是被系统常量化了的
function timeoutForPriorityLevel(priorityLevel) {
  switch (priorityLevel) {
    case ImmediatePriority:
      return IMMEDIATE_PRIORITY_TIMEOUT;
    case UserBlockingPriority:
      return USER_BLOCKING_PRIORITY_TIMEOUT;
    case IdlePriority:
      return IDLE_PRIORITY_TIMEOUT;
    case LowPriority:
      return LOW_PRIORITY_TIMEOUT;
    case NormalPriority:
    default:
      return NORMAL_PRIORITY_TIMEOUT;
  }
}

// ...other code

其中 priorityLevel 定义在 schedulerPriorities.js 中,非常直观:

export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5;

// 啊哈,将来可能用 symbols 来实现,
// 那样的话,大小的对比是不是又得抽象一个规则出来呢?
// TODO: Use symbols?
export const NoPriority = 0;
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;

看来,任务执行的时机就是由 当前时间(currentTime)+延时(delay)+优先级定时(XXX_PRIORITY_TIMEOUT) 来决定,而定时时长的增量则由 shedulerPriorities.js 中的各个值来决定

  • C D E: 这三个点是非常相关的,因此直接放在一起

    • sortIndex: 即排序索引,根据前面的内容和 [B] 的阐释,我们可以知道,该属性的值要么是 startTime,要么是 expirationTime,显然都是越小越早嘛——因此,用这个值来排序,势必也就将任务的优先级排出来了
    • timerQueue 和 taskQueue:害,sortIndex 肯定是用于在这两个同构队列中排序了嘛。_看到这里,熟悉数据结构的同学应该已经猜到,这两个队列的数据结构可能就是处理优先级事务的标准方案——最小优先队列。_

果然,我们溯源到 push 方法是在一个叫 schedulerMinHeap.js 的文件中,而最小优先队列就是基于最小堆(min-heap)来实现的。我们待会儿看看 push 到底对这个队列做了什么。

  • F: flushWork!听这个名字就很通畅对不对这个名字已经很好的告诉了我们,它就是要将当前所有的任务一一处理掉!它是怎么做的呢?留个悬念,先跳出 scheduleCallback

最小堆

最小堆本质上是一棵完全二叉树,经排序后,其所有非终端节点的元素值都不大于其左节点和右节点,即如下:
min-heap.png

原理

Sheduler 采用了数组对这个最小堆进行实现,现在我们简单的来解析一下它的工作原理

PUSH

我们向上面这个最小堆中 push 进一个值为 5 的元素,其工作流程如下所示:
min-heap-push.png
可以看到,在 push 的过程中,调用 siftUp 方法将值为 5 的元素排到了我们想要的位置,成了右边这棵树。相关代码如下:

type Heap = Array<Node>;
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);
}

function siftUp(heap, node, i) {
  let index = i;
  while (true) {
    const parentIndex = (index - 1) >>> 1;
    const parent = heap[parentIndex];
    if (parent !== undefined && compare(parent, node) > 0) {
      // The parent is larger. Swap positions.
      heap[parentIndex] = node;
      heap[index] = parent;
      index = parentIndex;
    } else {
      // The parent is smaller. Exit.
      return;
    }
  }
}

function compare(a, b) {
  // Compare sort index first, then task id.
  const diff = a.sortIndex - b.sortIndex;
  return diff !== 0 ? diff : a.id - b.id;
}

可以看到,siftUp 中对于父节点位置的计算还使用了移位操作符>>>1 等价于除以 2 再去尾)进行优化,以提升计算效率

POP

那么,我们要从其中取出一个元素来用(在 Scheduler 中即调度一个任务出来执行),工作流程如下所示:
min-heap-pop.png
当我们取出第一个元素——即值最小,优先级最高——后,树失去了顶端,势必需要重新组织其枝叶结构,而 siftDown 方法就是用于重新梳理剩余的元素,使其仍然保持为一个最小堆,相关代码如下:

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 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 the left or right node is smaller, swap with the smaller of those.
    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 {
      // Neither child is smaller. Exit.
      return;
    }
  }
}

Emm x5... 和 PUSH 部分的代码合并一下,就是一个最小堆的标准实现了
剩下地,SchedulerMinHeap.js 源码中还提供了一个 peek(看一下) 方法,用于查看顶端元素:

export function peek(heap: Heap): Node | null {
  const first = heap[0];
  return first === undefined ? null : first;
}

其作用显然就是取第一个元素出来 peek peek 咯~ 我们马上就会遇到它

flushWork

现在,我们来看看 Scheduler 是如何将任务都 flush 掉的:

function flushWork(hasTimeRemaining, initialTime) {
  if (enableProfiling) {
    markSchedulerUnsuspended(initialTime);
  }

  // [A]:为什么要重置这些状态呢?
  isHostCallbackScheduled = false;
  if (isHostTimeoutScheduled) {
    // We scheduled a timeout but it's no longer needed. Cancel it.
    isHostTimeoutScheduled = false;
    cancelHostTimeout();
  }

  // [B]:从逻辑上看,在任务本身没有抛出错误的情况下,flushWork 就是返回 workLoop 的结果,那么 workLoop 做了些什么呢?
  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 {
      // 特意留下了这条官方注释,它告诉我们在生产环境下,flushWork 不会去 catch workLoop 中抛出的错误的,
           // 因为在开发模式下或调试过程中,这种错误一般会造成白页并给予开发者一个提示,显然这个功能不能影响到用户
      // No catch in prod codepath.
      return workLoop(hasTimeRemaining, initialTime);
    }
  } finally {
    // 如果任务执行出错,则终结当前的调度工作
    currentTask = null;
    currentPriorityLevel = previousPriorityLevel;
    isPerformingWork = false;
    if (enableProfiling) {
      const currentTime = getCurrentTime();
      markSchedulerSuspended(currentTime);
    }
  }
}

现在来分析一下这段代码中的 ABC~

  • A: 为什么要重置这些状态呢?

由于 rHC 并不一定立即执行传入的回调函数,所以 isHostCallbackScheduled 状态可能会维持一段时间;等到 flushWork 开始处理任务时,则需要释放该状态以支持其他的任务被 schedule 进来;isHostTimeoutScheduled 也是同样的道理,关于这是个什么 timeout,我们很快就会遇到

  • B: workLoop,Emm x6... 快要到这段旅程的终点了。就像连载小说的填坑一样,这个方法将会解答很多问题

workLoop

顾名思义,该方法一定会包含一个用于处理任务的循环,那么这个循环里都发生了什么呢?

function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  // [A]:这个方法是干嘛的?
  advanceTimers(currentTime);
  // 将任务队列最顶端的任务 peek 一下
  currentTask = peek(taskQueue);
  // 只要 currentTask 存在,这个 loop 就会继续下去
  while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      // dealine 到了,但是当前任务尚未过期,因此让它在下次调度周期内再执行
      // [B]:shouldYieldToHost 是怎么做判断的呢?
      break;
    }
    const callback = currentTask.callback;
    if (callback !== null) {
      // callback 不为 null,则说明当前任务是可用的
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      // 判断当前任务是否过期
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      markTaskRun(currentTask, currentTime);
      // [C]:continuationCallback?这是什么意思?让任务继续执行?
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      if (typeof continuationCallback === 'function') {
          // 看来,如果 continuationCallback 成立,则用它来取代当前的 callback
        currentTask.callback = continuationCallback;
        markTaskYield(currentTask, currentTime);
      } else {
        if (enableProfiling) {
          markTaskCompleted(currentTask, currentTime);
          currentTask.isQueued = false;
        }
        // 如果 continuationCallback 不成立,就会 pop 掉当前任务,
        // 逻辑上则应该是判定当前任务已经完成
        // Emm x7... 那么 schedule 进来的任务,实际上应该是要遵循这个规则的
        // [D]:我们待会儿再强调一下这个问题
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }
      // advanceTimers 又来了...
      advanceTimers(currentTime);
    } else {
      // 如果当前的任务已经不可用,则将它 pop 掉
      pop(taskQueue);
    }
    // 再次从 taskQueue 中 peek 一个任务出来
    // 注意,如果前面的 continuationCallback 成立,taskQueue 则不会发生 pop 行为,
    // 因此 peek 出的任务依然是当前的任务,只是 callback 已经是 continuationCallback 了
    currentTask = peek(taskQueue);
  }
  // Bingo!这不就是检查还有没有更多的任务吗?
  // 终于回归到 performWorkUntilDealine 中的 hasMoreWork 逻辑上了!
  if (currentTask !== null) {
    return true;
  } else {
    // [E]:诶,这儿好像不太单纯,干了点儿啥呢?
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

我们终于解答了前面的 But How 问题
现在,我们解析一下上述代码中的 ABC,看看这个循环是怎么运作起来的

  • A:上述代码两次出现了 advanceTimers,它究竟是用来干嘛的呢?上代码一看便知:
function advanceTimers(currentTime) {
  // 其实下面的官方注解已经很明确了,就是把 timerQueue 中排队的任务根据需要转移到 taskQueue 中去
  // Check for tasks that are no longer delayed and add them to the queue.
  let timer = peek(timerQueue);
  while (timer !== null) {
    if (timer.callback === null) {
      // Timer was cancelled.
      pop(timerQueue);
    } else if (timer.startTime <= currentTime) {
      // Timer fired. Transfer to the task queue.
      pop(timerQueue);
      timer.sortIndex = timer.expirationTime;
      push(taskQueue, timer);
      if (enableProfiling) {
        markTaskStart(timer, currentTime);
        timer.isQueued = true;
      }
    } else {
      // Remaining timers are pending.
      return;
    }
    timer = peek(timerQueue);
  }
}

其实这段代码相当的简单,就是根据 startTimecurrentTime 来判断某个 timer 是否到了该执行的时间,然后将它转移到 taskQueue 中,大致可以总结为以下示意:
advanceTimers.png
因此,workLoop 中第一次调用它的作用就是将当前需要执行的任务重新梳理一下;
那么第二次调用则是由于 while 语句中的任务执行完后,已经消耗掉一定时间,再次进入 while 的时候当然也需要重新梳理 taskQueue 了

  • B:shouldYieldToHosthasTimeRemaning 一起判定了是否还有时间来执行任务,如果没有的话,break 出 while 循环,由此 保持了一个以 5ms 为周期的循环调度 ——啊,又解决一个疑问;其中 shouldYieldToHost 的源码有点儿料的,可以看看:
if (
    enableIsInputPending &&
    navigator !== undefined &&
    navigator.scheduling !== undefined &&
    navigator.scheduling.isInputPending !== undefined
  ) {
    const scheduling = navigator.scheduling;
    shouldYieldToHost = function() {
      const currentTime = getCurrentTime();
      if (currentTime >= deadline) {
        // There's no time left. We may want to yield control of the main
        // thread, so the browser can perform high priority tasks. The main ones
        // are painting and user input. If there's a pending paint or a pending
        // input, then we should yield. But if there's neither, then we can
        // yield less often while remaining responsive. We'll eventually yield
        // regardless, since there could be a pending paint that wasn't
        // accompanied by a call to `requestPaint`, or other main thread tasks
        // like network events.
        // 译:没空了。我们可能需要将主线程的控制权暂时交出去,因此浏览器能够执行高优先级的任务。
        // 所谓的高优先级的任务主要是”绘制“及”用户输入”。如果当前有执行中的绘制或者输入,那么
        // 我们就应该让出资源来让它们优先的执行;如果没有,我们则可以让出更少的资源来保持响应。
        // 但是,毕竟存在非 `requestPaint` 发起的绘制状态更新,及其他的主线程任务——如网络请求等事件,
        // 我们最终也会在某个临界点一定地让出资源来
        if (needsPaint || scheduling.isInputPending()) {
          // There is either a pending paint or a pending input.
          return true;
        }
        // There's no pending input. Only yield if we've reached the max
        // yield interval.
        return currentTime >= maxYieldInterval;
      } else {
        // There's still time left in the frame.
        return false;
      }
    };

    requestPaint = function() {
      needsPaint = true;
    };
  } else {
    // `isInputPending` is not available. Since we have no way of knowing if
    // there's pending input, always yield at the end of the frame.
    shouldYieldToHost = function() {
      return getCurrentTime() >= deadline;
    };

    // Since we yield every frame regardless, `requestPaint` has no effect.
    requestPaint = function() {};
  }

可以看到,对于支持 navigator.scheduling 属性的环境,React 有更进一步的考虑,也就是 浏览器绘制 和 用户输入 要优先进行,这其实就是 React 设计理念中的 Scheduling 部分所阐释的内涵
当然了,由于这个属性并非普遍支持,因此也 else 分支里的定义则是单纯的判断是否超过了 deadline
考虑到 API 的健壮性,requestPaint 也根据情况有了不同的定义

  • C: 我们仔细看看 continuationCallback 的赋值—— continuationCallback = callback(didUserCallbackTimeout) ,它将任务是否已经过期的状态传给了任务本身,如果该任务支持根据过期状态有不同的行为——例如在过期状态下,将当前的执行结果缓存起来,等到下次调度未过期的时候再复用缓存的结果继续执行后面的逻辑,那么则返回新的处理方式并赋值到 continuationCallback 上。这就是 React 中的 Fiber Reconciler 实现联系最紧密的地方了;而 callback 本身若并没有对过期状态进行处理,则返回的东西从逻辑上来讲,需要控制为非函数类型的值,也就是使得 typeof continuationCallback === 'function' 判断为假。也正因为 callback 不一定会对过期状态有特别待遇,所以它的执行时间可能会大大超出预料,就更需要在之后再执行一次 advanceTimers 了。
  • D: 前面说到了,我们传入的 callback 一定要遵循与 continuationCallback 相关逻辑一致的规则。由于 Scheduler 现在尚未正式的独立于 React 做推广,所以也没有相关文档来显式的做讲解,因此我们在直接使用 Scheduler 的时候一定要注意这点
  • E: 其实这里就是将 timer 中剩下的任务再进行一次梳理,我们看看 requestHostTimeouthandleTimeout 都做了什么就知道了:

现在,看 requestHostTimeout 个名字就知道他一定来自于 SchedulerHostConfig.default.js 这个文件🙂:

// 很简单,就是在下一轮浏览器 eventloop 的定时器阶段执行回调,如果传入了具体时间则另说  
requestHostTimeout = function(callback, ms) {
    taskTimeoutID = setTimeout(() => {
      callback(getCurrentTime());
    }, ms);
  };

// 相关的 cancel 方法则是直接 clear 掉定时器并重置 taskTimoutID
cancelHostTimeout = function() {
  clearTimeout(taskTimeoutID);
  taskTimeoutID = -1;
};

再看 handleTimeout,它的定义就在 Scheduler.js 中:

function handleTimeout(currentTime) {
  isHostTimeoutScheduled = false;
  // 这里再次重新梳理了 task
  advanceTimers(currentTime);

  // 如果这时候 isHostCallbackScheduled 再次被设为 true
  // 说明有新的任务注册了进来
  // 从逻辑上来看,这些任务将再次被滞后
  if (!isHostCallbackScheduled) {
    // flush 新进入 taskQueue 的任务
    if (peek(taskQueue) !== null) {
      // 如果本方法中的 advanceTimer 有对 taskQueue push 进任务
      // 则直接开始 flush 它们
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    } else {
      // 如果 taskQueue 仍然为空,就开始递归的调用该方法
      // 直到清理掉 timerQueue 中所有的任务
      // (我想,对于交互频繁的应用,这个递归应该不太会有停止的机会)
      const firstTimer = peek(timerQueue);
      if (firstTimer !== null) {
        // startTime - currentTime,不就是 XXX_PRIORITY_TIMEOUT 的值嘛!
        requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
      }
    }
  }
}

可以概括为是 workLoop 的善后工作...
现在,我们可以总结出一个大致的 workLoop 示意图了:
workLoop.png
Emm x7... 拉得挺长,其实也没多少内容
至此,Scheduler 的核心运作方式就剖开了
而源码中还有一些其他的方法,有些是用于 cancel 掉当前的调度循环(即递归过程),有些是提供给开发者使用的工具接口,有兴趣的同学可以戳这里进行进一步地了解

总结

由于贴入了大量的源码,因此本文篇幅也比较长,但其实总得来说就是解释了两个问题

postMessage 如何运作?

主要就是通过 performWorkUntilDeadline 这个方法来实现一个递归的消息 发送-接收-处理 流程,来实现任务的处理

任务如何被处理?

一切都围绕着两个最小优先队列进行:

  • taskQueue
  • timerQueue

任务被按照一定的优先级规则进行预设,而这些预设的主要目的就是确认执行时机(timeoutForPriorityLevel)。
没当开始处理一系列任务的时候(flushWork),会产生一个 while 循环(workLoop)来不断地对队列中的内容进行处理,这期间还会逐步的将被递延任务从 timerQueue 中梳理(advanceTimers)到 taskQueue 中,使得任务能按预设的优先级有序的执行。甚至,对于更高阶的任务回调实现,还可以将任务“分段进行”(continuationCallback)。
而穿插在这整个过程中的一个原则是所有的任务都尽量不占用与用户感知最密切的浏览器任务(needsPainiting & isInputPending),当然,这一点能做得多极致也与浏览器的实现(navigator.scheduling)有关

总览

现在,我们将前面的示意图都整合起来,并加上两个队列的示意,可以得到一张大大的运作原理总览:
scheduler.png
啊,真的很大... 其实主要是空白多...
总的来说,相比旧的实现(rIC 和 rAF),postMessage 的方式更加独立,对设备本身的运作流程有了更少的依赖,这不仅提升了任务处理的效率,也减少了因不可控因素导致应用出错的风险,是相当不错的尝试。尽管它没有显式地对各个 React 应用产生影响,甚至也无须开发者对它有深刻的理解,但也许我们知道了它的运作原理,也就增添了代码优化及排错查误的思路。
然而,前面也提到了,这个实现的一些东西目前也正处于试验阶段,因此我们如果要直接使用 Scheduler 来实现一些东西,也是需要慎重考虑的。
Emm x8... 是不是可以用它来做一些弹幕应用的渲染管理呢——毕竟飞机礼物的通知比纯文字的吹水优先级要高吧,贵的礼物要比……哎,有点讨打了,拜托忘记破折号后的内容。
有兴趣的同学可以实践一下,也是帮助 Scheduler 的试验了~


最后,如果有什么本文理解有误的地方,还望指出🙏

查看原文

赞 20 收藏 3 评论 2

nero 赞了文章 · 2020-12-07

【Copy攻城狮日志】聊聊JavaScript heap out of memory

JavaScript heap out of memory
↑开局一张图,故事全靠编↑


从一次宕机说起

这是一个很狗血的故事,故事的开头是一个项目,这个项目十分草率,草率到什么程度?没有设计稿,没有文档,需求全靠口口相传,当然最草率的是交给了我,我简单列了下需求:

  • 官网的形式,主要介绍公司某些业务
  • 要能发文章

尽管很简单的需求,对于水得一匹的我来说,简直是“难于上青天”,三大件(html,css,javascript)我样样精通个P,网站部署我也只略知一二,代码编水平更是不学无术。作为Copy工程师,遇到需求我便开始了copy之路,先github溜达了一圈,找了几个满足需求的项目,最终对比了一下,选择了一个名叫iBlog2的项目--基于 Node.js 的个人开源博客系统。您没看错,就是一个博客系统!这跟官网有个毛关系?这个宕机又有个毛关系?我想说的是,经过copy然后小改之后,iBlog2摇身一变就成了能发布文章的官网项目,就是这么简单粗暴,就是这么不学无术(温馨提示:少壮不努力,老大偷代码)。

iBlog2.png

这个3年之前的项目,在现在看来的确是有些陈旧,但作者@eshengsky依旧坚持不懈的在更新维护,而对于我而言,只是为了完成能发文章的官网,所以只关注文章是如何发布和储存的,恰恰是因为我关注的面窄,忽略了部署和部署之后可能会遇到的各种问题,比如window下pm2可能出现问题、比如这次的JavaScript heap out of memory。当然并不是人家开源项目有问题,而是实际部署的时候压根没按照作者的文档来,如果按照文档,我应该用pm2部署,或者启用redis,或者使用Noginx,或者使用本机的MongoDB服务,然而,这一切,我只是在我们那个服务器新开了个端口,然后直接npm run dev就开始跑在线上了,所以呢,这么“锈”的操作,不宕机才是天理难容,印象中JavaScript heap out of memory遇到两次了,才两三个月啊!

检索JavaScript heap out of memory

通常遇到问题,我首选的解决流程是打开Chrome--输入关键词--搜索--浏览--copy--尝试,好像从来没有去思考过产生问题的根源,甚至都没有去记录这个问题以及解决的方案,导致再遇到同样的坑,又掉进去了,然后又是一通检索尝试等操作,这也是我从业这么多年来,一直没养成的习惯,也是这么多年一直没成长的某一个小的原因,“少抱怨,多思考,未来会更美好”,而我一直以反面教材在诠释这个金句。

JavaScript heap out of memory.png

通常来说,只要您的关键词够准确,您就能通过google搜索找到尽可能满意的解决方案,如果连关键词都没把握好,我想就算请教的大牛,也不一定能有效的回答,当然思否Stack Overflow都可能有填您那个坑的“铁楸”,还有一个阵地就是github

clipboard.png

通常来说,程序报错一般都有详细的报错说明,比如哪一行、出了什么错、出错明细等,就比如文章开头的那张报错图,我找到了其他用户遇到的一模一样的问题:

    <--- Last few GCs --->
    
    [8138:0x102801600]   145460 ms: Mark-sweep 1265.6 (1301.6) -> 1265.6 (1308.6) MB, 289.8 / 0.0 ms  allocation failure GC in old space requested
    [8138:0x102801600]   145740 ms: Mark-sweep 1265.6 (1308.6) -> 1265.6 (1277.6) MB, 280.6 / 0.0 ms  last resort gc 
    [8138:0x102801600]   146035 ms: Mark-sweep 1265.6 (1277.6) -> 1265.6 (1277.6) MB, 295.0 / 0.0 ms  last resort gc 
    
    
    <--- JS stacktrace --->
    
    ==== JS stack trace =========================================
    
    Security context: 0x39c891dc0d31 <JS Object>
        1: DoJoin(aka DoJoin) [native array.js:~97] [pc=0x5d1facabad4](this=0x39c891d04311 <undefined>,q=0x5a024bf3be1 <JS Array[2241635]>,r=2241635,F=0x39c891d043b1 <true>,B=0x39c891ddafe9 <String[1]\: \n>,A=0x39c891d04421 <false>)
        2: Join(aka Join) [native array.js:~122] [pc=0x5d1fb5cde96](this=0x39c891d04311 <undefined>,q=0x5a024bf3be1 <JS Array[2241635]>,r=2241635,B=0x39c891ddafe9 <String[1...
    
    FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
     1: node::Abort() [/Users/erossignon/.nvm/versions/node/v7.2.0/bin/node]
     2: node::FatalException(v8::Isolate*, v8::Local<v8::Value>, v8::Local<v8::Message>) [/Users/erossignon/.nvm/versions/node/v7.2.0/bin/node]
     3: v8::internal::V8::FatalProcessOutOfMemory(char const*, bool) [/Users/erossignon/.nvm/versions/node/v7.2.0/bin/node]
     4: v8::internal::Factory::NewRawTwoByteString(int, v8::internal::PretenureFlag) [/Users/erossignon/.nvm/versions/node/v7.2.0/bin/node]
     5: v8::internal::Runtime_StringBuilderJoin(int, v8::internal::Object**, v8::internal::Isolate*) [/Users/erossignon/.nvm/versions/node/v7.2.0/bin/node]
     6: 0x5d1faa063a7
    Abort trap: 6

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory这个是报错的关键词,通常也是我们检索的关键词,至于为什么会导致这个错误,报错信息就显示JavaScript堆内存不足,信息中也显示了最近几次GC的详情,GC(Garbage collection
)是垃圾回收机制,具体可以阅读一下JavaScript 内存泄漏教程。经过初步了解,就是我们的应用内容泄露的,通常治标不治本的解决方案就是加大Node.js运行时内存中保留的“未使用”空间量:

node --max-old-space-size=4096 yourFile.js

JavaScript heap out of memory的原因及解决方案

Node运行时V8内存的限制

Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.,一般情况下,Node在运行时只能使用到系统的一部分内存,64位系统下约为1.4GB,32位系统下约为0.7GB【有待考证,出处@JerryC】。当GC时,如果老生区大小超过设定的值时,就会报错。
一般解决方案:
在启动node程序的时候,可以传递两个参数来调整内存限制的大小,解除默认的限制

node --max-nex-space-size=1024 app.js // 单位为KB
node --max-old-space-size=2000 app.js // 单位为MB

实践中的解决可能会有以下操作:

代码问题

除了环境问题,最关键的问题就是代码本身存在问题,毕竟上面的方法治标不治本,要根治这个毛病,可能需要审视代码,先监测到内存泄漏的原因,把这部分代码找出优化。一般是无限制增长的数组、无限制设置属性和值、大循环等【出处:@林小新】。这部分由于Copy攻城狮并为深入,可以参考如何定位Node.js 的内存泄漏node内存泄漏以及定位

查看原文

赞 8 收藏 2 评论 3