React 组件
React 组件
React 组件可以定义为 class
组件 或 函数组件 两种形式。
React 组件本质上就是类和函数,与常规的类和函数不同的是,组件承载了渲染视图的 UI 和更新视图的 setState
、 useState
等方法。
React 在底层逻辑上会像正常实例化类和正常执行函数那样处理的组件。因此,函数与类上的特性在 React 组件上同样具有,比如原型链,继承,静态属性等。
类组件与函数组件示例
// 类组件
class Welcome extends React.Component {
state = { message: `Hello World!` }
sayHelloJs = () => this.setState({ message: 'Hello JavaScript!' })
render() {
return (
<div style={{ marginTop: '20px' }} onClick={this.sayHelloJs}>
{this.state.message}
</div>
)
}
}
// 函数组件
function FunComponent() {
const [message, setMessage] = useState('Hello World!')
return <div onClick={() => setMessage('Hello JavaScript!')}>{message}</div>
}
类组件
类组件的组成部分
class Welcome extends React.Component {
constructor(...arg) {
/* 执行 react 底层 Component 函数 */
/* 如果在 super 中,没有传入相关参数(例如:props),则在 constructor 执行上下文中就获取不到 props */
/* 因为绑定 props 是在父类 Component 构造函数中,执行 super 等于执行 Component 函数,此时 props 没有作为第一个参数传给 super() ,在 Component 中就会找不到 props 参数 */
super(...arg)
}
state = {} /* state */
static number = 1 /* 内置静态属性 */
/* 生命周期 */
componentDidMount() {} // 在组件挂载后(插入 DOM 树中)立即调用
componentDidUpdate(prevProps, prevState, snapshot) {} // 在更新后会被立即调用
componentWillUnmount() {} // 在组件卸载及销毁之前直接调用
// ... 省略其他的生命周期函数的执行
handleClick = () => {
console.log('handleClick') /* 方法:箭头函数方法直接绑定在 this 实例上 */
}
/* 渲染函数 */
render() {
return (
{/* 触发 onClick 之后,此时输出结果为 handleClick,因为实例对象上方法属性 > 原型链对象上方法属性 */}
<div style={{ marginTop: '50px' }} onClick={this.handerClick}>
hello,React!
</div>
)
}
}
/* 外置静态属性 */
Welcome.number1 = 2
/* 方法: 绑定在 Welcome 原型链上的方法*/
Welcome.prototype.handleClick = () => { console.log('Welcome.prototype.handleClick') }
类组件的继承
在类组件的继承中,state
和生命周期会被继承后的组件修改。
在 Facebook,我们在成百上千个组件中使用 React。我们并没有发现需要使用继承来构建组件层次的情况。
Props 和组合提供了清晰而安全地定制组件外观和行为的灵活方式。注意:组件可以接受任意 props,包括基本数据类型,React 元素以及函数。
想要在组件间复用非 UI 的功能,建议将其提取为一个单独的 JavaScript 模块,如函数、对象或者类。组件可以直接引入(import)而无需通过 extend 继承它们。
import React from 'react'
class Person extends React.Component {
constructor(props) {
super(props)
}
// componentDidMount : 在组件挂载后(插入 DOM 树中)立即调用
// 被继承后,componentDidMount 生命周期将不会被执行
componentDidMount() {
console.log('Person componentDidMount')
}
render() {
return <div className="person">Person Class Component</div>
}
}
class Programmer extends Person {
constructor(props) {
super(props)
}
// componentDidMount : 在组件挂载后(插入 DOM 树中)立即调用
componentDidMount() {
console.log('Programmer componentDidMount')
}
render() {
return (
<div className="programmer">
{super.render()}
<div>Programmer Class Component</div>
</div>
)
}
}
类组件的实现
在
beginWork(current, workInProgress, renderLanes)
(创建当前节点的子Fiber
节点)中,通过case ClassComponent
调用updateClassComponent
对未初始化的类组件进行初始化,对已初始化的组件更新重用。【beginWork】函数: 创建当前节点的子 Fiber 节点
// packages\react-reconciler\src\ReactFiberBeginWork.old.js function beginWork( current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes ): Fiber | null { // ... switch (workInProgress.tag) { case IndeterminateComponent: { // ... } case LazyComponent: { // ... } case FunctionComponent: { // ... } case ClassComponent: { const Component = workInProgress.type const unresolvedProps = workInProgress.pendingProps const resolvedProps = workInProgress.elementType === Component ? unresolvedProps : resolveDefaultProps(Component, unresolvedProps) return updateClassComponent( current, workInProgress, Component, resolvedProps, renderLanes ) } case HostRoot: { // ... } case HostComponent: { // ... } // .. } // ... }
【updateClassComponent】函数:对未初始化的类组件进行初始化,对已初始化的组件更新重用
// packages\react-reconciler\src\ReactFiberBeginWork.old.js function updateClassComponent( current: Fiber | null, workInProgress: Fiber, Component: any, nextProps: any, renderLanes: Lanes ) { // Push context providers early to prevent context stack mismatches. // During mounting we don't know the child context yet as the instance doesn't exist. // We will invalidate the child context in finishClassComponent() right after rendering. let hasContext if (isLegacyContextProvider(Component)) { hasContext = true pushLegacyContextProvider(workInProgress) } else { hasContext = false } prepareToReadContext(workInProgress, renderLanes) const instance = workInProgress.stateNode let shouldUpdate if (instance === null) { resetSuspendedCurrentOnMountInLegacyMode(current, workInProgress) // In the initial pass we might need to construct the instance. constructClassInstance(workInProgress, Component, nextProps) mountClassInstance(workInProgress, Component, nextProps, renderLanes) shouldUpdate = true } else if (current === null) { // In a resume, we'll already have an instance we can reuse. shouldUpdate = resumeMountClassInstance( workInProgress, Component, nextProps, renderLanes ) } else { shouldUpdate = updateClassInstance( current, workInProgress, Component, nextProps, renderLanes ) } const nextUnitOfWork = finishClassComponent( current, workInProgress, Component, shouldUpdate, hasContext, renderLanes ) return nextUnitOfWork }
在
updateClassComponent
函数中:未创建类组件实例,则表示第一渲染(即:
instance === null
)调用
constructClassInstance(workInProgress, Component, nextProps)
函数,通过let instance = new ctor(props, context)
执行构造函数,并返回类组件实例instance
【constructClassInstance】函数:创建一个类组件实例 instance
// packages\react-reconciler\src\ReactFiberClassComponent.old.js const classComponentUpdater = { isMounted, enqueueSetState(inst, payload, callback) { // ... }, enqueueReplaceState(inst, payload, callback) { // ... }, enqueueForceUpdate(inst, callback) { // ... }, } function adoptClassInstance(workInProgress: Fiber, instance: any): void { // 类组件实例设置 updater // 组件中调用 setState 和 forceUpdate 本质上是调用了 updater 对象上的 enqueueSetState 和 enqueueForceUpdate 方法 instance.updater = classComponentUpdater workInProgress.stateNode = instance // The instance needs access to the fiber so that it can schedule updates setInstance(instance, workInProgress) } function constructClassInstance( workInProgress: Fiber, ctor: any, props: any ): any { // ... let instance = new ctor(props, context) const state = (workInProgress.memoizedState = instance.state !== null && instance.state !== undefined ? instance.state : null) adoptClassInstance(workInProgress, instance) // Cache unmasked context so we can avoid recreating masked context unless necessary. // ReactFiberContext usually updates this cache but can't for newly-created instances. if (isLegacyContextConsumer) { cacheContext(workInProgress, unmaskedContext, context) } return instance }
【Component】构造函数
// packages\react\src\ReactBaseClasses.js /** * Base class helpers for the updating state of a component. */ function Component(props, context, updater) { this.props = props this.context = context // If a component has string refs, we will assign a different object later. this.refs = emptyObject // We initialize the default updater but the real one gets injected by the // renderer. this.updater = updater || ReactNoopUpdateQueue } Component.prototype.isReactComponent = {} /** * Sets a subset of the state. Always use this to mutate * state. You should treat `this.state` as immutable. * * There is no guarantee that `this.state` will be immediately updated, so * accessing `this.state` after calling this method may return the old value. * * There is no guarantee that calls to `setState` will run synchronously, * as they may eventually be batched together. You can provide an optional * callback that will be executed when the call to setState is actually * completed. * * When a function is provided to setState, it will be called at some point in * the future (not synchronously). It will be called with the up to date * component arguments (state, props, context). These values can be different * from this.* because your function may be called after receiveProps but before * shouldComponentUpdate, and this new state, props, and context will not yet be * assigned to this. * * @param {object|function} partialState Next partial state or function to * produce next partial state to be merged with current state. * @param {?function} callback Called after state is updated. * @final * @protected */ Component.prototype.setState = function (partialState, callback) { if ( typeof partialState !== 'object' && typeof partialState !== 'function' && partialState != null ) { throw new Error( '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') } /** * Forces an update. This should only be invoked when it is known with * certainty that we are **not** in a DOM transaction. * * You may want to call this when you know that some deeper aspect of the * component's state has changed but `setState` was not called. * * This will not invoke `shouldComponentUpdate`, but it will invoke * `componentWillUpdate` and `componentDidUpdate`. * * @param {?function} callback Called after update is complete. * @final * @protected */ Component.prototype.forceUpdate = function (callback) { this.updater.enqueueForceUpdate(this, callback, 'forceUpdate') }
调用
mountClassInstance(workInProgress, Component, nextProps, renderLanes)
,挂载类组件实例,主要是更新instance.state
,并且执行一些生命周期【mountClassInstance】函数
// packages\react-reconciler\src\ReactFiberClassComponent.old.js function mountClassInstance( workInProgress: Fiber, ctor: any, newProps: any, renderLanes: Lanes ): void { const instance = workInProgress.stateNode instance.props = newProps instance.state = workInProgress.memoizedState instance.refs = emptyRefsObject initializeUpdateQueue(workInProgress) const contextType = ctor.contextType if (typeof contextType === 'object' && contextType !== null) { instance.context = readContext(contextType) } else if (disableLegacyContext) { instance.context = emptyContextObject } else { const unmaskedContext = getUnmaskedContext(workInProgress, ctor, true) instance.context = getMaskedContext(workInProgress, unmaskedContext) } instance.state = workInProgress.memoizedState // 判断是否有 getDerivedStateFromProps 生命周期并且执行,这个生命周期可能改变State // getDerivedStateFromProps 会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。 const getDerivedStateFromProps = ctor.getDerivedStateFromProps if (typeof getDerivedStateFromProps === 'function') { applyDerivedStateFromProps( workInProgress, ctor, getDerivedStateFromProps, newProps ) // 更新成最新的state instance.state = workInProgress.memoizedState } // In order to support react-lifecycles-compat polyfilled components, // Unsafe lifecycles should not be invoked for components using the new APIs. // // 判断是否有 componentWillMount 生命周期并且执行,该生命周期也可能改变 state if ( typeof ctor.getDerivedStateFromProps !== 'function' && typeof instance.getSnapshotBeforeUpdate !== 'function' && (typeof instance.UNSAFE_componentWillMount === 'function' || typeof instance.componentWillMount === 'function') ) { callComponentWillMount(workInProgress, instance) // If we had additional state updates during this life-cycle, let's // process them now. // 如果改变了 state,就有新的 update 加入 updateQueue processUpdateQueue(workInProgress, newProps, instance, renderLanes) instance.state = workInProgress.memoizedState } if (typeof instance.componentDidMount === 'function') { let fiberFlags: Flags = Update if (enableSuspenseLayoutEffectSemantics) { fiberFlags |= LayoutStatic } workInProgress.flags |= fiberFlags } }
组件渲染被中断(即:
instance !== null
且current === null
),调用resumeMountClassInstance(workInProgress, Component, nextProps, renderLanes)
函数,复用实例并调用首次渲染的生命周期。该函数返回为false
,则表示组件无需更新。【resumeMountClassInstance】函数
// packages\react-reconciler\src\ReactFiberClassComponent.old.js function resumeMountClassInstance( workInProgress: Fiber, ctor: any, newProps: any, renderLanes: Lanes ): boolean { const instance = workInProgress.stateNode const oldProps = workInProgress.memoizedProps instance.props = oldProps const oldContext = instance.context const contextType = ctor.contextType let nextContext = emptyContextObject if (typeof contextType === 'object' && contextType !== null) { nextContext = readContext(contextType) } else if (!disableLegacyContext) { const nextLegacyUnmaskedContext = getUnmaskedContext( workInProgress, ctor, true ) nextContext = getMaskedContext( workInProgress, nextLegacyUnmaskedContext ) } const getDerivedStateFromProps = ctor.getDerivedStateFromProps const hasNewLifecycles = typeof getDerivedStateFromProps === 'function' || typeof instance.getSnapshotBeforeUpdate === 'function' // Note: During these life-cycles, instance.props/instance.state are what // ever the previously attempted to render - not the "current". However, // during componentDidUpdate we pass the "current" props. // In order to support react-lifecycles-compat polyfilled components, // Unsafe lifecycles should not be invoked for components using the new APIs. if ( !hasNewLifecycles && (typeof instance.UNSAFE_componentWillReceiveProps === 'function' || typeof instance.componentWillReceiveProps === 'function') ) { if (oldProps !== newProps || oldContext !== nextContext) { callComponentWillReceiveProps( workInProgress, instance, newProps, nextContext ) } } resetHasForceUpdateBeforeProcessing() const oldState = workInProgress.memoizedState let newState = (instance.state = oldState) processUpdateQueue(workInProgress, newProps, instance, renderLanes) newState = workInProgress.memoizedState if ( oldProps === newProps && oldState === newState && !hasContextChanged() && !checkHasForceUpdateAfterProcessing() ) { // If an update was already in progress, we should schedule an Update // effect even though we're bailing out, so that cWU/cDU are called. if (typeof instance.componentDidMount === 'function') { let fiberFlags: Flags = Update if (enableSuspenseLayoutEffectSemantics) { fiberFlags |= LayoutStatic } if ( __DEV__ && enableStrictEffects && (workInProgress.mode & StrictEffectsMode) !== NoMode ) { fiberFlags |= MountLayoutDev } workInProgress.flags |= fiberFlags } return false } if (typeof getDerivedStateFromProps === 'function') { applyDerivedStateFromProps( workInProgress, ctor, getDerivedStateFromProps, newProps ) newState = workInProgress.memoizedState } const shouldUpdate = checkHasForceUpdateAfterProcessing() || checkShouldComponentUpdate( workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext ) if (shouldUpdate) { // In order to support react-lifecycles-compat polyfilled components, // Unsafe lifecycles should not be invoked for components using the new APIs. if ( !hasNewLifecycles && (typeof instance.UNSAFE_componentWillMount === 'function' || typeof instance.componentWillMount === 'function') ) { if (typeof instance.componentWillMount === 'function') { instance.componentWillMount() } if (typeof instance.UNSAFE_componentWillMount === 'function') { instance.UNSAFE_componentWillMount() } } if (typeof instance.componentDidMount === 'function') { let fiberFlags: Flags = Update if (enableSuspenseLayoutEffectSemantics) { fiberFlags |= LayoutStatic } workInProgress.flags |= fiberFlags } } else { // If an update was already in progress, we should schedule an Update // effect even though we're bailing out, so that cWU/cDU are called. if (typeof instance.componentDidMount === 'function') { let fiberFlags: Flags = Update if (enableSuspenseLayoutEffectSemantics) { fiberFlags |= LayoutStatic } workInProgress.flags |= fiberFlags } // If shouldComponentUpdate returned false, we should still update the // memoized state to indicate that this work can be reused. workInProgress.memoizedProps = newProps workInProgress.memoizedState = newState } // Update the existing instance's state, props, and context pointers even // if shouldComponentUpdate returns false. instance.props = newProps instance.state = newState instance.context = nextContext return shouldUpdate }
更新组件(即:
instance !== null
且current !== null
),调用updateClassInstance
函数,并调用didUpdate
和componentWillReceiveProp
生命周期。该函数返回为false
,则表示组件无需更新。【updateClassInstance】函数
// packages\react-reconciler\src\ReactFiberClassComponent.old.js // Invokes the update life-cycles and returns false if it shouldn't rerender. function updateClassInstance( current: Fiber, workInProgress: Fiber, ctor: any, newProps: any, renderLanes: Lanes ): boolean { const instance = workInProgress.stateNode cloneUpdateQueue(current, workInProgress) const unresolvedOldProps = workInProgress.memoizedProps const oldProps = workInProgress.type === workInProgress.elementType ? unresolvedOldProps : resolveDefaultProps(workInProgress.type, unresolvedOldProps) instance.props = oldProps const unresolvedNewProps = workInProgress.pendingProps const oldContext = instance.context const contextType = ctor.contextType let nextContext = emptyContextObject if (typeof contextType === 'object' && contextType !== null) { nextContext = readContext(contextType) } else if (!disableLegacyContext) { const nextUnmaskedContext = getUnmaskedContext( workInProgress, ctor, true ) nextContext = getMaskedContext(workInProgress, nextUnmaskedContext) } const getDerivedStateFromProps = ctor.getDerivedStateFromProps const hasNewLifecycles = typeof getDerivedStateFromProps === 'function' || typeof instance.getSnapshotBeforeUpdate === 'function' // Note: During these life-cycles, instance.props/instance.state are what // ever the previously attempted to render - not the "current". However, // during componentDidUpdate we pass the "current" props. // In order to support react-lifecycles-compat polyfilled components, // Unsafe lifecycles should not be invoked for components using the new APIs. if ( !hasNewLifecycles && (typeof instance.UNSAFE_componentWillReceiveProps === 'function' || typeof instance.componentWillReceiveProps === 'function') ) { if ( unresolvedOldProps !== unresolvedNewProps || oldContext !== nextContext ) { callComponentWillReceiveProps( workInProgress, instance, newProps, nextContext ) } } resetHasForceUpdateBeforeProcessing() const oldState = workInProgress.memoizedState let newState = (instance.state = oldState) processUpdateQueue(workInProgress, newProps, instance, renderLanes) newState = workInProgress.memoizedState if ( unresolvedOldProps === unresolvedNewProps && oldState === newState && !hasContextChanged() && !checkHasForceUpdateAfterProcessing() && !( enableLazyContextPropagation && current !== null && current.dependencies !== null && checkIfContextChanged(current.dependencies) ) ) { // If an update was already in progress, we should schedule an Update // effect even though we're bailing out, so that cWU/cDU are called. if (typeof instance.componentDidUpdate === 'function') { if ( unresolvedOldProps !== current.memoizedProps || oldState !== current.memoizedState ) { workInProgress.flags |= Update } } if (typeof instance.getSnapshotBeforeUpdate === 'function') { if ( unresolvedOldProps !== current.memoizedProps || oldState !== current.memoizedState ) { workInProgress.flags |= Snapshot } } return false } if (typeof getDerivedStateFromProps === 'function') { applyDerivedStateFromProps( workInProgress, ctor, getDerivedStateFromProps, newProps ) newState = workInProgress.memoizedState } const shouldUpdate = checkHasForceUpdateAfterProcessing() || checkShouldComponentUpdate( workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext ) || // TODO: In some cases, we'll end up checking if context has changed twice, // both before and after `shouldComponentUpdate` has been called. Not ideal, // but I'm loath to refactor this function. This only happens for memoized // components so it's not that common. (enableLazyContextPropagation && current !== null && current.dependencies !== null && checkIfContextChanged(current.dependencies)) if (shouldUpdate) { // In order to support react-lifecycles-compat polyfilled components, // Unsafe lifecycles should not be invoked for components using the new APIs. if ( !hasNewLifecycles && (typeof instance.UNSAFE_componentWillUpdate === 'function' || typeof instance.componentWillUpdate === 'function') ) { if (typeof instance.componentWillUpdate === 'function') { instance.componentWillUpdate(newProps, newState, nextContext) } if (typeof instance.UNSAFE_componentWillUpdate === 'function') { instance.UNSAFE_componentWillUpdate( newProps, newState, nextContext ) } } if (typeof instance.componentDidUpdate === 'function') { workInProgress.flags |= Update } if (typeof instance.getSnapshotBeforeUpdate === 'function') { workInProgress.flags |= Snapshot } } else { // If an update was already in progress, we should schedule an Update // effect even though we're bailing out, so that cWU/cDU are called. if (typeof instance.componentDidUpdate === 'function') { if ( unresolvedOldProps !== current.memoizedProps || oldState !== current.memoizedState ) { workInProgress.flags |= Update } } if (typeof instance.getSnapshotBeforeUpdate === 'function') { if ( unresolvedOldProps !== current.memoizedProps || oldState !== current.memoizedState ) { workInProgress.flags |= Snapshot } } // If shouldComponentUpdate returned false, we should still update the // memoized props/state to indicate that this work can be reused. workInProgress.memoizedProps = newProps workInProgress.memoizedState = newState } // Update the existing instance's state, props, and context pointers even // if shouldComponentUpdate returns false. instance.props = newProps instance.state = newState instance.context = nextContext return shouldUpdate }
最终执行
finishClassComponent
,进行错误判断处理和判断是否可以跳过更新的过程,重新调和子节点reconcileChildren
【finishClassComponent】函数
// packages\react-reconciler\src\ReactFiberBeginWork.old.js function finishClassComponent( current: Fiber | null, workInProgress: Fiber, Component: any, shouldUpdate: boolean, hasContext: boolean, renderLanes: Lanes ) { // Refs should update even if shouldComponentUpdate returns false markRef(current, workInProgress) const didCaptureError = (workInProgress.flags & DidCapture) !== NoFlags if (!shouldUpdate && !didCaptureError) { // Context providers should defer to sCU for rendering if (hasContext) { invalidateContextProvider(workInProgress, Component, false) } return bailoutOnAlreadyFinishedWork( current, workInProgress, renderLanes ) } const instance = workInProgress.stateNode // Rerender ReactCurrentOwner.current = workInProgress let nextChildren if ( didCaptureError && typeof Component.getDerivedStateFromError !== 'function' ) { // If we captured an error, but getDerivedStateFromError is not defined, // unmount all the children. componentDidCatch will schedule an update to // re-render a fallback. This is temporary until we migrate everyone to // the new API. // TODO: Warn in a future release. nextChildren = null if (enableProfilerTimer) { stopProfilerTimerIfRunning(workInProgress) } } else { if (enableSchedulingProfiler) { markComponentRenderStarted(workInProgress) } if (__DEV__) { setIsRendering(true) nextChildren = instance.render() if ( debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictLegacyMode ) { setIsStrictModeForDevtools(true) try { instance.render() } finally { setIsStrictModeForDevtools(false) } } setIsRendering(false) } else { nextChildren = instance.render() } if (enableSchedulingProfiler) { markComponentRenderStopped() } } // React DevTools reads this flag. workInProgress.flags |= PerformedWork if (current !== null && didCaptureError) { // If we're recovering from an error, reconcile without reusing any of // the existing children. Conceptually, the normal children and the children // that are shown on error are two different sets, so we shouldn't reuse // normal children even if their identities match. forceUnmountCurrentAndReconcile( current, workInProgress, nextChildren, renderLanes ) } else { reconcileChildren(current, workInProgress, nextChildren, renderLanes) } // Memoize state using the values we just used to render. // TODO: Restructure so we never read values from the instance. workInProgress.memoizedState = instance.state // The context might have changed so we need to recalculate it. if (hasContext) { invalidateContextProvider(workInProgress, Component, true) } return workInProgress.child }
函数组件
函数组件的组成部分
function Welcome() {
console.log(Welcome.number) // 输出结果为 1
/* Hook API */
/* useState hook:返回一个 state,以及更新 state 的函数 */
const [message, setMessage] = useState('hello,world')
/* useEffect hook:接收一个包含命令式、且可能有副作用代码的函数 */
useEffect(() => {
document.title = `You clicked ${count} times`
})
// ... 省略相关 Hook API 的使用
/* return 返回值作为渲染 UI */
return (
<div onClick={() => setMessage('let us learn React!')}> {message} </div>
)
}
Welcome.number = 1 /* 绑定静态属性 */
注意:不要尝试给函数组件 prototype
绑定属性或方法,即使绑定了也没有任何作用,因为通过上面源码中 React 对函数组件的调用,是采用直接执行函数的方式,而不是通过 new
的方式。
类组件与函数组件的区别:
- 在类组件中,底层只需要实例化一次,实例中保存了组件的
state
等状态。对于每一次更新只需要调用render
方法以及对应的生命周期就可以了 - 在函数组件中,每一次更新都是一次新的函数执行,一次函数组件的更新,里面的变量会重新声明
函数组件的实现
在
beginWork(current, workInProgress, renderLanes)
(创建当前节点的子Fiber
节点)中,通过case ClassComponent
调用updateFunctionComponent
函数【beginWork】函数: 创建当前节点的子 Fiber 节点
// packages\react-reconciler\src\ReactFiberBeginWork.old.js function beginWork( current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes ): Fiber | null { // ... switch (workInProgress.tag) { case IndeterminateComponent: { // ... } case LazyComponent: { // ... } case FunctionComponent: { const Component = workInProgress.type const unresolvedProps = workInProgress.pendingProps const resolvedProps = workInProgress.elementType === Component ? unresolvedProps : resolveDefaultProps(Component, unresolvedProps) return updateFunctionComponent( current, workInProgress, Component, resolvedProps, renderLanes ) } case ClassComponent: { // ... } case HostRoot: { // ... } case HostComponent: { // ... } // .. } // ... }
【updateFunctionComponent】函数
// packages\react-reconciler\src\ReactFiberBeginWork.old.js function updateFunctionComponent( current, workInProgress, Component, nextProps: any, renderLanes ) { let context if (!disableLegacyContext) { const unmaskedContext = getUnmaskedContext( workInProgress, Component, true ) context = getMaskedContext(workInProgress, unmaskedContext) } let nextChildren let hasId prepareToReadContext(workInProgress, renderLanes) if (enableSchedulingProfiler) { markComponentRenderStarted(workInProgress) } if (__DEV__) { // ... } else { nextChildren = renderWithHooks( current, workInProgress, Component, nextProps, context, renderLanes ) hasId = checkDidRenderIdHook() } if (enableSchedulingProfiler) { markComponentRenderStopped() } if (current !== null && !didReceiveUpdate) { bailoutHooks(current, workInProgress, renderLanes) return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes) } if (getIsHydrating() && hasId) { pushMaterializedTreeId(workInProgress) } // React DevTools reads this flag. workInProgress.flags |= PerformedWork reconcileChildren(current, workInProgress, nextChildren, renderLanes) return workInProgress.child }
在
updateFunctionComponent
函数中:通过调用
renderWithHooks
函数,执行let children = Component(props, secondArg)
调用函数组件(即:用户编写的函数组件),获得一个ReactElement
(即:nextChildren
)【renderWithHooks】函数
// packages\react-reconciler\src\ReactFiberHooks.old.js export function renderWithHooks<Props, SecondArg>( current: Fiber | null, workInProgress: Fiber, Component: (p: Props, arg: SecondArg) => any, props: Props, secondArg: SecondArg, nextRenderLanes: Lanes ): any { renderLanes = nextRenderLanes currentlyRenderingFiber = workInProgress workInProgress.memoizedState = null workInProgress.updateQueue = null workInProgress.lanes = NoLanes // The following should have already been reset // currentHook = null; // workInProgressHook = null; // didScheduleRenderPhaseUpdate = false; // localIdCounter = 0; // TODO Warn if no hooks are used at all during mount, then some are used during update. // Currently we will identify the update render as a mount because memoizedState === null. // This is tricky because it's valid for certain types of components (e.g. React.lazy) // Using memoizedState to differentiate between mount/update only works if at least one stateful hook is used. // Non-stateful hooks (e.g. context) don't get added to memoizedState, // so memoizedState would be null during updates and mounts. if (__DEV__) { // ... } else { ReactCurrentDispatcher.current = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate } let children = Component(props, secondArg) // Check if there was a render phase update if (didScheduleRenderPhaseUpdateDuringThisPass) { // Keep rendering in a loop for as long as render phase updates continue to // be scheduled. Use a counter to prevent infinite loops. let numberOfReRenders: number = 0 do { didScheduleRenderPhaseUpdateDuringThisPass = false localIdCounter = 0 if (numberOfReRenders >= RE_RENDER_LIMIT) { throw new Error( 'Too many re-renders. React limits the number of renders to prevent ' + 'an infinite loop.' ) } numberOfReRenders += 1 // Start over from the beginning of the list currentHook = null workInProgressHook = null workInProgress.updateQueue = null ReactCurrentDispatcher.current = __DEV__ ? HooksDispatcherOnRerenderInDEV : HooksDispatcherOnRerender children = Component(props, secondArg) } while (didScheduleRenderPhaseUpdateDuringThisPass) } // We can assume the previous dispatcher is always this one, since we set it // at the beginning of the render phase and there's no re-entrance. ReactCurrentDispatcher.current = ContextOnlyDispatcher // This check uses currentHook so that it works the same in DEV and prod bundles. // hookTypesDev could catch more cases (e.g. context) but only in DEV bundles. const didRenderTooFewHooks = currentHook !== null && currentHook.next !== null renderLanes = NoLanes currentlyRenderingFiber = (null: any) currentHook = null workInProgressHook = null didScheduleRenderPhaseUpdate = false // This is reset by checkDidRenderIdHook // localIdCounter = 0; if (didRenderTooFewHooks) { throw new Error( 'Rendered fewer hooks than expected. This may be caused by an accidental ' + 'early return statement.' ) } if (enableLazyContextPropagation) { if (current !== null) { if (!checkIfWorkInProgressReceivedUpdate()) { // If there were no changes to props or state, we need to check if there // was a context change. We didn't already do this because there's no // 1:1 correspondence between dependencies and hooks. Although, because // there almost always is in the common case (`readContext` is an // internal API), we could compare in there. OTOH, we only hit this case // if everything else bails out, so on the whole it might be better to // keep the comparison out of the common path. const currentDependencies = current.dependencies if ( currentDependencies !== null && checkIfContextChanged(currentDependencies) ) { markWorkInProgressReceivedUpdate() } } } } return children }
调用
reconcileChildren
函数,改变workInProgress.child
当前节点为
null
(即:current === null
),则表示第一次渲染,调用mountChildFibers
函数,并将函数返回值赋值给workInProgress.child
// packages\react-reconciler\src\ReactChildFiber.old.js export const mountChildFibers = ChildReconciler(false)
当前节点不为
null
(即:current !== null
),则表示更新节点,调用reconcileChildFibers
函数,并将函数返回值赋值给workInProgress.child
// packages\react-reconciler\src\ReactChildFiber.old.js export const reconcileChildFibers = ChildReconciler(true)
【reconcileChildren】函数
export function reconcileChildren( current: Fiber | null, workInProgress: Fiber, nextChildren: any, renderLanes: Lanes ) { if (current === null) { // 第一次渲染 // If this is a fresh new component that hasn't been rendered yet, we // won't update its child set by applying minimal side-effects. Instead, // we will add them all to the child before it gets rendered. That means // we can optimize this reconciliation pass by not tracking side-effects. workInProgress.child = mountChildFibers( workInProgress, null, nextChildren, renderLanes ) } else { // 更新组件 // If the current child is the same as the work in progress, it means that // we haven't yet started any work on these children. Therefore, we use // the clone algorithm to create a copy of all the current children. // If we had any progressed work already, that is invalid at this point so // let's throw it out. workInProgress.child = reconcileChildFibers( workInProgress, current.child, nextChildren, renderLanes ) } }
【ChildReconciler】函数
// packages\react-reconciler\src\ReactChildFiber.old.js // This wrapper function exists because I expect to clone the code in each path // to be able to optimize each path individually by branching early. This needs // a compiler or we can do it manually. Helpers that don't need this branching // live outside of this function. function ChildReconciler(shouldTrackSideEffects) { // ... // This API will tag the children with the side-effect of the reconciliation // itself. They will be added to the side-effect list as we pass through the // children and the parent. function reconcileChildFibers( returnFiber: Fiber, currentFirstChild: Fiber | null, newChild: any, lanes: Lanes ): Fiber | null { // This function is not recursive. // If the top level item is an array, we treat it as a set of children, // not as a fragment. Nested arrays on the other hand will be treated as // fragment nodes. Recursion happens at the normal flow. // Handle top level unkeyed fragments as if they were arrays. // This leads to an ambiguity between <>{[...]}</> and <>...</>. // We treat the ambiguous cases above the same. const isUnkeyedTopLevelFragment = typeof newChild === 'object' && newChild !== null && newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null if (isUnkeyedTopLevelFragment) { newChild = newChild.props.children } // Handle object types if (typeof newChild === 'object' && newChild !== null) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: return placeSingleChild( reconcileSingleElement( returnFiber, currentFirstChild, newChild, lanes ) ) case REACT_PORTAL_TYPE: return placeSingleChild( reconcileSinglePortal( returnFiber, currentFirstChild, newChild, lanes ) ) case REACT_LAZY_TYPE: const payload = newChild._payload const init = newChild._init // TODO: This function is supposed to be non-recursive. return reconcileChildFibers( returnFiber, currentFirstChild, init(payload), lanes ) } if (isArray(newChild)) { return reconcileChildrenArray( returnFiber, currentFirstChild, newChild, lanes ) } if (getIteratorFn(newChild)) { return reconcileChildrenIterator( returnFiber, currentFirstChild, newChild, lanes ) } throwOnInvalidObjectType(returnFiber, newChild) } if ( (typeof newChild === 'string' && newChild !== '') || typeof newChild === 'number' ) { return placeSingleChild( reconcileSingleTextNode( returnFiber, currentFirstChild, '' + newChild, lanes ) ) } if (__DEV__) { if (typeof newChild === 'function') { warnOnFunctionType(returnFiber) } } // Remaining cases are all treated as empty. return deleteRemainingChildren(returnFiber, currentFirstChild) } return reconcileChildFibers }
在
ChildReconciler
函数中,最后会返回reconcileChildFibers
函数。在
reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes)
函数中:判断
newChild
节点(调用函数组件返回的新child
)的类型,并执行不同的操作。newChild
为object
且不为null
的情况下newChild.$$typeof
为REACT_ELEMENT_TYPE
,则调用placeSingleChild(reconcileSingleElement())
newChild.$$typeof
为REACT_PORTAL_TYPE
,则调用placeSingleChild(reconcileSinglePortal())
newChild.$$typeof
为REACT_LAZY_TYPE
,则调用reconcileChildFibers()
newChild
为Array
,则调用reconcileChildrenArray()
newChild
为Iterator
,则调用reconcileChildrenIterator()
- 否则,调用
throwOnInvalidObjectType
抛出错误
newChild
为string
(不为空字符串)或者number
,则调用placeSingleChild(reconcileSingleTextNode())
更新渲染时
placeSingleChild
会把新创建的fiber
节点标记为Placement
, 待到commit
阶段处理,其中ReactElement
,Portal
,TextNode
三种类型的节点需要进行处理然后,调用
deleteRemainingChildren
删除掉所有子节点。因为,最后只有可能newChild === null
,说明新的更新清空掉了所有子节点。在
deleteRemainingChildren
函数中,通过调用deleteChild
逐个删除子节点,但删除子节点并不是真的删除这个对象,而是通过firstEffect
、lastEffect
、nextEffect
属性来维护一个EffectList
(链表结构),通过effectTag
标记当前删除操作,在后续commit
阶段会使用到。
最终,返回
workInProgress.child
React 组件通信方式
props / callback 方式
父组件:通过
props
将信息传递给子组件【父组件】 -> 通过自身
state
改变,重新渲染,传递props
-> 通知【子组件】子组件:通过执行
props
中的回调函数callback
来触发父组件的方法【子组件】 -> 通过调用【父组件】
props
方法 -> 通知【父组件】
import { useState } from 'react'
function Child(props) {
const { parentVal, onChangeParentVal } = props
return (
<div className="child">
<div>【子组件】</div>
<div>父组件 parentVal :{parentVal}</div>
<input
placeholder="修改 childVal 的值"
onChange={e => onChangeParentVal(e.target.value)}
/>
</div>
)
}
function Parent() {
const [parentVal, setParentVal] = useState('')
const [childVal, setChildVal] = useState('')
return (
<div className="parent">
<div>【父组件】</div>
<div>子组件 childVal : {childVal}</div>
<input
placeholder="修改 parentVal 的值"
onChange={e => setParentVal(e.target.value)}
/>
<Child parentVal={parentVal} onChangeParentVal={setChildVal}></Child>
</div>
)
}
ref 方式
React 支持一个特殊的、可以附加到任何组件上的 ref
属性。此属性可以是一个由 React.createRef()
函数创建的对象、或者一个回调函数、或者一个字符串(遗留 API)。
当 ref
属性是一个回调函数时,此函数会(根据元素的类型)接收底层 DOM 元素或 class
实例作为其参数,能够直接访问 DOM 元素或组件实例。
// React.forwardRef 接受渲染函数作为参数。React 将使用 props 和 ref 作为参数来调用此函数。此函数应返回 React 节点。
const Child = forwardRef((props, ref) => {
const inputRef = useRef()
// useImperativeHandle: 配合 forwardRef 自定义暴露给父组件的实例值
// useImperativeHandle(ref, createHandle, deps) 接受三个参数:
// > ref : 接受 forWardRef 传递过来的 ref
// > createHandle : 处理函数,返回值作为暴露给父组件的ref对象
// > deps : 依赖项 deps,依赖项更改形成新的ref对象
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus()
},
}))
return (
<div className="child">
<div>【子组件】</div>
<input type="text" ref={inputRef} />
</div>
)
})
const Parent = () => {
const childRef = useRef(null)
return (
<div className="parent">
<div>【父组件】</div>
<button onClick={() => childRef.current.focus()}>
调用子组件方法获取焦点
</button>
<Child ref={childRef} />
</div>
)
}
Context 方式
Context
设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。主要应用场景在于很多不同层级的组件需要访问同样一些的数据。
import React, { useState, useContext } from 'react'
const themes = {
light: { foreground: '#000000', background: '#eeeeee' },
dark: { foreground: '#ffffff', background: '#222222' },
}
const ThemeContext = React.createContext(themes.light)
const Child = () => {
// useContext hook : 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。
// 当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。
// 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。
const { foreground, background, setThemeContext } = useContext(ThemeContext)
return (
<button
style={{ color: foreground, background: background }}
onClick={() => setThemeContext(themes.light)}
>
I am styled by theme context!
</button>
)
}
const Parent = () => {
const [themeContextValue, setThemeContext] = useState(themes.dark)
return (
// Context.Provider : 每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化。
// Provider 接收一个 value 属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。
// 当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。
<ThemeContext.Provider value={{ ...themeContextValue, setThemeContext }}>
<Child />
</ThemeContext.Provider>
)
}
Event Bus 事件总线方式
在绝大多数情况下,不鼓励使用全局的事件总线在组件之间进行通信。在短期内往往是最简单的解决方案,从长期来看,它维护起来比较困难。
事件总线模式可以使用 npm 库进行处理,例如 mitt
或 tiny-emitter
。
import { useState, useEffect } from 'react'
import mitt from 'mitt'
const emitter = mitt()
function Child() {
const [parentVal, setParentVal] = useState('')
useEffect(() => {
emitter.on('parentChange', value => {
setParentVal(value)
})
})
return (
<div className="child">
<div>【子组件】</div>
<div>父组件 parentVal :{parentVal}</div>
<input
placeholder="子组件通过 Event Bus 触发父组件的事件"
onChange={e => emitter.emit('childChange', e.target.value)}
/>
</div>
)
}
function Parent() {
const [childVal, setChildVal] = useState('')
useEffect(() => {
emitter.on('childChange', value => {
setChildVal(value)
})
})
return (
<div className="parent">
<div>【父组件】</div>
<div>子组件 childVal : {childVal}</div>
<input
placeholder="修改 parentVal 的值"
onChange={e => emitter.emit('parentChange', e.target.value)}
/>
<Child />
</div>
)
}
React-redux / React-mobx 状态管理方式
import { configureStore, createSlice } from '@reduxjs/toolkit'
import { Provider, useSelector, useDispatch, shallowEqual } from 'react-redux'
const initialState = {
parentVal: '',
childVal: '',
}
const welcomeSlice = createSlice({
name: 'welcome',
initialState,
reducers: {
changeParentVal(state, { payload }) {
state.parentVal = payload
},
changeChildVal(state, { payload }) {
state.childVal = payload
},
},
extraReducers: {},
})
const store = configureStore({
reducer: {
welcome: welcomeSlice.reducer,
},
})
function Child() {
const { parentVal } = useSelector(state => state.welcome, shallowEqual)
const { changeChildVal } = welcomeSlice.actions
const dispatch = useDispatch()
return (
<div className="child">
<div>【子组件】</div>
<div>父组件 parentVal :{parentVal}</div>
<input
placeholder="修改 childVal 的值"
onChange={e => dispatch(changeChildVal(e.target.value))}
/>
</div>
)
}
function Parent() {
const { childVal } = useSelector(state => state.welcome, shallowEqual)
const { changeParentVal } = welcomeSlice.actions
const dispatch = useDispatch()
return (
<div className="parent">
<div>【父组件】</div>
<div>子组件 childVal : {childVal}</div>
<input
placeholder="修改 parentVal 的值"
onChange={e => dispatch(changeParentVal(e.target.value))}
/>
<Child />
</div>
)
}
export default function App() {
return (
<div>
<Provider store={store}>
<Parent />
</Provider>
</div>
)
}