1. 程式人生 > 程式設計 >進擊React原始碼之磨刀試煉2

進擊React原始碼之磨刀試煉2

進擊React原始碼之磨刀試煉部分為原始碼解讀基礎部分,會包含多篇文章,本篇為第二篇,第一篇《進擊React原始碼之磨刀試煉1》入口(點選進入)。

初探Component與PureComponent

如果有沒用過PureComponent或不瞭解的同學,可以看看這篇文章何時使用Component還是PureComponent?

猜猜元件內部如何實現?

Component(元件)作為React中最重要的概念,每當建立類元件都要繼承ComponentPureComponent,在未開始看原始碼的時候,大家可以先跟自己談談對於ComponentPureComponent的印象,不妨根據經驗猜一猜Component

內部將會為我們實現怎樣的功能?

先來寫個簡單的元件

class CompDemo extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      msg: 'hello world'
    }
  }

  componentDidMount() {
    setTimeout(() => {
      this.setState({
        msg: 'Hello React'
      });
    },1000)
  }

  render() {
    return
( <div className="CompDemo"> <div className="CompDemo__text"> {this.state.msg} </div> </div> ) } } 複製程式碼

通過這個簡單的元件,我們猜猜,Component/PureComponent元件內部可能幫我們處理了props,state,定義了生命週期函式,setStaterender等很多功能。

原始碼實現

開啟packages/react/src/ReactBaseClasses.js

,開啟后里面有很多英文註釋,希望大家不管通過什麼手段先翻譯看看,自己先大致瞭解一下。之後貼出的原始碼中我會過濾掉自帶的註釋和if(__DEV__)語句,有興趣瞭解的同學可以翻閱原始碼研究。

Component

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,'forceUpdate');
};

複製程式碼

以上就是Component相關的原始碼,它竟如此出奇的簡潔!字面來看懂它也很簡單,首先定義了Component建構函式,之後在其原型鏈上設定了isReactComponent(Component元件標誌)、setState方法和forceUpdate方法。

Component建構函式可以接收三個引數,其中propscontext我們大多數人應該都接觸過,在函式中還定義了this.refs為一個空物件,但updater就是一個比較陌生的東西了,在setStateforceUpdate方法中我們可以看到它的使用:

  • setState並沒有具體實現更新state的方法,而是呼叫了updaterenqueueSetStatesetState接收兩個引數:partialState就是我們要更新的state內容,callback可以讓我們在state更新後做一些自定義的操作,this.updater.enqueueSetState在這裡傳入了四個引數,我們可以猜到第一個為當前例項物件,第二個是我們更新的內容,第三個是傳入的callback,最後一個是當前操作的名稱。這段程式碼上面invariant的作用是判斷partialState是否是物件、函式或者null,如果不是則會給出提示。在這裡我們可以看出,setState第一個引數不僅可以為Object,也可以是個函式,大家在實際操作中可以嘗試使用。
  • forceUpdate相比於setState,只有callback,同時在使用enqueueForceUpdate時候也少傳遞了一個引數,其他引數跟setState中呼叫保持一致。

這個updater.enqueueForceUpdate來自ReactDomReactReactDom是分開的兩個不同的內容,很多複雜的操作都被封裝在了ReactDom中,因此React才保持如此簡潔。React在不同平臺(native和web)使用的都是相同的程式碼,但是不同平臺的DOM操作流程可能是不同的,因此將state的更新操作通過物件方式傳遞過來,可以讓不同的平臺去自定義自己的操作邏輯,React就可以專注於大體流程的實現。

PureComponent

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

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

const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;

Object.assign(pureComponentPrototype,Component.prototype);
pureComponentPrototype.isPureReactComponent = true;
複製程式碼

看完Cpomponent的內容,再看PureComponent就很簡單了,單看PureComponent的定義是與Component是完全一樣的,這裡使用了寄生組合繼承的方式,讓PureComponent繼承了Component,之後設定了isPureReactComponent標誌為true。

如果有同學對JavaScript繼承不是很瞭解,這裡找了一篇掘金上的文章深入JavaScript繼承原理 大家可以點選進入檢視

Refs的用法與實現

ref的使用

通過ref我們可以獲得元件內某個子節點的資訊病對其進行操作,ref的使用方式有三種:

class RefDemo extends PureComponent {
  constructor() {
    super()
    this.objRef = React.createRef()
  }

  componentDidMount() {
    setTimeout(() => {
      this.refs.stringRef.textContent = "String ref content changed";
      this.methodRef.textContent = "Method ref content changed";
      this.objRef.current.textContent = "Object ref content changed";
    },3000)
  }

  render() {
    return (
      <div className="RefDemo">
        <div className="RefDemo__stringRef" ref="stringRef">this is string ref</div>
        <div className="RefDemo__methodRef" ref={el => this.methodRef = el}>this is method ref</div>
        <div className="RefDemo__objRef" ref={this.objRef}>this is object ref</div>
      </div>
    )
  }
}

export default RefDemo;
複製程式碼

Jietu20190818-124212

  1. string ref(不推薦,可能廢棄):通過字串方式設定ref,會在this.refs物件上掛在一個key為所設字串的屬性,用來表示該節點的例項物件。如果該節點為dom,則對應dom示例,如果是class component則對應該元件例項物件,如果是function component,則會出現錯誤,function component沒有例項,但可以通過forward ref來使用ref
  2. method ref:通過function來建立ref(筆者在之前實習工作中基本都是使用這種方式,非常好用)。
  3. 通過createRef()建立物件,預設建立的物件為{current: null},將其傳遞個某個節點,在元件渲染結束後會將此節點的例項物件掛在到current

createRef的實現

原始碼位置packages/react/src/ReactCreactRef.js

export function createRef(): RefObject {
  const refObject = {
    current: null,};
  return refObject;
}
複製程式碼

它上方有段註釋an immutable object with a single mutable value,告訴我們創建出來的物件具有單個可變值,但是這個物件是不可變的。在其內部跟我們上面說的一樣,建立了{current: null}並將其返回。

forwardRef的使用

const FunctionComp = React.forwardRef((props,ref) => (
  <div type="text" ref={ref}>Hello React</div>
))

class FnRefDemo extends PureComponent {
  constructor() {
    super();
    this.ref = React.createRef();
  }

  componentDidMount() {
    setTimeout(() => {
      this.ref.current.textContent = "Changed"
    },3000)
  }

  render() {
    return (
      <div className="RefDemo">
        <FunctionComp ref={this.ref}/>
      </div>
    )
  }
}
複製程式碼

forwardRef的使用,可以讓Function Component使用ref,傳遞引數時需要注意傳入第二個引數ref

forwardRef的實現

export default function forwardRef<Props,ElementType: React$ElementType>(
  render: (props: Props,ref: React$Ref<ElementType>) => React$Node,) {
  return {
    $$typeof: REACT_FORWARD_REF_TYPE,render,};
}

複製程式碼

forwardRef接收一個函式作為引數,這個函式就是我們的函式元件,它包含propsref屬性,forwardRef最終返回的是一個物件,這個物件包含兩個屬性:

  1. $$typeof:這個屬性看過上一篇文章的小夥伴應該還記得,它是標誌React Element型別的東西。
  2. render: 我們傳遞進來的函式元件。

這裡說明一下,儘管forwardRef返回的物件中$$typeofREACT_FORWARD_REF_TYPE,但是最終建立的ReactElement的$$typeof仍然是REACT_ELEMENT_TYPE

這裡文字描述有點繞,配合圖片來看文字會好點。

enter description here

在上述forwardRef使用的程式碼中建立的FunctionComp{$$typeof:REACT_FORWARD_REF_TYPE,render}這個物件,在使用<FunctionComp ref={this.ref}/>時,它的本質是React.createElement(FunctionComp,{ref: xxxx},null)這樣的,此時FunctionComp是我們傳進createElement中的type引數,createElement返回的element$$typeof仍然是REACT_ELEMENT_TYPE

ReactChildren的使用方法和實現

ReactChildren的使用

function ParentComp ({children}) {
  return (
    <div className="parent">
      <div className="title">Parent Component</div>
      <div className="content">
        {children}
      </div>
    </div>
  )
}
複製程式碼

這樣的程式碼大家平時用的應該多一點,在使用ParentComp元件時候,可以在標籤中間寫一些內容,這些內容就是children。

來看看React.Children.map的使用

function ParentComp ({children}) {
  return (
    <div className="parent">
      <div className="title">Parent Component</div>
      <div className="content">
        {React.Children.map(children,c => [c,c,[c]])}
      </div>
    </div>
  )
}

class ChildrenDemo extends PureComponent{
  constructor() {
    super()
    this.state = {}
  }

  render() {
    return (
      <div className="childrenDemo">
        <ParentComp>
          <div>child 1 content</div>
          <div>child 2 content</div>
          <div>child 3 content</div>
        </ParentComp>
      </div>
    )
  }
}

export default ChildrenDemo;
複製程式碼

結果

我們在使用這個API的時候,傳遞了兩個引數,第一個是children,大家應該比較熟悉,第二個是一個回撥函式,回撥函式傳入一個引數(代表children的一個元素),返回一個陣列(陣列不是一位陣列,裡面三個元素最後一個還是陣列),在結果中我們可以看到,這個API將我們返回的陣列平鋪為一層[c1,c1,c2,c3,c3],瀏覽器中顯示的也就如上圖所示。

有興趣的小夥伴可以嘗試閱讀官方檔案對於這個api的介紹

ReactChildren的實現

react.js中定義React時候我們可以看到一段關於Children的定義

  Children: {
    map,forEach,count,toArray,only,},複製程式碼

Children包含5個API,這裡我們先詳細討論map API。這一部分並不是很好懂,請大家看的時候一定要用心。

筆者讀這一部分也是費了很大的勁,然後用思維導圖軟體畫出了這個思維導圖+流程圖的東西(暫時就給它起名為思維流程圖,其實更流程一點,而不思維),畫得還是比較詳細的,所以就很大,小夥伴最好把這個圖下載下來放大看(可以配合原始碼,也可以配合下文),圖片地址user-gold-cdn.xitu.io/2019/8/21/1…

enter description here

由於圖太小不清楚,下面也會分別截出每個函式的流程圖。

開啟packages/react/src/ReactChildren.js,找到mapChildren

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

enter description here

這段程式碼短小精悍,給我們提供了直接使用的API。它內部邏輯也非常簡單,首先看看children是否為null,如果如果為null就直接返回null,如果不是,則定義result(初始為空陣列)來存放結果,經過mapIntoWithKeyPrefixInternal的一系列處理,得到結果。結果不管是null還是result,其實我們再寫程式碼的時候都遇到過,如果一個元件中間什麼都沒傳,結果就是null什麼都不會顯示,如果傳遞了一個<div>那就顯示這個div,如果傳遞了一組div那就顯示這一組(此時就是children不為null的情況),最後顯示出來的東西也就是result這個陣列。

這一系列處理就是什麼處理?

function mapIntoWithKeyPrefixInternal(children,array,prefix,context) {
  let escapedPrefix = '';
  if (prefix != null) {
    escapedPrefix = escapeUserProvidedKey(prefix) + '/';
  }
  const traverseContext = getPooledTraverseContext(
    array,escapedPrefix,);
  traverseAllChildren(children,mapSingleChildIntoContext,traverseContext);
  releaseTraverseContext(traverseContext);
}
複製程式碼

在進入這個函式的時候,一定要注意使用這個函式時候傳遞進來的引數究竟是哪幾個,不然後面傳遞次數稍微一多就會暈頭轉向。

enter description here

從上一個函式跳過來的時候傳遞了5個引數,大家可以注意一下這五個引數代表的是什麼:

  1. children:我們再元件中間寫的JSX程式碼
  2. result: 最終處理完成存放結果的陣列
  3. prefix: 字首,這裡為null
  4. func: 我們在演示使用的過程中傳入的第二個引數,是個回撥函式c => [c,[c]]
  5. context: 上下文物件

這個函式首先對prefix字首字串做了個處理,處理完之後還是個字串。然後通過getPooledTraverseContext函式從物件重用池中拿出一個物件,說到這裡,我們就不得不打斷一下這個函式的講解,突然出現一個物件重用池的概念,很多人會很懵逼,並且如果強制把這個函式解析完再繼續下一個,會讓很多讀者產生很多疑惑,不利於後面原始碼的理解。

暫時跳到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,};
  }
}
複製程式碼

enter description here

首先看在使用getPooledTraverseContext獲取物件的時候,傳遞了4個引數:

  1. array: 上個函式中對應的result,表示最終返回結果的陣列
  2. escapedPrefix: 字首,一個字串,沒什麼好說的
  3. func: 我們使用API傳遞的回撥函式 c=>[c,[c]]
  4. context: 上下文物件

然後我們看看它做了什麼,它去一個traverseContextPool陣列(這個陣列預設為空陣列,最多存放10個元素)中嘗試pop取出一個元素,如果能取出來的話,這個元素是一個物件,有5個屬性,這裡會把傳進來的4個引數儲存在這四個元素中,方便後面使用,另外一個屬性是個用來計數的計數器。如果沒取出來,就返回一個新物件,包含的也是這五個屬性。這裡要跟大家說說物件重用池了。這個物件有5個屬性,如果每次使用這個物件都重新建立一個,那麼會有較大的建立物件開銷,為了節省這部分建立的開銷,我們可以在使用完這個物件之後,把它的5個屬性都置為空(count就是0了),然後扔回這個陣列(物件重用池)中,後面要用的時候就直接從物件重用池中拿出來,不必重新建立物件,增加開銷了。

再回到mapIntoWithKeyPrefixInternal函式中繼續向下讀 通過上一步拿到一個帶有5個屬性的物件之後,繼續經過traverseAllChildren函式的一系列處理,得到了最終的結果result,其中具體內容太多下面再說,然後通過releaseTraverseContext函式釋放了那個帶5個引數的物件。我們先來看看如何釋放的:

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

這裡也跟我們上面說的物件重用池有所對應,這裡先把這個物件的5個屬性清空,然後看看物件重用池是不是有空,有空的話就把這個清空的屬性放進去,方便下次使用,節省建立開銷。

traverseAllChildren和traverseAllChildrenImpl的實現

function traverseAllChildren(children,traverseContext) {
  if (children == null) {
    return 0;
  }

  return traverseAllChildrenImpl(children,'',traverseContext);
}
複製程式碼

enter description here

這個函式基本沒做什麼重要的事,僅僅判斷了children是否為null,如果是的話就返回0,不是的話就進行具體的處理。還是強調這裡傳遞的引數,一定要注意,看圖就可以了,就不用文字描述了。

重要的是traverseAllChildrenImpl函式,這個函式有點長,這裡給大家分成了兩部分,可以分開看

function traverseAllChildrenImpl(
  children,nameSoFar,traverseContext,) {
// 第一部分
  const type = typeof children;

  if (type === 'undefined' || type === 'boolean') {
    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,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,);
    }
  } else {
    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,);
      }
    } 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;
}
複製程式碼

enter description here

上面的流程圖說的很詳細了,大家可以參照來看原始碼。這裡就簡單說一下這個函式的兩部分分別作了什麼事。 第一部分是對children型別進行了檢查(沒有檢查為Array或迭代器物件的情況),如果檢查children是合法的ReactElement就會進行callback的呼叫,這裡一定要注意callback傳進來的是誰,這裡是callback為mapSingleChildIntoContext,一直讓大家關注傳參問題,就是怕大家看著看著就搞混了。 第二部分就是針對children是陣列和迭代器物件的情況進行了處理(迭代器物件檢查的原理是obj[Symbol.iterator],比較簡單大家可以自己定位原始碼找一下具體實現),然後對他們進行遍歷,每個元素都重新執行traverseAllChildrenImpl函式形成遞迴。 它其實只讓可渲染的單元素進行下一步callback的呼叫,如果是陣列或迭代器,就進行遍歷。

最後一步callback => mapSingleChildIntoContext的實現

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

  let mappedChild = func.call(context,bookKeeping.count++);
  if (Array.isArray(mappedChild)) {
    mapIntoWithKeyPrefixInternal(mappedChild,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);
  }
}
複製程式碼

enter description here

這裡我們就用到了從物件重用池拿出來的物件,那個物件作用其實就是利用那5個屬性幫我們儲存了一些需要使用的變數和函式,然後執行我們傳入的funcc => [c,[c]]),如果結果不是陣列而是元素並且不為null就會直接儲存到result結果中,如果是個陣列就會對它進行遍歷,從mapIntoWithKeyPrefixInternal開始重新執行形成遞迴呼叫,直到最後將巢狀陣列中所有元素都拿出來放到result中,這樣就形成了我們最初看到的那種效果,不管我們的回撥函式是多少層的陣列,最後都會變成一層。

小結

這裡文字性的小結就留給大家,給大家畫了一張總結性的流程圖(有參考yck大神的圖),但其實是根據自己看原始碼畫出來的並不是搬運的。

enter description here

ReactChildren的其他方法

{
  forEach,}
複製程式碼

對於這幾個方法,大家可以自行查看了,建議先瀏覽一遍forEach,跟map非常相似,但是比map少了點東西。其他幾個都是四五行的程式碼,大家自己看看。裡面用到的函式我們上面都有講到。

小結

這篇文章跟大家一起讀了ComponentrefsChildren相關的原始碼,最複雜的還是數Children了,說實話,連看大神部落格,看原始碼、畫圖帶寫文章,花了七八個小時,其實內容跟大神們的文章比起來還是很不一樣的,如果基礎不是很好的同學,我感覺這裡會講的更詳細。 大家一起努力,明天的我們一定會感謝今天努力的自己。

原創不易,如果本篇文章對你有幫助,希望可以幫忙點個贊,有興趣也可以幫忙github點個star,感謝各位。本篇文章github地址