Skip to content

Latest commit

 

History

History
330 lines (261 loc) · 10.6 KB

context.md

File metadata and controls

330 lines (261 loc) · 10.6 KB

源码解析二十八 context

contextreact中一个非常方便的特性,它可以跨越任意组件,进行数据的传递及触发渲染,react-reduxmobx-react都使用了context

我们可以通过createContext来创建一个context,整个context的数据结构非常简单

export interface ReactContext<T> {
  $$typeof: string,
  Consumer: ReactContext<T>,
  Provider: ReactProviderType<T>,
  _calculateChangedBits: (a: T, b: T) => number,
  _currentValue: T,
}

创建的过程就是生成了一个context,并注册好ProviderConsumer,将默认值挂在context_currentValue

function createContext<T>(defaultValue: T, calculateChangedBits?: (a: T, b: T) => number): ReactContext<T> {
  if (calculateChangedBits === undefined) {
    calculateChangedBits = null
  }

  const context = {
    $$typeof: REACT_CONTEXT_TYPE,
    _calculateChangedBits: calculateChangedBits,

    _currentValue: defaultValue,

    Provider: null,
    Consumer: null,
  }

  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  }

  context.Consumer = context

  return context
}

另外,在fiber中也有一个contextDependencies属性,记录当前fiber上挂载的context队列,同其他所有的队列一样,这里的context队列也是使用链表实现

export interface ContextDependencyList {
  first: ContextDependency<any>,
  expirationTime: ExpirationTime,
}

interface ContextDependency<T> {
  context: ReactContext<T>,
  observedBits: number,
  next: ContextDependency<T>,
}

Provider

在使用的时候,我们都会先通过provider注册一个context的生产者,在beginWork中,如果判断出是一个Provider,会调用updateContextProvider

在这个函数中,我们首先拿到新的value值,并通过pushProvider,将当前的context作为上下文,这里的上下文同样使用了前面提到的栈,并完全遵循同样的规则,beginWork中入栈,completeWork中出栈。最后,将新的value注入到context

function pushProvider(providerFiber: Fiber, nextValue: any) {
  const context = providerFiber.type._context
  push(valueCursor, context)

  context._currentValue = nextValue
}

createContext接受第二个参数,这个参数类似于memo中的第二个参数以及shouldComponentUpdate,是用来给我们作优化用的,pushProvider后,会调用context模块的calculateChangedBits,这个函数会拿到我们注册context时传入的函数并调用它

function calculateChangedBits<T>(context: ReactContext<T>, newValue: T, oldValue: T) {
  if (Object.is(oldValue, newValue)) {
    return 0
  } else {
    const { _calculateChangedBits } = context
    const changedBits = isFunction(_calculateChangedBits) ? _calculateChangedBits(oldValue, newValue) : MAX_SIGNED_31_BIT_INT

    return changedBits | 0
  }
}

根据calculateChangedBits的返回结果,如果是0则跳过这次更新,反之则继续执行propagateContextChange

function updateContextProvider(current: Fiber, workInProgress: Fiber, renderExpirationTime: ExpirationTime): Fiber {
  const providerType = workInProgress.type
  const context = providerType._context

  const oldProps = workInProgress.pendingProps
  const newProps = workInProgress.memoizedProps

  const newValue = newProps.value

  pushProvider(workInProgress, newValue)

  if (oldProps !== null) {
    const oldValue = oldProps.value
    const changedBits = calculateChangedBits(context, newValue, oldValue)

    if (changedBits === 0 && oldProps.children === newProps.children) {
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderExpirationTime)
    } else {
      propagateContextChange(workInProgress, context, changedBits, renderExpirationTime)
    }
  }

  const newChildren = newProps.children
  reconcileChildren(current, workInProgress, newChildren, renderExpirationTime)
  return workInProgress.child
}

最终执行的是propagateContextChange,它会从当前fiber开始,遍历它的子树,如果节点有contextDependencies,则遍历context队列,若存在与当前context相同的context,则更新当前fiberexpirationTime,以及它上级节点的childExpirationTime,使其成为一个待更新节点

function propagateContextChange(workInProgress: Fiber, context: ReactContext<any>, changedBits: number, renderExpirationTime: ExpirationTime) {
  let fiber: Fiber = workInProgress.child
  if (fiber !== null) {
    fiber.return = workInProgress
  }

  while (fiber != null) {
    let nextFiber: Fiber = fiber.child

    const list = fiber.contextDependencies
    if (list !== null) {

      let dependency: ContextDependency<any> = list.first
      while (dependency !== null) {
        if (dependency.context === context && (dependency.observedBits & changedBits) !== 0) {
          // classComponent 需打上更新标
          if (fiber.tag === ClassComponent) {
            const update = new Update(renderExpirationTime, ForceUpdate)
            enqueueUpdate(fiber, update)
          }

          if (fiber.expirationTime < renderExpirationTime) {
            fiber.expirationTime = renderExpirationTime
          }

          const { alternate } = fiber
          if (alternate !== null && alternate.expirationTime < renderExpirationTime) {
            fiber.expirationTime = renderExpirationTime
          }

          scheduleWorkOnParentPath(fiber.return, renderExpirationTime)

          if (list.expirationTime < renderExpirationTime) {
            list.expirationTime = renderExpirationTime
          }

          break
        }
        dependency = dependency.next
      }
    } else if (fiber.tag === ContextProvider) {
      nextFiber = fiber.type === workInProgress.type ? null : nextFiber
    }

    if (nextFiber !== null) {
      nextFiber.return = fiber
    } else {
      nextFiber = fiber
      while (nextFiber !== null) {
        if (nextFiber === workInProgress) {
          nextFiber = null
          break
        }
        const { sibling } = nextFiber
        if (sibling !== null) {
          sibling.return = nextFiber.return
          nextFiber = sibling
          break
        }
        nextFiber = nextFiber.return
      }
    }
    fiber = nextFiber
  }
}

function scheduleWorkOnParentPath(parent: Fiber, renderExpirationTime: ExpirationTime) {
  let node: Fiber = parent
  while (node !== null) {
    const { alternate } = node
    if (node.childExpirationTime < renderExpirationTime) {
      node.childExpirationTime = renderExpirationTime
      if (alternate !== null && alternate.childExpirationTime < renderExpirationTime) {
        alternate.childExpirationTime = renderExpirationTime
      }
    } else if (alternate !== null && alternate.childExpirationTime < renderExpirationTime) {
      alternate.childExpirationTime = renderExpirationTime
    } else {
      break
    }
    node = node.return
  }
}

Consumer

在所有可能访问到context的节点上,都会调用prepareToReadContext,将当前的fibercontextDependencies进行重置,并更新context模块中的全局变量

function prepareToReadContext(workInProgress: Fiber, renderExpirationTime: ExpirationTime) {
  currentlyRenderingFiber = workInProgress
  lastContextDependency = null
  lastContextWithAllBitsObserved = null

  const currentDependencies = workInProgress.contextDependencies

  if (currentDependencies !== null && currentDependencies.expirationTime >= renderExpirationTime) {
    markWorkInProgressReceivedUpdate()
  }

  workInProgress.contextDependencies = null
}

在使用context时,需要通过readContext获得新的context

由于我们已经在prepareToReadContext更新了全局变量,所以,这里我们直接生成一个ContextDependency对象,并放到当前fibercontextDependencies队列末尾,并且返回当前context新的value,整个实现非常的简单

function readContext<T>(context: ReactContext<T>, observedBits: void | number | boolean): any {
  if (lastContextWithAllBitsObserved !== context && !(observedBits === false || observedBits === 0)) {
    let resolvedObservedBits: number
    if (!isNumber(observedBits) || observedBits === MAX_SIGNED_31_BIT_INT) {
      lastContextWithAllBitsObserved = context
      resolvedObservedBits = MAX_SIGNED_31_BIT_INT
    } else {
      resolvedObservedBits = observedBits as number
    }

    const contextItem: ContextDependency<T> = {
      context,
      observedBits: resolvedObservedBits,
      next: null,
    }

    if (lastContextDependency === null) {
      lastContextDependency = contextItem
      currentlyRenderingFiber.contextDependencies = {
        first: contextItem,
        expirationTime: NoWork,
      }
    } else {
      lastContextDependency = lastContextDependency.next = contextItem
    }
  }
  return context._currentValue
}

使用context一般有三种:

  • Class.contextType
  • Consumer+render props
  • useContext
Class.contextType

我们可以通过将contextType直接赋予ClassComponent的静态对象上,实例上可以直接拿到context

class MyClass extends React.Component {
  static contextType = MyContext;
  render() {
    let value = this.context;
    /* render something based on the value */
  }
}

若有满足条件的contextType,则调用readContext生成context并挂载在实例上

// context 操作
const { contextType } = ctor
if (isObject(contextType)) {
  instance.context = readContext(contextType)
}
Consumer+render props
<MyContext.Consumer>
  {value => /* render something based on the context value */}
</MyContext.Consumer>

若是个consumer组件,则调用updateContextConsumer,同样是通过rendContext获得新的context,并直接执行render()

function updateContextConsumer(current: Fiber, workInProgress: Fiber, renderExpirationTime: ExpirationTime): Fiber {
  const context: ReactContext<any> = workInProgress.type

  const newProps = workInProgress.pendingProps
  const render = newProps.children

  prepareToReadContext(workInProgress, renderExpirationTime)
  const newValue = readContext(context, newProps.unstable_observedBits)
  const newChildren = render(newValue)

  workInProgress.effectTag |= PerformedWork
  reconcileChildren(current, workInProgress, newChildren, renderExpirationTime)
  return workInProgress.child
}
useContext
const value = useContext(MyContext)

FunctionComponent中,我们可以通过useContext来获取context, 它仅仅是将readContext重命名了一下

  useContext: readContext