应该如何去读react源码_react 源码 - CSDN
  • `scheduler`这个包主要是在react做diff做任务分配机制, 核心机制类似于[requestidlecallback](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback), >`window.requestIdleCallback()`会...

    1. 前言

    为了读代码更加有效率,提前看了一篇如何阅读源码的文章:https://zxc0328.github.io/2018/05/01/react-source-reading-howto/
    因此此次本人阅读源码主要想看懂以下6个问题:

    1. ReactDOM.render()是如何挂载到真实DOM上的
    2. setState实现原理,为什么是异步的
    3. 生命周期结合2号问题一起看
    4. react16的fiber架构是什么
    5. jsx是如何解析的
    6. react hook是如何做到的

    分析代码基于React V16.8.6,react源码目录截图如下图所示:
    在这里插入图片描述

    所需要看的代码一个库就够了,https://github.com/facebook/react/


    先看的react-dom代码, 一点点单步到了scheduler, 这个包的代码看起来不多(可能是我第一次看框架源码, 看的有点恶心… 一堆全局变量, 各个函数来回调用, 看了好几天, 如果下面哪里有问题, 还请各位同行指点), 那就先来梳理下这个包吧.

    scheduler这个包主要是在react做diff做任务分配机制, 核心机制类似于requestidlecallback,

    window.requestIdleCallback()会在浏览器空闲时期依次调用函数, 这就可以让开发者在主事件循环中执行后台或低优先级的任务,而且不会对像动画和用户交互这样延迟敏感的事件产生影响。

    但这个函数支持度太惨
    在这里插入图片描述
    react则使用requestAnimationFramepostMessage来模拟实现的requestidlecallback. 工作原理是调度requestAnimationFrame,存储帧开始的时间,然后调度postMessage,后者在绘制后进行调度。

    该包主要流程是把所有任务通过双向链表连接起来, 通过requestAnimationFrame来在浏览器每帧的空闲时间循环处理所有任务, 直到链表为空为止.


    2. getCurrentTime

    这个函数后面会经常用到的, 先到前面来说下, 先看代码:

    // packages\scheduler\src\forks\SchedulerHostConfig.default.js
    const hasNativePerformanceNow =
      typeof performance === 'object' && typeof performance.now === 'function';
    const localDate = Date;
    
    if (hasNativePerformanceNow) {
      const Performance = performance;
      getCurrentTime = function() {
        return Performance.now();
      };
    } else {
      getCurrentTime = function() {
        // 该方法在 ECMA-262 第五版中被标准化, Date.now() === new Date().getTime();
        // 出处 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Date/now#Compatibility
        return localDate.now();
      };
    }
    

    performance.now()Date.now() 不同的是,返回了以微秒(百万分之一秒)为单位的时间,更加精准。

    并且与 Date.now() 会受系统程序执行阻塞的影响不同,performance.now() 的时间是以恒定速率递增的,不受系统时间的影响(系统时间可被人为或软件调整)。

    注意Date.now()输出的是 UNIX 时间,即距离 1970 的时间,而performance.now()输出的是相对于 time origin(页面初始化: https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp#The_time_origin) 的时间。

    使用 Date.now() 的差值并非绝对精确,因为计算时间时受系统限制(可能阻塞)。但使用 performance.now() 的差值,并不影响我们计算程序执行的精确时间。

    3. unstable_scheduleCallback函数

    • 函数前面的unstable表示不稳定的意思, 之后还会有变动.
    • 这个方法主要就是将任务组成双向链表, 并按照过期时间作为优先级.

    先定义优先级, 代码如下:

    // packages\scheduler\src\Scheduler.js
    
    // Max 31 bit integer. The max integer size in V8 for 32-bit systems.
    // 这是32位系统V8引擎里最大的整数
    // Math.pow(2, 30) - 1
    // 0b111111111111111111111111111111
    var maxSigned31BitInt = 1073741823;
    // Times out immediately 立即过期
    var IMMEDIATE_PRIORITY_TIMEOUT = -1;
    // Eventually times out
    var USER_BLOCKING_PRIORITY = 250;
    var NORMAL_PRIORITY_TIMEOUT = 5000;
    var LOW_PRIORITY_TIMEOUT = 10000;
    // Never times out
    var IDLE_PRIORITY = maxSigned31BitInt;
    

    主函数代码如下:

    // packages\scheduler\src\Scheduler.js
    
    // 组成双向链表, 开始安排任务
    function unstable_scheduleCallback(priorityLevel,  callback,  deprecated_options) {
      var startTime =
        currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();
    
      // 过期时间 = 加入时间 + 优先级时间
      var expirationTime;
      if (
        typeof deprecated_options === 'object' &&
        deprecated_options !== null &&
        typeof deprecated_options.timeout === 'number'
      ) {
        expirationTime = startTime + deprecated_options.timeout;
      } else {
        // 根据不同的优先级, 赋予不同的过期时间
        switch (priorityLevel) {
          case ImmediatePriority:
            expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
            break;
          case UserBlockingPriority:
            expirationTime = startTime + USER_BLOCKING_PRIORITY;
            break;
          case IdlePriority:
            expirationTime = startTime + IDLE_PRIORITY;
            break;
          case LowPriority:
            expirationTime = startTime + LOW_PRIORITY_TIMEOUT;
            break;
          case NormalPriority:
          default:
            expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;
        }
      }
      
    // 未完
    

    上面先计算一下callback的过期时间, 接下来创建链表节点, 并组成链表, 代码如下:

    // packages\scheduler\src\Scheduler.js
    
    // 续上
    // 基于上面的优先级和过期时间创建一个节点
    var newNode = {
        callback,
        priorityLevel: priorityLevel,
        expirationTime,
        next: null,
        previous: null,
      };
    
      if (firstCallbackNode === null) {
        // This is the first callback in the list. 如果firstCallbackNode没有, 说明是第一个节点
        firstCallbackNode = newNode.next = newNode.previous = newNode;
        scheduleHostCallbackIfNeeded(); // 之后再说, 先忽略
      } else {
        var next = null;
        var node = firstCallbackNode;
        do {
          if (node.expirationTime > expirationTime) {
            // The new callback expires before this one.
            next = node;
            break;
          }
          node = node.next;
        } while (node !== firstCallbackNode);
    
        if (next === null) {
          // No callback with a later expiration was found, which means the new
          // callback has the latest expiration in the list.
          // 列表中新插入的节点具有最大的到期时间, 插入最后
          next = firstCallbackNode;
        } else if (next === firstCallbackNode) {
          // The new callback has the earliest expiration in the entire list.
          // 新插入的节点具有最小的到期时间, 插在最前面
          firstCallbackNode = newNode;
          scheduleHostCallbackIfNeeded();
        }
    
        var previous = next.previous;
        previous.next = next.previous = newNode;
        newNode.next = next;
        newNode.previous = previous;
      }
    
      return newNode;
    }
    

    这部分当初第一次看的时候也看了很久, 看明白之后才发现原来创建双向链表居然这么简单, 就是有点绕, 双向链表的定义:

    双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。
    https://baike.baidu.com/item/双向链表/2968731?fr=aladdin

    这部分创建初始双链表其实就只有用了一句话firstCallbackNode = newNode.next = newNode.previous = newNode;, 从右向左赋值, 也就是说这段代码中用的变量全都指向newNode这个对象的地址.


    4. scheduleHostCallbackIfNeeded

    // packages\scheduler\src\Scheduler.js
    // This is set while performing work, to prevent re-entrancy.
    var isPerformingWork = false;
    var isHostCallbackScheduled = false;
    
    function scheduleHostCallbackIfNeeded() {
      if (isPerformingWork) {
        // Don't schedule work yet; wait until the next time we yield.
        // 有个work正在执行
        return;
      }
    
      if (firstCallbackNode !== null) {
        // Schedule the host callback using the earliest expiration in the list.
        // firstCallbackNode的过期时间是最早的
        var expirationTime = firstCallbackNode.expirationTime;
        if (isHostCallbackScheduled) {
          // Cancel the existing host callback.
          // 取消存在的回调函数
          cancelHostCallback();
        } else {
          isHostCallbackScheduled = true;
        }
        requestHostCallback(flushWork, expirationTime);
      }
    }
    

    requestHostCallback这个函数内部主要是通过本文开头所讲的requestAnimationFramepostMessage来模拟实现的requestidlecallback调度任务执行这个flushWork, 那么我们先来看下这个flushWork.

    4.1. flushWork

    这个函数的参数didUserCallbackTimeout只会有两种情况:

    1. 当前帧没过期;didUserCallbackTimeout = false
    2. 当前帧过期且当前任务过期didUserCallbackTimeout = true
    // packages\scheduler\src\Scheduler.js
    
    function flushWork(didUserCallbackTimeout) {
      // Exit right away if we're currently paused
      if (enableSchedulerDebugging && isSchedulerPaused) {
        return;
      }
    
      // We'll need a new host callback the next time work is scheduled.
      // 安排callback完成了
      isHostCallbackScheduled = false;
    
      isPerformingWork = true;
      const previousDidTimeout = currentHostCallbackDidTimeout;
      currentHostCallbackDidTimeout = didUserCallbackTimeout;
      try {
        if (didUserCallbackTimeout) {
          // 当前帧过期且当前任务过期
          // Flush all the expired callbacks without yielding.
          while (
            firstCallbackNode !== null &&
            !(enableSchedulerDebugging && isSchedulerPaused)
          ) {
            // TODO Wrap in feature flag
            // Read the current time. Flush all the callbacks that expire at or
            // earlier than that time. Then read the current time again and repeat.
            // This optimizes for as few performance.now calls as possible.
            // 读取当前时间, 刷新在该时间之前过期的所有回调
            var currentTime = getCurrentTime();
            if (firstCallbackNode.expirationTime <= currentTime) {
              do {
                flushFirstCallback(); // 见4.2节
              } while (
                firstCallbackNode !== null &&
                firstCallbackNode.expirationTime <= currentTime &&
                !(enableSchedulerDebugging && isSchedulerPaused)
              );
              continue;
            }
            break;
          }
        } else {
          // Keep flushing callbacks until we run out of time in the frame.
          // 进入此循环说明, 帧没有过期
          // 继续刷新回调,直到我们在帧中耗尽时间。
          if (firstCallbackNode !== null) {
            do {
              if (enableSchedulerDebugging && isSchedulerPaused) {
                break;
              }
              flushFirstCallback(); // 见4.2节
            } while (firstCallbackNode !== null && !shouldYieldToHost());
          }
        }
      } finally {
        isPerformingWork = false;
        currentHostCallbackDidTimeout = previousDidTimeout;
        // There's still work remaining. Request another callback.
        scheduleHostCallbackIfNeeded();
      }
    }
    

    这部分主要基于当前帧以及当前任务过期时间来决定是否执行flushFirstCallback函数, 这个函数就是最终的执行任务函数.

    4.2. flushFirstCallback

    这个函数比较长, 主要功能为: 执行队首任务并把队首任务从链表移除,把第二个任务置为队首任务。执行任务可能产生新的任务,再把新任务插入到任务链表中. 见下方代码:

    // packages\scheduler\src\Scheduler.js
    
    // 执行任务, 并更新链表
    function flushFirstCallback() {
      const currentlyFlushingCallback = firstCallbackNode;
    
      // Remove the node from the list before calling the callback. That way the
      // list is in a consistent state even if the callback throws.
      // 在执行callback之前, 最好先把这个节点从列表中删掉
      var next = firstCallbackNode.next;
      if (firstCallbackNode === next) {
        // This is the last callback in the list.
        // 由于是双向链表, 这种情况就是现在只剩一个节点了
        // 全部设置为null
        firstCallbackNode = null;
        next = null;
      } else {
        // 链表中删除firstCallbackNode
        var lastCallbackNode = firstCallbackNode.previous;
        firstCallbackNode = lastCallbackNode.next = next;
        next.previous = lastCallbackNode;
      }
    
      // 从链表中彻底剥离, 把原对象引用置空
      currentlyFlushingCallback.next = currentlyFlushingCallback.previous = null;
    
      // Now it's safe to call the callback.
      // 获取属性
      var callback = currentlyFlushingCallback.callback;
      var expirationTime = currentlyFlushingCallback.expirationTime;
      var priorityLevel = currentlyFlushingCallback.priorityLevel;
    
      // 临时保存
      var previousPriorityLevel = currentPriorityLevel;
      var previousExpirationTime = currentExpirationTime;
      currentPriorityLevel = priorityLevel;
      currentExpirationTime = expirationTime;
      var continuationCallback;
      try {
        const didUserCallbackTimeout =
          currentHostCallbackDidTimeout ||
          // Immediate priority callbacks are always called as if they timed out
          priorityLevel === ImmediatePriority;
        continuationCallback = callback(didUserCallbackTimeout);
      } catch (error) {
        throw error;
      } finally {
        // 还原
        currentPriorityLevel = previousPriorityLevel;
        currentExpirationTime = previousExpirationTime;
      }
    
      // A callback may return a continuation. The continuation should be scheduled
      // with the same priority and expiration as the just-finished callback.
      // 如果返回的是函数, 则和刚刚完成的回调函数具有相同的过期时间和优先级
      if (typeof continuationCallback === 'function') {
        var continuationNode: CallbackNode = {
          callback: continuationCallback,
          priorityLevel,
          expirationTime,
          next: null,
          previous: null,
        };
    
        // Insert the new callback into the list, sorted by its expiration. This is
        // almost the same as the code in `scheduleCallback`, except the callback
        // is inserted into the list *before* callbacks of equal expiration instead
        // of after.
        // 下面和scheduleCallback函数一个逻辑, 只有一点不同, `callback`在等于到期时间的回调之前插入到列表中而不是插入到之后
        if (firstCallbackNode === null) {
          // This is the first callback in the list.
          firstCallbackNode = continuationNode.next = continuationNode.previous = continuationNode;
        } else {
          var nextAfterContinuation = null;
          var node = firstCallbackNode;
          do {
            if (node.expirationTime >= expirationTime) {
              // This callback expires at or after the continuation. We will insert
              // the continuation *before* this callback.
              nextAfterContinuation = node;
              break;
            }
            node = node.next;
          } while (node !== firstCallbackNode);
    
          if (nextAfterContinuation === null) {
            // No equal or lower priority callback was found, which means the new
            // callback is the lowest priority callback in the list.
            // 没有找到相等或者低优先级的callback, 因此放到第一个
            nextAfterContinuation = firstCallbackNode;
          } else if (nextAfterContinuation === firstCallbackNode) {
            // The new callback is the highest priority callback in the list.
            firstCallbackNode = continuationNode;
            scheduleHostCallbackIfNeeded();
          }
    
          // 插入操作
          var previous = nextAfterContinuation.previous;
          previous.next = nextAfterContinuation.previous = continuationNode;
          continuationNode.next = nextAfterContinuation;
          continuationNode.previous = previous;
        }
      }
    }
    

    5. requestHostCallback

    接下来我们最后再看下这个函数, 代码如下:

    // packages\scheduler\src\forks\SchedulerHostConfig.default.js
    
      // absoluteTimeout => 链表node的过期时间(expirationTime)
      // callback => flushWork函数
      requestHostCallback = function(callback, absoluteTimeout) {
        scheduledHostCallback = callback;
        timeoutTime = absoluteTimeout;
        if (isFlushingHostCallback || absoluteTimeout < 0) {
          // Don't wait for the next frame. Continue working ASAP, in a new event.
          // 如果absoluteTimeout时间小于1, 则此次work为ImmediatePriority优先级
          // 应该立即执行
          port.postMessage(undefined);
        } else if (!isAnimationFrameScheduled) {
          // If rAF didn't already schedule one, we need to schedule a frame.
          // TODO: If this rAF doesn't materialize because the browser throttles, we
          // might want to still have setTimeout trigger rIC as a backup to ensure
          // that we keep performing work.
          isAnimationFrameScheduled = true;
          requestAnimationFrameWithTimeout(animationTick); // animationTick为一个函数, 见5.2节
        }
      };
    

    这部分没多少代码

    5.1. requestAnimationFrameWithTimeout

    我们接下来看下requestAnimationFrameWithTimeout函数, 代码如下:

    // packages\scheduler\src\forks\SchedulerHostConfig.default.js
    
    // requestAnimationFrame does not run when the tab is in the background. If
    // we're backgrounded we prefer for that work to happen so that the page
    // continues to load in the background. So we also schedule a 'setTimeout' as
    // a fallback.
    // TODO: Need a better heuristic for backgrounded work.
    
    // requestAnimationFrame在切换tab之后不再运行,如果切换tab之后
    // 我们还想让他在后台继续运行,应使用setTimeout作为兜底操作
    const ANIMATION_FRAME_TIMEOUT = 100;
    let rAFID;
    let rAFTimeoutID;
    const requestAnimationFrameWithTimeout = function(callback) {
      // schedule rAF and also a setTimeout
      rAFID = localRequestAnimationFrame(function(timestamp) {
        // timestamp 实际上就是performance.now()
        // cancel the setTimeout, 如果RAF好使就不使用Timeout
        // callback实际就是下面的animationTick函数, 见5.2节
        localClearTimeout(rAFTimeoutID);
        callback(timestamp); // callback就是animationTick函数, 见5.2小节
      });
      rAFTimeoutID = localSetTimeout(function() {
        // cancel the requestAnimationFrame
        localCancelAnimationFrame(rAFID);
        callback(getCurrentTime());
      }, ANIMATION_FRAME_TIMEOUT);
    };
    

    5.2. animationTick

    接着上面那个callback(timestamp);

    React默认使用30fps运行, 在运行时每次都会比对前一次帧运行时间和下次帧运行时间, 来动态调整至合适数值.

      let frameDeadline = 0;
      let previousFrameTime = 33;
      let activeFrameTime = 33;
      
      const animationTick = function(rafTime) {
      // scheduledHostCallback 就是flushWork函数
        if (scheduledHostCallback !== null) {
          // Eagerly schedule the next animation callback at the beginning of the
          // frame. If the scheduler queue is not empty at the end of the frame, it
          // will continue flushing inside that callback. If the queue *is* empty,
          // then it will exit immediately. Posting the callback at the start of the
          // frame ensures it's fired within the earliest possible frame. If we
          // waited until the end of the frame to post the callback, we risk the
          // browser skipping a frame and not firing the callback until the frame
          // after that.
          // 在当前的帧的开始安排下一个animation callback
          // 如果任务队列在当前帧不为空, 会继续执行下去, 如果队列为空了, 会立即退出
          requestAnimationFrameWithTimeout(animationTick);
        } else {
          // No pending work. Exit.
          isAnimationFrameScheduled = false;
          return;
        }
    
        // rafTime raf执行时的时间
        let nextFrameTime = rafTime - frameDeadline + activeFrameTime;
        if (
          nextFrameTime < activeFrameTime &&
          previousFrameTime < activeFrameTime &&
          !fpsLocked // 如果没有手动把fps数值死的话
        ) {
          if (nextFrameTime < 8) {
            // Defensive coding. We don't support higher frame rates than 120hz.
            // If the calculated frame time gets lower than 8, it is probably a bug.
            // 不支持比120hz更好的帧率
            nextFrameTime = 8;
          }
          // If one frame goes long, then the next one can be short to catch up.
          // If two frames are short in a row, then that's an indication that we
          // actually have a higher frame rate than what we're currently optimizing.
          // We adjust our heuristic dynamically accordingly. For example, if we're
          // running on 120hz display or 90hz VR display.
          // Take the max of the two in case one of them was an anomaly due to
          // missed frame deadlines.
          // 如果两次帧时间都比较短, 调整当前帧率, 取这两次中大的那个
          activeFrameTime =
            nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
        } else {
          previousFrameTime = nextFrameTime;
        }
        frameDeadline = rafTime + activeFrameTime;
        if (!isMessageEventScheduled) {
          isMessageEventScheduled = true;
          // postMessage触发任务链表队列, 见下节5.3.
          port.postMessage(undefined);
        }
      };
    

    5.3. port.postMessage

    这个函数就是调用flushWork, 并传入帧是否过期的地方, 看下方代码:

      // We use the postMessage trick to defer idle work until after the repaint.
      // 使用postMessage来将工作推迟到重绘之后
      const channel = new MessageChannel();
      // Channel Messaging API的Channel Messaging接口允许我们创建一个新的消息通道,并通过它的两个MessagePort 属性(port1, port2)发送数据。
      
      const port = channel.port2;
      channel.port1.onmessage = function(event) {
        isMessageEventScheduled = false;
    
        const prevScheduledCallback = scheduledHostCallback;
        const prevTimeoutTime = timeoutTime;
        scheduledHostCallback = null;
        timeoutTime = -1;
    
        const currentTime = getCurrentTime();
    
        let didTimeout = false;
        // 当前帧过期
        if (frameDeadline - currentTime <= 0) {
          // There's no time left in this idle period. Check if the callback has
          // a timeout and whether it's been exceeded.
          // 已经执行过了
          if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {
            // Exceeded the timeout. Invoke the callback even though there's no
            // time left.
            // 任务过期
            didTimeout = true;
          } else {
            // No timeout.
            // 没有执行过
            if (!isAnimationFrameScheduled) {
              // Schedule another animation callback so we retry later.
              isAnimationFrameScheduled = true;
              requestAnimationFrameWithTimeout(animationTick);
            }
            // Exit without invoking the callback.
            scheduledHostCallback = prevScheduledCallback;
            timeoutTime = prevTimeoutTime;
            return;
          }
        }
    
        if (prevScheduledCallback !== null) {
          isFlushingHostCallback = true;
          try {
            prevScheduledCallback(didTimeout);
          } finally {
            isFlushingHostCallback = false;
          }
        }
      };
    

    在这里只有一点困扰了我很久, 就是为什么要使用postMessage来去配合raf, 虽然官方注释也提及了.

    We use thepostMessage trick to defer idle work until after the repaint.
    我们使用postMessage来将工作推迟到重绘之后

    这部分没有找到合理的出处来证明这一点, 不过我想应该和postMessage本身属于macrotask有关, 来看下下面的代码例子:

      var channel = new MessageChannel();
      channel.port1.onmessage = function () {
        console.log(6)
      }
      console.log(1);
      setTimeout(function () {
        console.log(7);
      }, 0);
    
      new Promise(function (resolve, reject) {
        console.log(2)
        resolve()
      }).then(function () {
        console.log(4);
      }).then(function () {
        console.log(5);
      });
      console.log(3);
      channel.port2.postMessage(undefined)
    

    上面代码console的数字就是输出顺序, 接下来解释一下:

    javascript所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous. 同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行.

    macrotaskmicrotask 两个词最早应该出现在实现Promise/A+库的过程中(https://promisesaplus.com/#notes 这里出现了这两个单词)

    二者都是属于异步任务中的一种,我们先看一下他们分别是哪些 API :

    // 出处: https://stackoverflow.com/questions/25915634/difference-between-microtask-and-macrotask-within-an-event-loop-context#
    macrotasks: setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering, postMessage
    microtasks: process.nextTick, Promises, Object.observe, MutationObserver
    

    具体来说,异步执行的运行机制如下.(同步执行也是如此,因为它可以被视为没有异步任务的异步执行.

    1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack).
    2. 主线程之外,还存在一个"任务队列"(task queue ==> task queue = macrotask queue != microtask queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件.
    3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行.
    4. 主线程不断重复上面的第三步.

    使用postMessage而不使用setTimeout应该是因为后者最少4m的延迟吧.


    6. 总结

    1. 任务基于过期时间创建双向链表
    2. 当双向链表从无到有或者更换第一节点时开启任务调度
    3. 任务调度是指在当前帧空闲的时机去执行任务,这里通过requestAnimationFramemessageChannel来模拟
    4. 如果满足执行任务条件,则根据任务是否过期来确定如何执行任务.

    如有漏洞, 麻烦指正.

    本文最后关于js的任务执行顺序, 说的比较浅, 读者可以参考如下链接:

    1. JavaScript 运行机制详解:再谈Event Loop
    2. Difference between microtask and macrotask within an event loop context
    3. Tasks, microtasks, queues and schedules
    4. 理解 JavaScript 中的 macrotask 和 microtask
    5. javascript中的异步 macrotask 和 microtask 简介

    …并且在Node中执行任务顺序和浏览器还不太一致, 后续会抽空好好研究一下, 重新开一篇新bolg.

    展开全文
  • 前言 距离第一篇《React源码解析(一)》已经过去将近4个月的时间,由于是我第一次进行源码解析相关的写作,思路和文笔还不够成熟。一百多天以来,我基于读者反馈反思这几篇文章中的不足,同时也在不断学习借...

    前言

    距离第一篇《React源码解析(一)》已经过去将近4个月的时间,由于是我第一次进行源码解析相关的写作,思路和文笔还不够成熟。一百多天以来,我基于读者反馈反思这几篇文章中的不足,同时也在不断学习借鉴其他优秀作者的写作方法和写作思路。最终总结出对于自己的源码写作来说,需要改进的几点:

    问题

    1.示例代码太多

    这可能是源码解析写作的通病吧。其实大多数人从vue和react这样框架的生命周期API就可以大致猜出内部流程是什么,示例代码太多反而会产生枯燥感,而且部分示例代码又长又臭,甚至不得不加上大段的注释,严重降低了阅读体验。

    2.解析流水账

    既然代码是给计算机看的流水账,那解析就不能再是给人看的流水账。对于源码解析来说,最大的误区是去弄懂每一行甚至每一个变量的意义,而这最终会导致事倍功半。所以源码解析应以高屋建瓴的意识去写作,而不是钻牛角尖,或者像和尚念经一样干干巴巴,逐字逐句地阐述。

    3.缺乏提炼

    我们初高中上语文课的时候,总会被问到这篇文章的中心思想是什么。同样,写完源码解析和读完源码解析,作者和读者收获了什么?很多源码解析文章并没有告诉我们,读者读完好像明白了点什么又好像没明白,一脸茫然。

    针对以上三个问题,结合已有的《React源码解析》系列,我做出了如下改进:

    改进

    1.减少示例代码,尽可能使用语言概括和图形表达

    人对图形的接受和理解度远远高于对语言的处理和理解,这也是为什么《图解HTTP》等系列书籍畅销全世界的原因。新的《React源码解析》去掉了许多无用代码和语言描述,改为使用思维导图或流程图,提升阅读体验,降低理解难度。当然,对于源码中重要的段落依然需要展示。

    2.美化文章排版

    在进行必要的代码示例时,如果代码较长,那么无论使用掘金编辑器还是代码截图,最后实际的阅读体验并不好,尤其在手机上时这一点更为明显。新的《React源码解析》将采用carbon展示代码。

    3.思想提炼

    阅读源码的最终作用并不是为了再造一个vue/react,而是借鉴其中的思想和方法,应用到实际业务中去,最终提升我们编程的能力。新的《React源码解析》将在每一篇文章的结尾阐述是什么和为什么,使读者读完有所收获,不会似懂非懂甚至白白浪费宝贵的时间。

    3.总结

    新的《React源码解析》依旧分为四篇,分别阐述React中重要的概念,目录如下:

    思维导图如下图(在mac下查看效果最好):

    四篇文章对导图的每个部分都做了详细的说明。如有欠妥之处,欢迎读者朋友们不吝指正和讨论,共同学习进步。

    联系邮箱:ssssyoki@foxmail.com

    展开全文
  • 阅读源码成了今年的学习目标之一,在选择 Vue 和 React 之间,我想先阅读 React 。 在考虑到哪个版本的时候,我想先接触到源码早期的思想可能会更轻松一些,最终我选择阅读 0.3-stable 。 那么接下来,我将从几个...

    阅读源码成了今年的学习目标之一,在选择 Vue 和 React 之间,我想先阅读 React 。 在考虑到读哪个版本的时候,我想先接触到源码早期的思想可能会更轻松一些,最终我选择阅读 0.3-stable 。 那么接下来,我将从几个方面来解读这个版本的源码。

    1. React 源码学习(一):HTML 元素渲染
    2. React 源码学习(二):HTML 子元素渲染
    3. React 源码学习(三):CSS 样式及 DOM 属性
    4. React 源码学习(四):事务机制
    5. React 源码学习(五):事件机制
    6. React 源码学习(六):组件渲染
    7. React 源码学习(七):生命周期
    8. React 源码学习(八):组件更新

    React 组件渲染 React.createClass

    终于要说到组件渲染了,其实组件渲染就是基于 HTML 元素渲染,我们在重新回到熟悉的例子,这是一个完整的例子:

    // #examples
    var ExampleApplication = React.createClass({
      render: function() {
        var elapsed = Math.round(this.props.elapsed  / 100);
        var seconds = elapsed / 10 + (elapsed % 10 ? '' : '.0' );
        var message =
          'React has been successfully running for ' + seconds + ' seconds.';
    
        return React.DOM.p(null, message);
      }
    });
    var start = new Date().getTime();
    
    React.renderComponent(
      ExampleApplication({elapsed: new Date().getTime() - start}),
      document.getElementById('container')
    );
    复制代码

    使用 type="text/jsx" 的形式编写:

    /** @jsx React.DOM */
    // #examples
    var ExampleApplication = React.createClass({
      render: function() {
        var elapsed = Math.round(this.props.elapsed  / 100);
        var seconds = elapsed / 10 + (elapsed % 10 ? '' : '.0' );
        var message =
          'React has been successfully running for ' + seconds + ' seconds.';
    
        return <p>{message}</p>;
      }
    });
    var start = new Date().getTime();
    
    React.renderComponent(
      <ExampleApplication elapsed={new Date().getTime() - start} />,
      document.getElementById('container')
    );
    复制代码

    复合组件

    在这里,我们看到创建组件的方法是 React.createClass ,组件就是一堆 HTML 元素的集合,但是组件具有状态 (state) 和属性 (props) ,还具有生命周期,并且组件可以更新。所以我们会一一将其到来,那么本次我们仅讨论组件渲染。我们看到复合组件

    // core/ReactCompositeComponent.js
    var ReactCompositeComponent = {
      createClass: function(spec) {
        var Constructor = function(initialProps, children) {
          this.construct(initialProps, children);
        };
        Constructor.prototype = new ReactCompositeComponentBase();
        Constructor.prototype.constructor = Constructor;
        mixSpecIntoComponent(Constructor, spec);
        invariant(
          Constructor.prototype.render,
          'createClass(...): Class specification must implement a `render` method.'
        );
    
        var ConvenienceConstructor = function(props, children) {
          return new Constructor(props, children);
        };
        ConvenienceConstructor.componentConstructor = Constructor;
        ConvenienceConstructor.originalSpec = spec;
        return ConvenienceConstructor;
      },
    };
    复制代码

    看到 React.createClass 是不是发现和 ReactDOM.js 下的 createDOMComponentClass 函数很相似。确实如此,在你看懂了 HTML 元素渲染的实现后来看这个确实会容易很多,那么接受参数 spec 是什么呢?是 specification 规范,按照官方注释翻译为:在给定类规范的情况下创建复合组件类。我们从上面代码也可以看出 render 方法是必不可少的。他的原型映射到 ReactCompositeComponentBase ,那么让我们来看看他。

    // core/ReactCompositeComponent.js
    var ReactCompositeComponentBase = function() {};
    mixInto(ReactCompositeComponentBase, ReactComponent.Mixin);
    mixInto(ReactCompositeComponentBase, ReactOwner.Mixin);
    mixInto(ReactCompositeComponentBase, ReactPropTransferer.Mixin);
    mixInto(ReactCompositeComponentBase, ReactCompositeComponentMixin);
    复制代码

    渲染至 DOM 节点方法 mountComponent

    熟悉吧, React 这种写法,原型链上的属性疯狂合并,并且原型链上的属性互相被连续调用。当然在方法中也有这样一行代码 mixSpecIntoComponent(Constructor, spec); ,他在 mixInto 的基础上做了更多的处理,但是他与 mixInto 的功能效果是一样的。那么接下来,先让我们看到当执行 React.renderComponent 方法后,复合组件会做出什么样的反应。我们看到代码:

    // core/ReactCompositeComponent.js
    var ReactCompositeComponentMixin = {
      construct: function(initialProps, children) {
        ReactComponent.Mixin.construct.call(this, initialProps, children);
        this.state = null;
        this._pendingState = null;
        this._compositeLifeCycleState = null;
      },
      mountComponent: function(rootID, transaction) {
        // 这里与 ReactNativeComponent.Mixin.mountComponent 一致
        // 都执行了此方法,剩下的区别只是针对 HTML 组件和复合组件的区别
        // HTML 组件只需产出 markup 标记,添加 CSS 样式, DOM 属性,事件监听
        // 复合组件却不一样,上面已经提到过。
        ReactComponent.Mixin.mountComponent.call(this, rootID, transaction);
    
        // 组件的生命周期和复合组件的生命周期
        // Unset `this._lifeCycleState` until after this method is finished.
        this._lifeCycleState = ReactComponent.LifeCycle.UNMOUNTED;
        this._compositeLifeCycleState = CompositeLifeCycle.MOUNTING;
    
        // 参数校验
        if (this.constructor.propDeclarations) {
          this._assertValidProps(this.props);
        }
    
        // 绑定 this 执行上下文有关,与 mixSpecIntoComponent 有关联
        if (this.__reactAutoBindMap) {
          this._bindAutoBindMethods();
        }
    
        // 组件状态相关
        this.state = this.getInitialState ? this.getInitialState() : null;
        this._pendingState = null;
    
        // 生命周期相关
        if (this.componentWillMount) {
          this.componentWillMount();
          // When mounting, calls to `setState` by `componentWillMount` will set
          // `this._pendingState` without triggering a re-render.
          if (this._pendingState) {
            this.state = this._pendingState;
            this._pendingState = null;
          }
        }
    
        // 生命周期相关
        if (this.componentDidMount) {
          // 这里需要特别提一下,你可以看到,这是 React 调度事务,并且 ReactOnDOMReady 在调度事务 close 时
          // 会执行队列中的事务,所以你可以理解组件被挂载后这个生命周期是如何实现的。
          transaction.getReactOnDOMReady().enqueue(this, this.componentDidMount);
        }
    
        // 调用 _renderValidatedComponent 方法
        // 返回 ReactNativeComponent
        this._renderedComponent = this._renderValidatedComponent();
    
        // 生命周期相关
        // Done with mounting, `setState` will now trigger UI changes.
        this._compositeLifeCycleState = null;
        this._lifeCycleState = ReactComponent.LifeCycle.MOUNTED;
    
        // 调用 ReactNativeComponent.mountComponent 生成 markup 标记
        return this._renderedComponent.mountComponent(rootID, transaction);
      },
      _renderValidatedComponent: function() {
        // 这一步使用来控制正确的 Owner
        ReactCurrentOwner.current = this;
        // 调用 render 方法的返回
        var renderedComponent = this.render();
        // 这一步使用来控制正确的 Owner
        // 在 ReactNativeComponent 实例化时 ReactCurrentOwner.current 会被记录
        ReactCurrentOwner.current = null;
        invariant(
          ReactComponent.isValidComponent(renderedComponent),
          '%s.render(): A valid ReactComponent must be returned.',
          this.constructor.displayName || 'ReactCompositeComponent'
        );
        return renderedComponent;
      },
    };
    复制代码

    那么组件渲染讲的也差不多了,我们对其他一些,也先在这边提起来。回到这段代码:

    // core/ReactCompositeComponent.js
    var ReactCompositeComponentBase = function() {};
    // 一些 DOM 操作, React 调度事务相关,并包括生命周期
    // ReactNativeComponent 也同样含有
    mixInto(ReactCompositeComponentBase, ReactComponent.Mixin);
    // 用于操作 ref ,相关代码随行在 ReactComponent 中,这里也不做详细解读
    mixInto(ReactCompositeComponentBase, ReactOwner.Mixin);
    // props 传输功能
    mixInto(ReactCompositeComponentBase, ReactPropTransferer.Mixin);
    mixInto(ReactCompositeComponentBase, ReactCompositeComponentMixin);
    复制代码

    React.autoBind 处理

    简单介绍了这些后我们来看看, mixSpecIntoComponent 方法,既然看到这个方法,我们也要同样提到 React.autoBind 方法。本次我们仅讨论绑定上下文,不讨论其他相关:

    // core/ReactCompositeComponent.js
    var ReactCompositeComponent = {
      // 使用 autoBind 的时候返回 unbound 函数
      // 将需要被绑定上下文的方法赋值至 __reactAutoBind
      autoBind: function(method) {
        function unbound() {
          invariant(
            false,
            'React.autoBind(...): Attempted to invoke an auto-bound method that ' +
            'was not correctly defined on the class specification.'
          );
        }
        unbound.__reactAutoBind = method;
        return unbound;
      }
    };
    
    function mixSpecIntoComponent(Constructor, spec) {
      var proto = Constructor.prototype;
      for (var name in spec) {
        if (!spec.hasOwnProperty(name)) {
          continue;
        }
        var property = spec[name];
    
        // 目前仅保留了与 autoBind 相关代码
        // 合并 specification 时,检查是否存在 __reactAutoBind 属性
        // 并加入 __reactAutoBindMap
        if (property && property.__reactAutoBind) {
          if (!proto.__reactAutoBindMap) {
            proto.__reactAutoBindMap = {};
          }
          proto.__reactAutoBindMap[name] = property.__reactAutoBind;
        } else {
          // 其他属性正常合并
          proto[name] = property;
        }
      }
    }
    
    
    
    var ReactCompositeComponentMixin = {
      mountComponent: function(rootID, transaction) {
        //...
        // 挂载组件时检查 __reactAutoBindMap 存在则执行 _bindAutoBindMethods 方法
        if (this.__reactAutoBindMap) {
          this._bindAutoBindMethods();
        }
        //...
      },
      _bindAutoBindMethods: function() {
        // 遍历 __reactAutoBindMap
        for (var autoBindKey in this.__reactAutoBindMap) {
          if (!this.__reactAutoBindMap.hasOwnProperty(autoBindKey)) {
            continue;
          }
          // 获取对应的方法
          var method = this.__reactAutoBindMap[autoBindKey];
          // 重新返回被绑定过后的方法
          this[autoBindKey] = this._bindAutoBindMethod(method);
        }
      },
      _bindAutoBindMethod: function(method) {
        // 获取到当前的组件上下文作用域
        var component = this;
        var hasWarned = false;
        function autoBound(a, b, c, d, e, tooMany) {
          invariant(
            typeof tooMany === 'undefined',
            'React.autoBind(...): Methods can only take a maximum of 5 arguments.'
          );
          // 判断组件声明周期为已挂载进行绑定
          if (component._lifeCycleState === ReactComponent.LifeCycle.MOUNTED) {
            return method.call(component, a, b, c, d, e);
          } else if (!hasWarned) {
            hasWarned = true;
            if (__DEV__) {
              console.warn(
                'React.autoBind(...): Attempted to invoke an auto-bound method ' +
                'on an unmounted instance of `%s`. You either have a memory leak ' +
                'or an event handler that is being run after unmounting.',
                component.constructor.displayName || 'ReactCompositeComponent'
              );
            }
          }
        }
        return autoBound;
      }
    };
    复制代码

    卸载组件

    那么到这里关于绑定组件上下文的内容也解读完了,既然有组件渲染(挂载),那么就有组件(移除)卸载,这里我们主要看到 React.unmountAndReleaseReactRootNode 方法:

    // core/ReactMount.js
    var ReactMount = {
      unmountAndReleaseReactRootNode: function(container) {
        // 意思很简单,获取 container 对应的 reactRootID
        // 从映射中找到对应的 component
        var reactRootID = getReactRootID(container);
        var component = instanceByReactRootID[reactRootID];
        // TODO: Consider throwing if no `component` was found.
        // 执行卸载方法
        component.unmountComponentFromNode(container);
        // 清除对应映射缓存
        delete instanceByReactRootID[reactRootID];
        delete containersByReactRootID[reactRootID];
      },
    };
    复制代码

    unmountComponentFromNode 方法我们看到 ReactComponent.js

    // core/ReactComponent.js
    var ReactComponent = {
      Mixin: {
        unmountComponentFromNode: function(container) {
          // 调用对应的 ReactNativeComponent 或 ReactCompositeComponent 组件的 unmountComponent 方法
          this.unmountComponent();
          // http://jsperf.com/emptying-a-node
          // 遍历移除节点
          while (container.lastChild) {
            container.removeChild(container.lastChild);
          }
        },
        unmountComponent: function() {
          // 释放内存相关,会被 ReactNativeComponent 或 ReactCompositeComponent 调起
          invariant(
            this._lifeCycleState === ComponentLifeCycle.MOUNTED,
            'unmountComponent(): Can only unmount a mounted component.'
          );
          var props = this.props;
          if (props.ref != null) {
            ReactOwner.removeComponentAsRefFrom(this, props.ref, props[OWNER]);
          }
          this._rootNode = null;
          this._rootNodeID = null;
          this._lifeCycleState = ComponentLifeCycle.UNMOUNTED;
        },
      }
    };
    复制代码

    我们来分别看下 ReactNativeComponentReactCompositeComponent 组件的 unmountComponent 方法:

    // core/ReactNativeComponent.js
    ReactNativeComponent.Mixin = {
      unmountComponent: function() {
        // 释放内存
        ReactComponent.Mixin.unmountComponent.call(this);
        // 移除子节点,方法来自于 ReactMultiChild.unmountMultiChild
        this.unmountMultiChild();
        // 移除事件监听
        ReactEvent.deleteAllListeners(this._rootNodeID);
      }
    };
    复制代码

    遍历卸载子组件

    // core/ReactMultiChild.js
    var ReactMultiChildMixin = {
      unmountMultiChild: function() {
        var renderedChildren = this._renderedChildren;
        // 遍历卸载
        for (var name in renderedChildren) {
          if (renderedChildren.hasOwnProperty(name) && renderedChildren[name]) {
            var renderedChild = renderedChildren[name];
            renderedChild.unmountComponent && renderedChild.unmountComponent();
          }
        }
        // 释放内存
        this._renderedChildren = null;
      },
    };
    复制代码
    // core/ReactCompositeComponent.js
    var ReactCompositeComponentMixin = {
      unmountComponent: function() {
        this._compositeLifeCycleState = CompositeLifeCycle.UNMOUNTING;
        // 执行生命周期
        if (this.componentWillUnmount) {
          this.componentWillUnmount();
        }
        this._compositeLifeCycleState = null;
    
        ReactComponent.Mixin.unmountComponent.call(this);
        // 执行 ReactNativeComponent.unmountComponent
        this._renderedComponent.unmountComponent();
        // 释放内存
        this._renderedComponent = null;
    
        if (this.refs) {
          this.refs = null;
        }
    
        // Some existing components rely on this.props even after they've been
        // destroyed (in event handlers).
        // TODO: this.props = null;
        // TODO: this.state = null;
      },
    };
    复制代码

    那么到此,实现组件渲染功能。


    验证组件方法

    这里还有一点需要提的到是 React.isValidComponent 方法,用来校验是否为 React 组件:

    // core/ReactComponent.js
    var ReactComponent = {
      isValidComponent: function(object) {
        // 鸭子类型校验法
        // object 存在切具有 mountComponentIntoNode 方法 和 receiveProps 方法
        return !!(
          object &&
          typeof object.mountComponentIntoNode === 'function' &&
          typeof object.receiveProps === 'function'
        );
      },
    };
    复制代码
    展开全文
  • 前言 阅读源码时,有许多变量在程序运行过程中不断...如果靠脑袋记,简单点的代码还好。复杂的代码。。。你懂的。随着react被广泛使用,很多人会好奇react是怎么实现的。会有一探源码的想法。如果直接阅读react.d...
        

    前言

    阅读源码时,有许多变量在程序运行过程中不断的产生,其中存放着什么东西,一直是一个比较头疼的问题。不停的推导增加了验算的负担,随着代码逐渐的深入,也会产生一定的记忆负担。如果靠脑袋去记,简单点的代码还好。复杂的代码。。。你懂的。
    随着react被广泛使用,很多人会好奇react是怎么实现的。会有一探源码的想法。如果直接阅读react.development.js是很简单,页面引入就好了。但是react.development.js终于是经过编译工具编译过的代码,很多的代码看起来并不直观。理想的情况是直接引用源文件,也就是github上react仓库中,packages目录下的代码,直接阅读es6的代码。
    但是es6代码浏览器支持并不友好。所以需要配置webpack打包成es5。同时需要配上sourceMap。这样,既可以让源码跑在浏览器环境,也可以直接读es6的代码,而且可以随时打断点,查看变量里保存的值。

    那么,闲言少叙,开始本章的主题。

    目录结构

    概述

    这是我目前用的一个简单的目录结构。此次调试的代码为react 16.4.0

    详情

    • node_modules 存放依赖的包
    • packages github上的packages文件夹直接拿来用
    • test-env 测试用的目录

      • index.js #引用react、react-dom的启动文件
      • index.less
      • tpl.html # html模板文件

    clipboard.png

    webpack的配置

    概要

    webpack的配置就是常规的babel,和一堆loader。为了提高打包速度,可以使用Happypack插件。如果觉得速度还不够快,可以再引入DLLplugin。此处webpack的使用不是重点,在此只是简单给出打包需要的基本配置
    代码如下:

    详情

    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const path = require('path');
    const webpack = require('webpack');
    const os = require('os');
    const HappyPack = require('happypack');
    const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
    module.exports = {
      mode: 'development',
      entry: './test-env/index.js',
      output: {
        path: __dirname + '/dist',
        filename: 'index_bundle.js'
      },
      module: {
        rules: [{
            test: /\.css$/,
            use: {
              loader: 'happypack/loader?id=happyLess',
            }
          },
          {
            test: /\.less$/,
            use: {
              loader: 'happypack/loader?id=happyLess',
            }
          },
          {
            test: /\.js$/,
            use: {
              loader: 'happypack/loader?id=happyBabel',
            }
          }
        ]
      },
      plugins: [
        
        new HtmlWebpackPlugin({
          template: './test-env/tpl.html'
        }), new webpack.HotModuleReplacementPlugin(),
        new webpack.DefinePlugin({
          __DEV__: true,
          NODE_ENV:JSON.stringify("development"),
          spyOnDev: false,
          spyOnDevAndProd: false,
          spyOnProd: false,
          __PROFILE__: true,
        }),
        new HappyPack({
            //用id来标识 happypack处理那里类文件
          id: 'happyBabel',
          //如何处理  用法和loader 的配置一样
          loaders: [{
            loader: 'babel-loader?cacheDirectory=true',
          }],
          //共享进程池
          threadPool: happyThreadPool,
          //允许 HappyPack 输出日志
          verbose: true,
        }),
        new HappyPack({
          //用id来标识 happypack处理那里类文件
          id: 'happyLess',
          //如何处理  用法和loader 的配置一样
          loaders: ['style-loader','css-loader','less-loader'],
          //共享进程池
          threadPool: happyThreadPool,
          //允许 HappyPack 输出日志
          verbose: true,
        }),
      ],
      devtool: "inline-source-map", // enum
      devServer: {
        contentBase: path.join(__dirname, 'test-env'),
        port: 9000,
        hot: true,
        overlay: true
      },
      resolve: {
        modules: [
          "node_modules",
          path.resolve(__dirname, "packages"),
          path.resolve(__dirname, "packages/shared")
        ],
        alias: {
          '@packages': path.resolve(__dirname, 'packages/'),
        }
      }
    };
    

    babel配置

    {
      "presets": ["env","react","stage-0"],
    }
    

    入口文件

    概述

    为了简化项目流程,此处用了一个最简单的tpl.html模板文件,通过webpackhtmlplugin引入了打包好后的带sourcemap的js。
    其中,js只是用react实现了一个简单的Hello world并且用react-render渲染到页面上。

    index.js

    import './index.less';
    import React, {Component} from '@packages/react';
    import ReactDOM from '@packages/react-dom/index.js';
    
    class Hello extends Component{
      constructor(){
        super();
        console.log('hello world');
      }
    
      render() {
        return <div>hello world</div>
      }
    }
    
    ReactDOM.render(<Hello></Hello>, document.getElementById('root'));

    其中@packages是我在webpack中配置的路径别名,对应packages目录。

    tpl.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>Document</title>
    </head>
    <body>
      <div id="root">
        hello world;
      </div>
    </body>
    </html>

    概述

    至此,环境已经配的差不多了。只是实际运行的时候还是需要安装一些包。此处忽略了装包的过程。但是实际项目运行中控制台还是报了一些错。其中一个最纠结的地方

    This module must be shimmed by a specific renderer.

    自环境配好后我遇到的第一个问题是这句话。起初以为是react的问题。纠结了好几个小时候,才找到了问题的根源。

    问题并不来react这个包

    既然问题不是react,那来自哪里呢。没错,来自react-dom,因为页面上总共就引用了两个包,非react则react-dom

    问题解决思路

    通过错误信息处函数调用堆栈可以看出,该错误来源于一个ReactFiberHostConfig.js中。虽然不知道这里为什么会有这个,但是还是想去源仓库找找答案。看了react官方实际使用了rollup去打包代码。但是rollup里一大坨看不懂得代码。那怎么办呢。去搜索这个hostconfig.js是什么鬼。

    貌似的答案

    react为了实现将同样的结构,render到不同的平台,使用了react-reconciler做了一个中间层。提供接口可以让用户自定义render的实现,并且给出了一个render-dom的示例。但是不管使用哪种render,都要提供一个hostconfig,源码中这个react-reconciler文件夹中引用的config总是会抛出一段错误,但是实际上,正确的config已经在文件夹中给出,只要将抛错这段代码给替换成引用正确的config即可。

    归纳总结

    错误调用栈 react-dom-》react-reconciler-》hostconfig error
    

    解决

    react-reconclier中src下的ReactFiberHostConfig中的报错改为引用正确的config。贴上引用代码
    
    export * from './forks/ReactFiberHostConfig.dom';

    完结撒花

    至此已经可以在浏览器中调试es6版本的react代码。不过这个环境配置过程中并非一帆风顺。首先遇到错首先以为是react的问题。纠结了好几个晚上(下班后才开始想),网上查阅了很多资料,也没想出所以然,最后发现是react-dom的问题,也纠结了好久,最终终于配起了这套环境。

    喜欢的朋友点个赞哈。谢谢支持。

    展开全文
  • React源码解析

    2018-11-12 15:36:15
    距离第一篇《React源码解析(一)》已经过去将近4个月的时间,由于是我第一次进行源码解析相关的写作,思路和文笔还不够成熟。一百多天以来,我基于读者反馈反思这几篇文章中的不足,同时也在不断学习借鉴其他优秀作者...
  • 阅读源码成了今年的学习目标之一,在选择 Vue 和 React 之间,我想先阅读 React 。 在考虑到哪个版本的时候,我想先接触到源码早期的思想可能会更轻松一些,最终我选择阅读 0.3-stable 。 那么接下来,我将从几个...
  • react源码分析

    千次阅读 2017-05-23 11:03:44
    原文地址:http://www.html-js.com/article/JS-analysis-of-the-single-row-from-zero-reactjs-source-first-rendering-principle%203154 前端的发展特别快,经历过jQuery一统天下的工具库时代后,现在各种框架又...
  • 阅读源码成了今年的学习目标之一,在选择 Vue 和 React 之间,我想先阅读 React 。 在考虑到哪个版本的时候,我想先接触到源码早期的思想可能会更轻松一些,最终我选择阅读 0.3-stable 。 那么接下来,我将从几个...
  • 这是我的剖析 React 源码的第四篇文章,...React 16.8.6 源码中文注释,如果你想读源码但是又怕看不懂的话,可以通过我这个仓库来学习 之前的所有文章合集 为什么需要调度? 大家都知道 JS 和渲染引擎是一个互斥关...
  • 这是我的 React 源码解读课的第一篇文章,首先来说说为啥要写这个系列文章: 现在工作中基本都用 React 了,由此想了解下内部原理 市面上 Vue 的源码解读数不胜数,但是反观 React 相关的却寥寥无几,也是因为 ...
  • 由于微信外链限制,推荐阅读等链接无法点击,可点击阅读原文跳转至原文,查看外链。React源码分析与实现(二):状态、属性更新 -> setState原文链接地址:h...
  • 阅读源码成了今年的学习目标之一,在选择 Vue 和 React 之间,我想先阅读 React 。 在考虑到哪个版本的时候,我想先接触到源码早期的思想可能会更轻松一些,最终我选择阅读 0.3-stable 。 那么接下来,我将从几个...
  • 阅读源码成了今年的学习目标之一,在选择 Vue 和 React 之间,我想先阅读 React 。 在考虑到哪个版本的时候,我想先接触到源码早期的思想可能会更轻松一些,最终我选择阅读 0.3-stable 。 那么接下来,我将从几个...
  • 很多新入行的前端开发新手想提高前端技术,进阶必备...网络 上解读React源码的文章很多,但知识点零散,学习起来抓不住重点,本课程将系统性的带你从API层面实现React的大部分核心功能,轻松搞定React源码级核心原理。
  • 新建react_simple目录,并新建index.html和index.js两个文件。 全局安装parcel,一个比webpack更简单的打包工具。 npm install parcel -g 然后在开发环境中安装parcel-bundler npm install parcel-bundler --save-...
  • 阅读源码成了今年的学习目标之一,在选择 Vue 和 React 之间,我想先阅读 React 。 在考虑到哪个版本的时候,我想先接触到源码早期的思想可能会更轻松一些,最终我选择阅读 0.3-stable 。 那么接下来,我将从几个...
  • 本篇前两部分内容简单介绍一下ReactNative,后面的章节会把整个RN框架的iOS部分,进行代码层面的一一梳理 全文是不是有点太长了,我要不要分拆成几篇文章 函数栈代码流程图,由于采用层次缩进的形式,层次关系比较...
  • React的组件其实就是一个个js对象, 组件之间的嵌套其实就是在父组件对象的props里面增加一个children属性   组件的挂载: ReactDOM.render(component,mountNode)
  • 本文通过对React事件系统和源码进行浅析,回答“为什么React需要自己实现一套事件系统?”和“React的事件系统是怎么运作起来的?”两个问题。React为了性能和复用,采用了事件代理,池,批量更新,跨浏览器和跨平台...
  • 分享之前我还是要推荐下我自己创建的...,今天的源码已经上传到群文件,不定期分享干货,包括我自己整理的一份最新的适合2018年学习的前端资料和零基础入门教程,欢迎初学和进阶中的小伙伴。如何学习React如果你是一...
1 2 3 4 5 ... 20
收藏数 6,020
精华内容 2,408
关键字:

应该如何去读react源码