React源码06 - 完成节点任务

06 - 完成节点任务

完成节点更新之后完成节点的创建,并提供优化到最小幅度的DOM更新列表。

1. completeUnitOfWork

第 04 篇说过 renderRoot 做的事情:

  • 调用 workLoop 进行循环单元更新
  • 捕获错误并进行处理
  • 走完流程之后善后

现在在 workLoop 中调用 performUnitOfWork。
next = beginWork(current, workInProgress, nextRenderExpirationTime)
每次都 return child,以便继续循环向下单路查找,直到触及叶子节点(树枝到头了就是叶子)返回 null,也就是叶子节点没有 child 了。这算是 renderRoot 中完成了一次 workLoop 调用。
此时,传入该叶子节点执行 completeUnitOfWork。
之后尝试继续循环执行 wokrLoop,处理其他路。

completeUnitOfWork:

    1. 根据是否中断来调用不同的处理方法

在外部 renderRoot 使用 do...while 调用 workLoop 时会使用 try...catch,只要不是致命错误,就记录相应错误(比如 Suspense 的 promise 在中间过程中的合理报错),然后继续执行循环。

    1. 判断是否有兄弟节点来执行不同的操作
    1. 完成节点之后赋值 effect 链

在之前的 beginWork 中为节点标记了相应的 sideEffect,也就是等到 commit 阶段中更新 dom 时的操作依据(增删改等)。而在 completeUnitOfWork 中则将 fiber 上的 sideEffect 进一步进行串联,方便 commit 时使用。
之前第 03 篇中说过每个 fiber 上都有:

  • effectTag: SideEffectTag。用来记录 SideEffect。
  • nextEffect: Fiber | null。单链表用来快速查找下一个side effect。
  • firstEffect: Fiber | null。 子树中第一个side effect。
  • lastEffect: Fiber | null。子树中最后一个side effect。

有些像是层层嵌套的文件夹 A/B/C/D,B中只记录了C/D。这些打了 effectTag 标记的 fiber 节点通过这些指针单独组成单向链表,反正都是些指针引用,也不占多少空间。

通过不断地 completeUnitOfWork 将 effect 汇总串联到上层节点,最终 RootFiber 上的 firstEffect 到 lastEffect 这个链表中记录了所有带有 effectTag 的 fiber 节点,即最终在 commit 阶段所有需要应用到 dom 节点上的 SideEffect。

TODO:commitRoot 方法
**
SideEffectTag 清单:

export type SideEffectTag = number;

// Don't change these two values. They're used by React Dev Tools.
export const NoEffect = /*              */ 0b00000000000;
export const PerformedWork = /*         */ 0b00000000001;

// You can change the rest (and add more).
export const Placement = /*             */ 0b00000000010;
export const Update = /*                */ 0b00000000100;
export const PlacementAndUpdate = /*    */ 0b00000000110;
export const Deletion = /*              */ 0b00000001000;
export const ContentReset = /*          */ 0b00000010000;
export const Callback = /*              */ 0b00000100000;
export const DidCapture = /*            */ 0b00001000000;
export const Ref = /*                   */ 0b00010000000;
export const Snapshot = /*              */ 0b00100000000;

// Update & Callback & Ref & Snapshot
export const LifecycleEffectMask = /*   */ 0b00110100100;

// Union of all host effects
export const HostEffectMask = /*        */ 0b00111111111;

export const Incomplete = /*            */ 0b01000000000;
export const ShouldCapture = /*         */ 0b10000000000;

performUnitOfWork:

function performUnitOfWork(workInProgress: Fiber): Fiber | null {
  // The current, flushed, state of this fiber is the alternate.
  // Ideally nothing should rely on this, but relying on it here
  // means that we don't need an additional field on the work in
  // progress.
  const current = workInProgress.alternate;

  // See if beginning this work spawns more work.
  startWorkTimer(workInProgress);

  let next;
  if (enableProfilerTimer) {
    if (workInProgress.mode & ProfileMode) {
      startProfilerTimer(workInProgress);
    }

    next = beginWork(current, workInPrognextRenderExpirationTimeress, );
    workInProgress.memoizedProps = workInProgress.pendingProps;

    if (workInProgress.mode & ProfileMode) {
      // Record the render duration assuming we didn't bailout (or error).
      stopProfilerTimerIfRunningAndRecordDelta(workInProgress, true);
    }
  } else {
    next = beginWork(current, workInProgress, nextRenderExpirationTime);
    workInProgress.memoizedProps = workInProgress.pendingProps;
  }

  if (next === null) {
    // 如果这次遍历时调用beginWork返回了null,说明已经到了单路的叶子节点了,于是调用completeUnitOfWork。
    // 当前的workInProgress就是叶子节点,因为寻找child却返回了null,说明到头了。
    // If this doesn't spawn new work, complete the current work.
    next = completeUnitOfWork(workInProgress);
  }

  ReactCurrentOwner.current = null;

  return next;
}

completeUnitOfWork:
如果说 workLoop 在局部子树中是从上向下处理节点。那么 completeUnitOfWork 中则是在局部子树中从下向上处理节点。

if (next === null) {
  // 如果这次遍历时调用beginWork返回了null,说明已经到了单路的叶子节点了,于是调用completeUnitOfWork。
  // 当前的workInProgress就是叶子节点,因为寻找child却返回了null,说明到头了。
  // If this doesn't spawn new work, complete the current work.
  next = completeUnitOfWork(workInProgress);
}

completeUnitOfWork 代码有些多,主体是个 while 循环,其中有下面这个遍历逻辑:

  • 在 performUnitOfWork 中,如果从上至下单路遍历到了叶子节点,则开始调用 completeUnitOfWork 进行向上遍历。
  • 如果有 sibling 兄弟节点则 return 兄弟节点,以便 workLoop 中再次调用 performUnitOfWork 对刚才的兄弟节点进行遍历。
  • 又一次单路到头了,遇到了叶子节点,则再次 completeUnitOfWork 处理叶子节点。
  • 如果当前子树兄弟节点全处理完了,则向上对父节点进行 completeUnitOfWork 处理。如果父节点也有兄弟节点,则同理。

最终效果就是一整棵 RootFiber 树:

  • 每个节点都会先使用 performUnitOfWork 处理一次。
  • 再使用 completeUnitOfWork 处理一次。

**
completeUnitOfWork 节选:

function completeUnitOfWork(workInProgress: Fiber): Fiber | null
// ...
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 {
  // 说明更新过程完成了,到rootFiber了。等着commit阶段了
  return null;
}

completeUnitOfWork 中的一些工作:

  • 重设 ChildExpirationTime
  • completeWork

2. 重设 ChildExpirationTime

ChildExpirationTime 记录了一个 fiber 的子树中优先级最高的更新时间。尽管产生更新需求的节点可能是整个应用 fiber 树中的某个节点,但进行更新调度时是从顶部 RootFiber 开始参与调度的。
因此通过 ChildExpirationTime 不断向上汇总子树的最高优先级的更新时间,最终 RootFiber 的 ChildExpirationTime 记录了整棵树中最高优先级的更新时间。
而在 completeUnitOfWork 从下向上进行信息汇总时,如果某个节点的更新任务已经得到执行,也就是没有自身的 expirationTime 了,那么 completeUnitOfWork 中就需要顺便不断向上重置 ChildExpirationTime。通过调用:
resetChildExpirationTime(workInProgress, nextRenderExpirationTime);

3. completeWork

  • pop 各种 context 相关的内容
  • 对于 HostComponent 执行初始化
  • 初始化监听事件

大部分类型的 fiber 节点不需要在这一步做什么事情(Suspense 类型以后再说),而以下两种类型有点东西:

  • HostComponent
  • HostText

下面主要谈谈 HostComponent(该 fiber 类型对应到原生 dom)在 completeWork 中的相关操作。

4. HostComponent

HostComponent 中涉及到 updateHostComponent。在一次更新而非渲染中:

  • diffProperties 计算需要更新的内容
  • 不同的 dom property 处理方式不同

首次渲染时

创建对应的 dom 实例:

let instance = createInstance(
  type,
  newProps,
  rootContainerInstance,
  currentHostContext,
  workInProgress,
);

appendAllChildren(instance, workInProgress, false, false);

if (
  finalizeInitialChildren( // finalizeInitialChildren 最终返回是否需要 auto focus 自动聚焦
    instance,
    type,
    newProps,
    rootContainerInstance,
    currentHostContext,
  )
) {
  markUpdate(workInProgress); // 如果是需要 autoFocus,那么还要设置 sideEffect。
}
workInProgress.stateNode = instance;
function markUpdate(workInProgress: Fiber) {
  // Tag the fiber with an update effect. This turns a Placement into
  // a PlacementAndUpdate.
  workInProgress.effectTag |= Update;
}

function markRef(workInProgress: Fiber) {
  workInProgress.effectTag |= Ref;
}

appendAllChildren:
找到子树中第一层 dom 类型的节点,append 到自身对应的 dom 实例下。即 workInProgress.stateNode 所记录的原生 dom 实例。
因为在每次 completeUnitOfWork 时遇到 HostComponent 即原生 dom 对应的 fiber 类型时,都会 appendAllChildren。
所以也就是对 fiber 树这个虚拟 dom 进行提纯,最终从下向上构建出纯 dom 树。
**
finalizeInitialChildren:
appendAllChildren 之后紧接着 finalizeInitialChildren 初始化事件监听体系。
setInitialProperties 初始化事件监听(后面会单独讲事件监听)。
初始化 dom attribute(主要是一些原生 dom 操作)。
初始化markUpdate、mark*、 defaultValue、isControlled、处理 style 属性,px 补全等。

下面展开叙述。

finalizeInitialChildren 最终返回是否需要 auto focus 自动聚焦:

export function finalizeInitialChildren(
  domElement: Instance,
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
): boolean {
  setInitialProperties(domElement, type, props, rootContainerInstance);
  return shouldAutoFocusHostComponent(type, props);
}

function shouldAutoFocusHostComponent(type: string, props: Props): boolean {
  switch (type) {
    case 'button':
    case 'input':
    case 'select':
    case 'textarea':
      return !!props.autoFocus;
  }
  return false;
}

// ...
setInitialDOMProperties(
  tag,
  domElement,
  rootContainerElement,
  props,
  isCustomComponentTag,
);

setInitialDOMProperties:

function setInitialDOMProperties(
  tag: string,
  domElement: Element,
  rootContainerElement: Element | Document,
  nextProps: Object,
  isCustomComponentTag: boolean,
): void {
  for (const propKey in nextProps) {
    if (!nextProps.hasOwnProperty(propKey)) {
      continue;
    }
    const nextProp = nextProps[propKey];
    if (propKey === STYLE) {
      // 设置 style 属性
      // Relies on `updateStylesByID` not mutating `styleUpdates`.
      CSSPropertyOperations.setValueForStyles(domElement, nextProp);
    } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
      const nextHtml = nextProp ? nextProp[HTML] : undefined;
      if (nextHtml != null) {
        setInnerHTML(domElement, nextHtml);
      }
    } else if (propKey === CHILDREN) {
      if (typeof nextProp === 'string') {
        // Avoid setting initial textContent when the text is empty. In IE11 setting
        // textContent on a <textarea> will cause the placeholder to not
        // show within the <textarea> until it has been focused and blurred again.
        // https://github.com/facebook/react/issues/6731#issuecomment-254874553
        const canSetTextContent = tag !== 'textarea' || nextProp !== '';
        if (canSetTextContent) {
          setTextContent(domElement, nextProp);
        }
      } else if (typeof nextProp === 'number') {
        setTextContent(domElement, '' + nextProp);
      }
    } else if (
      propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
      propKey === SUPPRESS_HYDRATION_WARNING
    ) {
      // Noop
    } else if (propKey === AUTOFOCUS) {
      // We polyfill it separately on the client during commit.
      // We could have excluded it in the property list instead of
      // adding a special case here, but then it wouldn't be emitted
      // on server rendering (but we *do* want to emit it in SSR).
    } else if (registrationNameModules.hasOwnProperty(propKey)) {
      if (nextProp != null) {
        if (__DEV__ && typeof nextProp !== 'function') {
          warnForInvalidEventListener(propKey, nextProp);
        }
        ensureListeningTo(rootContainerElement, propKey);
      }
    } else if (nextProp != null) {
      DOMPropertyOperations.setValueForProperty(
        domElement,
        propKey,
        nextProp,
        isCustomComponentTag,
      );
    }
  }
}

比如设置 style 样式属性 setValueForStyles
分为 -- 开头的自定义和常规 css 属性,就是直接操作原生 dom 上的 style。

/**
 * Sets the value for multiple styles on a node.  If a value is specified as
 * '' (empty string), the corresponding style property will be unset.
 *
 * @param {DOMElement} node
 * @param {object} styles
 */
export function setValueForStyles(node, styles) {
  const style = node.style;
  for (let styleName in styles) {
    if (!styles.hasOwnProperty(styleName)) {
      continue;
    }
    const isCustomProperty = styleName.indexOf('--') === 0;
    // 尝试为number自动补全px
    const styleValue = dangerousStyleValue(
      styleName,
      styles[styleName],
      isCustomProperty,
    );
    if (styleName === 'float') {
      styleName = 'cssFloat';
    }
    if (isCustomProperty) {
      style.setProperty(styleName, styleValue);
    } else {
      style[styleName] = styleValue;
    }
  }
}

值得一提的是,会为一些缺省的 number 数值会自动补全 'px' 单位(显然还有好些例外的 css 属性本身的值就是纯 number):

function dangerousStyleValue(name, value, isCustomProperty) {
  const isEmpty = value == null || typeof value === 'boolean' || value === '';
  if (isEmpty) {
    return '';
  }

  if (
    !isCustomProperty &&
    typeof value === 'number' &&
    value !== 0 &&
    !(isUnitlessNumber.hasOwnProperty(name) && isUnitlessNumber[name]) // 
  ) {
    return value + 'px'; // Presumes implicit 'px' suffix for unitless numbers
  }

  return ('' + value).trim();
}

这份列表就是刚才说的“例外”,它们的样式值就是纯 number 类型,没有 px:

/**
 * CSS properties which accept numbers but are not in units of "px".
 */
export const isUnitlessNumber = {
  animationIterationCount: true,
  borderImageOutset: true,
  borderImageSlice: true,
  borderImageWidth: true,
  boxFlex: true,
  boxFlexGroup: true,
  boxOrdinalGroup: true,
  columnCount: true,
  columns: true,
  flex: true,
  flexGrow: true,
  flexPositive: true,
  flexShrink: true,
  flexNegative: true,
  flexOrder: true,
  gridArea: true,
  gridRow: true,
  gridRowEnd: true,
  gridRowSpan: true,
  gridRowStart: true,
  gridColumn: true,
  gridColumnEnd: true,
  gridColumnSpan: true,
  gridColumnStart: true,
  fontWeight: true,
  lineClamp: true,
  lineHeight: true,
  opacity: true,
  order: true,
  orphans: true,
  tabSize: true,
  widows: true,
  zIndex: true,
  zoom: true,

  // SVG-related properties
  fillOpacity: true,
  floodOpacity: true,
  stopOpacity: true,
  strokeDasharray: true,
  strokeDashoffset: true,
  strokeMiterlimit: true,
  strokeOpacity: true,
  strokeWidth: true,
};

**
更新时(进行 diff 判断)

对于 input、select、textarea 就算 props 没变,也要 markUPdate(workInProgress) 标记后续需要更新

for 循环遍历 lastProps 的 propKey,如果本次不是删除该 props,则继续遍历,否则进行后面的删除操作。
通过 prop 来判断相关 dom attribute 是否发生变化:

  • 删除 style 相关属性:把 dom style 相关 css 属性设置为 '' 空字符串即可。
  • 其他 props 的若要删除:设置为 null。
  • 对于 children,只检查 string 或者 number 类型的 props 值是否变化,从而能否跳过更新。其他则一律更新。
  • 经过以上判断对比,可能成功地跳过了部分不必要的 props 的更新。而剩余的所有情况,则一律更新:

updatePayload.push(propKey, nextProp);

  • 然后返回 updatePalyload 数组,通过 markUpdate(workInprogress) 进行标记。
updateHostComponent(
  current,
  workInProgress,
  type,
  newProps,
  rootContainerInstance,
);

if (current.ref !== workInProgress.ref) {
  markRef(workInProgress);
}

updateHostComponent:

  • 如果当前 fiber 节点新老 props 没变,则跳过更新。TODO 未完待续,涉及 dom diff or props diff?
  updateHostComponent = function(
    current: Fiber,
    workInProgress: Fiber,
    type: Type,
    newProps: Props,
    rootContainerInstance: Container,
  ) {
    const oldProps = current.memoizedProps;
    // 跳过dom更新
    if (oldProps === newProps) {
      // In mutation mode, this is sufficient for a bailout because
      // we won't touch this node even if children changed.
      return;
    }

    const instance: Instance = workInProgress.stateNode; // 拿到dom实例
    const currentHostContext = getHostContext();
    const updatePayload = prepareUpdate(
      instance,
      type,
      oldProps,
      newProps,
      rootContainerInstance,
      currentHostContext,
    );
    // TODO: Type this specific to this type of component.
    workInProgress.updateQueue = (updatePayload: any);
    // If the update payload indicates that there is a change or if there
    // is a new ref we mark this as an update. All the work is done in commitWork.
    if (updatePayload) {
      markUpdate(workInProgress);
    }
  };

5. HostText

HostText 中涉及到 updateHostText,因为是 dom text 节点,所以只需对比新老 text 字符串。

首次渲染
调用 createTextInstance,其中调用 createTextNode,里面其实就是在调用原生的 document.createTextNode(text),另外想要找到 document 节点,只需在任意原生 dom 节点上查询 ownerDocument 属性即可。

precacheFiberNode HostComponent 讲过了?我怎么没印象。

6. renderRoot 中的错误处理

  • 给报错节点增加 Incomplete 副作用
  • 给父链上具有 error boundary 的节点增加副作用,收集错误以及进行一定的处理。
  • 创建错误相关的更新。

首先,在 renderRoot 中:

do {
    try {
      workLoop(isYieldy);
    } catch (thrownValue) {
      if (nextUnitOfWork === null) {
        // This is a fatal error.
        didFatal = true;
        onUncaughtError(thrownValue);
      } else {
        
        const failedUnitOfWork: Fiber = nextUnitOfWork;
        if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
          replayUnitOfWork(failedUnitOfWork, thrownValue, isYieldy);
        }

        const sourceFiber: Fiber = nextUnitOfWork;
        let returnFiber = sourceFiber.return;
        if (returnFiber === null) {
          // This is the root. The root could capture its own errors. However,
          // we don't know if it errors before or after we pushed the host
          // context. This information is needed to avoid a stack mismatch.
          // Because we're not sure, treat this as a fatal error. We could track
          // which phase it fails in, but doesn't seem worth it. At least
          // for now.
          didFatal = true;
          onUncaughtError(thrownValue);
        } else {
          throwException(
            root,
            returnFiber,
            sourceFiber,
            thrownValue,
            nextRenderExpirationTime,
          );
          nextUnitOfWork = completeUnitOfWork(sourceFiber);
          continue;
        }
      }
    }
    break;
  } while (true);

onUncaughtError:
**
对于致命错误,会使用 onUncaughtError 来处理:

function onUncaughtError(error: mixed) {
  invariant(
    nextFlushedRoot !== null,
    'Should be working on a root. This error is likely caused by a bug in ' +
      'React. Please file an issue.',
  );
  // Unschedule this root so we don't work on it again until there's
  // another update.
  nextFlushedRoot.expirationTime = NoWork;
  if (!hasUnhandledError) {
    hasUnhandledError = true;
    unhandledError = error;
  }
}

throwException:
**
否则使用 throwException 对捕获到的错误进行处理:

  • 先尝试处理 Suspense 中的异常。
  • 否则向上寻找可以错误处理的 ClassComponent 组件(比如使用了 getDerivedStateFromErrorcomponentDidCatch )。为该 ClassComponent 所在节点增加 ShouldCapture 副作用标记。 然后创建错误 update 并入队列,以便在 commit 阶段被调用。
  • 如果向上没能找到能处理错误的 class component,则最终会在 HostRoot 上进行类似操作。
function throwException(
  root: FiberRoot,
  returnFiber: Fiber,
  sourceFiber: Fiber,
  value: mixed,
  renderExpirationTime: ExpirationTime,
) {
  // The source fiber did not complete.
  sourceFiber.effectTag |= Incomplete;
  // Its effect list is no longer valid.
  sourceFiber.firstEffect = sourceFiber.lastEffect = null;

  if (
    value !== null &&
    typeof value === 'object' &&
    typeof value.then === 'function'
  ) {
  // 处理 Suspense 相关合理的中间状态带来的异常
  }
  
  // We didn't find a boundary that could handle this type of exception. Start
  // over and traverse parent path again, this time treating the exception
  // as an error.
  renderDidError();
  value = createCapturedValue(value, sourceFiber);
  let workInProgress = returnFiber;
  do {
    switch (workInProgress.tag) {
      case HostRoot: {
        const errorInfo = value;
        workInProgress.effectTag |= ShouldCapture;
        workInProgress.expirationTime = renderExpirationTime;
        const update = createRootErrorUpdate(
          workInProgress,
          errorInfo,
          renderExpirationTime,
        );
        enqueueCapturedUpdate(workInProgress, update);
        return;
      }
      case ClassComponent:
        // Capture and retry
        const errorInfo = value;
        const ctor = workInProgress.type;
        const instance = workInProgress.stateNode;
        if (
          (workInProgress.effectTag & DidCapture) === NoEffect &&
          (typeof ctor.getDerivedStateFromError === 'function' ||
            (instance !== null &&
              typeof instance.componentDidCatch === 'function' &&
              !isAlreadyFailedLegacyErrorBoundary(instance)))
        ) {
          workInProgress.effectTag |= ShouldCapture;
          workInProgress.expirationTime = renderExpirationTime;
          // Schedule the error boundary to re-render using updated state
          const update = createClassErrorUpdate(
            workInProgress,
            errorInfo,
            renderExpirationTime,
          );
          enqueueCapturedUpdate(workInProgress, update);
          return;
        }
        break;
      default:
        break;
    }
    workInProgress = workInProgress.return;
  } while (workInProgress !== null);
}

** nextUnitOfWork = completeUnitOfWork(sourceFiber) :**
**
如果某个节点产生了错误,那么处理完错误相关的之后,会调用 completeUnitOfwork 做收尾工作,向上遍历。不会再往下遍历渲染子节点,因为已经报错了,向下的局部子树没意义了。

7. unwindWork

  • 类似于 completeWork 对不同类型组件进行处理(其整个过程是跟 completeWork 区分开的)。
  • 对于 ShouldCapture 组件设置 DidCapture 副作用

unwindWork 和 completeWork 最大的区别是前者会判断有 ShouldCapture ,然后取消设置,并新增 DidCapture ,然后 return。

如果一个节点报错了,对于父组件 completeUnitOfWork 时会全部走 unwindWork 而不是 completeWork。
会全部从跟组件重走一遍更新过程,即 workLoop 那一套向下遍历。

TODO 待深究。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269