packages/nerv/src/lifecycle.ts

Summary

Maintainability
D
1 day
Test Coverage
// import { extend, isFunction, isNumber, isString } from 'nerv-utils'
import { extend, isFunction, isNumber, isString, clone, isUndefined, isArray } from 'nerv-utils'
import CurrentOwner from './current-owner'
import createElement from './vdom/create-element'
import createVText from './vdom/create-vtext'
import { createVoid } from './vdom/create-void'
import patch from './vdom/patch'
import {
  isNullOrUndef,
  CompositeComponent,
  isComponent,
  isInvalid,
  VText,
  VVoid,
  VNode,
  VType,
  EMPTY_OBJ,
  isComposite
} from 'nerv-shared'
import FullComponent from './full-component'
import { unmount } from './vdom/unmount'
import Ref from './vdom/ref'
import Component from './component'
import { invokeEffects } from './hooks'
import { Emiter } from './emiter'
export type ParentContext = Record<string, Emiter<any>>

const readyComponents: any[] = []

export function errorCatcher (fn: Function, component: Component<any, any>) {
  try {
    return fn()
  } catch (error) {
    errorHandler(component, error)
  }
}

function errorHandler (component: Component<any, any>, error) {
  // if(!component) { throw error ; return }
  let boundary
  while (true) {
    const { getDerivedStateFromError } = (component as any).constructor
    if (isFunction(getDerivedStateFromError) || isFunction(component.componentDidCatch)) {
      boundary = component
      break
    } else if (component._parentComponent) {
      component = component._parentComponent
    } else {
      break
    }
  }
  if (boundary) {
    const { getDerivedStateFromError } = (boundary as any).constructor
    const _disable = boundary._disable
    boundary._disable = false
    if (isFunction(getDerivedStateFromError)) {
      component.setState(getDerivedStateFromError(error))
    } else if (isFunction(component.componentDidCatch)) {
      boundary.componentDidCatch(error)
    }
    boundary._disable = _disable
  } else {
    throw error
  }
}

export function ensureVirtualNode (rendered: any): VText | VVoid | VNode {
  if (isNumber(rendered) || isString(rendered)) {
    return createVText(rendered)
  } else if (isInvalid(rendered)) {
    return createVoid()
  } else if (isArray(rendered)) {
    rendered = rendered.length === 0 ?  createVoid() : rendered.map(ensureVirtualNode)
  }
  return rendered
}

export function mountVNode (vnode, parentContext: any, parentComponent?) {
  return createElement(vnode, false, parentContext, parentComponent)
}

export function getContextByContextType (vnode: FullComponent, parentContext: ParentContext) {
  const contextType = vnode.type.contextType
  const hasContextType = !isUndefined(contextType)
  const provider = hasContextType ? (parentContext[contextType._id]) : null
  const context = hasContextType
    ? (
      !isNullOrUndef(provider) ? provider.value : contextType._defaultValue
    )
    : parentContext
  return context
}

export function mountComponent (
  vnode: FullComponent,
  parentContext: ParentContext,
  parentComponent
) {
  const ref = vnode.ref
  if (vnode.type.prototype && vnode.type.prototype.render) {
    const context = getContextByContextType(vnode, parentContext)
    vnode.component = new vnode.type(vnode.props, context)
  } else {
    const c = new Component(vnode.props, parentContext)
    c.render = () => vnode.type.call(c, c.props, c.context)
    vnode.component = c
  }
  const component = vnode.component
  component.vnode = vnode
  if (isComponent(parentComponent)) {
    component._parentComponent = parentComponent as any
  }
  const newState = callGetDerivedStateFromProps(vnode.props, component.state, component)
  if (!isUndefined(newState)) {
    component.state = newState
  }
  if (!hasNewLifecycle(component) && isFunction(component.componentWillMount)) {
    errorCatcher(() => {
      (component as any).componentWillMount()
    }, component)
    component.state = component.getState()
    component.clearCallBacks()
  }
  component._dirty = false
  const rendered = renderComponent(component)
  rendered.parentVNode = vnode
  component._rendered = rendered
  if (!isNullOrUndef(ref)) {
    Ref.attach(vnode, ref, vnode.dom as Element)
  }
  const dom = (vnode.dom = mountVNode(
    rendered,
    getChildContext(component, parentContext),
    component
  ) as Element)
  invokeEffects(component)
  if (isFunction(component.componentDidMount)) {
    readyComponents.push(component)
  }
  component._disable = false
  return dom
}

export function getChildContext (component, context = EMPTY_OBJ) {
  if (isFunction(component.getChildContext)) {
    return extend(clone(context), component.getChildContext())
  }
  return clone(context)
}

export function renderComponent (component: Component<any, any>) {
  CurrentOwner.current = component
  CurrentOwner.index = 0
  invokeEffects(component, true)
  let rendered
  errorCatcher(() => {
    rendered = component.render()
  }, component)
  rendered = ensureVirtualNode(rendered)
  CurrentOwner.current = null
  return rendered
}

export function flushMount () {
  if (!readyComponents.length) {
    return
  }
  // @TODO: perf
  const queue = readyComponents.slice(0)
  readyComponents.length = 0
  queue.forEach((item) => {
    if (isFunction(item)) {
      item()
    } else if (item.componentDidMount) {
      errorCatcher(() => {
        item.componentDidMount()
      }, item)
    }
  })
}

function getFragmentHostNode (children: VNode[]): Node | null {
  const child = children[0]
  if (isArray(child)) {
    return getFragmentHostNode(child)
  } else if (isComposite(child) && child.dom == null) {
    return getFragmentHostNode(child.component._rendered)
  }
  return child != null ? child.dom : null
}

export function reRenderComponent (
  prev: CompositeComponent,
  current: CompositeComponent
) {
  const component = (current.component = prev.component) as any
  const nextProps = current.props
  const nextContext = current.context
  component._disable = true
  if (!hasNewLifecycle(component) && isFunction(component.componentWillReceiveProps)) {
    errorCatcher(() => {
      component.componentWillReceiveProps(nextProps, nextContext)
    }, component)
  }
  component._disable = false
  component.prevProps = component.props
  component.prevState = component.state
  component.prevContext = component.context
  component.props = nextProps
  component.context = nextContext
  if (!isNullOrUndef(current.ref)) {
    Ref.update(prev, current)
  }
  return updateComponent(component)
}

function callShouldComponentUpdate (props, state, context, component) {
  let shouldUpdate = true
  errorCatcher(() => {
    shouldUpdate = component.shouldComponentUpdate(props, state, context)
  }, component)
  return shouldUpdate
}

export function updateComponent (component, isForce = false) {
  let vnode = component.vnode
  let dom = vnode.dom
  const props = component.props
  let state = component.getState()
  const context = component.context
  const prevProps = component.prevProps || props
  const prevState = component.prevState || component.state
  const prevContext = component.prevContext || context

  const stateFromProps = callGetDerivedStateFromProps(props, state, component)

  if (!isUndefined(stateFromProps)) {
    state = stateFromProps
  }

  component.props = prevProps
  component.context = prevContext
  let skip = false
  const onSCU = props.onShouldComponentUpdate
  if (
    !isForce &&
    (
      (isFunction(component.shouldComponentUpdate) &&
      callShouldComponentUpdate(props, state, context, component) === false) ||
      (isFunction(onSCU) && onSCU(prevProps, props) === false)
    )
  ) {
    skip = true
  } else if (!hasNewLifecycle(component) && isFunction(component.componentWillUpdate)) {
    errorCatcher(() => {
      component.componentWillUpdate(props, state, context)
    }, component)
  }

  if (!isUndefined(stateFromProps)) {
    component.state = stateFromProps
  }

  component.props = props
  component.state = state
  component.context = context
  component._dirty = false
  if (!skip) {
    const lastRendered = component._rendered
    const rendered = renderComponent(component)
    rendered.parentVNode = vnode
    const childContext = getChildContext(component, context)
    const snapshot = callGetSnapshotBeforeUpdate(prevProps, prevState, component)
    let parentDom = lastRendered.dom && lastRendered.dom.parentNode
    if (isArray(lastRendered)) {
      const hostNode = getFragmentHostNode(lastRendered)
      if (hostNode != null) {
        parentDom = (lastRendered as any).dom = hostNode.parentNode
      }
    }
    dom = vnode.dom = patch(lastRendered, rendered, parentDom || null, childContext)
    component._rendered = rendered
    if (isFunction(component.componentDidUpdate)) {
      errorCatcher(() => {
        component.componentDidUpdate(prevProps, prevState, snapshot)
      }, component)
    }
    while (vnode = vnode.parentVNode) {
      if ((vnode.vtype & (VType.Composite)) > 0) {
        vnode.dom = dom
      }
    }
  }
  component.prevProps = component.props
  component.prevState = component.state
  component.prevContext = component.context
  component.clearCallBacks()
  flushMount()
  invokeEffects(component)
  return dom
}

export function unmountComponent (vnode: FullComponent) {
  const component = vnode.component
  component.hooks.forEach((hook) => {
    if (isFunction(hook.cleanup)) {
      hook.cleanup()
    }
  })
  if (isFunction(component.componentWillUnmount)) {
    errorCatcher(() => {
      (component as any).componentWillUnmount()
    }, component)
  }
  component._disable = true
  unmount(component._rendered)
  if (!isNullOrUndef(vnode.ref)) {
    Ref.detach(vnode, vnode.ref, vnode.dom as any)
  }
}

function callGetDerivedStateFromProps (props, state, inst) {
  const { getDerivedStateFromProps } = inst.constructor
  let newState
    // @TODO show warning
  errorCatcher(() => {
    if (isFunction(getDerivedStateFromProps)) {
      const partialState = getDerivedStateFromProps.call(
        null,
        props,
        state
      )
      if (!isUndefined(partialState)) {
        newState = extend(clone(state), partialState)
      }
    }
  }, inst)
  return newState
}

function callGetSnapshotBeforeUpdate (props, state, inst) {
  const { getSnapshotBeforeUpdate } = inst
  let snapshot
  errorCatcher(() => {
    if (isFunction(getSnapshotBeforeUpdate)) {
      snapshot = getSnapshotBeforeUpdate.call(inst, props, state)
    }
  }, inst)
  return snapshot
}

function hasNewLifecycle (component) {
  if (isFunction(component.constructor.getDerivedStateFromProps)) {
    return true
  }
  return false
}