React源碼解析 API概覽 + React

最近閒來無事,研究一波React源碼,一開始是以Vue源碼起步的,結果發現我對Vue實在是不熟悉,看Vue源碼還未入流,相比而言,我更喜歡React,多是由於第一個學的框架學的就是React,因此對React更加的充滿熱情,也更加的熟練,我的觀點,React仍是要比Vue牛逼一點好看一點的。html

React自己的源碼是不多的,根據打包出來的Commonjs版本看來,React只有兩千多行代碼,可是ReactDom聽說有兩萬多行,框架開發者實屬偉大!致敬!!!node

那麼這一篇是React一些通用的API概況和React.Children方法的解析,若有不到位或錯誤的地方歡迎指教,個人郵箱 1103107216@qq.com 您也能夠下方評論。react

React源碼獲取

我發現有兩種方式,一種呢就是從github上拉取react項目的源碼,github地址你們能夠本身找,git clone下來以後,在/packages/react下面就是react的源碼了,能夠看到下面是分紅了不少個小文件的,這個我通常用來看的不是用來調試的。webpack

另外一個呢就是建一個項目,安裝一下cnpm i react react-dom -S以後在node_modules裏面找到react的源碼,建一個項目,用webpack打包,裝個babel一套,畢竟es6比es5好使多了,開個熱更新,以後就直接修改這個node_modules裏面的源碼進行打印調試了,我我的喜歡console.log不解釋,只有在調試一些算法問題時我纔會開Debug模式。git

通用API

首先先來一個簡單的 React 應用,這邊使用es6的class寫法,我的建議多練練函數式編程,寫函數組件比寫class舒服多了,畢竟React16提供了這麼多強大的Hookes6

import React from 'react';
class App extends React.Component {
    constructor(props) {
        super(props)
    }
    render() {
        return (
            <div>
                Hello World
            </div>
        )
    }
}
複製代碼

OK, Hello World 致敬,咱們能夠開始幹活了。首先看一下React的源碼,在/packages/react/src/React.js這個文件裏面,能夠看到React的定義,你會發現和Vue的源碼很不同,這也是我更喜歡React的緣由,慢慢的親切感。github

const React = {
  Children: {
    map,
    forEach,
    count,
    toArray,
    only,
  },

  createRef,
  Component,
  PureComponent,

  createContext,
  forwardRef,
  lazy,
  memo,

  useCallback,
  useContext,
  useEffect,
  useImperativeHandle,
  useDebugValue,
  useLayoutEffect,
  useMemo,
  useReducer,
  useRef,
  useState,

  Fragment: REACT_FRAGMENT_TYPE,
  StrictMode: REACT_STRICT_MODE_TYPE,
  Suspense: REACT_SUSPENSE_TYPE,

  createElement: __DEV__ ? createElementWithValidation : createElement,
  cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement,
  createFactory: __DEV__ ? createFactoryWithValidation : createFactory,
  isValidElement: isValidElement,

  version: ReactVersion,

  unstable_ConcurrentMode: REACT_CONCURRENT_MODE_TYPE,
  unstable_Profiler: REACT_PROFILER_TYPE,
  // 這一行跳過
  __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: ReactSharedInternals,
};
複製代碼

這邊定義了React裏面的全部的通用方法,這邊只作一個概覽,每個具體的用處會在後面進行詳細的介紹。web

Children

這個裏面封裝的是對一個組件的子組件進行遍歷等的一些操做,咱們通常不會用到,講真我除了看源碼會用他來試一試其餘的真沒見到有人用它。算法

  • forEach,map 相似於數組的遍歷對象遍歷啥的npm

  • count 用來計算子組件的數量

  • only 官方解釋:驗證 children 是否只有一個子節點(一個 React 元素),若是有則返回它,不然此方法會拋出錯誤。 Tips:不可使用React.Children.map方法的返回值做爲參數,由於map的返回值是一個數組而不是一個React元素

  • toArray 將Children按照數組的形式扁平展開並返回

搞不懂不要緊,後面會介紹,有一個印象就好

createRef

ref 屬性是在開發中常用的,說白了就是用來獲取真實Dom的,新版的React中使用ref的操做也變了

class MyComponent extends React.Component {
  constructor(props) {
    super(props);

    this.inputRef = React.createRef();
  }
  // 這是一種
  render() {
    return <input type="text" ref={this.inputRef} />;
  }
  // 這是另一種
  render() {
    return <input type="text" ref={node => this.inputRef = node}>
  }
}
複製代碼

Component, PureComponent

這兩個你們應該都很熟悉,建立一個React組件,PureComponent在判斷組件是否改更新的時候更加的方便。

createContext

建立一個上下文,返回一個Context對象,裏面包含了Provider,Consumer屬性,通常用來往組件樹的更深處傳遞數據,避免一個組件一個組件的往下傳,不方便解藕

forwardRef

建立一個React組件,這個組件可以將其接受的 ref 屬性轉發到其組件樹下的另外一個組件中.React.forwardRef 接受渲染函數做爲參數。React 將使用 props 和 ref 做爲參數來調用此函數。此函數應返回 React 節點。

lazy

組件懶加載

const SomeComponent = React.lazy(() => import('./SomeComponent'));
複製代碼

memo

用來建立一個HOC的

useState...

接下來這幾個就是React16大名鼎鼎的Hook函數,功能強大,函數式組件的福音,親切感倍足

Fragment StrictMode Suspense unstable_ConcurrentMode unstable_Profiler

這四個都是React提供的組件,但他們呢其實都只是佔位符,都是一個Symbol,在React實際檢測到他們的時候會作一些特殊的處理,好比StrictMode和AsyncMode會讓他們的子節點對應的Fiber的mode都變成和他們同樣的mode

createElement

createElement 這是React中最重要的方法了,用來建立ReactElement

cloneElement

顧名思義,克隆一個ReactElement

createFactory

建立一個工廠,這個工廠專門用來建立某一類ReactElement

isValidElement

用來檢測是不是一個ReactElement

version

記錄React的當前版本號

React.Children 詳解

React.Children 提供了用於處理 this.props.children 不透明數據結構的實用方法。

這一部分的代碼在 packages/react/react/src/ReactChildren.js裏面,主要分裝了forEach map count only toArray,前二者用於遍歷Reach Children。

  • count 用於返回該組件的children數量

  • only 用於判斷該組件是否是隻有一個子節點

  • toArray 將React.Children以扁平的形式返回出來,並附加key

React中,一段文本能夠被稱爲一個子節點,一段標籤也能夠被成爲一個節點。

class App extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        // Hello World
        console.log(this.props.children);
        return (
            <div></div>
        )
    }
}

ReactDom.render(
    <App>
        // 一段文本也是一個子節點
        Hello World
    </App> ,
    document.getElementById('root')
);
複製代碼
class App extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        // 被標記爲一個React.Element
        console.log(this.props.children);
        return (
            <div></div>
        )
    }
}

ReactDom.render(
    <App>
        // 一段標籤也能夠是一個子節點
        <div>Hello World</div>
    </App> ,
    document.getElementById('root')
);
複製代碼

在上面的示例代碼中,若是傳遞的子節點是一段html標籤,那麼打印出來的結果是這樣的:

image_1dkp91mf4199d1kds1ul615rklpr9.png-36.3kB

咱們也能夠在App組件中顯示咱們傳遞的這個Children

class App extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        console.log(this.props.children);
        return (
            <div>{ this.props.children }</div>
        )
    }
}
複製代碼

image_1dkp98tod16gl3u91q614tj1a7jm.png-2.4kB

若是傳遞的是多個節點,那麼就會被解析成一個數組

<App>
    <div>Hello World</div>
    <div>Hello China</div>
</App>
複製代碼

image_1dkp9bfcjqb5ptn1dt02dm8lp13.png-22.8kB

那麼Reach.Children的方法應該就是在這裏進行使用,由於我實際上也沒有使用過,作個簡單的示例,咱們能夠打印一下App這個組件的子節點�數,使用count方法

class App extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        // 2
        console.log(React.Children.count(this.props.children));
        return (
            <div>{ this.props.children }</div>
        )
    }
}

ReactDom.render(
    <App>
        <div>Hello World</div>
        <div>Hello China</div>
    </App> ,
    document.getElementById('root')
);
複製代碼

這邊會打印出來一個 2 由於咱們傳遞的是兩個節點

示例看完了咱們能夠來分析一下源碼了,介紹一下map的源碼

找到ReactChildren.js(這是在React源碼裏,不是在node_modules裏),找到最下面模塊導出語句

export {
  forEachChildren as forEach,
  mapChildren as map,
  countChildren as count,
  onlyChild as only,
  toArray,
};
複製代碼

能夠看到mapmapChildren的一個別名,下面找到這個函數

/**
 * Maps children that are typically specified as `props.children`.
 *
 * See https://reactjs.org/docs/react-api.html#reactchildrenmap
 *
 * The provided mapFunction(child, key, index) will be called for each
 * leaf child.
 *
 * @param {?*} children Children tree container.
 * @param {function(*, int)} func The map function.
 * @param {*} context Context for mapFunction.
 * @return {object} Object containing the ordered map of results.
 */
function mapChildren(children, func, context) {
  if (children == null) {
    return children;
  }
  const result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, func, context);
  return result;
}
複製代碼

方法接受三個參數,第一個參數是咱們傳遞的this.props.children,也是必選參數,第二個參數是一個function,在遍歷的過程當中,會對每個節點都使用這個function,這個function接受一個參數,參數就是當前遍歷的節點,第三個參數是一個上下文,通常不用傳。 能夠看出重點是mapIntoWithKeyPrefixInternal這個方法。

使用示例

class App extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        React.Children.map(this.props.children, (item) => {
            console.log(item);
        })
        return (
            <div>{ this.props.children }</div>
        )
    }
}
複製代碼
function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
  // 被忽視的前綴
  let escapedPrefix = '';
  if (prefix != null) {
    escapedPrefix = escapeUserProvidedKey(prefix) + '/';
  }
  // 遍歷上下文
  const traverseContext = getPooledTraverseContext(
    array,
    escapedPrefix,
    func,
    context,
  );
  traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
  releaseTraverseContext(traverseContext);
}
複製代碼

首先是獲取一下遍歷的上下文,這個在後面的方法應該會用到,下面就是開始遍歷全部的Children了,重點是traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);,第一個參數好理解就是咱們傳遞的this.props.children,第二個參數是一個方法,第三個參數就是前面獲取到的遍歷上下文。

首先看一下這個getPooledTraverseContext方法

const POOL_SIZE = 10;
const traverseContextPool = [];

function getPooledTraverseContext(
  mapResult,
  keyPrefix,
  mapFunction,
  mapContext,
) {
  if (traverseContextPool.length) {
    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,
    };
  }
}
複製代碼

用了一個閉包,外層有一個traverseContextPool記錄者遍歷上下文的一個pool,我腦海中蹦出來的詞是鏈接池,因此暫且就這麼理解他,這個鏈接池的容量爲10,若是這個鏈接池裏有東西的話,也就是說這個traverseContextPool.length !== 0的話,那麼會彈出最後一個進行賦值而後返回,若是池裏沒有東西的話就直接返回一個新的對象。

下面看重點方法traverseAllChildren

/**
 * Traverses children that are typically specified as `props.children`, but
 * might also be specified through attributes:
 *
 * - `traverseAllChildren(this.props.children, ...)`
 * - `traverseAllChildren(this.props.leftPanelChildren, ...)`
 *
 * The `traverseContext` is an optional argument that is passed through the
 * entire traversal. It can be used to store accumulations or anything else that
 * the callback might find relevant.
 *
 * @param {?*} children Children tree object.
 * @param {!function} callback To invoke upon traversing each child.
 * @param {?*} traverseContext Context for traversal.
 * @return {!number} The number of children in this subtree.
 */
function traverseAllChildren(children, callback, traverseContext) {
  if (children == null) {
    return 0;
  }

  return traverseAllChildrenImpl(children, '', callback, traverseContext);
}
複製代碼

主要看這個方法的實現traverseAllChildrenImpl

/**
 * @param {?*} children Children tree container.
 * @param {!string} nameSoFar Name of the key path so far.
 * @param {!function} callback Callback to invoke with each child found.
 * @param {?*} traverseContext Used to pass information throughout the traversal
 * process.
 * @return {!number} The number of children in this subtree.
 */
function traverseAllChildrenImpl(
  children,
  nameSoFar,
  callback,
  traverseContext,
) {
  const type = typeof children;

  if (type === 'undefined' || type === 'boolean') {
    // All of the above are perceived as null.
    children = null;
  }

  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:
          case REACT_PORTAL_TYPE:
            invokeCallback = true;
        }
    }
  }

  if (invokeCallback) {
    callback(
      traverseContext,
      children,
      // If it's the only child, treat the name as if it was wrapped in an array // so that it's consistent if the number of children grows.
      nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,
    );
    return 1;
  }

  let child;
  let nextName;
  let subtreeCount = 0; // Count of children found in the current subtree.
  const nextNamePrefix =
    nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;

  if (Array.isArray(children)) {
    for (let i = 0; i < children.length; i++) {
      child = children[i];
      nextName = nextNamePrefix + getComponentKey(child, i);
      subtreeCount += traverseAllChildrenImpl(
        child,
        nextName,
        callback,
        traverseContext,
      );
    }
  } else {
    const iteratorFn = getIteratorFn(children);
    if (typeof iteratorFn === 'function') {
      if (__DEV__) {
        // Warn about using Maps as children
        if (iteratorFn === children.entries) {
          warning(
            didWarnAboutMaps,
            'Using Maps as children is unsupported and will likely yield ' +
              'unexpected results. Convert it to a sequence/iterable of keyed ' +
              'ReactElements instead.',
          );
          didWarnAboutMaps = true;
        }
      }

      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 = '';
      if (__DEV__) {
        addendum =
          ' If you meant to render a collection of children, use an array ' +
          'instead.' +
          ReactDebugCurrentFrame.getStackAddendum();
      }
      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;
}
複製代碼

分步解析

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:
          case REACT_PORTAL_TYPE:
            invokeCallback = true;
        }
    }
  }

  if (invokeCallback) {
    callback(
      traverseContext,
      children,
      // If it's the only child, treat the name as if it was wrapped in an array // so that it's consistent if the number of children grows.
      nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,
    );
    return 1;
  }
複製代碼

這一塊是用來判斷 children 類型的,若是是string好比說傳遞一個文本,number,object好比說一個dom節點,那麼代表 children 只是一個節點,那麼就直接執行 callback 返回一個 1

if (Array.isArray(children)) {
    for (let i = 0; i < children.length; i++) {
      child = children[i];
      nextName = nextNamePrefix + getComponentKey(child, i);
      subtreeCount += traverseAllChildrenImpl(
        child,
        nextName,
        callback,
        traverseContext,
      );
    }
  }
複製代碼

若是咱們傳遞的是多個節點,那麼會遍歷children數組,進行遞歸遍歷,直到返回的是上面顯示的幾個類型。

上邊提到的callback就是傳遞的mapSingleChildIntoContext,這邊就是利用到以前的traverseContextPool被我稱之爲鏈接池的東西.

function mapSingleChildIntoContext(bookKeeping, child, childKey) {
  const {result, keyPrefix, func, context} = bookKeeping;

  let mappedChild = func.call(context, child, bookKeeping.count++);
  if (Array.isArray(mappedChild)) {
    mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);
  } else if (mappedChild != null) {
    if (isValidElement(mappedChild)) {
      mappedChild = cloneAndReplaceKey(
        mappedChild,
        // Keep both the (mapped) and old keys if they differ, just as
        // traverseAllChildren used to do for objects as children
        keyPrefix +
          (mappedChild.key && (!child || child.key !== mappedChild.key)
            ? escapeUserProvidedKey(mappedChild.key) + '/'
            : '') +
          childKey,
      );
    }
    result.push(mappedChild);
  }
}
複製代碼

這邊的mappedChild就是咱們傳遞的funcion的返回值,function呢就是調用React.Children.map(children,callback)這裏的callback了,若是這個返回值返回的是一個數組的話,那麼就進行遞歸調用,這個時候就須要用到以前的鏈接池了。

採用這個鏈接池的目的我也是在其餘的地方看到了

由於對Children的處理通常在render裏面,因此會比較頻繁,因此設置一個pool減小聲明和gc的開銷

這就是React.Children.map的實現。

相關文章
相關標籤/搜索