React傳-2

大Y發表於2019-11-21

原文地址

Suspense 與 lazy

src/React.js裡,有幾個元件長得比較奇怪。

Fragment: REACT_FRAGMENT_TYPE,
Profiler: REACT_PROFILER_TYPE,
StrictMode: REACT_STRICT_MODE_TYPE,
Suspense: REACT_SUSPENSE_TYPE,
unstable_SuspenseList: REACT_SUSPENSE_LIST_TYPE,
複製程式碼

它們都是一個symbol常量,作為一種標識,現在暫時佔個位。

直接看lazy程式碼

export function lazy<T, R>(ctor: () => Thenable<T, R>): LazyComponent<T> {
  // lazy的用法
  // lazy(()=>import('./xxx.js')) 這裡的ctor指的就是懶載入函式
  let lazyType = {
    $$typeof: REACT_LAZY_TYPE, // 這裡的$$typeof 並不是指createElement的$$type,整個物件是createElement返回物件的type欄位
    _ctor: ctor,
    // React uses these fields to store the result.
    _status: -1, // 狀態碼
    _result: null, // 結果
  };

  if (__DEV__) {
    // 開發環境下如果設定了defaultProps和propTypes會進行警告,
    // 可能是因為懶載入元件並不能在開發環境中merge原有的defaultProps
    let defaultProps;
    let propTypes;
    Object.defineProperties(lazyType, {
      defaultProps: {
        configurable: true,
        get() {
          return defaultProps;
        },
        set(newDefaultProps) {
          warning(
            false,
            'React.lazy(...): It is not supported to assign `defaultProps` to a lazy component import. Either specify them where the component  is defined, or create a wrapping component around it.',
          );
          defaultProps = newDefaultProps;
          // Match production behavior more closely:
          Object.defineProperty(lazyType, 'defaultProps', {
            enumerable: true,
          });
        },
      },
      propTypes: {
        configurable: true,
        get() {
          return propTypes;
        },
        set(newPropTypes) {
          warning(
            false,
            'React.lazy(...): It is not supported to assign `propTypes` to ' +
              'a lazy component import. Either specify them where the component ' +
              'is defined, or create a wrapping component around it.',
          );
          propTypes = newPropTypes;
          // Match production behavior more closely:
          Object.defineProperty(lazyType, 'propTypes', {
            enumerable: true,
          });
        },
      },
    });
  }

  return lazyType;
}

複製程式碼

傳入一個Thenable函式,Thenable代表promise,最終返回一個LazyComponent型別的元件。

createContext

context 提供了一種在 元件之間共享此類值 的方式,使得我們無需每層顯式新增 props 或傳遞元件 ,就能夠實現在 元件樹中傳遞資料 。

使用方式

const { Provider,Consumer } = React.createContext('default');


class App extends React.Component {
  render() {
    return (
      <Provider value='Foo'>
        <Content />
      </Provider>
    );
  }
}

function Child(){
  return (
    <Consumer>
      {
        value => <p>{value}</p>
      }
    </Consumer>
  )
}
複製程式碼

React.createContext 的第一個引數是 defaultValue,代表預設值,除了可以設定預設值外,它還可以讓開發者手動控制更新粒度的方式,第二個引數calculateChangedBits,接受 newValue 與 oldValue 的函式,返回值作為 changedBits,在 Provider 中,當 changedBits = 0,將不再觸發更新,而在 Consumer 中有一個不穩定的 props,unstable_observedBits,若 Provider 的changedBits & observedBits = 0,也將不觸發更新。

以下是完整的測試用例

// From React v16.11.0 release react-reconciler/src/__tests__/ReactNewContext-test.internal.js
it('can skip consumers with bitmask', () => {
  const Context = React.createContext({foo: 0, bar: 0}, (a, b) => {
    let result = 0;
    if (a.foo !== b.foo) {
      result |= 0b01;
    }
    if (a.bar !== b.bar) {
      result |= 0b10;
    }
    return result;
  });

  function Provider(props) {
    return (
      <Context.Provider value={{foo: props.foo, bar: props.bar}}>
        {props.children}
      </Context.Provider>
    );
  }

  function Foo() {
    return (
      <Context.Consumer unstable_observedBits={0b01}>
        {value => {
          ReactNoop.yield('Foo');
          return <span prop={'Foo: ' + value.foo} />;
        }}
      </Context.Consumer>
    );
  }

  function Bar() {
    return (
      <Context.Consumer unstable_observedBits={0b10}>
        {value => {
          ReactNoop.yield('Bar');
          return <span prop={'Bar: ' + value.bar} />;
        }}
      </Context.Consumer>
    );
  }

  class Indirection extends React.Component {
    shouldComponentUpdate() {
      return false;
    }
    render() {
      return this.props.children;
    }
  }

  function App(props) {
    return (
      <Provider foo={props.foo} bar={props.bar}>
        <Indirection>
          <Indirection>
            <Foo />
          </Indirection>
          <Indirection>
            <Bar />
          </Indirection>
        </Indirection>
      </Provider>
    );
  }

  // 表示首次渲染
  ReactNoop.render(<App foo={1} bar={1} />);
  // ReactNoop.yield('Foo'); 與 ReactNoop.yield('Bar'); 均執行了
  expect(Scheduler).toFlushAndYield(['Foo', 'Bar']);
  // 實際渲染結果校驗
  expect(ReactNoop.getChildren()).toEqual([
    span('Foo: 1'),
    span('Bar: 1'),
  ]);
  
  // 僅更新foo,foo 的值為 2
  // 此時 a.foo !== b.foo,changedBits = 0b01,Provider 發生更新
  ReactNoop.render(<App foo={2} bar={1} />);
  expect(Scheduler).toFlushAndYield(['Foo']);
  expect(ReactNoop.getChildren()).toEqual([
    span('Foo: 2'),
    span('Bar: 1'),
  ]);

  // 僅更新 bar ,bar 的值為 2
  // a.foo === b.foo 不更新
  // a.bar !== b.bar changedBits = 0b01,Provider 發生更新
  ReactNoop.render(<App foo={2} bar={2} />);
  expect(Scheduler).toFlushAndYield(['Bar']);
  expect(ReactNoop.getChildren()).toEqual([
    span('Foo: 2'),
    span('Bar: 2'),
  ]);

  // 同時更新
  // a.foo !== b.foo ,並且 a.bar !== b.bar changedBits = 0b01,Provider 發生更新
  ReactNoop.render(<App foo={3} bar={3} />);
  expect(Scheduler).toFlushAndYield(['Foo', 'Bar']);
  expect(ReactNoop.getChildren()).toEqual([
    span('Foo: 3'),
    span('Bar: 3'),
    ]);
});
複製程式碼

原始碼

export function createContext<T>(
  defaultValue: T,
  calculateChangedBits: ?(a: T, b: T) => number,
): ReactContext<T> {
  if (calculateChangedBits === undefined) {
    calculateChangedBits = null;
  } else {
    if (__DEV__) {
      // 開發環境下判斷是否函式
      warningWithoutStack(
        calculateChangedBits === null || typeof calculateChangedBits === 'function',
        'createContext: Expected the optional second argument to be a  function. Instead received: %s',
        calculateChangedBits,
      );
    }
  }

  const context: ReactContext<T> = {
    $$typeof: REACT_CONTEXT_TYPE,
    // 用來比對的函式
    _calculateChangedBits: calculateChangedBits,
    // 記錄最新的context值,_currentValue和_currentValue2它們的值是一樣的,用的地方會不一樣
    _currentValue: defaultValue,
    _currentValue2: defaultValue,
    //用於跟蹤此上下文當前支援多少個併發程式。例如並行伺服器渲染。
    _threadCount: 0,
    Provider: (null: any),
    Consumer: (null: any),
  };
  // 將Provide的_context指向自身,而Consumer則直接指向自身,就可以直接通過_context和自身取到最新值
  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  };

  let hasWarnedAboutUsingNestedContextConsumers = false;
  let hasWarnedAboutUsingConsumerProvider = false;

  if (__DEV__) {
    // A separate object, but proxies back to the original context object for backwards compatibility. It has a different $$typeof, so we can properly warn for the incorrect usage of Context as a Consumer.
    // 在開發環境下,如果開發者使用了舊版本的createContext,將提醒開發者使用最新的方式。
    const Consumer = {
      $$typeof: REACT_CONTEXT_TYPE,
      _context: context,
      _calculateChangedBits: context._calculateChangedBits,
    };
    // $FlowFixMe: Flow complains about not setting a value, which is intentional here
    Object.defineProperties(Consumer, {
      Provider: {
        get() {
          if (!hasWarnedAboutUsingConsumerProvider) {
            hasWarnedAboutUsingConsumerProvider = true;
            warning(
              false,
              'Rendering <Context.Consumer.Provider> is not supported and will be removed in ' +
                'a future major release. Did you mean to render <Context.Provider> instead?',
            );
          }
          return context.Provider;
        },
        set(_Provider) {
          context.Provider = _Provider;
        },
      },
      _currentValue: {
        get() {
          return context._currentValue;
        },
        set(_currentValue) {
          context._currentValue = _currentValue;
        },
      },
      _currentValue2: {
        get() {
          return context._currentValue2;
        },
        set(_currentValue2) {
          context._currentValue2 = _currentValue2;
        },
      },
      _threadCount: {
        get() {
          return context._threadCount;
        },
        set(_threadCount) {
          context._threadCount = _threadCount;
        },
      },
      Consumer: {
        get() {
          if (!hasWarnedAboutUsingNestedContextConsumers) {
            hasWarnedAboutUsingNestedContextConsumers = true;
            warning(
              false,
              'Rendering <Context.Consumer.Consumer> is not supported and will be removed in ' +
                'a future major release. Did you mean to render <Context.Consumer> instead?',
            );
          }
          return context.Consumer;
        },
      },
    });
    // $FlowFixMe: Flow complains about missing properties because it doesn't understand defineProperty
    context.Consumer = Consumer;
  } else {
    // Provider 與 Consumer 使用同一個context裡的 _currentValue,等同於上面
    // 當 Consumer 需要渲染時,直接從自身取得 context 最新值 _currentValue 去渲染
    context.Consumer = context;
  }

  if (__DEV__) {
    // 在開發模式下,會跟蹤有多少個檢視進行渲染,react不支援一個context渲染多個檢視
   // 如果有多個渲染器同時渲染,這裡置為null是為了檢測
    context._currentRenderer = null;
    context._currentRenderer2 = null;
  }

  return context;
}
複製程式碼

forwardRef

forwardRef用來轉發ref,前面說到過,createElement在建立ReactElement的時候回將ref過濾掉,而當custom元件是無法接收到ref,所以需要做一層轉發。

使用方式

function App(){
  const inputRef = useRef()

  return (
    <>
      <MyInput ref={inputRef}></MyInput>
    </>
  )
}

const MyInput = React.forwardRef((props,ref)=>{
  return <input type="text" ref={ref}></input>
})
複製程式碼

這樣就可以通過轉發拿到input節點。

原始碼

export default function forwardRef<Props, ElementType: React$ElementType>(
  render: (props: Props, ref: React$Ref<ElementType>) => React$Node,
) {
  // 接收一個函式元件作為引數
  if (__DEV__) {
    // 必須將forwardRef放在最外層,例如memo(forwardRef((props,ref)=>{ ... }))是錯誤的
    if (render != null && render.$$typeof === REACT_MEMO_TYPE) {
      warningWithoutStack(
        false,
        'forwardRef requires a render function but received a `memo` ' +
          'component. Instead of forwardRef(memo(...)), use ' +
          'memo(forwardRef(...)).',
      );
    } else if (typeof render !== 'function') {
      // 只接受函式元件
      warningWithoutStack(
        false,
        'forwardRef requires a render function but was given %s.',
        render === null ? 'null' : typeof render,
      );
    } else {
      // forwardRef 接收的函式元件必須為2個或者0個(使用形參)引數
      warningWithoutStack(
        // Do not warn for 0 arguments because it could be due to usage of the 'arguments' object
        render.length === 0 || render.length === 2,
        'forwardRef render functions accept exactly two parameters: props and ref. %s',
        render.length === 1
          ? 'Did you forget to use the ref parameter?'
          : 'Any additional parameter will be undefined.',
      );
    }

    if (render != null) {
      // forwardRef 不支援轉發defaultProps和PropTypes ,不能將帶有propTypes的元件傳給forwardRef
      warningWithoutStack(
        render.defaultProps == null && render.propTypes == null,
        'forwardRef render functions do not support propTypes or defaultProps.  Did you accidentally pass a React component?',
      );
    }
  }
  // 在dom裡對REACT_FORWARD_REF_TYPE做一層轉發
  return {
    $$typeof: REACT_FORWARD_REF_TYPE,
    render,
  };
}
複製程式碼

如果只看react部分的原始碼貌似沒有什麼頭緒,因為大部分處理邏輯都在react-domreact-reconciler 裡面.

memo

memo會返回一個純化(purified)的元件MemoFuncComponent,它的作用等同於PureComponent,只不過前者用於函式元件,後者用於類元件。

function App(){
  const [value,setValue] = useState(0)
  return <MyInput value={value}></MyInput>
}

const MyInput = memo((props)=>{
  return <input value={props.value}></input>
})
複製程式碼

react會自動對props的值做Object.is比較,如果此次的值與上次的值不一樣,則更新,否則不更新。也可以自定義比較方法,當compare函式返回true時代表更新,false則不更新。

const MyInput = memo((props)=>{
  return <input value={props.value}></input>
},(newProps,oldProps) => false) // 始終不會更新
複製程式碼

原始碼

export default function memo<Props>(
  type: React$ElementType,
  compare?: (oldProps: Props, newProps: Props) => boolean,
) {
  if (__DEV__) {
    // memo只接受函式元件
    if (!isValidElementType(type)) {
      warningWithoutStack(
        false,
        'memo: The first argument must be a component. Instead ' +
          'received: %s',
        type === null ? 'null' : typeof type,
      );
    }
  }
  return {
    $$typeof: REACT_MEMO_TYPE,
    type,
    compare: compare === undefined ? null : compare,
  };
}
複製程式碼

comparenull時代表預設Object.is比較。

相關文章