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-dom
和 react-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,
};
}
複製程式碼
當compare
為null
時代表預設Object.is
比較。