Skip to content
On this page

Table of Contents generated with DocToc

Análise do Ciclo de vida React

O Fiber foi introduzido no lançamento da V16. O mecanismo afeta alguma das chamadas do ciclo de vida até certo ponto e foi introduzida duas novas APIs para resolver problemas.

Nas versões anteriores, se eu tiver um componente composto complexo e então mudar o state na camada mais alta do componente, a pilha de chamada poderia ser grande.

Se a pilha de chamada for muito longa, e complicadas operações estiverem no meio, isso pode causar um bloqueio a thread principal por um longe tempo, resultando em uma experiência ruim para o usuário. Fiber nasceu para resolver esse problema.

Fiber é na essência uma pilha virtual de quadros, e o novo agendador espontaneamente agenda esses quadros de acordo com sua prioridade, desse modo, mudando a renderização síncrona anterior para renderização assíncrona, e segmentando a atualização sem afetar a experiência.

React tem seu proprio conjunto de lógica sobre como priorizar. Para coisas que requerem alta performance em tempo-real, tal como animação, que significa isso deve ser renderizado uma vez dentro de 16 ms para garantir que não está emperrando, React pausa o update a cada 16 ms (dentro de 16 ms) e retorna para continuar renderizando a animação.

Para renderização assíncrona, existe agora dois estagios de renderização: reconciliation e commit. O primeiro processo pode ser interrompido, enquanto o último não poder ser suspenso, e a interface será atualizada até isso ser completo.

Reconciliation etapa

  • componentWillMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate

Commit etapa

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

Pelo fato que a fase de reconciliation pode ser interrompida, as funções do ciclo de vida que executaram na fase de reconciliation podem ser chamadas multiplas vezes, o que pode causar vários bugs. Então para essas funções, exceto para shouldComponentUpdate, devemos evitar assim que possivel, e uma nova API está introduzida na V16 para resolver esse problema.

getDerivedStateFromProps é usado para substituir componentWillReceiveProps, do qual é chamado durando a inicialização e atualização

js
class ExampleComponent extends React.Component {
  // Inicializa o state no construtor,
  // Ou com a propriedade initializer.
  state = {};

  static getDerivedStateFromProps(nextProps, prevState) {
    if (prevState.someMirroredValue !== nextProps.someValue) {
      return {
        derivedData: computeDerivedState(nextProps),
        someMirroredValue: nextProps.someValue
      };
    }

    // Retorna nulo para indicar que não há mudança no state.
    return null;
  }
}

getSnapshotBeforeUpdate é usado para substituir o componentWillUpdate, do qual é chamado depois do update mas antes do DOM atualizar para leitura o último dado do DOM.

O conselho usado nos métodos do ciclo de vida no React V16

js
class ExampleComponent extends React.Component {
  // Usado para iniciar o state
  constructor() {}
  // Usado para substituir o `componentWillReceiveProps`, do qual ira ser chamado quando inicializado e `update`
  // Porque a função é estática, você não pode acessar o `this`
  // Se você precisar comparar `prevProps`, você precisa manter ele separado no `state`
  static getDerivedStateFromProps(nextProps, prevState) {}
  // Determina se você precisa atualizar os componentes, usado na maioria das vezes para otimização de performance do componente
  shouldComponentUpdate(nextProps, nextState) {}
  // Chamado depois do componente ser montado
  // Pode requisitar ou subscrever nessa função
  componentDidMount() {}
  // Obter o último dado do DOM
  getSnapshotBeforeUpdate() {}
  // É sobre o componente ser destruido
  // Pode remover subscrições, timers, etc.
  componentWillUnmount() {}
  // Chamado depois do componente ser destruido
  componentDidUnMount() {}
  // Chamado depois da atualização do componente
  componentDidUpdate() {}
  // renderiza o componente
  render() {}
  // As seguintes funções não são recomendadas
  UNSAFE_componentWillMount() {}
  UNSAFE_componentWillUpdate(nextProps, nextState) {}
  UNSAFE_componentWillReceiveProps(nextProps) {}
}

setState

setState é uma API que é frequentemente usada no React, mas ele tem alguns problemas que podem levar a erros. O centro das razões é que a API é assíncrona.

Primeiro, chamando setState não casa mudança imediata no state, e se você chamar multiplos setState de uma vez, o resultado pode não ser como o esperado.

js
handle() {
  // Iniciado o `count` em 0
  console.log(this.state.count) // -> 0
  this.setState({ count: this.state.count + 1 })
  this.setState({ count: this.state.count + 1 })
  this.setState({ count: this.state.count + 1 })
  console.log(this.state.count) // -> 0
}

Primeiro, ambos os prints são 0, porque o setState é uma API assíncrona e irá apenas executar depois do código síncrono terminar sua execução. O motivo para o setState ser assíncrono é que setState pode causar repintar no DOM. Se a chamada repintar imediatamente depois da chamada, a chamada vai causar uma perca de performance desnecessária. Desenhando para ser assíncrono, você pode colocar multiplas chamadas dentro da fila e unificar os processos de atualização quando apropriado.

Segundo, apesar do setState ser chamado três vezes, o valor do count ainda é 1. Porque multiplas chamadas são fundidas em uma, o state só vai mudar quando a atualização terminar, e três chamadas são equivalente para o seguinte código.

js
Object.assign(  
  {},
  { count: this.state.count + 1 },
  { count: this.state.count + 1 },
  { count: this.state.count + 1 },
)

De fato, você pode também chamar setState três vezes da seguinte maneira para fazer count 3

js
handle() {
  this.setState((prevState) => ({ count: prevState.count + 1 }))
  this.setState((prevState) => ({ count: prevState.count + 1 }))
  this.setState((prevState) => ({ count: prevState.count + 1 }))
}

Se você quer acessar o state correto depois de cada chamada ao setState, você pode fazer isso com o seguinte código:

js
handle() {
    this.setState((prevState) => ({ count: prevState.count + 1 }), () => {
        console.log(this.state)
    })
}

Análise de código do Redux

Vamos dar uma olhada na função combineReducers primeiro.

js
// passe um objeto
export default function combineReducers(reducers) {
 // capture as chaves desse objeto
  const reducerKeys = Object.keys(reducers)
  // reducers depois filtrados
  const finalReducers = {}
  // obtenha os valores correspondentes para cada chave
  // no ambiente de desenvolvimento, verifique se o valor é undefined
  // então coloque os valores do tipo de função dentro do finalReducers
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]

    if (process.env.NODE_ENV !== 'production') {
      if (typeof reducers[key] === 'undefined') {
        warning(`No reducer provided for key "${key}"`)
      }
    }

    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  // obtenha as chaves dos reducers depois de filtrado
  const finalReducerKeys = Object.keys(finalReducers)
  
  // no ambiente de desenvolvimento verifique e salvo as chaves inesperadas em cache para alertas futuros
  let unexpectedKeyCache
  if (process.env.NODE_ENV !== 'production') {
    unexpectedKeyCache = {}
  }
    
  let shapeAssertionError
  try {
  // explicações de funções estão abaixo
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }
// combineReducers retorna outra função, que é reduzido depois de fundido
// essa função retorna o state raiz
// também percena que um encerramento é usado aqui. A função usa algumas propriedades externas
  return function combination(state = {}, action) {
    if (shapeAssertionError) {
      throw shapeAssertionError
    }
    // explicações das funções estão abaixo
    if (process.env.NODE_ENV !== 'production') {
      const warningMessage = getUnexpectedStateShapeWarningMessage(
        state,
        finalReducers,
        action,
        unexpectedKeyCache
      )
      if (warningMessage) {
        warning(warningMessage)
      }
    }
    // if state changed
    let hasChanged = false
    // state depois das mudanças
    const nextState = {}
    for (let i = 0; i < finalReducerKeys.length; i++) {
    // obter a chave com index
      const key = finalReducerKeys[i]
      // obter a função de reducer correspondente com a chave
      const reducer = finalReducers[key]
      // a chave na arvore do state é a mesma chave no finalReducers
      // então a chave passada nos parametros para o combineReducers representa cada reducer assim como cada state
      const previousStateForKey = state[key]
      // execute as funções reducer para pegar o state correspondente a chave
      const nextStateForKey = reducer(previousStateForKey, action)
      // verifique o valor do state, reporte erros se ele não estiver undefined
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      // coloque o valor dentro do nextState
      nextState[key] = nextStateForKey
      // se o state mudaou
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    // enquanto o state mudar, retorne um novo state
    return hasChanged ? nextState : state
  }
}

combineReducers é simples e generico. Resumindo, ele aceita um objeto e retorna uma função depois processado os parâmetros. Essa função tem um objeto finalReducers que armazena os parametros processados. O objeto é então iterado, cada função reducer nela é executada, e o novo state é executado.

Vamos então olhar as duas funções usadas no combineReducers.

js
// a primeira função usada para lançar os erros
function assertReducerShape(reducers) {
// iterar nós paramêtros no combineReducers
  Object.keys(reducers).forEach(key => {
    const reducer = reducers[key]
    // passar uma ação
    const initialState = reducer(undefined, { type: ActionTypes.INIT })
    // lança um erro se o state estiver undefined
    if (typeof initialState === 'undefined') {
      throw new Error(
        `Reducer "${key}" retorna undefined durante a inicialização. ` +
          `Se o state passado para o reducer for undefined, você deve ` +
          `explicitamente retornar o state inicial. O state inicial não deve ` +
          `ser undefined. Se você não quer definir um valor para esse reducer, ` +
          `você pode user null ao invés de undefined.`
      )
    }
    // processe novamente, considerando o caso que o usuário retornou um valor para ActionTypes.INIT no reducer
    // passa uma ação aleatória e verificar se o valor é undefined
    const type =
      '@@redux/PROBE_UNKNOWN_ACTION_' +
      Math.random()
        .toString(36)
        .substring(7)
        .split('')
        .join('.')
    if (typeof reducer(undefined, { type }) === 'undefined') {
      throw new Error(
        `Reducer "${key}" retorna undefined quando sondado com um tipo aleatório. ` +
          `Não tente manipular ${
            ActionTypes.INIT
          } ou outras ações no "redux/*" ` +
          `namespace.Eles são considerados privado. Ao invés, você deve retornar o ` +
          `state atual para qualquer action desconhecida, a menos que esteja undefined, ` +
          `nesse caso você deve retorna o state inicial, independemente do ` +
          `tipo da ação. O state inicial não deve ser undefined, mas pode ser null.`
      )
    }
  })
}

function getUnexpectedStateShapeWarningMessage(
  inputState,
  reducers,
  action,
  unexpectedKeyCache
) {
  // aqui os reducers já estão no finalReducers
  const reducerKeys = Object.keys(reducers)
  const argumentName =
    action && action.type === ActionTypes.INIT
      ? 'preloadedState argumento passado para o createStore'
      : 'state anterior recebido pelo reducer'
  
  // se finalReducers estiver vázio
  if (reducerKeys.length === 0) {
    return (
      'Store não tem um reducer válido. Certifique-se de que um argumento foi passado ' +
      'para o combineReducers é um objeto do qual os valores são reducers.'
    )
  }
  // se o state passado não é um objeto
  if (!isPlainObject(inputState)) {
    return (
      `O ${argumentName} tem um tipo inesperado de "` +
      {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +
      `". O argumento esperado deve ser um objeto com as seguintes ` +
      `chaves: "${reducerKeys.join('", "')}"`
    )
  }
  // compara as chaves do state a do finalReducers e filtra as chaves extras
  const unexpectedKeys = Object.keys(inputState).filter(
    key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
  )

  unexpectedKeys.forEach(key => {
    unexpectedKeyCache[key] = true
  })

  if (action && action.type === ActionTypes.REPLACE) return

// se unexpectedKeys não estiver vázia
  if (unexpectedKeys.length > 0) {
    return (
      `Inesperada ${unexpectedKeys.length > 1 ? 'chaves' : 'chave'} ` +
      `"${unexpectedKeys.join('", "')}" encontrada em ${argumentName}. ` +
      `Esperado encontrar uma das chaves do reducer conhecida ao invés: ` +
      `"${reducerKeys.join('", "')}". Chaves inesperadas serão ignoradas.`
    )
  }
}

Vamos então dar uma olhada na função compose

js
// Essa função é bem elegante. Ela nos permite empilhar diversas funções passando a
// referências da função. O termo é chamado de Higher-order function.
// chama funções a partir da direita para esquerda com funções reduce
// por exemplo, no objeto acima
compose(
    applyMiddleware(thunkMiddleware),
    window.devToolsExtension ? window.devToolsExtension() : f => f
)
// Com compose ele retorna dentro do applyMiddleware(thunkMiddleware)(window.devToolsExtension()())
// então você deveria retorna uma função quando window.devToolsExtension não for encontrada
export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

Vamos então analisar pare do código da função createStore

js
export default function createStore(reducer, preloadedState, enhancer) {
  // normalmente preloadedState é raramente usado
  // verificar o tipo, é o segundo parâmetro da função e não existe terceiro parâmetro, então troque as posições
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }
  // verifique se enhancer é uma função
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('É esperado que enhancer seja uma função.')
    }
    // se não existe um tipo error, primeiro execute o enhancer, então execute o createStore
    return enhancer(createStore)(reducer, preloadedState)
  }
  // verifique se o reducer é uma função
  if (typeof reducer !== 'function') {
    throw new Error('É esperado que o reducer seja uma função.')
  }
  // reducer atual
  let currentReducer = reducer
  // state atual
  let currentState = preloadedState
  // atual listener array
  let currentListeners = []
  // esse é um design muito importante. O proposito é que o currentListeners array seja invariante quando os listeners estiverem sendo interado
  // Nós podemos considerar se apenas um currentListeners existe. Se nós executarmos o subscribe novamente em alguma execução do subscribe, ou unsubscribe isso mudaria o tamanho do currentListeners array, então devemos ter um index erro
  let nextListeners = currentListeners
  // se o reducer está executando
  let isDispatching = false
  // se o currentListeners é o mesmo que o nextListeners, atribua o valor de volta
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }
  // ......
}

Vamos dar uma olhada na função applyMiddleware

Antes eu preciso introduzir um conceito chamado Currying. Currying é uma tecnologia para mudar uma função com multiplos parâmetros em uma série de funções com um único parâmetro.

js
function add(a,b) { return a + b }   
add(1, 2) => 3
// para a função abaixo, nós usamos Currying igual a
function add(a) {
    return b => {
        return a + b
    }
}
add(1)(2) => 3
// você pode entender Currying como:
// nós armazenamos uma variável do lado de fora com um closure, e retornamos uma função que leva um parâmetro. Nessa função, nós usamos a variável armazenada e retornamos um valor.
js
// essa função deve ser a parte mais obstrusa de todo código
// essa função retorna um função Curried
// assim sendo a função deve se chamada como: applyMiddleware(...middlewares)(createStore)(...args)
export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    // aqui nós executamos createStore, e passamos o parâmetro passado por último a função applyMiddleware
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error(
        `Dispatching while constructing your middleware is not allowed. ` +
          `Other middleware would not be applied to this dispatch.`
      )
    }
    let chain = []
    // todo middleware deve ter essas duas funções
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    // passar cada middleware nos middlewares para o middlewareAPI
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    // assim como antes, chame cada middleware da esquerda para direita, e passo para o store.dispatch
    dispatch = compose(...chain)(store.dispatch)
    // essa parte é um pouco abstrata, nós iremos analisar juntos com o código do redux-thunk
    // createThunkMiddleware retorna uma função de 3-nível, o primeiro nível aceita um parâmetro middlewareAPI
    // o segundo nível aceita store.dispatch
    // o terceiro nível aceita parâmentros no dispatch
{function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    // verifique se o parâmetro no dispatch é uma função
    if (typeof action === 'function') {
      // se assim for, passe esses parâmetros, até as acões não sejam mais uma função, então execute dispatch({type: 'XXX'})
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}
const thunk = createThunkMiddleware();

export default thunk;}
// retorn o middleware-empowered dispatch e o resto das propriedades no store.
    return {
      ...store,
      dispatch
    }
  }
}

Agora nós passamos a parte difícil. Vamos olhar uma parte mais fácil.

js
// Não há muito para dizer aqui, retorne o state atual, mas nós não podemos chamar essa função quando o reducer estiver executando
function getState() {
    if (isDispatching) {
      throw new Error(
        'You may not call store.getState() while the reducer is executing. ' +
          'The reducer has already received the state as an argument. ' +
          'Pass it down from the top reducer instead of reading it from the store.'
      )
    }

    return currentState
}
// aceita uma função parâmetro
function subscribe(listener) {
    if (typeof listener !== 'function') {
      throw new Error('Expected listener to be a function.')
    }
    // a maior parte desse design já foi coberto na descrição sobre nextListeners. Não há muito para falar sobre.
    if (isDispatching) {
      throw new Error(
        'You may not call store.subscribe() while the reducer is executing. ' +
          'If you would like to be notified after the store has been updated, subscribe from a ' +
          'component and invoke store.getState() in the callback to access the latest state. ' +
          'See http://redux.js.org/docs/api/Store.html#subscribe for more details.'
      )
    }

    let isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

// retorne a função de cancelar a subscription
    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

      if (isDispatching) {
        throw new Error(
          'You may not unsubscribe from a store listener while the reducer is executing. ' +
            'See http://redux.js.org/docs/api/Store.html#subscribe for more details.'
        )
      }

      isSubscribed = false

      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }
 
function dispatch(action) {
  // o prototype dispatch vai verificar se a ação é um objeto
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
          'Use custom middleware for async actions.'
      )
    }

    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
          'Have you misspelled a constant?'
      )
    }
    // perceba que você não pode chamar uma função dispatch nos reducers
    // isso causaria um estouro de pilha
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }
    // execute a função composta depois do combineReducers
    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }
    // itere nos currentListeners e execute as funções salvas no array de funções
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }
  // no fim do createStore, invoce uma ação dispatch({ type: ActionTypes.INIT });
  // para inicializar o state