react源码学习(一)render过程

render过程

我们想了解react的工作机制,我们直接去看源码很难去弄懂每一步到底是做什么的,在复杂的函数调用中我们很容易让自己迷失,所以我决定跟随一些常用方法来分析工作机制,第一篇就是ReactDOM.render这个入口方法,在讲解中我会直接忽略dev和调试的代码,因为这与工作机制无关。
先来看下主要的流程

render流程.png

先来看一下入口代码

/**
 * 渲染dom的入口方法
 * @param {*} element
 * @param {*} container
 * @param {*} callback
 */
export function render(
  element: React$Element<any>,
  container: DOMContainer,
  callback: ?Function,
) {
  invariant(
    isValidContainer(container),
    'Target container is not a DOM element.',
  );
  return legacyRenderSubtreeIntoContainer(
    null,
    element,
    container,
    false,
    callback,
  );
}

/**
 * render方法真正调用的主方法
 * 主要步骤有初次渲染,创建fiberroot对象->将更新
 */
function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>,
  children: ReactNodeList,
  container: DOMContainer,
  forceHydrate: boolean,
  callback: ?Function,
) {
  let root: RootType = (container._reactRootContainer: any);
  let fiberRoot;
  // 首次渲染时不存在这个元素,初次渲染进入这个逻辑
  if (!root) {
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate,
    );
    fiberRoot = root._internalRoot;
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    // 初次渲染不需要批处理要立即同步更新
    unbatchedUpdates(() => {
      updateContainer(children, fiberRoot, parentComponent, callback);
    });
  } else {
    fiberRoot = root._internalRoot;
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    // 不是首次渲染,比如之后调用setState更新都会将更新加入队列,等待事务调度更新
    updateContainer(children, fiberRoot, parentComponent, callback);
  }
  return getPublicRootInstance(fiberRoot);
}

这里我们实际调用的是legacyRenderSubtreeIntoContainer,将我们传入的组件也就是element挂载到传入的dom元素上。
首先我们会获取dom上的一个root元素,如果没有证明我们是初次渲染,如果不是调用更新的方法。
官网上有一段例子,我觉得能很好理解这个过程当我们第一次执行tick会走初次渲染的逻辑,后边的我们会走更新的逻辑,这也是为什么我们不用setState也能达到更新页面的效果

function tick() {
  const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>
  );
  // 重复调用
  ReactDOM.render(element, document.getElementById('root'));
}
setInterval(tick, 1000);

在react中我们将这个root叫fiberRoot元素,这是整个渲染树唯一的根节点,上边相应的也会挂载很多属性。这里我们先不去看这个数据结构。只看大体流程这里我们将的是render所以只说初次渲染的逻辑
我们实际会在unbatchedUpdates中调用updateContainer
这个unbatchedUpdates实际上是一种强制同步更新的方法我们先看源码。这里我们其实就是处理了传入函数的executionContext上下文
executionContext &= ~BatchedContext;executionContext |= LegacyUnbatchedContext;这里的意思就是我们要将LegacyUnbatchedContext这种类型合并进当前上下文,在方法执行完后再恢复之前的执行环境。
当在这种上下文的环境下react的更新会走同步的逻辑,因为这是第一次更新,用户要尽快的看到页面的内容,所以不需要走异步更新的逻辑

/**
 * 同步更新任务
 */
export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
  const prevExecutionContext = executionContext;
  executionContext &= ~BatchedContext;
  executionContext |= LegacyUnbatchedContext;
  try {
    return fn(a);
  } finally {
    executionContext = prevExecutionContext;
    if (executionContext === NoContext) {
      // Flush the immediate callbacks that were scheduled during this batch
      flushSyncCallbackQueue();
    }
  }
}

接下来我们就要看看这个updateContainer了,这里我们进入了更新的主逻辑。方便理解还是先贴出主要代码。

/**
 * 更新的主逻辑,
 * 计算过期时间->创建更新的update对象->加入到调度队列->并开启任务调度
 */
export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): ExpirationTime {
  // 获取root上的根fiber对象
  const current = container.current;
  // 获取当前的时间节点
  const currentTime = requestCurrentTimeForUpdate();
  // 计算当前的到期时间
  const expirationTime = computeExpirationForFiber(
    currentTime,
    current,
    suspenseConfig,
  );
  // 处理context相关
  const context = getContextForSubtree(parentComponent);
  if (container.context === null) {
    container.context = context;
  } else {
    container.pendingContext = context;
  }
  // 生成update对象,是批处理更新的一个单元
  const update = createUpdate(expirationTime, suspenseConfig);
  // 为update对象具体要更新的参数赋值,传入的是ReactElement元素
  update.payload = {element};
  // 将update将入fiber根对象上的任务队列
  enqueueUpdate(current, update);
  // 开始执行任务调度,在到期时间内
  scheduleWork(current, expirationTime);

  return expirationTime;
}

主要流程就是如下步骤 计算过期时间->创建更新的update对象->加入到调度队列->并开启任务调度
什么是过期时间,说到这里要先说下react16之后的新概念fiber,能支持我们在执行耗时任务的时候可以跳出来相应一些高优先级的事件,比如我们在一个循环中执行一些复杂计算。但这时候用户通过input打字,我们就要即时响应输入操作,这在原来是做不到的。我们来实现这个功能主要靠的就是expirationTime过期时间这个概念。保证任务要在这个时间段内完成,如果超时了那么就要立即在下一个事件循环中完成
然后就是生成一个update对象用来记录更新的内容,将这个update对象插入rootFiber上的更新队列(基于链表实现)
最后开启任务调度,这里render的执行阶段就执行完了,接下来的任务就交给react的任务调度器去完成这也是下一篇要说的

expirationTime

先来看看关于过期时间的计算

// 值越大优先级越高
export function msToExpirationTime(ms: number): ExpirationTime {
  // Always add an offset so that we don't clash with the magic number for NoWork.
  return MAGIC_NUMBER_OFFSET - ((ms / UNIT_SIZE) | 0);
}
function ceiling(num: number, precision: number): number {
  return (((num / precision) | 0) + 1) * precision;
}

function computeExpirationBucket(
  currentTime,
  expirationInMs,
  bucketSizeMs,
): ExpirationTime {
  return (
    MAGIC_NUMBER_OFFSET -
    ceiling(
      MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE,
      bucketSizeMs / UNIT_SIZE,
    )
  );
}

export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150;
export const HIGH_PRIORITY_BATCH_SIZE = 100;
// 计算高优先级的时间
export function computeInteractiveExpiration(currentTime: ExpirationTime) {
  return computeExpirationBucket(
    currentTime,
    HIGH_PRIORITY_EXPIRATION,
    HIGH_PRIORITY_BATCH_SIZE,
  );
}

export const LOW_PRIORITY_EXPIRATION = 5000;
export const LOW_PRIORITY_BATCH_SIZE = 250;
// 计算低优先级的时间也就是过期时间
export function computeAsyncExpiration(
  currentTime: ExpirationTime,
): ExpirationTime {
  return computeExpirationBucket(
    currentTime,
    LOW_PRIORITY_EXPIRATION,
    LOW_PRIORITY_BATCH_SIZE,
  );
}

这里主要用的就是computeAsyncExpirationcomputeInteractiveExpiration这个两个不同优先级时间的计算,关于ceiling的计算我这里有一个例子

ceiling(10011, 10)//10020
ceiling(10019, 10)//10020

可以看到在计算值的时候会在每10个时间间隔内的过期时间都相同,相对的Async的间隔为25,而Interactive的时间间隔为10。这也保证了在这个时间间隔内的时间都会有相同的过期时间,这保证了在这段时间内触发的任务的优先级相同。保证一同触发的任务同时完成

再来看看当前时间的获取

/**
 * 计算当前时间
 */
export function requestCurrentTimeForUpdate() {
  if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
    // We're inside React, so it's fine to read the actual time.
    // 执行的上下文是render或者commit,在执行阶段获取真实时间
    return msToExpirationTime(now());
  }
  // We're not inside React, so we may be in the middle of a browser event.
  // 如果我们没在react内部更新中,可能是在执行浏览器的任务中
  if (currentEventTime !== NoWork) {
    // Use the same start time for all updates until we enter React again.
    return currentEventTime;
  }
  // This is the first update since React yielded. Compute a new start time.
  // 之前的任务已经执行完,开启新的任务时候需要重新计算时间
  currentEventTime = msToExpirationTime(now());
  return currentEventTime;
}


首先在render和commit阶段我们直接获取当前真实时间。
然后如果当前有任务在执行我们返回之前计算的当前时间,这也就确保了几毫秒之内触发任务我们会以相同的当前时间计算。
最后如果没有任务我们计算一个新的当前时间并赋给全局变量。
然后就是过期时间的计算了在初次渲染时会直接返回同步更新的标识

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,056评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,842评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,938评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,296评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,292评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,413评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,824评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,493评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,686评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,502评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,553评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,281评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,820评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,873评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,109评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,699评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,257评论 2 341

推荐阅读更多精彩内容

  • 坐在咖啡店的那把蓝色木椅上, 挺直着脊背,望着窗外的婆娑。 徐徐的微风吹佛水中的倒影, 垂柳不卑不亢,不声不响。 ...
    轻安安阅读 440评论 1 1
  • 以地质坚硬,不易磨损的毛竹为骨架,将36根伞骨一根根打磨成一头扁,一头圆的形状,用柿子胶把伞形纱纸黏合在伞骨上;再...
    _生花_阅读 179评论 0 2
  • 例子一 在页面中实时显示当前的时间 例子二 过滤器修改date的属性值这时候图片出现了这种形式的过滤页面 过滤器 ...
    Frank_Yi阅读 217评论 0 0
  • 今日体验 今天下班有点早,晚上给奥迪换风扇,由于第一次拆,也没有点思路的去干,确实空间有点小不好拿,经过第一次的失...
    任武科阅读 93评论 0 1