React傳-1

大Y發表於2019-11-20

原文

寫在前面

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

起源

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

從入口開始

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

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 標誌

Component

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

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

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所有屬性放回去,做到複用,避免了一次性建立大量物件和釋放物件消耗效能造成的記憶體抖動。的概念在計算機領域被大量引用,包括Node的Buffer的記憶體分配策略也是用的池的概念。

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;
}
複製程式碼

相關文章