GVKun编程网logo

react 16 渲染整理(react的渲染)

28

本文将介绍react16渲染整理的详细情况,特别是关于react的渲染的相关信息。我们将通过案例分析、数据研究等多种方式,帮助您更全面地了解这个主题,同时也将涉及一些关于create-react-ap

本文将介绍react 16 渲染整理的详细情况,特别是关于react的渲染的相关信息。我们将通过案例分析、数据研究等多种方式,帮助您更全面地了解这个主题,同时也将涉及一些关于create-react-app搭配react16+ts+less、Egg + React (React Router + Redux) (SSR) 服务端渲染实际、Egg + React + React Router + Redux 服务端渲染同构实践、GL的学习资料短暂整理-水的渲染整理的知识。

本文目录一览:

react 16 渲染整理(react的渲染)

react 16 渲染整理(react的渲染)

背景

老的react架构在渲染时会有一些性能问题,从setstate到render,程序一直在跑,一直到render完成。才能继续下一步操作。如果组件比较多,或者有复杂的计算逻辑,这之间的消耗的时间是比较多的。 假设更新一个组件需要1ms,如果有200个组件要更新,那就需要200ms,这200ms之间是不能响应的。如果这时候用户在input框输入什么东西,表现出来的就是明显的卡顿。 React这样的调度策略对动画的支持也不好。如果React更新一次状态,占用浏览器主线程的时间超过16.6ms,就会被人眼发觉前后两帧不连续,呈现出动画卡顿。

Fiber

react团队经过两年的工作,重写了react中核心算法reconciliation。并在v16版本中发布了这个新的特性。为了区别之前和之后的reconciler,通常将之前的reconciler称为stack reconciler,重写后的称为fiber reconciler,简称为Fiber。

区别

最大的变化就是支持了任务帧,把各个任务都增加了优先级,同步和异步。比如用户输入input是优先级比较高的,它可以打断低优先级的任务。 比如再处理dom diff的时候耗时严重,fiber任务处理大概会有50ms的帧时长,超过这个时间就会先去看看有没高优任务去做。然后回来做低优先级任务。

  • 优先级高的任务可以中断低优先级的任务。
  • 还增加了异步任务,调用requestIdleCallback api,浏览器空闲的时候执行。(不过用户操作默认是同步的,暂时还没开放这个特性)
  • dom diff树变成了链表,一个dom对应两个fiber(一个链表),对应两个队列,这都是为找到被中断的任务,重新执行而设计的。

渲染流程

scheduleWork - requestWork - 同步/异步 - performSyncWork- performWork - performWorkOnRoot - renderRoot/completeRoot - workLoop-performUnitOfWork-beginWork/completeUnitOfWork -updateClassComponent-reconcileChildrenAtExpirationTime- reconcileChildFibers-reconcileChildrenArray 源码基于react v16.3.0 (8e3d94ff)

setstate

Component.prototype.setState = function(partialState, callback) {
  this.updater.enqueueSetState(this, partialState, callback, ''setState'');
};

enqueueSetState

主要是把任务插入fiber的update queue,然后调度任务

enqueueSetState(instance, partialState, callback) {
    const fiber = ReactInstanceMap.get(instance);
    callback = callback === undefined ? null : callback;

    const expirationTime = computeExpirationForFiber(fiber);
    const update = {
        expirationTime,
        partialState,
        callback,
        isReplace: false,
        isForced: false,
        capturedValue: null,
        next: null,
    };
    insertUpdateIntoFiber(fiber, update);
    scheduleWork(fiber, expirationTime);
},

insertUpdateIntoFiber

插入fiber两棵树的update queue

每个react 结点都有2个fiber链表,一个叫current fiber,一个叫alternate fiber,而每个链表又对应两个updateQueue。 而currentFiber.alternate = alternateFiber; alternateFiber.alternate = currentFiber。通过alternate属性连接起来。初始化的时候,alternate fiber是current fiber 的clone。 处理diff的时候,操作的是alternateFiber,处理完diff,让currentFiber = alternateFiber;这样一个处理就完成了。

scheduleWork

scheduleWork会更新每个节点的优先级,然后循环到root,以后的操作都从root开始遍历。

  • expirationTime 优先级 expirationTime 不为 1 的时候,则其值越低,优先级越高。
{
  NoWork: 0,              // No work is pending.
  SynchronousPriority: 1, // For controlled text inputs. Synchronous side-effects.
  AnimationPriority: 2,   // Needs to complete before the next frame.
  HighPriority: 3,        // Interaction that needs to complete pretty soon to feel responsive.
  LowPriority: 4,         // Data fetching, or result from updating stores.
  OffscreenPriority: 5,   // Won''t be visible but do the work in case it becomes visible.
};
function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
    return scheduleWorkImpl(fiber, expirationTime, false);
}
function scheduleWorkImpl(
    fiber: Fiber,
    expirationTime: ExpirationTime,
    isErrorRecovery: boolean,
  ) {
    recordScheduleUpdate(); // 记录更新,实际啥也没干

    let node = fiber;
    while (node !== null) {
      // Walk the parent path to the root and update each node''s
      // expiration time.
      // 更新每个node的优先级
      if (
        node.expirationTime === NoWork ||
        node.expirationTime > expirationTime
      ) {
        node.expirationTime = expirationTime;
      }
      if (node.alternate !== null) {
        if (
          node.alternate.expirationTime === NoWork ||
          node.alternate.expirationTime > expirationTime
        ) {
          node.alternate.expirationTime = expirationTime;
        }
      }
      if (node.return === null) {
        if (node.tag === HostRoot) {
          const root: FiberRoot = (node.stateNode: any);
          if (
            !isWorking &&
            nextRenderExpirationTime !== NoWork &&
            expirationTime < nextRenderExpirationTime
          ) {
            // This is an interruption. (Used for performance tracking.)
            interruptedBy = fiber;
            resetStack();
          }
          if (
            // If we''re in the render phase, we don''t need to schedule this root
            // for an update, because we''ll do it before we exit...
            !isWorking ||
            isCommitting ||
            // ...unless this is a different root than the one we''re rendering.
            nextRoot !== root
          ) {
            // Add this root to the root schedule.
            requestWork(root, expirationTime);
          }
        } else {
          }
          return;
        }
      }
      node = node.return;
    }
  }

requestWork

同步执行performSyncWork,异步执行scheduleCallbackWithExpiration, scheduleCallbackWithExpiration会调浏览器的requestidlecallback,在浏览器空闲的时候进行处理。 react还对这个api做了polyfill

function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
    if (isRendering) {
      return;
    }
    if (isBatchingUpdates) { // 这里是BatchingUpdates的处理。
      // Flush work at the end of the batch.
      if (isUnbatchingUpdates) {
        // ...unless we''re inside unbatchedUpdates, in which case we should
        // flush it now.
        nextFlushedRoot = root;
        nextFlushedExpirationTime = Sync;
        performWorkOnRoot(root, Sync, false);
      }
      return;
    }
    if (expirationTime === Sync) {
      performSyncWork();
    } else {
      scheduleCallbackWithExpiration(expirationTime);
    }
  }

performSyncWork 主要的任务调度

这里会找到高优任务先执行。 同步任务会直接调用performWorkOnRoot进行下一步, 异步任务也会调performWorkOnRoot,但处理不太一样 如果有上次遗留的任务,留到空闲时运行

  function performSyncWork() {
    performWork(Sync, false, null);
  }

  function performWork(
    minExpirationTime: ExpirationTime,
    isAsync: boolean,
    dl: Deadline | null,
  ) {
    deadline = dl;

    findHighestPriorityRoot();

    if (isAsync) {
      while (
        nextFlushedRoot !== null &&
        nextFlushedExpirationTime !== NoWork &&
        (minExpirationTime === NoWork ||
          minExpirationTime >= nextFlushedExpirationTime) &&
        (!deadlineDidExpire ||
          recalculateCurrentTime() >= nextFlushedExpirationTime)
      ) {
        performWorkOnRoot(
          nextFlushedRoot,
          nextFlushedExpirationTime,
          !deadlineDidExpire,
        );
        findHighestPriorityRoot();
      }
    } else {
      while (
        nextFlushedRoot !== null &&
        nextFlushedExpirationTime !== NoWork &&
        (minExpirationTime === NoWork ||
          minExpirationTime >= nextFlushedExpirationTime)
      ) {
        performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, false);
        findHighestPriorityRoot();
      }
    }

    if (deadline !== null) {
      callbackExpirationTime = NoWork;
      callbackID = -1;
    }
    // If there''s work left over, schedule a new callback.
    if (nextFlushedExpirationTime !== NoWork) {
      scheduleCallbackWithExpiration(nextFlushedExpirationTime);
    }

    // Clean-up.
    deadline = null;
    deadlineDidExpire = false;

    finishRendering();
  }

performWorkOnRoot (异步任务和同步任务的异同)

如果有上次遗留,直接调用completeRoot进到渲染阶段。如果没有就调renderRoot开始reconcilation阶段。 异步任务主要是渲染的时候判断一下时间,如果没时间了,先把finishedWork赋给全局,下次循环处理。

function performWorkOnRoot(
    root: FiberRoot,
    expirationTime: ExpirationTime,
    isAsync: boolean,
  ) {
    isRendering = true;

    // Check if this is async work or sync/expired work.
    if (!isAsync) {
      // Flush sync work.
      let finishedWork = root.finishedWork;
      if (finishedWork !== null) {
        // This root is already complete. We can commit it.
        completeRoot(root, finishedWork, expirationTime);
      } else {
        root.finishedWork = null;
        finishedWork = renderRoot(root, expirationTime, false);
        if (finishedWork !== null) {
          // We''ve completed the root. Commit it.
          completeRoot(root, finishedWork, expirationTime);
        }
      }
    } else {
      // Flush async work.
      let finishedWork = root.finishedWork;
      if (finishedWork !== null) {
        // This root is already complete. We can commit it.
        completeRoot(root, finishedWork, expirationTime);
      } else {
        root.finishedWork = null;
        finishedWork = renderRoot(root, expirationTime, true);
        if (finishedWork !== null) {
          // We''ve completed the root. Check the deadline one more time
          // before committing.
          if (!shouldYield()) {
            // Still time left. Commit the root.
            completeRoot(root, finishedWork, expirationTime);
          } else {
            // There''s no time left. Mark this root as complete. We''ll come
            // back and commit it later.
            root.finishedWork = finishedWork;
          }
        }
      }
    }

    isRendering = false;
  }

renderRoot

如果是第一次进入,会创建一个nextUnitOfWork。 nextUnitOfWork是每个工作的粒度。 然后调用workLoop

function renderRoot(
    root: FiberRoot,
    expirationTime: ExpirationTime,
    isAsync: boolean,
  ): Fiber | null {
    isWorking = true;

    // Check if we''re starting from a fresh stack, or if we''re resuming from
    // previously yielded work.
    if (
      expirationTime !== nextRenderExpirationTime ||
      root !== nextRoot ||
      nextUnitOfWork === null
    ) {
      // Reset the stack and start working from the root.
      resetStack();
      nextRoot = root;
      nextRenderExpirationTime = expirationTime;
      nextUnitOfWork = createWorkInProgress(
        nextRoot.current,
        null,
        nextRenderExpirationTime,
      );
      root.pendingCommitExpirationTime = NoWork;
    }

    let didFatal = false;

    startWorkLoopTimer(nextUnitOfWork);

    do {
      try {
        workLoop(isAsync);
      } catch (thrownValue) {
        // ...
      }
      break;
    } while (true);

    // We''re done performing work. Time to clean up.
    // ...
  }

workLoop

异步任务在处理的时候会调用shouldYield,shouldYield会判断是不是已经超时了,超时暂时先不做。

function workLoop(isAsync) {
    if (!isAsync) {
      // Flush all expired work.
      while (nextUnitOfWork !== null) {
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
      }
    } else {
      // Flush asynchronous work until the deadline runs out of time.
      while (nextUnitOfWork !== null && !shouldYield()) {
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
      }
    }
  }
function shouldYield() {
    if (deadline === null) {
      return false;
    }
    if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
      // Disregard deadline.didTimeout. Only expired work should be flushed
      // during a timeout. This path is only hit for non-expired work.
      return false;
    }
    deadlineDidExpire = true;
    return true;
  }

performUnitOfWork (reconcilation阶段)

reconcilation又分两步 1是beginWork,beginWork会开始处理组件,针对不同组件不同处理。包括dom diff 2 是completeUnitOfWork,completeUnitOfWork会对begin work产生的effect list进行一些处理。

 function performUnitOfWork(workInProgress: Fiber): Fiber | null {
    const current = workInProgress.alternate;
    startWorkTimer(workInProgress);
    let next = beginWork(current, workInProgress, nextRenderExpirationTime);

    if (next === null) {
      next = completeUnitOfWork(workInProgress);
    }

    ReactCurrentOwner.current = null;
    return next;
  }

beginWork

主要是对react 组件进行一些操作。和调用一些生命周期, 我们主要关注classComponent,就是react的组件 HostConponent在浏览器下就是dom

function beginWork(
    current: Fiber | null,
    workInProgress: Fiber,
    renderExpirationTime: ExpirationTime,
  ): Fiber | null {
    if (
      workInProgress.expirationTime === NoWork ||
      workInProgress.expirationTime > renderExpirationTime
    ) {
      return bailoutOnLowPriority(current, workInProgress);
    }

    switch (workInProgress.tag) {
      case FunctionalComponent:
        return updateFunctionalComponent(current, workInProgress);
      case ClassComponent:
        return updateClassComponent(
          current,
          workInProgress,
          renderExpirationTime,
        );
      case HostRoot:
        return updateHostRoot(current, workInProgress, renderExpirationTime);
      case HostComponent:
        return updateHostComponent(
          current,
          workInProgress,
          renderExpirationTime,
        );
      case HostText:
        return updateHostText(current, workInProgress);
      case ForwardRef:
        return updateForwardRef(current, workInProgress);
      case Fragment:
        return updateFragment(current, workInProgress);
      case Mode:
        return updateMode(current, workInProgress);
      case ContextProvider:
        return updateContextProvider(
          current,
          workInProgress,
          renderExpirationTime,
        );
      case ContextConsumer:
        return updateContextConsumer(
          current,
          workInProgress,
          renderExpirationTime,
        );
      default:
        invariant(
          false,
          ''Unknown unit of work tag. This error is likely caused by a bug in '' +
            ''React. Please file an issue.'',
        );
    }
  }

updateClassComponent

mount组件,构建组件实例,调用生命周期比如willMount,初始化组件的的updateQueue。

  • updateClassInstance中,如果props不一致,会调willReceiveProps方法,然后checkShouldCompoentUpdate,也就是 shouldCompoentUpdate。
  • finishClassComponent中,会判断之前的shouldUpdate,如果是true就要调用组件的render,产出children,然后对children进行dom diff。
 function updateClassComponent(
    current: Fiber | null,
    workInProgress: Fiber,
    renderExpirationTime: ExpirationTime,
  ) {
    // 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.
    const hasContext = pushLegacyContextProvider(workInProgress);
    let shouldUpdate;
    if (current === null) {
      if (workInProgress.stateNode === null) {
        // In the initial pass we might need to construct the instance.
        constructClassInstance(workInProgress, workInProgress.pendingProps);
        mountClassInstance(workInProgress, renderExpirationTime);

        shouldUpdate = true;
      } else {
        // In a resume, we''ll already have an instance we can reuse.
        shouldUpdate = resumeMountClassInstance(
          workInProgress,
          renderExpirationTime,
        );
      }
    } else {
      shouldUpdate = updateClassInstance(
        current,
        workInProgress,
        renderExpirationTime,
      );
    }

    let didCaptureError = false;
    const updateQueue = workInProgress.updateQueue;
    if (updateQueue !== null && updateQueue.capturedValues !== null) {
      shouldUpdate = true;
      didCaptureError = true;
    }
    return finishClassComponent(
      current,
      workInProgress,
      shouldUpdate,
      hasContext,
      didCaptureError,
      renderExpirationTime,
    );
  }

reconcileChildFibers (virtul dom diff)

finishClassComponent会调用reconcileChildFibers进行dom diff。

 function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any,
    expirationTime: ExpirationTime,
  ): Fiber | null {
    if (
      typeof newChild === ''object'' &&
      newChild !== null &&
      newChild.type === REACT_FRAGMENT_TYPE &&
      newChild.key === null
    ) {
      newChild = newChild.props.children;
    }

    // Handle object types
    const isObject = typeof newChild === ''object'' && newChild !== null;

    if (isObject) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(
            reconcileSingleElement(
              returnFiber,
              currentFirstChild,
              newChild,
              expirationTime,
            ),
          );
        case REACT_PORTAL_TYPE:
          return placeSingleChild(
            reconcileSinglePortal(
              returnFiber,
              currentFirstChild,
              newChild,
              expirationTime,
            ),
          );
      }
    }

    if (typeof newChild === ''string'' || typeof newChild === ''number'') {
      return placeSingleChild(
        reconcileSingleTextNode(
          returnFiber,
          currentFirstChild,
          '''' + newChild,
          expirationTime,
        ),
      );
    }

    if (isArray(newChild)) {
      return reconcileChildrenArray(
        returnFiber,
        currentFirstChild,
        newChild,
        expirationTime,
      );
    }

    if (getIteratorFn(newChild)) {
      return reconcileChildrenIterator(
        returnFiber,
        currentFirstChild,
        newChild,
        expirationTime,
      );
    }
  }

reconcileChildrenArray

大部分情况是reconcileChildrenArray,就那这个来说。

function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array<*>,
    expirationTime: ExpirationTime,
  ): Fiber | null {
    let resultingFirstChild: Fiber | null = null;
    let previousNewFiber: Fiber | null = null;

    let oldFiber = currentFirstChild;
    let lastPlacedIndex = 0;
    let newIdx = 0;
    let nextOldFiber = null;
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
     // 没有采用两端同时对比,受限于Fiber列表的单向结构
      if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber;
        oldFiber = null;
      } else {
        nextOldFiber = oldFiber.sibling;
      }
      const newFiber = updateSlot( // 生成新的fiber
        returnFiber,
        oldFiber,
        newChildren[newIdx],
        expirationTime,
      );
      //如果在遍历中发现key值不相等的情况,则直接跳出第一轮遍历
      if (newFiber === null) { 
        if (oldFiber === null) {
          oldFiber = nextOldFiber;
        }
        break;
      }
      if (shouldTrackSideEffects) {
        if (oldFiber && newFiber.alternate === null) {
          // 我们找到了匹配的节点,但我们并不保留当前的Fiber,所以我们需要删除当前的子节点
          // We matched the slot, but we didn''t reuse the existing fiber, so we
          // need to delete the existing child.
          deleteChild(returnFiber, oldFiber);
        }
      }
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      // 记录上一个更新的子节点
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }

    if (newIdx === newChildren.length) {
      // 我们已经遍历完了所有的新节点,直接删除剩余旧节点
      // We''ve reached the end of the new children. We can delete the rest.
      deleteRemainingChildren(returnFiber, oldFiber);
      return resultingFirstChild;
    }

    if (oldFiber === null) {
      // 如果旧节点先遍历完,则按顺序插入剩余的新节点
      // If we don''t have any more existing children we can choose a fast path
      // since the rest will all be insertions.
      for (; newIdx < newChildren.length; newIdx++) {
        const newFiber = createChild(
          returnFiber,
          newChildren[newIdx],
          expirationTime,
        );
        if (!newFiber) {
          continue;
        }
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          // TODO: Move out of the loop. This only happens for the first run.
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
      return resultingFirstChild;
    }

    // 把子节点都设置快速查找的map映射集
    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

    // 使用map查找需要保存或删除的节点
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = updateFromMap(
        existingChildren,
        returnFiber,
        newIdx,
        newChildren[newIdx],
        expirationTime,
      );
      if (newFiber) {
        if (shouldTrackSideEffects) {
          if (newFiber.alternate !== null) {
            // 新的Fiber也是一个工作线程,但是如果已有当前的实例,那我们就可以复用这个Fiber,
            // 我们要从Map中删除这个新的,避免准备复用的Fiber被删除
            existingChildren.delete(
              newFiber.key === null ? newIdx : newFiber.key,
            );
          }
        }
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
    }

    if (shouldTrackSideEffects) {
      // Any existing children that weren''t consumed above were deleted. We need
      // to add them to the deletion list.
      // 到此所有剩余的Map的节点都将被删除,加入删除队列
      existingChildren.forEach(child => deleteChild(returnFiber, child));
    }
    // 最终返回Fiber子节点列表的第一个节点
    return resultingFirstChild;
  }

可以看到其实删除节点并不是直接删除而是打个Deletion的tag。生成effect list

function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
    const last = returnFiber.lastEffect;
    if (last !== null) {
      last.nextEffect = childToDelete;
      returnFiber.lastEffect = childToDelete;
    } else {
      returnFiber.firstEffect = returnFiber.lastEffect = childToDelete;
    }
    childToDelete.nextEffect = null;
    childToDelete.effectTag = Deletion;
  }

completeUnitOfWork

在dom diff之后会有一个收尾工作大概就是effect的各种处理,就是workLoop之后的completeUnitOfWork函数。 同步effect list到 current 的host root 树。 调用completeWork

  function completeUnitOfWork(workInProgress: Fiber): Fiber | null {
    while (true) {
      const current = workInProgress.alternate;

      const returnFiber = workInProgress.return;
      const siblingFiber = workInProgress.sibling;

      if ((workInProgress.effectTag & Incomplete) === NoEffect) {
        // This fiber completed.
        let next = completeWork(
          current,
          workInProgress,
          nextRenderExpirationTime,
        );
        stopWorkTimer(workInProgress);
        resetExpirationTime(workInProgress, nextRenderExpirationTime);

        if (next !== null) {
          stopWorkTimer(workInProgress);
          if (__DEV__ && ReactFiberInstrumentation.debugTool) {
            ReactFiberInstrumentation.debugTool.onCompleteWork(workInProgress);
          }
          // If completing this work spawned new work, do that next. We''ll come
          // back here again.
          return next;
        }

        // 将当前fiber子树上的effect list 插入到当前hostRoot 树的effectlist中
        if (
          returnFiber !== null &&
          // Do not append effects to parents if a sibling failed to complete
          (returnFiber.effectTag & Incomplete) === NoEffect
        ) {
          // Append all the effects of the subtree and this fiber onto the effect
          // list of the parent. The completion order of the children affects the
          // side-effect order.
          if (returnFiber.firstEffect === null) {
            returnFiber.firstEffect = workInProgress.firstEffect;
          }
          if (workInProgress.lastEffect !== null) {
            if (returnFiber.lastEffect !== null) {
              returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
            }
            returnFiber.lastEffect = workInProgress.lastEffect;
          }

          // If this fiber had side-effects, we append it AFTER the children''s
          // side-effects. We can perform certain side-effects earlier if
          // needed, by doing multiple passes over the effect list. We don''t want
          // to schedule our own side-effect on our own list because if end up
          // reusing children we''ll schedule this effect onto itself since we''re
          // at the end.
          const effectTag = workInProgress.effectTag;
          // Skip both NoWork and PerformedWork tags when creating the effect list.
          // PerformedWork effect is read by React DevTools but shouldn''t be committed.
          if (effectTag > PerformedWork) {
            if (returnFiber.lastEffect !== null) {
              returnFiber.lastEffect.nextEffect = workInProgress;
            } else {
              returnFiber.firstEffect = workInProgress;
            }
            returnFiber.lastEffect = workInProgress;
          }
        }

        if (siblingFiber !== null) {
          // If there is more work to do in this returnFiber, do that next.
          return siblingFiber;
        } else if (returnFiber !== null) {
          // If there''s no more work in this returnFiber. Complete the returnFiber.
          workInProgress = returnFiber;
          continue;
        } else {
          // We''ve reached the root.
          isRootReadyForCommit = true;
          return null;
        }
      } else {
        // This fiber did not complete because something threw. Pop values off
        // the stack without entering the complete phase. If this is a boundary,
        // capture values if possible.
        const next = unwindWork(workInProgress);
        // Because this fiber did not complete, don''t reset its expiration time.
        if (workInProgress.effectTag & DidCapture) {
          // Restarting an error boundary
          stopFailedWorkTimer(workInProgress);
        } else {
          stopWorkTimer(workInProgress);
        }
        if (next !== null) {
          stopWorkTimer(workInProgress);
          if (__DEV__ && ReactFiberInstrumentation.debugTool) {
            ReactFiberInstrumentation.debugTool.onCompleteWork(workInProgress);
          }
          // If completing this work spawned new work, do that next. We''ll come
          // back here again.
          // Since we''re restarting, remove anything that is not a host effect
          // from the effect tag.
          next.effectTag &= HostEffectMask;
          return next;
        }

        if (returnFiber !== null) {
          // Mark the parent fiber as incomplete and clear its effect list.
          returnFiber.firstEffect = returnFiber.lastEffect = null;
          returnFiber.effectTag |= Incomplete;
        }
        if (siblingFiber !== null) {
          // If there is more work to do in this returnFiber, do that next.
          return siblingFiber;
        } else if (returnFiber !== null) {
          // If there''s no more work in this returnFiber. Complete the returnFiber.
          workInProgress = returnFiber;
          continue;
        } else {
          return null;
        }
      }
    }

    return null;
  }

completeWork

比较长,不贴代码了。主要做的事情就是根据不同的component类型进行不同的处理。 重点是对HostComponent的props进行diff,并标记更新。 如果是react首次渲染,调用createInstance创建一个HostComponent。 如果已经存在HostComponent,检查节点是否需要更新,调用prepareUpdate,进行diff dom属性。

到此reconciliation阶段结束,主要负责产出effect list。 可以说reconcile的过程相当于是一个纯函数,输入是fiber节点,输出一个effect list。

因为纯函数的可预测性,让我们可以随时中断reconciliation阶段的执行,而不用担心side-effects给让组件状态和实际UI产生不一致

渲染阶段 completeRoot/commitRoot

function commitRoot(finishedWork: Fiber): ExpirationTime {
    isWorking = true;
    isCommitting = true;
    startCommitTimer();

    const root: FiberRoot = finishedWork.stateNode;
    const committedExpirationTime = root.pendingCommitExpirationTime;
    root.pendingCommitExpirationTime = NoWork;

    const currentTime = recalculateCurrentTime();

    // Reset this to null before calling lifecycles
    ReactCurrentOwner.current = null;

    let firstEffect;
    if (finishedWork.effectTag > PerformedWork) {
      // fiber的effect list只包括它子树中的effects,将节点的effect加到effect list链表中
      if (finishedWork.lastEffect !== null) {
        finishedWork.lastEffect.nextEffect = finishedWork;
        firstEffect = finishedWork.firstEffect;
      } else {
        firstEffect = finishedWork;
      }
    } else {
      // There is no effect on the root.
      firstEffect = finishedWork.firstEffect;
    }

    // 做一些dom事件相关设置
    prepareForCommit(root.containerInfo);

    // Commit all the side-effects within a tree. We''ll do this in two passes.
    // The first pass performs all the host insertions, updates, deletions and
    // ref unmounts.
    nextEffect = firstEffect;
    startCommitHostEffectsTimer();
    while (nextEffect !== null) {
      let didError = false;
      let error;
        try {
             // 遍历fiber树的effect list,调用相关的生命周期,比如willUnmount。操作dom,完成渲染。
            commitAllHostEffects();
        } catch (e) {
            didError = true;
            error = e;
        }
    }
    stopCommitHostEffectsTimer();

    resetAfterCommit(root.containerInfo);

    root.current = finishedWork;
    nextEffect = firstEffect;
    startCommitLifeCyclesTimer();
    while (nextEffect !== null) {
      let didError = false;
      let error;
        try {
            // 再遍历effect list,如果effect发生在classComponent上,加调didMount和didUpdate方法。
            // 如果发生在hostComponents上,会调用commitMount方法,主要是为了在render一个节点渲染之后做一些操作。比如input的auto-focus。
            commitAllLifeCycles(root, currentTime, committedExpirationTime);
        } catch (e) {
            didError = true;
            error = e;
        }
    }

    isCommitting = false;
    isWorking = false;
    stopCommitLifeCyclesTimer();
    stopCommitTimer();
    if (typeof onCommitRoot === ''function'') {
      onCommitRoot(finishedWork.stateNode);
    }

    const remainingTime = root.current.expirationTime;
    if (remainingTime === NoWork) {
      // If there''s no remaining work, we can clear the set of already failed
      // error boundaries.
      legacyErrorBoundariesThatAlreadyFailed = null;
    }
    return remainingTime;
  }

create-react-app搭配react16+ts+less

create-react-app搭配react16+ts+less

脚手架默认不支持less
create-react-app创建ts类型的react项目

// 全局安装脚手架工具
npm install -g create-react-app

// 使用脚手架创建react项目
npx create-react-app demo --template typescript

 

项目中,你写less,并引入tsx中,如下

import React from ''react'';
import style from ''./style.less'';

const Login = () => {
  return (
    <div className={style.Login}>登录</div>
  );
}

export default Login;
.Login {
  color: red;
}

 

添加less的全局类型声明,编译阶段不报错

你会发现,导入less时如果被提示找不到模块xxx.less,需要在项目的根路径文件react-app-env.d.ts(理论上,只要定义全局类型声明就行,非本文件也行)中,添加一下内容

declare module "*.less" {
  const less: any;
  export default less;
}

如果是tsx,即非ts的react项目,忽略此步骤

 

安装webpack对应的less支持
安装对应的包

npm install less-loader --save-dev

配置webpack对less支持
运行npm run eject暴漏webpack的配置文件config/webpack.config.js
在第50行配置如下:

//添加如下支持less配置代码
const lessRegex = /\.less$/;
const lessModuleRegex = /\.module\.less$/;

在第500行添加如下代码:

            {
              test: lessRegex,
              exclude: lessModuleRegex,
              use: getStyleLoaders(
                  {
                    importLoaders: 2,
                    // modules: true, 如果仅打开cssModule  那么原类名 将会没有前缀,无法与自己的样式类名关联,所以下边做法可取
                    modules:{
                      localIdentName: ''[local]_[hash:base64:5]'',
                    },
                    sourceMap: isEnvProduction && shouldUseSourceMap,
                  },
                  ''less-loader''
              ),
              sideEffects: true,
            },
            {
              test: lessModuleRegex,
              use: getStyleLoaders(
                  {
                    importLoaders: 2,
                    sourceMap: isEnvProduction && shouldUseSourceMap,
                    modules: true,
                    getLocalIdent: getCSSModuleLocalIdent,
                  },
                  ''less-loader''
              ),
            },

 



Egg + React (React Router + Redux) (SSR) 服务端渲染实际

Egg + React (React Router + Redux) (SSR) 服务端渲染实际

概述

在实现 Egg + React 服务端渲染解决方案 egg-react-webpack-boilerplate 时,因在 React + React Router + Redux 方面没有深入的实践过以及精力问题, 只实现了多页面服务端渲染方案。最近收到社区的一些咨询,想知道 Egg + React Router + Redux 如何实现 SPA 同构实现。如是就开始了 Egg + React Router + Redux 的摸索之路,实践过程中遇到 React-Router 版本问题,Redux 使用问题等问题,折腾了几天,但最终还是把想要的方案实践出来。

摸索阶段

在查阅 react router 和 redux 的相关资料,发现 react router 有 V3 和 V4 版本, V4 新版本又分为 react-router,react-router-dom,react-router-config,react-router-redux 插件, redux 相关的有 redux,react-redux,只能硬着头皮一个一个看看啥含义,看一下简单的Todo例子, 相比 Vue 的 vuex + vue-router 的工程搭建过程,这个要复杂的多,只好采用分阶段完成。先完成了纯前端渲染的 React Router + Redux 结合的例子,把 React Router 和 Redux 的相关 API 撸了一遍,基本掌握 React-Redux actions, reducer, store使用(这里自己先通过简单的例子让整个流程跑通,然后逐渐添砖加瓦,实现自己想要的功能. 比如不考虑异步,不考虑数据请求,直接hack数据,跑通后,再逐渐改造完善)。

依赖说明

react router(v4)

react-router React Router 核心
react-router-dom 用于 DOM 绑定的 React Router
react-router-native 用于 React Native 的 React Router
react-router-redux React Router 和 Redux 的集成
react-router-config 静态路由配置辅助
// 客户端用BrowserRouter, 服务端渲染用 StaticRouter 静态路由组件
import { BrowserRouter, StaticRouter } from ''react-router-dom'';

redux 和 react-redux

这里直接借个图(https://segmentfault.com/a/1190000011473973):

react-redux.png | center

Redux 介绍

Redux 是 javaScript 状态管理容器

通过 Redux 可以很方便进行数据集中管理和实现组件之间的通信,同时视图和数据逻辑分离,对于大型复杂(业务复杂,交互复杂,数据交互频繁等)的 React 项目, Redux 能够让代码结构(数据查询状态、数据改变状态、数据传播状态)层次更合理。另外,Redux 和 React 之间没有关系。Redux 支持 React、Angular、jQuery 甚至纯 JavaScript。

Redux 的设计思想很简单

Redux是在借鉴Flux思想上产生的,基本思想是保证数据的单向流动,同时便于控制、使用、测试

  • Web 应用是一个状态机,视图与状态是一一对应的。
  • 所有的状态,保存在一个对象里面,也就是单一数据源
Redux 核心由三部分组成:Store, Action, Reducer。
  • Store : 贯穿你整个应用的数据都应该存储在这里。
// component/spa/ssr/actions 创建store,初始化store数据
export function create(initalState){
 return createStore(reducers, initalState);
}
  • Action: 必须包含type这个属性,reducer将根据这个属性值来对store进行相应的处理。除此之外的属性,就是进行这个操作需要的数据。
// component/spa/ssr/actions
export function add(item) {
  return {
    type: ADD,
    item
  }
}

export function del(id) {
  return {
    type: DEL,
    id
  }
}
  • Reducer: 是个函数。接受两个参数:要修改的数据(state) 和 action对象。根据action.type来决定采用的操作,对state进行修改,最后返回新的state。
// component/spa/ssr/reducers
export default function update(state, action) {
  const newState = Object.assign({}, state);
  if (action.type === ADD) {
    const list = Array.isArray(action.item) ? action.item : [action.item];
    newState.list = [...newState.list, ...list];
  }
  else if (action.type === DEL) {
    newState.list = newState.list.filter(item => {
      return item.id !== action.id;
    });
  } else if (action.type === LIST) {
    newState.list = action.list;
  }
  return newState
}
redux 使用
// store的创建
var createStore = require(''redux'').createStore;
var store = createStore(update);

// store 里面的数据发生改变时,触发的回调函数
store.subscribe(function () {
  console.log(''the state:'', store.getState());
});

// action触发state改变的唯一方法, 改变store里面的方法
store.dispatch(add({id:1, title:''redux''})); 
store.dispatch(del(1));

react-redux

react-redux 对 redux 流程的一种简化,可以简化手动 dispatch 繁琐过程。 react-redux 重要提供以下两个API,详细介绍请见:http://cn.redux.js.org/docs/react-redux/api.html

  • connect(mapStateToProps, mapDispatchToProps, mergeToProps)(App)
  • provider

react_redux.png | center

更多信息请参考 http://cn.redux.js.org/

服务端渲染同构实现

页面模板实现

  • home.jsx
// component/spa/ssr/components/home.jsx
import React, { Component } from ''react''
import { connect } from ''react-redux''
import { add, del } from ''component/spa/ssr/actions'';

class Home extends Component {
 // 服务端渲染调用,这里mock数据,实际请改为服务端数据请求
  static fetch() {
    return Promise.resolve({
      list:[{
        id: 0,
        title: `Egg+React 服务端渲染骨架`,
        summary: ''基于Egg + React + Webpack3/Webpack2 服务端渲染同构工程骨架项目'',
        hits: 550,
        url: ''https://github.com/hubcarl/egg-react-webpack-boilerplate''
      }, {
        id: 1,
        title: ''前端工程化解决方案easywebpack'',
        summary: ''programming instead of configuration, webpack is so easy'',
        hits: 550,
        url: ''https://github.com/hubcarl/easywebpack''
      }, {
        id: 2,
        title: ''前端工程化解决方案脚手架easywebpack-cli'',
        summary: ''easywebpack command tool, support init Vue/Reac/Weex boilerplate'',
        hits: 278,
        url: ''https://github.com/hubcarl/easywebpack-cli''
      }]
    }).then(data => {
      return data;
    })
  }

  render() {
    const { add, del, list } = this.props;
    const id = list.length + 1;
    const item = {
      id,
      title: `Egg+React 服务端渲染骨架-${id}`,
      summary: ''基于Egg + React + Webpack3/Webpack2 服务端渲染骨架项目'',
      hits: 550 + id,
      url: ''https://github.com/hubcarl/egg-react-webpack-boilerplate''
    };
    return <div className="redux-nav-item">
      <h3>SPA Server Side</h3>
      <div className="container">
        <div className="row row-offcanvas row-offcanvas-right">
          <div className="col-xs-12 col-sm-9">
            <ul className="smart-artiles" id="articleList">
              {list.map(function(item) {
                return <li key={item.id}>
                  <div className="point">+{item.hits}</div>
                  <div className="card">
                    <h2><a href={item.url} target="_blank">{item.title}</a></h2>
                    <div>
                      <ul className="actions">
                        <li>
                          <time className="timeago">{item.moduleName}</time>
                        </li>
                        <li className="tauthor">
                          <a href="#" target="_blank" className="get">Sky</a>
                        </li>
                        <li><a>+收藏</a></li>
                        <li>
                          <span className="timeago">{item.summary}</span>
                        </li>
                        <li>
                          <span className="redux-btn-del" onClick={() => del(item.id)}>Delete</span>
                        </li>
                      </ul>
                    </div>
                  </div>
                </li>;
              })}
            </ul>
          </div>
        </div>
      </div>
      <div className="redux-btn-add" onClick={() => add(item)}>Add</div>
    </div>;
  }
}

function mapStateToProps(state) {
  return {
    list: state.list
  }
}

export default connect(mapStateToProps, { add, del })(Home)
  • about.jsx
// component/spa/ssr/components/about.jsx
import React, { Component } from ''react''
export default class About extends Component {
  render() {
    return <h3 className="spa-title">React+Redux+React Router SPA Server Side Render Example</h3>;
  }
}

react-router 路由定义

// component/spa/ssr/ssr
import { connect } from ''react-redux''
import { BrowserRouter, Route, Link, Switch } from ''react-router-dom''
import Home from ''component/spa/ssr/components/home'';
import About from ''component/spa/ssr/components/about'';

import { Menu, Icon } from ''antd'';

const tabKey = { ''/spa/ssr'': ''home'', ''/spa/ssr/about'': ''about'' };
class App extends Component {
  constructor(props) {
    super(props);
    const { url } = props;
    this.state = { current: tabKey[url] };
  }

  handleClick(e) {
    console.log(''click '', e, this.state);
    this.setState({
      current: e.key,
    });
  };

  render() {
    return <div>
      <Menu onClick={this.handleClick.bind(this)} selectedKeys={[this.state.current]} mode="horizontal">
        <Menu.Item key="home">
          <Link to="/spa/ssr">SPA-Redux-Server-Side-Render</Link>
        </Menu.Item>
        <Menu.Item key="about">
          <Link to="/spa/ssr/about">About</Link>
        </Menu.Item>
      </Menu>
      <Switch>
        <Route path="/spa/ssr/about" component={About}/>
        <Route path="/spa/ssr" component={Home}/>
      </Switch>
    </div>;
  }
}

export default App;

SPA前端渲染同构实现

import React, { Component } from ''react''
import ReactDOM from ''react-dom''
import { Provider } from ''react-redux''
import {match, RouterContext} from ''react-router''
import { BrowserRouter, StaticRouter } from ''react-router-dom'';
import { matchRoutes, renderRoutes } from ''react-router-config''
import Header from ''component/layout/standard/header/header'';
import SSR from ''component/spa/ssr/ssr'';
import { create } from ''component/spa/ssr/store'';
import routes from ''component/spa/ssr/routes''
const store = create(window.__INITIAL_STATE__);
const url = store.getState().url;
ReactDOM.render(
    <div>
      <Header></Header>
      <Provider store={ store }>
        <BrowserRouter>
          <SSR url={ url }/>
        </BrowserRouter>
      </Provider>
    </div>,
    document.getElementById(''app'')
);

SPA服务端渲染同构实现

在服务端渲染时,这里纠结了一下,遇到两个问题

  • 参考一些资料的写法Node服务端都是在路由里面处理的,写起来好别扭, 希望 render时
  • ReactDOMServer.renderToString(ReactElement) 参数必须是ReactElement
  • 组件异步获取的数据Node render怎么获取到

这里通过函数回调的方式可以解决上面问题,也就是 export 出去的是一个函数,然后 render 判断是否直接renderToString还是调用函数,然后再进行renderToString。目前在 egg-view-react-ssr 做了一层简单判断,代码如下:

app.react.renderElement = (reactElement, locals, options) => {
    if (reactElement.prototype && reactElement.prototype.isReactComponent) {
      return Promise.resolve(app.react.renderToString(reactElement, locals));
    }
    const context = { state: locals };
    return reactElement(context, options).then(element => {
      return app.react.renderToString(element, context.state);
    });
  }

这样处理了以后,Node 服务端controller处理时就无需自己处理路由匹配问题和store问题,全部交给底层处理。现在的这种处理方式与Vue服务端渲染render思路一致,把服务端逻辑写到模板文件里面,然后由Webpack构建js文件。

SPA服务端渲染入口文件

Webpack 构建的文件 app/ssr.js 到 app/view 目录

import React, { Component } from ''react''
import ReactDOM from ''react-dom''
import { Provider } from ''react-redux''
import {match, RouterContext} from ''react-router''
import { BrowserRouter, StaticRouter } from ''react-router-dom'';
import { matchRoutes, renderRoutes } from ''react-router-config''
import Header from ''component/layout/standard/header/header'';
import SSR from ''component/spa/ssr/ssr'';
import { create } from ''component/spa/ssr/store'';
import routes from ''component/spa/ssr/routes''
// context 为服务端初始化数据
export default function(context, options) {
    const url = context.state.url;
    // 根据服务端url地址找到匹配的组件
    const branch = matchRoutes(routes, url);
    // 收集组件数据
    const promises = branch.map(({route}) => {
      const fetch = route.component.fetch;
      return fetch instanceof Function ? fetch() : Promise.resolve(null)
    });
    // 获取组件数据,然后初始化store, 同时返回ReactElement
    return Promise.all(promises).then(data => {
      const initState = {};
      data.forEach(item => {
        Object.assign(initState, item);
      });
      context.state = Object.assign({}, context.state, initState);
      const store = create(initState);
      return () =>(
        <div>
          <Header></Header>
          <Provider store={store}>
            <StaticRouter location={url} context={{}}>
              <SSR url={url}/>
            </StaticRouter>
          </Provider>
        </div>
      )
    });
};

Node服务端controller调用

  • controller 实现
exports.ssr = function* (ctx) {
  yield ctx.render(''spa/ssr.js'', { url: ctx.url });
};
  • 路由配置
 app.get(''/spa(/.+)?'', app.controller.spa.spa.ssr);
  • 效果演示

图片描述

服务端实现与普通模板渲染调用无差异,写起来简单明了。如果你对 Egg + React 技术敢兴趣,赶快来玩一玩 egg-react-webpack-boilerplate 项目吧!

Egg + React + React Router + Redux 服务端渲染同构实践

Egg + React + React Router + Redux 服务端渲染同构实践

概述

在实现 Egg + React 服务端渲染解决方案 egg-react-webpack-boilerplate 时,因在 React + React Router + Redux 方面没有深入的实践过以及精力问题, 只实现了多页面服务端渲染方案。最近收到社区的一些咨询,想知道 Egg + React Router + Redux 如何实现 SPA 同构实现。如是就开始了 Egg + React Router + Redux 的摸索之路,实践过程中遇到 React-Router 版本问题,Redux 使用问题等问题,折腾了几天,但最终还是把想要的方案实践出来。

摸索阶段

在查阅 react router 和 redux 的相关资料,发现 react router 有 V3 和 V4 版本, V4 新版本又分为 react-router,react-router-dom,react-router-config,react-router-redux 插件, redux 相关的有 redux,react-redux,只能硬着头皮一个一个看看啥含义,看一下简单的Todo例子, 相比 Vue 的 vuex + vue-router 的工程搭建过程,这个要复杂的多,只好采用分阶段完成。先完成了纯前端渲染的 React Router + Redux 结合的例子,把 React Router 和 Redux 的相关 API 撸了一遍,基本掌握 React-Redux actions,reducer,store使用(这里自己先通过简单的例子让整个流程跑通,然后逐渐添砖加瓦,实现自己想要的功能. 比如不考虑异步,不考虑数据请求,直接hack数据,跑通后,再逐渐改造完善)。

依赖说明

react router(v4)

react-router React Router 核心
react-router-dom 用于 DOM 绑定的 React Router
react-router-native 用于 React Native 的 React Router
react-router-redux React Router 和 Redux 的集成
react-router-config 静态路由配置辅助
// 客户端用browserRouter, 服务端渲染用 StaticRouter 静态路由组件
import { browserRouter,StaticRouter } from 'react-router-dom';

redux 和 react-redux

这里直接借个图(http://www.jb51.cc/article/p-duyiwrsw-boa.html):

Redux 介绍

Redux 是 javaScript 状态管理容器

通过 Redux 可以很方便进行数据集中管理和实现组件之间的通信,同时视图和数据逻辑分离,对于大型复杂(业务复杂,交互复杂,数据交互频繁等)的 React 项目, Redux 能够让代码结构(数据查询状态、数据改变状态、数据传播状态)层次更合理。另外,Redux 和 React 之间没有关系。Redux 支持 React、Angular、jQuery 甚至纯 JavaScript。

Redux 的设计思想很简单

Redux是在借鉴Flux思想上产生的,基本思想是保证数据的单向流动,同时便于控制、使用、测试

  • Web 应用是一个状态机,视图与状态是一一对应的。
  • 所有的状态,保存在一个对象里面,也就是单一数据源
Redux 核心由三部分组成:Store,Action,Reducer。
  • Store : 贯穿你整个应用的数据都应该存储在这里。
// component/spa/ssr/actions 创建store,初始化store数据
export function create(initalState){
 return createStore(reducers,initalState);
}
  • Action: 必须包含type这个属性,reducer将根据这个属性值来对store进行相应的处理。除此之外的属性,就是进行这个操作需要的数据。
// component/spa/ssr/actions
export function add(item) {
  return {
    type: ADD,item
  }
}

export function del(id) {
  return {
    type: DEL,id
  }
}
  • Reducer: 是个函数。接受两个参数:要修改的数据(state) 和 action对象。根据action.type来决定采用的操作,对state进行修改,最后返回新的state。
// component/spa/ssr/reducers
export default function update(state,action) {
  const newState = Object.assign({},state);
  if (action.type === ADD) {
    const list = Array.isArray(action.item) ? action.item : [action.item];
    newState.list = [...newState.list,...list];
  }
  else if (action.type === DEL) {
    newState.list = newState.list.filter(item => {
      return item.id !== action.id;
    });
  } else if (action.type === LIST) {
    newState.list = action.list;
  }
  return newState
}
redux 使用
// store的创建
var createStore = require('redux').createStore;
var store = createStore(update);

// store 里面的数据发生改变时,触发的回调函数
store.subscribe(function () {
  console.log('the state:',store.getState());
});

// action触发state改变的唯一方法,改变store里面的方法
store.dispatch(add({id:1,title:'redux'})); 
store.dispatch(del(1));

react-redux

react-redux 对 redux 流程的一种简化,可以简化手动 dispatch 繁琐过程。 react-redux 重要提供以下两个API,详细介绍请见:http://cn.redux.js.org/docs/react-redux/api.html

  • connect(mapStatetoProps,mapdispatchToProps,mergetoProps)(App)
  • provider

更多信息请参考 http://cn.redux.js.org/

服务端渲染同构实现

页面模板实现

  • home.jsx
// component/spa/ssr/components/home.jsx
import React,{ Component } from 'react'
import { connect } from 'react-redux'
import { add,del } from 'component/spa/ssr/actions';

class Home extends Component {
 // 服务端渲染调用,这里mock数据,实际请改为服务端数据请求
  static fetch() {
    return Promise.resolve({
      list:[{
        id: 0,title: `Egg+React 服务端渲染骨架`,summary: '基于Egg + React + Webpack3/Webpack2 服务端渲染同构工程骨架项目',hits: 550,url: 'https://github.com/hubcarl/egg-react-webpack-boilerplate'
      },{
        id: 1,title: '前端工程化解决方案easywebpack',summary: 'programming instead of configuration,webpack is so easy',url: 'https://github.com/hubcarl/easywebpack'
      },{
        id: 2,title: '前端工程化解决方案脚手架easywebpack-cli',summary: 'easywebpack command tool,support init Vue/Reac/Weex boilerplate',hits: 278,url: 'https://github.com/hubcarl/easywebpack-cli'
      }]
    }).then(data => {
      return data;
    })
  }

  render() {
    const { add,del,list } = this.props;
    const id = list.length + 1;
    const item = {
      id,title: `Egg+React 服务端渲染骨架-${id}`,summary: '基于Egg + React + Webpack3/Webpack2 服务端渲染骨架项目',hits: 550 + id,url: 'https://github.com/hubcarl/egg-react-webpack-boilerplate'
    };
    return <div className="redux-nav-item">
      <h3>SPA Server Side</h3>
      <div className="container">
        <div className="row row-offcanvas row-offcanvas-right">
          <div className="col-xs-12 col-sm-9">
            <ul className="smart-artiles" id="articleList">
              {list.map(function(item) {
                return <li key={item.id}>
                  <div className="point">+{item.hits}</div>
                  <div className="card">
                    <h2><a href={item.url} target="_blank">{item.title}</a></h2>
                    <div>
                      <ul className="actions">
                        <li>
                          <time className="timeago">{item.moduleName}</time>
                        </li>
                        <li className="tauthor">
                          <a href="#" target="_blank" className="get">Sky</a>
                        </li>
                        <li><a>+收藏</a></li>
                        <li>
                          <span className="timeago">{item.summary}</span>
                        </li>
                        <li>
                          <span className="redux-btn-del" onClick={() => del(item.id)}>Delete</span>
                        </li>
                      </ul>
                    </div>
                  </div>
                </li>;
              })}
            </ul>
          </div>
        </div>
      </div>
      <div className="redux-btn-add" onClick={() => add(item)}>Add</div>
    </div>;
  }
}

function mapStatetoProps(state) {
  return {
    list: state.list
  }
}

export default connect(mapStatetoProps,{ add,del })(Home)
  • about.jsx
// component/spa/ssr/components/about.jsx
import React,{ Component } from 'react'
export default class About extends Component {
  render() {
    return <h3 className="spa-title">React+Redux+React Router SPA Server Side Render Example</h3>;
  }
}

react-router 路由定义

// component/spa/ssr/ssr
import { connect } from 'react-redux'
import { browserRouter,Route,Link,Switch } from 'react-router-dom'
import Home from 'component/spa/ssr/components/home';
import About from 'component/spa/ssr/components/about';

import { Menu,Icon } from 'antd';

const tabKey = { '/spa/ssr': 'home','/spa/ssr/about': 'about' };
class App extends Component {
  constructor(props) {
    super(props);
    const { url } = props;
    this.state = { current: tabKey[url] };
  }

  handleClick(e) {
    console.log('click ',e,this.state);
    this.setState({
      current: e.key,});
  };

  render() {
    return <div>
      <Menu onClick={this.handleClick.bind(this)} selectedKeys={[this.state.current]} mode="horizontal">
        <Menu.Item key="home">
          <Link to="/spa/ssr">SPA-Redux-Server-Side-Render</Link>
        </Menu.Item>
        <Menu.Item key="about">
          <Link to="/spa/ssr/about">About</Link>
        </Menu.Item>
      </Menu>
      <Switch>
        <Route path="/spa/ssr/about" component={About}/>
        <Route path="/spa/ssr" component={Home}/>
      </Switch>
    </div>;
  }
}

export default App;

SPA前端渲染同构实现

import React,{ Component } from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import {match,RouterContext} from 'react-router'
import { browserRouter,StaticRouter } from 'react-router-dom';
import { matchRoutes,renderRoutes } from 'react-router-config'
import Header from 'component/layout/standard/header/header';
import SSR from 'component/spa/ssr/ssr';
import { create } from 'component/spa/ssr/store';
import routes from 'component/spa/ssr/routes'
const store = create(window.__INITIAL_STATE__);
const url = store.getState().url;
ReactDOM.render(
    <div>
      <Header></Header>
      <Provider store={ store }>
        <browserRouter>
          <SSR url={ url }/>
        </browserRouter>
      </Provider>
    </div>,document.getElementById('app')
);

SPA服务端渲染同构实现

在服务端渲染时,这里纠结了一下,遇到两个问题

  • 参考一些资料的写法Node服务端都是在路由里面处理的,写起来好别扭,希望 render时
  • ReactDOMServer.renderToString(ReactElement) 参数必须是ReactElement
  • 组件异步获取的数据Node render怎么获取到

这里通过函数回调的方式可以解决上面问题,也就是 export 出去的是一个函数,然后 render 判断是否直接renderToString还是调用函数,然后再进行renderToString。目前在 egg-view-react-ssr 做了一层简单判断,代码如下:

app.react.renderElement = (reactElement,locals,options) => {
    if (reactElement.prototype && reactElement.prototype.isReactComponent) {
      return Promise.resolve(app.react.renderToString(reactElement,locals));
    }
    const context = { state: locals };
    return reactElement(context,options).then(element => {
      return app.react.renderToString(element,context.state);
    });
  }

这样处理了以后,Node 服务端controller处理时就无需自己处理路由匹配问题和store问题,全部交给底层处理。现在的这种处理方式与Vue服务端渲染render思路一致,把服务端逻辑写到模板文件里面,然后由Webpack构建js文件。

SPA服务端渲染入口文件

Webpack 构建的文件 app/ssr.js 到 app/view 目录

import React,renderRoutes } from 'react-router-config'
import Header from 'component/layout/standard/header/header';
import SSR from 'component/spa/ssr/ssr';
import { create } from 'component/spa/ssr/store';
import routes from 'component/spa/ssr/routes'
// context 为服务端初始化数据
export default function(context,options) {
    const url = context.state.url;
    // 根据服务端URL地址找到匹配的组件
    const branch = matchRoutes(routes,url);
    // 收集组件数据
    const promises = branch.map(({route}) => {
      const fetch = route.component.fetch;
      return fetch instanceof Function ? fetch() : Promise.resolve(null)
    });
    // 获取组件数据,然后初始化store, 同时返回ReactElement
    return Promise.all(promises).then(data => {
      const initState = {};
      data.forEach(item => {
        Object.assign(initState,item);
      });
      context.state = Object.assign({},context.state,initState);
      const store = create(initState);
      return () =>(
        <div>
          <Header></Header>
          <Provider store={store}>
            <StaticRouter location={url} context={{}}>
              <SSR url={url}/>
            </StaticRouter>
          </Provider>
        </div>
      )
    });
};

Node服务端controller调用

  • controller 实现
exports.ssr = function* (ctx) {
  yield ctx.render('spa/ssr.js',{ url: ctx.url });
};
  • 路由配置
app.get('/spa(/.+)?',app.controller.spa.spa.ssr);
  • 效果演示

服务端实现与普通模板渲染调用无差异,写起来简单明了。如果你对 Egg + React 技术敢兴趣,赶快来玩一玩 egg-react-webpack-boilerplate 项目吧!

GL的学习资料短暂整理-水的渲染整理

GL的学习资料短暂整理-水的渲染整理

下面是关于水的算法。
----------------------水面
1.normal map,根据自己的理解,水波算法自己写,然后加上一张关于水的法线贴图,模拟表面的涟漪效果。
2.FFT 的算法, 傅里叶变换??? 具体可以百度
3.Flow Map,待研究。
    目前的资源有:
        Unity RTP
4.使用水的视频,作为动态文理。
关于2维中的水:
    1.模拟流体算法
    2.使用物理引擎box2d使用小球来模拟。

今天的关于react 16 渲染整理react的渲染的分享已经结束,谢谢您的关注,如果想了解更多关于create-react-app搭配react16+ts+less、Egg + React (React Router + Redux) (SSR) 服务端渲染实际、Egg + React + React Router + Redux 服务端渲染同构实践、GL的学习资料短暂整理-水的渲染整理的相关知识,请在本站进行查询。

本文标签: