React傳-1

原文html

寫在前面

計劃用半年的時間去深刻 React 源碼並記錄下來,本文是系列文章第一章,前面大多數會以功能爲主,不會涉及太多事務機制與流程,後半部分以架構、流程爲主。這個是一個水到渠成的事情。看的越多,對其理解的廣度就越大,深度也隨之沉澱,在深刻的同時站在做者的角度去思考,可以脫離源碼照葫蘆畫瓢,才能算理解,讀懂自己源碼並不重要。可能沒有什麼休息時間,可是會盡可能擠出來完善文章,也算是一種興趣與習慣。前端

起源

2011 年,前端工程師 Jordan Walke 建立了ReactJS的早期原型FaxJS。時間軸傳送門react

從入口開始

時至今日(2019.9.28),五個小時前React已經將版本更新到 16.10.0 了,預計大半年內將步入17大版本。但願在系列文章完結以後更新(省得我又得看一遍)。api

React 與 Vue 的源碼相同的使用 Facebook 開源的 Flow 靜態類型檢查工具,爲何要用 Flow 而不用 Typescript ? 緣由多是 React 誕生的時間較早,那時候尚未 Typescript,後來也因爲 Typescript 15年被社區普遍接受才火起來。還一個緣由是 Flow 沒有 Typescript 那麼「嚴格」,全部的檢查都是可選的。數組

fork/clone/open三部曲,找到 packages/react/src/React.js,剔除註釋和空白行的源碼還不到一百行,這個入口文件集成了全部的api暴露出去。前端工程師

React中的源碼與React-DOM分離,因此在packages/React內不少只是「形」上的API架構

import ReactVersion from '../../shared/ReactVersion';
import {
  REACT_FRAGMENT_TYPE,
  REACT_PROFILER_TYPE,
  REACT_STRICT_MODE_TYPE,
  REACT_SUSPENSE_TYPE,
  REACT_SUSPENSE_LIST_TYPE,
} from '../../shared/ReactSymbols';
複製代碼

最頂部導入了React當前版本號,ReactSymbols 文件管理着全局的 React功能組件 Symbol 標誌app

Component

import {Component, PureComponent} from './ReactBaseClasses';
複製代碼

ComponentPureComponent 組件都是常常用的, 猜也能猜到都是定義一些初始化方法。dom

function Component(props, context, updater) {
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue; // 更新器
}

Component.prototype.isReactComponent = {};

Component.prototype.setState = function(partialState, callback) {
  invariant(
    typeof partialState === 'object' ||
      typeof partialState === 'function' ||
      partialState == null,
    'setState(...): takes an object of state variables to update or a ' +
      'function which returns an object of state variables.',
  );
  this.updater.enqueueSetState(this, partialState, callback, 'setState');// 加入更新隊列
};

Component.prototype.forceUpdate = function(callback) {
  this.updater.enqueueForceUpdate(this, callback, 'forceUpdate'); // 強制加入更新隊列
};
複製代碼

定義了Component類的 setStateforceUpdate 方法,以便在組件實例化後調用,將當前的props,context,refs進行綁定,並初始化更新。異步

每一個組件內部都有一個 updater ,被用來驅動state更新的工具對象,執行更新隊列,沒傳入updater時,this.updater 默認爲 ReactNoopUpdateQueue,可是它沒什麼意義,只是作警告用的。

const ReactNoopUpdateQueue = {
  isMounted: function(publicInstance) {
    return false;
  },
  enqueueForceUpdate: function(publicInstance, callback, callerName) {
    warnNoop(publicInstance, 'forceUpdate');
  },
  enqueueReplaceState: function(publicInstance, completeState, callback, callerName) {
    warnNoop(publicInstance, 'replaceState');
  },
  enqueueSetState: function(publicInstance, partialState, callback, callerName) {
    warnNoop(publicInstance, 'setState');
  },
};
複製代碼

isMounted 在組件未掛載的狀況下isMounted 一直會返回 false,例如在 constructor 裏調用 setState或者組件已卸載/未使用,其餘方法的做用是在開發環境下警告用戶不要在constructor 內調用this原型上的方法。由於實際上真正的 updater 都是在 renderer 後注入的。真正的updater:

const classComponentUpdater = {
  isMounted, // fn() => true
  enqueueSetState(inst, payload, callback) {
     // 獲取fiber 也就是inst._reactInternalFiber
    const fiber = getInstance(inst);

    // 根據 msToExpirationTime(performance.now()) 獲得一個時間,後續涉及到 ExpirationTime
    const currentTime = requestCurrentTimeForUpdate();

    // 獲取suspense配置,與即將新增的 withSuspenseConfig Api強相關,默認狀況下都是null
    const suspenseConfig = requestCurrentSuspenseConfig();

    // 根據開始實際計算任務過時時間
    const expirationTime = computeExpirationForFiber(
      currentTime,
      fiber,
      suspenseConfig,
    );
    //建立update對象
    const update = createUpdate(expirationTime, suspenseConfig);

    //setState的更新對象
    update.payload = payload;

    // setState的callback
    if (callback !== undefined && callback !== null) {
      if (__DEV__) {
        warnOnInvalidCallback(callback, 'setState');
      }
      update.callback = callback;
    }
    // 放入隊列
    enqueueUpdate(fiber, update);

    // 開始調度
    scheduleWork(fiber, expirationTime);
  },
  // other ...
}
複製代碼

setState的任務調度以這種形式發出的,另外 與forceUpdate 、 replaceState 也差很少。

那上面提到的ExpirationTime是什麼?

ExpirationTime

ExpirationTime是一個「保險」,爲防止某個update由於優先級的緣由一直被打斷而未能執行。React會設置一個ExpirationTime,當時間到了ExpirationTime的時候,若是某個update還未執行的話,React將會強制執行該update,這就是ExpirationTime的做用。它有兩種計算方法,一種computeInteractiveExpiration同步更新,與 computeAsyncExpiration 返回異步更新的expirationTime

//整型最大數值,V8中針對32位系統所設置的最大值 Math.pow(2,30) - 1;
export const Sync = MAX_SIGNED_31_BIT_INT; // 1073741823
export const Batched = Sync - 1; // 1073741822

const UNIT_SIZE = 10;
const MAGIC_NUMBER_OFFSET = Batched - 1; //1073741821

function msToExpirationTime(ms) {
  return MAGIC_NUMBER_OFFSET - (ms / UNIT_SIZE | 0); // 1073741821 - now()/10|0
}

function ceiling(num: number, precision: number): number {
  return (((num / precision) | 0) + 1) * precision; // 取整,本次區間分類最大值
}

// 計算過時時間
function computeExpirationBucket(currentTime, expirationInMs, bucketSizeMs): ExpirationTime {
  /* LOW 任務 => 1073741821 - ceiling(1073741821 - currentTime + 5000 / 10, 250 / 10) 1073741821 - (((1073741821 - currentTime + 500) / 25) | 0) * 25 - 25 */

  /* HIGH任務 => 1073741821 - ceiling(1073741821 - currentTime + (__DEV__ ? 500 : 150) / 10, 100 / 10) DEV 1073741821 - ceiling(1073741821 - currentTime + 50, 10) 1073741821 - (((1073741821 - currentTime + 50) / 10) | 0) * 10 - 10 !DEV 1073741821 - ceiling(1073741821 - currentTime + 15, 10) 1073741821 - (((1073741821 - currentTime + 15) / 10) | 0) * 10 - 10 */
  return (
    MAGIC_NUMBER_OFFSET - ceiling(MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE, bucketSizeMs / UNIT_SIZE)
  );
}


// LOW 低優先級任務
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,
  );
}

// HIGH 高優先級任務
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 function computeSuspenseExpiration( currentTime: ExpirationTime, timeoutMs: number, ): ExpirationTime {
  return computeExpirationBucket(
    currentTime,
    timeoutMs,
    LOW_PRIORITY_BATCH_SIZE,
  );
}
複製代碼

經過performance.now()生成 currentTime,當不支持performance時轉利用Date.now(),這個值不用太過關注,只須要理解成時間戳便可。

const now1 = performance.now(); // 53028380
const now2 = performance.now(); // 53028389 ↑ 9
const now3 = performance.now(); // 53028391 ↑ 11
const now4 = performance.now(); // 53028405 ↑ 25
const now5 = performance.now(); // 53028420 ↑ 40
const now6 = performance.now(); // 53028430 ↑ 50
const now7 = performance.now(); // 53028444 ↑ 55
const now8 = performance.now(); // 53028468 ↑ 79

// LOW 任務
1073741821 - (((1073741821 - now1 + 500) / 25) | 0) * 25 - 25; // 53027871
1073741821 - (((1073741821 - now2 + 500) / 25) | 0) * 25 - 25; // 53027871 ↑ 0
1073741821 - (((1073741821 - now3 + 500) / 25) | 0) * 25 - 25; // 53027871 ↑ 0
1073741821 - (((1073741821 - now4 + 500) / 25) | 0) * 25 - 25; // 53027896 ↑ 25
1073741821 - (((1073741821 - now5 + 500) / 25) | 0) * 25 - 25; // 53027896 ↑ 25
1073741821 - (((1073741821 - now6 + 500) / 25) | 0) * 25 - 25; // 53027921 ↑ 50
1073741821 - (((1073741821 - now7 + 500) / 25) | 0) * 25 - 25; // 53027921 ↑ 50
1073741821 - (((1073741821 - now8 + 500) / 25) | 0) * 25 - 25; // 53027946 ↑ 75

// HIGH 任務 以DEV模式爲例
1073741821 - (((1073741821 - now1 + 50) / 10) | 0) * 10 - 10; // 53028321
1073741821 - (((1073741821 - now2 + 50) / 10) | 0) * 10 - 10; // 53028331 ↑ 10
1073741821 - (((1073741821 - now3 + 50) / 10) | 0) * 10 - 10; // 53028331 ↑ 10
1073741821 - (((1073741821 - now4 + 50) / 10) | 0) * 10 - 10; // 53028351 ↑ 30
1073741821 - (((1073741821 - now5 + 50) / 10) | 0) * 10 - 10; // 53028361 ↑ 40
1073741821 - (((1073741821 - now6 + 50) / 10) | 0) * 10 - 10; // 53028371 ↑ 50
1073741821 - (((1073741821 - now7 + 50) / 10) | 0) * 10 - 10; // 53028391 ↑ 70
1073741821 - (((1073741821 - now8 + 50) / 10) | 0) * 10 - 10; // 53028411 ↑ 90
複製代碼

經過規律,能夠看到LOW優先級任務時,區間<25的,獲得的都是同一個值,而HIGH高優先級任務的區間爲10,單位爲毫秒,這個有什麼用呢?

若是觸發了屢次事件,每次難道都要丟enqueueUpdate裏當即調度,那未免性能太差了。

React讓兩個相近(25ms內)的update獲得相同的expirationTime,它能夠將屢次事件分批打包丟入 enqueueUpdate裏,假如在24ms內觸發了兩個事件,那麼React會將他們丟入同一批車,目的就是讓這兩個update自動合併成一個Update,而且只會觸發一次更新,從而達到批量更新的目的。

從以前的代碼看 computeInteractiveExpiration傳入的是150、100,computeAsyncExpiration傳入的是5000、250,前者的優先級更高,而過時執行時間爲交互事件爲 100/UNIT_SIZE = 10,異步事件則爲 250/UNIT_SIZE = 25, 佐證了事實。

PureComponent

function ComponentDummy() {}
ComponentDummy.prototype = Component.prototype;

function PureComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}

const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;
// Avoid an extra prototype jump for these methods.
Object.assign(pureComponentPrototype, Component.prototype);
pureComponentPrototype.isPureReactComponent = true;
複製代碼

Component 類相同屬性的 PureComponent 類有所不一樣,首先建立了一個空函數 ComponentDummy,並將經過共享原型繼承的方式將實例原型指向了 Component 的原型,其構造函數指定爲PureComponent。其實就是在外面套了一層 pureComponentPrototypeComponent

createRef

import {createRef} from './ReactCreateRef';
複製代碼
export function createRef(): RefObject {
  const refObject = {
    current: null,
  };
  if (__DEV__) {
    Object.seal(refObject);
  }
  return refObject;
}
複製代碼

返回一個refObject,其current的屬性在組件掛載時進行關聯,與react-dom強相關,後面再瞭解。如今只須要知道它很簡單。

Children

import {forEach, map, count, toArray, only} from './ReactChildren';
複製代碼

React 將 children 的 API 暴露出來,這裏最常使用的應該是 React.Children.mapReact.Children.forEach

Children.map

適用於替代 this.props.children.map ,由於這種寫法一般用來嵌套組件,可是若是嵌套的是一個函數就會報錯。而 React.Children.map 則不會。當須要寫一個 Radio 組件須要依賴其父組件 RadioGroupprops 值,那麼this.props.children.map 配合 cloneElement 簡直不能再完美。還能夠用來過濾某些組件。

React.cloneElement(props.children, {
  name: props.name
})

render(){
  return (
    <div> { React.Children.map(children, (child, i) => { if ( i < 1 ) return; return child; }) } </div>
  )
}
複製代碼

mapChildren

function mapChildren(children, func, context) {
  if (children == null) {
    return children;
  }
  const result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, func, context);
  return result;
}
複製代碼

mapIntoWithKeyPrefixInternal

第一步,若是子組件爲null直接不處理。正常狀況下申明一個數組,進行加工。

function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
  let escapedPrefix = '';
  if (prefix != null) { // 顧名思義,處理key
    escapedPrefix = escapeUserProvidedKey(prefix) + '/';
  }

  // 取出一個對象,做爲上下文,遍歷children
  const traverseContext = getPooledTraverseContext(
    array,
    escapedPrefix,
    func,
    context,
  );
  traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
  // 釋放對象
  releaseTraverseContext(traverseContext);
}
複製代碼

getPooledTraverseContext 與 releaseTraverseContext

第二步,處理key的暫時不用管。最終經過 getPooledTraverseContext 到對象池裏取一個對象,給 traverseAllChildren 進行處理,結束的時候經過 releaseTraverseContext reset全部屬性放回去,作到複用,避免了一次性建立大量對象和釋放對象消耗性能形成的內存抖動。

getPooledTraverseContext 用來取。 releaseTraverseContext 用來清空後放回

// 維護一個大小爲 10 的對象重用池
const POOL_SIZE = 10;
const traverseContextPool = [];

function getPooledTraverseContext( mapResult, keyPrefix, mapFunction, mapContext, ) {
  if (traverseContextPool.length) { // 若是當前對象池內有可用對象,就從隊尾pop一個初始化後返回
    const traverseContext = traverseContextPool.pop();
    traverseContext.result = mapResult;
    traverseContext.keyPrefix = keyPrefix;
    traverseContext.func = mapFunction;
    traverseContext.context = mapContext;
    traverseContext.count = 0;
    return traverseContext;
  } else { // 不然返回一個新的對象
    return {
      result: mapResult,
      keyPrefix: keyPrefix,
      func: mapFunction,
      context: mapContext,
      count: 0,
    };
  }
}

function releaseTraverseContext(traverseContext) {
  traverseContext.result = null;
  traverseContext.keyPrefix = null;
  traverseContext.func = null;
  traverseContext.context = null;
  traverseContext.count = 0;
  if (traverseContextPool.length < POOL_SIZE) { // 若是對象池內沒滿,就放到對象池內,等待複用
    traverseContextPool.push(traverseContext);
  }
}
複製代碼

traverseAllChildren/traverseAllChildrenImpl

第三步,最重要的一步,在取出一個待複用對象後,traverseAllChildren 判斷爲null就不必處理了。直接 return。

function traverseAllChildren(children, callback, traverseContext) {
  if (children == null) {
    return 0; // return
  }
  return traverseAllChildrenImpl(children, '', callback, traverseContext);
}
複製代碼
function traverseAllChildrenImpl( children, // children nameSoFar, // 父級 key callback, // 若是是可渲染節點 traverseContext, // 對象池複用對象 ) {
  const type = typeof children;

  if (type === 'undefined' || type === 'boolean') {
    // 以上都被認爲是null。
    children = null;
  }
  // 若是是可渲染的節點則爲true,表示能調用callback
  let invokeCallback = false;
  
  if (children === null) {
    invokeCallback = true;
  } else {
    switch (type) {
      case 'string':
      case 'number':
        invokeCallback = true;
        break;
      case 'object':
        switch (children.$$typeof) {
          case REACT_ELEMENT_TYPE: // React元素 或者是 Portals
          case REACT_PORTAL_TYPE:
            invokeCallback = true;
        }
    }
  }

  if (invokeCallback) { // 可渲染節點,直接調用回調
    callback( // 調用 mapSingleChildIntoContext
      traverseContext,
      children,
      nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,
    );
    return 1;
  }

  let child;
  let nextName;
  let subtreeCount = 0; //在當前子樹中找到的子級的層級數。
  const nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;

  if (Array.isArray(children)) { // 若是children是數組,則遞歸處理
  // 例如 React.Children.map(this.props.children, c => [[c, c]])
  // c => [[c, c]] 會被攤平爲 [c, c, c, c]
    for (let i = 0; i < children.length; i++) {
      child = children[i];
      nextName = nextNamePrefix + getComponentKey(child, i); // 在每一層不斷用「:」分隔拼接key
      subtreeCount += traverseAllChildrenImpl(
        child,
        nextName,
        callback,
        traverseContext,
      );
    }
  } else { // 若是是對象的話經過 obj[Symbol.iterator] 取迭代器
    const iteratorFn = getIteratorFn(children);
    if (typeof iteratorFn === 'function') { // 若是是迭代器是函數就拿到結果
      const iterator = iteratorFn.call(children);
      let step;
      let ii = 0;
      while (!(step = iterator.next()).done) { // 繼續遞歸處理
        child = step.value;
        nextName = nextNamePrefix + getComponentKey(child, ii++);
        subtreeCount += traverseAllChildrenImpl(
          child,
          nextName,
          callback,
          traverseContext,
        );
      }
    } else if (type === 'object') { // 若是迭代器是普通對象也就沒法迭代
      let addendum = '';
      const childrenString = '' + children;
      invariant(
        false,
        'Objects are not valid as a React child (found: %s).%s',
        childrenString === '[object Object]'
          ? 'object with keys {' + Object.keys(children).join(', ') + '}'
          : childrenString,
        addendum,
      );
    }
  }

  return subtreeCount;
}
複製代碼
const Demo = ({ children }) => {
  console.log(React.Children.map(children, c => [[[[c, c]]]]));
  return (
    children
  );
};
const Children = ({ msg }) => (
  <span> { msg } </span>
);
複製代碼

alt

上面函數的核心做用就是經過把傳入的 children 數組經過遍歷攤平成單個節點,其中迭代的全部callback都是 mapSingleChildIntoContext

mapSingleChildIntoContext

// bookKeeping getPooledTraverseContext 內從複用對象池取出來的 traverseContext
// child 傳入的節點
// childKey 節點的 key
function mapSingleChildIntoContext(bookKeeping, child, childKey) {
  const {result, keyPrefix, func, context} = bookKeeping;
  
  // func === React.Children.map(props.children, c => c) 的 c => c 函數
  let mappedChild = func.call(context, child, bookKeeping.count++);
  // 若是func返回值設定爲數組 React.Children.map(this.props.children, c => [c, c])
  // 表示每一個元素將被返回兩次。假如children爲 c1,c2,那麼最後返回的應該是c1,c1,c2,c2
  if (Array.isArray(mappedChild)) {
    // 是數組的話,就再調用 mapIntoWithKeyPrefixInternal
    // 和 mapChildren 調用它的流程同樣。遞歸將其鋪平
    mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);
  } else if (mappedChild != null) {
    // 若是是不爲null而且是有效的 Element
    if (isValidElement(mappedChild)) {
      // 克隆 Element && 替換掉key 推入result
      mappedChild = cloneAndReplaceKey(
        mappedChild,
        keyPrefix + (mappedChild.key && (!child || child.key !== mappedChild.key)
            ? escapeUserProvidedKey(mappedChild.key) + '/' : '') + childKey,
      );
    }
    result.push(mappedChild);
  }
}
複製代碼

最終的邏輯又回到了 mapIntoWithKeyPrefixInternal ,經過遞歸調用使返回的數組結果展開鋪平。

總體流程

大概總體流程

`mapChildren` ===================> `mapIntoWithKeyPrefixInternal` ==================> `getPooledTraverseContext`(複用對象池)
                                                  /\                                                ||
                                                 /||\                                               ||
                                                  ||                                                ||
                                                  ||                                               \||/
                                                  ||                                                \/
                                                  ||Yes                                 `traverseAllChildren`(遍歷children樹)
                                                  ||                                                ||
                                                  ||                                                ||
                                                  ||                                               \||/
                                                  ||                                                \/
                                     No           ||                                        (children是數組又會從新遞歸執行)
`releaseTraverseContext`(釋放對象池)<=====`mapSingleChildIntoContext`(鋪平result)<=============`traverseAllChildrenImpl`
複製代碼

Children.forEach

相比 mapforEachChildren 則簡單的多,由於不用去返回一個新的結果,只須要對children作遍歷,

function forEachChildren(children, forEachFunc, forEachContext) {
  if (children == null) {
    return children;
  }
  const traverseContext = getPooledTraverseContext(
    null, // 不須要返回數組,因此result爲null
    null, // key也不須要
    forEachFunc,
    forEachContext,
  );
  // 第二個參數 forEachSingleChild 簡單調用了 forEachFunc
  traverseAllChildren(children, forEachSingleChild, traverseContext);
  releaseTraverseContext(traverseContext);
}

function forEachSingleChild(bookKeeping, child, name) {
  const {func, context} = bookKeeping;
  func.call(context, child, bookKeeping.count++);
}

複製代碼

Children.count

function countChildren(children) {
  return traverseAllChildren(children, () => null, null);
}
複製代碼

用來計算children的個數,平時用的較少。和前面2個方法的行爲差很少,預置的callback也不會進行任何處理。最終返回當前children的子元素,並不會向下遞歸查找。

Children.toArray

function toArray(children) {
  const result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, child => child);
  return result;
}
複製代碼

用來將children轉化成普通的數組,原理和 mapChildren 同樣,能夠用來將傳入的children從新進行排序。

class Sort extends React.Component {
   render () {
     const children = React.Children.toArray(this.props.children);
     return <p>{children.sort((a,b)=>a-b).join('-')}</p>
   }
 }

 <Sort>
  {2}{5}{8}{4}{9}
 </Sort>

//  view
2-4-5-8-9
複製代碼

Children.only

function onlyChild(children) {
  invariant(
    isValidElement(children),
    'React.Children.only expected to receive a single React element child.',
  );
  return children;
}
複製代碼

校驗children是否爲ReactElement,是則返回,不然報錯。能夠用來製作一個只接受一個 children<Single> 組件。

class Single extends Component{
  render(){
    return React.Children.only(this.props.children)
  }
}

function App(){
  return (
      <Single> <div>first</div> <div>second</div> {/* error */} </Single>
  )
}
複製代碼

ReactElement

import {
  createElement,
  createFactory,
  cloneElement,
  isValidElement,
  jsx,
} from './ReactElement';
const React = {
  // ...
  createElement: __DEV__ ? createElementWithValidation : createElement,
  cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement,
  createFactory: __DEV__ ? createFactoryWithValidation : createFactory,
  isValidElement: isValidElement
}

複製代碼

開發環境下會自動validator,切到packages/react/src/ReactElement.js,大綱以下

hasValidRef ------------------------------- 判斷是否有合理的ref
hasValidKey ------------------------------- 判斷是否有合理的key
defineRefPropWarningGetter ---------------- 鎖定props.ref
defineKeyPropWarningGetter ---------------- 鎖定props.key
ReactElement ------------------------------ 轉化ReactElement
jsx --------------------------------------- 使用jsx方式建立Element
jsxDEV ------------------------------------ 使用jsx方式建立Element(DEV)
createElement ----------------------------- 建立並返回指定類型的ReactElement
createFactory ----------------------------- 工廠模式createElement構造器
cloneAndReplaceKey ------------------------ 替換新key
cloneElement ------------------------------ 克隆Element
isValidElement ---------------------------- 斷定是否ReactElement
複製代碼

createElement

這個方法你們耳熟能詳。React用的最多的方法,沒有之一。它負責React內全部元素節點的建立及初始化。

該方法接受三個參數 type, config, children ,type 是標籤或者組件的名稱,div/span/ul,對應的自定義Component首字母必定是大寫。config 則包含全部的屬性配置,children 表明子節點。

export function createElement(type, config, children) {
  let propName;

  const props = {};

  let key = null;
  let ref = null;
  let self = null;
  let source = null;

  if (config != null) {
    if (hasValidRef(config)) {// 查找config內是否存在合理的ref
      ref = config.ref;
    }
    if (hasValidKey(config)) {// 查找config內是否存在合理的key
      key = '' + config.key;
    }

    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // 將剩餘屬性添加到新的props對象中
    // 這也就是爲何<Component key={Math.random()}/>子組件中爲何找不到key/ref/__self/__source屬性的緣由
    for (propName in config) {
      if (
        // 忽略原型鏈上的屬性,而且抽離key/ref/__self/__source屬性
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
  }

  // children參數能夠不止一個,除去前兩個參數,其餘的都是children
  const childrenLength = arguments.length - 2;

  // 對children作格式處理。一個爲對象,多個則爲Array
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    const childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    if (__DEV__) {
      if (Object.freeze) {
        Object.freeze(childArray);
      }
    }
    props.children = childArray;
  }

  // 解析defaultProps,定義組件的默認Props屬性
  // Com.defaultProps = { msg:'default' }
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
  // 在開發環境下,鎖定props上 key與 ref 的getter,不予獲取
  if (__DEV__) {
    if (key || ref) {
      const displayName = typeof type === 'function'
          ? type.displayName || type.name || 'Unknown'
          : type;
      if (key) {
        defineKeyPropWarningGetter(props, displayName);
      }
      if (ref) {
        defineRefPropWarningGetter(props, displayName);
      }
    }
  }
  // 最後轉化成React元素
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current, // 建立React元素的組件
    props,
  );
}
複製代碼
const ReactCurrentOwner = {
  /** * @internal * @type {ReactComponent} */
  current: (null: null | Fiber),
};
複製代碼

createElement 僅僅起到爲ReactElement加工前過濾屬性的做用。

create ReactElement

const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // $$typeof將其標識爲一個React元素
    $$typeof: REACT_ELEMENT_TYPE,

    // ReactElement的內置屬性
    type: type,
    key: key,
    ref: ref,
    props: props,

    // 建立此元素的組件。
    _owner: owner,
  };
  //爲了利於測試,在開發環境下忽略這些屬性(不可枚舉),而且凍結props與element
  if (__DEV__) {
    element._store = {};
    Object.defineProperty(element._store, 'validated', {
      configurable: false,
      enumerable: false,
      writable: true,
      value: false,
    });
    // self and source are DEV only properties.
    Object.defineProperty(element, '_self', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: self,
    });
    Object.defineProperty(element, '_source', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: source,
    });
    if (Object.freeze) {
      Object.freeze(element.props);
      Object.freeze(element);
    }
  }

  return element;
};
複製代碼

ReactElement 將其屬性作二次處理,等待被渲染成DOM,在日常開發咱們經過console.log打印出自定義Component 屬性與element一致。

alt

createFactory

工廠模式的 createElement,經過預置 type 參數建立指定類型的節點。

var child1 = React.createElement('li', null, 'First Text Content');
var child2 = React.createElement('li', null, 'Second Text Content');
// 等價於
var factory = React.createFactory("li");
var child1 = factory(null, 'First Text Content');
var child2 = factory(null, 'Second Text Content');


var root  = React.createElement('ul', {className: 'list'}, child1, child2);
ReactDOM.render(root,document.getElementById('root'));
複製代碼
export function createFactory(type) {
  // 經過bind預置type參數返回新的函數
  const factory = createElement.bind(null, type);
  // Expose the type on the factory and the prototype so that it can be easily accessed on elements. E.g. `<Foo />.type === Foo`.
  // This should not be named `constructor` since this may not be the function that created the element, and it may not even be a constructor.
  // Legacy hook: remove it
  factory.type = type;
  return factory;
}
複製代碼

cloneElement

經過傳入新的element與props及children,獲得clone後的一個新元素。element 爲cloneElement,config 是 newProps,能夠從新定義 keyrefchildren 子節點。 總體方法與createElement大體相同。

export function cloneElement(element, config, children) {
  // 判斷有效的ReactElement
  invariant(
    !(element === null || element === undefined),
    'React.cloneElement(...): The argument must be a React element, but you passed %s.',
    element,
  );

  let propName;

  // copy原始 props
  const props = Object.assign({}, element.props);

  // 提取保留key & ref
  let key = element.key;
  let ref = element.ref;
  // 爲了追蹤與定位,繼承被clone的Element這三個屬性
  const self = element._self;
  const source = element._source;
  let owner = element._owner;

  if (config != null) {
    // 這裏的處理和createElement差很少
    // unique ,ref 和 key 能夠自定義覆蓋
    if (hasValidRef(config)) {
      ref = config.ref;
      owner = ReactCurrentOwner.current;
    }
    if (hasValidKey(config)) {
      key = '' + config.key;
    }

    // 其餘屬性覆蓋current props
    let defaultProps;
    if (element.type && element.type.defaultProps) {
      defaultProps = element.type.defaultProps;
    }
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        if (config[propName] === undefined && defaultProps !== undefined) {
          // Resolve default props
          props[propName] = defaultProps[propName];
        } else {
          props[propName] = config[propName];
        }
      }
    }
  }
  // 剩餘的與 createElement 同樣
  const childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    const childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    props.children = childArray;
  }

  return ReactElement(element.type, key, ref, self, source, owner, props);
}
複製代碼

cloneAndReplaceKey

顧名思義,與 cloneElement 名字上雖然差很少,但實際返回的是經過ReactElement傳入newKey從新建立的舊Element。

export function cloneAndReplaceKey(oldElement, newKey) {
  const newElement = ReactElement(
    oldElement.type,
    newKey,
    oldElement.ref,
    oldElement._self,
    oldElement._source,
    oldElement._owner,
    oldElement.props,
  );

  return newElement;
}
複製代碼

isValidElement

也很簡單,經過判斷 $$typeof 是否爲內置的ReactElement類型。

export function isValidElement(object) {
  return (
    typeof object === 'object' &&
    object !== null &&
    object.$$typeof === REACT_ELEMENT_TYPE
  );
}
複製代碼

jsx/jsxDEV

沒怎麼使用過這個API,猜想應該是經過 react-dom 轉化後使用的建立語法。

從代碼邏輯上看,與 createElement 大體形同,jsxDEVjsx 多了兩個能自定義的屬性,sourceself,按照代碼註釋,是爲了防止出現 <div key="Hi" {...props} /> 狀況中 keyprops 先定義,致使被覆蓋的狀況。將對<div {...props} key="Hi" /> 以外的全部狀況統一使用 jsxDEV 來強行賦值 key 與 ref。

export function jsxDEV(type, config, maybeKey, source, self) {
  let propName;

  // Reserved names are extracted
  const props = {};

  let key = null;
  let ref = null;

  // Currently, key can be spread in as a prop. This causes a potential issue if key is also explicitly declared (ie. <div {...props} key="Hi" /> or <div key="Hi" {...props} /> ). We want to deprecate key spread,
  // but as an intermediary step, we will use jsxDEV for everything except <div {...props} key="Hi" />, because we aren't currently able to tell if key is explicitly declared to be undefined or not.
  if (maybeKey !== undefined) {
    key = '' + maybeKey;
  }

  if (hasValidKey(config)) {
    key = '' + config.key;
  }

  if (hasValidRef(config)) {
    ref = config.ref;
  }

  // Remaining properties are added to a new props object
  for (propName in config) {
    if (
      hasOwnProperty.call(config, propName) &&
      !RESERVED_PROPS.hasOwnProperty(propName)
    ) {
      props[propName] = config[propName];
    }
  }

  // Resolve default props
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }

  if (key || ref) {
    const displayName =
      typeof type === 'function'
        ? type.displayName || type.name || 'Unknown'
        : type;
    if (key) {
      defineKeyPropWarningGetter(props, displayName);
    }
    if (ref) {
      defineRefPropWarningGetter(props, displayName);
    }
  }

  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}
複製代碼

defineRefPropWarningGetter/defineKeyPropWarningGetter

key 是用來優化React渲染速度的,而 ref 是用來獲取到React渲染後的真實DOM節點。正常狀況下應該將這兩個屬性置之世外,彷彿這兩個屬性都應該是React自己的API。因此這兩個方法就是用來禁止獲取和設置的。

let specialPropKeyWarningShown, specialPropRefWarningShown;

function defineKeyPropWarningGetter(props, displayName) {
  const warnAboutAccessingKey = function() {
    if (!specialPropKeyWarningShown) { // 只會讀取一次
      specialPropKeyWarningShown = true;
      warningWithoutStack(
        false,
        '%s: `key` is not a prop. Trying to access it will result ' +
          'in `undefined` being returned. If you need to access the same ' +
          'value within the child component, you should pass it as a different ' +
          'prop. (https://fb.me/react-special-props)',
        displayName,
      );
    }
  };
  warnAboutAccessingKey.isReactWarning = true;
  Object.defineProperty(props, 'key', {
    get: warnAboutAccessingKey,
    configurable: true,
  });
}
複製代碼

當在組件內嘗試 console.log(props.key) 的時候,就會發現報錯。

alt

兩個方法邏輯如出一轍,就不寫粘貼兩遍了。

hasValidRef/hasValidKey

這兩個方法差很少,在開發模式下多了一個校驗,經過 Object.prototype.hasOwnProperty 檢查當前對象屬性上是否存在 ref/key,並獲取其訪問器函數 get,若是事先被defineKeyPropWarningGetter/defineRefPropWarningGetter 鎖定則 getter.isReactWarning 就必然爲 true(注意鎖定方法調用的時機)。

function hasValidRef(config) {
  if (__DEV__) {
    if (hasOwnProperty.call(config, 'ref')) {
      const getter = Object.getOwnPropertyDescriptor(config, 'ref').get;
      if (getter && getter.isReactWarning) {
        return false;
      }
    }
  }
  return config.ref !== undefined;
}
複製代碼
相關文章
相關標籤/搜索