LWXYFER

LWXYFER 查看完整档案

北京编辑  |  填写毕业院校  |  填写所在公司/组织 blog.lwxyfer.com 编辑
编辑

You want to do something in this fucking life , you go out and build it.

个人动态

LWXYFER 赞了文章 · 2018-10-15

精读《Epitath 源码 - renderProps 新用法》

1 引言

很高兴这一期的话题是由 epitath 的作者 grsabreu 提供的。

前端发展了 20 多年,随着发展中国家越来越多的互联网从业者涌入,现在前端知识玲琅满足,概念、库也越来越多。虽然内容越来越多,但作为个体的你的时间并没有增多,如何持续学习新知识,学什么将会是个大问题。

前端精读通过吸引优质的用户,提供最前沿的话题或者设计理念,虽然每周一篇文章不足以概括这一周的所有焦点,但可以保证你阅读的这十几分钟没有在浪费时间,每一篇精读都是经过精心筛选的,我们既讨论大家关注的焦点,也能找到仓库角落被遗忘的珍珠。

2 概述

在介绍 Epitath 之前,先介绍一下 renderProps。

renderProps 是 jsx 的一种实践方式,renderProps 组件并不渲染 dom,但提供了持久化数据与回调函数帮助减少对当前组件 state 的依赖。

RenderProps 的概念

react-powerplug 就是一个 renderProps 工具库,我们看看可以做些什么:

<Toggle initial={true}>
  {({ on, toggle }) => <Checkbox checked={on} onChange={toggle} />}
</Toggle>

Toggle 就是一个 renderProps 组件,它可以帮助控制受控组件。比如仅仅利用 Toggle,我们可以大大简化 Modal 组件的使用方式:

class App extends React.Component {
  state = { visible: false };

  showModal = () => {
    this.setState({
      visible: true
    });
  };

  handleOk = e => {
    this.setState({
      visible: false
    });
  };

  handleCancel = e => {
    this.setState({
      visible: false
    });
  };

  render() {
    return (
      <div>
        <Button type="primary" onClick={this.showModal}>
          Open Modal
        </Button>
        <Modal
          title="Basic Modal"
          visible={this.state.visible}
          onOk={this.handleOk}
          onCancel={this.handleCancel}
        >
          <p>Some contents...</p>
          <p>Some contents...</p>
          <p>Some contents...</p>
        </Modal>
      </div>
    );
  }
}

ReactDOM.render(<App />, mountNode);

这是 Modal 标准代码,我们可以使用 Toggle 简化为:

class App extends React.Component {
  render() {
    return (
      <Toggle initial={false}>
        {({ on, toggle }) => (
          <Button type="primary" onClick={toggle}>
            Open Modal
          </Button>
          <Modal
            title="Basic Modal"
            visible={on}
            onOk={toggle}
            onCancel={toggle}
          >
            <p>Some contents...</p>
            <p>Some contents...</p>
            <p>Some contents...</p>
          </Modal>
        )}
      </Toggle>
    );
  }
}

ReactDOM.render(<App />, mountNode);

省掉了 state、一堆回调函数,而且代码更简洁,更语义化。

renderProps 内部管理的状态不方便从外部获取,因此只适合保存业务无关的数据,比如 Modal 显隐。

RenderProps 嵌套问题的解法

renderProps 虽然好用,但当我们想组合使用时,可能会遇到层层嵌套的问题:

<Counter initial={5}>
  {counter => {
    <Toggle initial={false}>
      {toggle => {
        <MyComponent counter={counter.count} toggle={toggle.on} />;
      }}
    </Toggle>;
  }}
</Counter>

因此 react-powerplugin 提供了 compose 函数,帮助聚合 renderProps 组件:

import { compose } from 'react-powerplug'

const ToggleCounter = compose(
  <Counter initial={5} />,
  <Toggle initial={false} />
)

<ToggleCounter>
  {(toggle, counter) => (
    <ProductCard {...} />
  )}
</ToggleCounter>

使用 Epitath 解决嵌套问题

Epitath 提供了一种新方式解决这个嵌套的问题:

const App = epitath(function*() {
  const { count } = yield <Counter />
  const { on } = yield <Toggle />

  return (
    <MyComponent counter={count} toggle={on} />
  )
})

<App />

renderProps 方案与 Epitath 方案,可以类比为 回调 方案与 async/await 方案。Epitath 和 compose 都解决了 renderProps 可能带来的嵌套问题,而 compose 是通过将多个 renderProps merge 为一个,而 Epitath 的方案更接近 async/await 的思路,利用 generator 实现了伪同步代码。

3 精读

Epitath 源码一共 40 行,我们分析一下其精妙的方式。

下面是 Epitath 完整的源码:

import React from "react";
import immutagen from "immutagen";

const compose = ({ next, value }) =>
  next
    ? React.cloneElement(value, null, values => compose(next(values)))
    : value;

export default Component => {
  const original = Component.prototype.render;
  const displayName = `EpitathContainer(${Component.displayName ||
    "anonymous"})`;

  if (!original) {
    const generator = immutagen(Component);

    return Object.assign(
      function Epitath(props) {
        return compose(generator(props));
      },
      { displayName }
    );
  }

  Component.prototype.render = function render() {
    // Since we are calling a new function to be called from here instead of
    // from a component class, we need to ensure that the render method is
    // invoked against `this`. We only need to do this binding and creation of
    // this function once, so we cache it by adding it as a property to this
    // new render method which avoids keeping the generator outside of this
    // method's scope.
    if (!render.generator) {
      render.generator = immutagen(original.bind(this));
    }

    return compose(render.generator(this.props));
  };

  return class EpitathContainer extends React.Component {
    static displayName = displayName;
    render() {
      return <Component {...this.props} />;
    }
  };
};

immutagen

immutagen 是一个 immutable generator 辅助库,每次调用 .next 都会生成一个新的引用,而不是自己发生 mutable 改变:

import immutagen from "immutagen";

const gen = immutagen(function*() {
  yield 1;
  yield 2;
  return 3;
})(); // { value: 1, next: [function] }

gen.next(); // { value: 2, next: [function] }
gen.next(); // { value: 2, next: [function] }

gen.next().next(); // { value: 3, next: undefined }

compose

看到 compose 函数就基本明白其实现思路了:

const compose = ({ next, value }) =>
  next
    ? React.cloneElement(value, null, values => compose(next(values)))
    : value;
const App = epitath(function*() {
  const { count } = yield <Counter />;
  const { on } = yield <Toggle />;
});

通过 immutagen,依次调用 next,生成新组件,且下一个组件是上一个组件的子组件,因此会产生下面的效果:

yield <A>
yield <B>
yield <C>
// 等价于
<A>
  <B>
    <C />
  </B>
</A>

到此其源码精髓已经解析完了。

存在的问题

crimx 在讨论中提到,Epitath 方案存在的最大问题是,每次 render 都会生成全新的组件,这对内存是一种挑战。

稍微解释一下,无论是通过 原生的 renderProps 还是 compose,同一个组件实例只生成一次,React 内部会持久化这些组件实例。而 immutagen 在运行时每次执行渲染,都会生成不可变数据,也就是全新的引用,这会导致废弃的引用存在大量 GC 压力,同时 React 每次拿到的组件都是全新的,虽然功能相同。

4 总结

epitath 巧妙的利用了 immutagen 的不可变 generator 的特性来生成组件,并且在递归 .next 时,将顺序代码解析为嵌套代码,有效解决了 renderProps 嵌套问题。

喜欢 epitath 的同学赶快入手吧!同时我们也看到 generator 手动的步骤控制带来的威力,这是 async/await 完全无法做到的。

是否可以利用 immutagen 解决 React Context 与组件相互嵌套问题呢?还有哪些其他前端功能可以利用 immutagen 简化的呢?欢迎加入讨论。

5 更多讨论

讨论地址是:精读《Epitath - renderProps 新用法》 · Issue #106 · dt-fe/weekly

如果你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

查看原文

赞 9 收藏 4 评论 0

LWXYFER 赞了文章 · 2018-06-25

精读《JS 引擎基础之 Shapes and Inline Caches》

1 引言

本期精读的文章是:JS 引擎基础之 Shapes and Inline Caches

一起了解下 JS 引擎是如何运作的吧!

JS 的运作机制可以分为 AST 分析、引擎执行两个步骤:

image

JS 源码通过 parser(分析器)转化为 AST(抽象语法树),再经过 interperter(解释器)解析为 bytecode(字节码)。

为了提高运行效率,optimizing compiler(优化编辑器)负责生成 optimized code(优化后的机器码)。

本文主要从 AST 之后说起。

2 概述

JS 的解释器、优化器

JS 代码可能在字节码或者优化后的机器码状态下执行,而生成字节码速度很快,而生成机器码就要慢一些了。

image

V8 也类似,V8 将 interpreter 称为 Ignition(点火器),将 optimizing compiler 成为 TurboFan(涡轮风扇发动机)。

image

可以理解为将代码先点火启动后,逐渐进入涡轮发动机提速。

代码先快速解析成可执行的字节码,在执行过程中,利用执行中获取的数据(比如执行频率),将一些频率高的方法,通过优化编译器生成机器码以提速。

image

火狐使用的 Mozilla 引擎有一点点不同,使用了两个优化编译器,先将字节码优化为部分机器码,再根据这个部分优化后的代码运行时拿到的数据进行最终优化,生成高度优化的机器码,如果优化失败将会回退到部分优化的机器码。

笔者:不同前端引擎对 JS 优化方式大同小异,后面会继续列举不同前端引擎在解析器、编译器部分优化的方式。

image

微软的 Edge 浏览器,使用的 Chakra 引擎,优化方式与 Mozilla 很像,区别是第二个最终优化的编译器同时接收字节码和部分优化的机器码产生的数据,并且在优化失败后回退到第一步字节码而不是第二步。

image

Safari、React Native 使用的 JSC 引擎则更为极端,使用了三个优化编译器,其优化是一步步渐进的,优化失败后都会回退到第一步部分优化的机器码。

为什么不同前端引擎会使用不同的优化策略呢?这是由于 JS 要么使用解释器快速执行(生成字节码),或者优化成机器码后再执行,但优化消耗时间的并不总是小于字节码低效运行损耗的时间,所以有些引擎选择了多个优化编译器,逐层优化,尽可能在解析时间与执行效率中找到一个平衡点。

JS 的对象模型

JS 是基于面向对象的,那么 JS 引擎是如何实现 JS 对象模型的呢?他们用了哪些技巧加速访问 JS 对象的属性?

和解析器、优化器一样,大部分主流 JS 引擎在对象模型实现上也很类似。

image

ECMAScript 规范确定了对象模型就是一个以字符串为 key 的字典,除了其值以外,还定义了 WriteableEnumerableConfigurable 这些配置,表示这个 key 能否被重写、遍历访问、配置。

虽然规范定义了 [[]] 双括号的写法,那这不会暴露给用户,暴露给用户的是 Object.getOwnPropertyDescriptor 这个 API,可以拿到某个属性的配置。


在 JS 中,数组是对象的特殊场景,相比对象,数组拥有特定的下标,根据 ECMAScript 规范规定,数组下标的长度最大为 2³²−1。同时数组拥有 length 属性:

image

length 只是一个不可枚举、不可配置的属性,并且在数组赋值时,会自动更新数值:

image

所以数组是特殊的对象,结构完全一致。

属性访问效率优化

属性访问是最常见的,所以 JS 引擎必须对属性访问做优化。

Shapes

JS 编程中,给不同对象相同的 key 名很常见,访问不同对象的同一个 propertyKey 也很常见:

const object1 = { x: 1, y: 2 };
const object2 = { x: 3, y: 4 };

function logX(object) {
  console.log(object.x);
  //          ^^^^^^^^
}

logX(object1);
logX(object2);

这时 object1object2 拥有一个相同的 shape。拿拥有 xy 属性的对象来看:

image

如果访问 object.y,JS 引擎会先找到 key y,再查找 [[value]]

如果将属性值也存储在 JSObject 中,像 object1object2 就会出现许多冗余数据,因此引擎单独存储 Shape,与真实对象隔离:

image

这样具有相同结构的对象可以共享 Shape。所有 JS 引擎都是用这种方式优化对象,但并不都称为 Shape,这里就不详细罗列了,可以去原文查看在各引擎中 Shape 的别名。

Transition chains 和 Transition trees

如果给一个对象增加了 key,JS 引擎如何生成新的 Shape 呢?

这种 Shape 链式创建的过程,称为 Transition chains:

image

开始创建空对象时,JSObject 和 Shape 都是空,当为 x 赋值 5 时,在 JSObject 下标 0 的位置添加了 5,并且 Shape 指向了拥有字段 xShape(x),当赋值 y6 时,在 JSObject 下标 1 的位置添加了 6,并将 Shape 指向了拥有字段 xyShape(x, y)

而且可以再优化,Shape(x, y) 由于被 Shape(x) 指向,所以可以省略 x 这个属性:

image

笔者:当然这里说的主要是优化技巧,我们可以看出来,JS 引擎在做架构设计时没有考虑优化问题,而在架构设计完后,再回过头对时间和空间进行优化,这是架构设计的通用思路。

如果没有连续的父 Shape,比如分别创建两个对象:

const object1 = {};
object1.x = 5;
const object2 = {};
object2.y = 6;

这时要通过 Transition trees 来优化:

image

可以看到,两个 Shape(x)Shape(y) 别分继承 Shape(empty)。当然也不是任何时候都会创建空 Shape,比如下面的情况:

const object1 = {};
object1.x = 5;
const object2 = { x: 6 };

生成的 Shape 如下图所示:

image

可以看到,由于 object2 并不是从空对象开始的,所以并不会从 Shape(empty) 开始继承。

Inline Caches

大概可以翻译为“局部缓存”,JS 引擎为了提高对象查找效率,需要在局部做高效缓存。

比如有一个函数 getX,从 o.x 获取值:

function getX(o) {
  return o.x;
}

JSC 引擎 生成的字节码结构是这样的:

image

get_by_id 指令是获取 arg1 参数指向的对象 x,并存储在 loc0,第二步则返回 loc0

当执行函数 getX({ x: 'a' }) 时,引擎会在 get_by_id 指令中缓存这个对象的 Shape

image

这个对象的 Shape 记录了自己拥有的字段 x 以及其对应的下标 offset

image

执行 get_by_id 时,引擎从 Shape 查找下标,找到 x,这就是 o.x 的查找过程。但一旦找到,引擎就会将 Shape 保存的 offset 缓存起来,下次开始直接跳过 Shape 这一步:

image

以后访问 o.x 时,只要 Shape 相同,引擎直接从 get_by_id 指令中缓存的下标中可以直接命中要查找的值,而这个缓存在指令中的下标就是 Inline Cache.

数组存储优化

和对象一样,数组的存储也可以被优化,而由于数组的特殊性,不需要为每一项数据做完整的配置。

比如这个数组:

const array = ["#jsconfeu"];

JS 引擎同样通过 Shape 与数据分离的方式存储:

image

JS 引擎将数组的值单独存储在 Elements 结构中,而且它们通常都是可读可配置可枚举的,所以并不会像对象一样,为每个元素做配置。

但如果是这种例子:

// 永远不要这么做
const array = Object.defineProperty([], "0", {
  value: "Oh noes!!1",
  writable: false,
  enumerable: false,
  configurable: false
});

JS 引擎会存储一个 Dictionary Elements 类型,为每个数组元素做配置:

image

这样数组的优化就没有用了,后续的赋值都会基于这种比较浪费空间的 Dictionary Elements 结构。所以永远不要用 Object.defineProperty 操作数组。

通过对 JS 引擎原理的认识,作者总结了下面两点代码中的注意事项:

  1. 尽量以相同方式初始化对象,因为这样会生成较少的 Shapes
  2. 不要混淆对象的 propertyKey 与数组的下标,虽然都是用类似的结构存储,但 JS 引擎对数组下标做了额外优化。

3 精读

这次原理系列解读是针对 JS 引擎执行优化这个点的,而网页渲染流程大致如下:

image

可以看到 Script 在整个网页解析链路中位置是比较靠前的,JS 解析效率会直接影响网页的渲染,所以 JS 引擎通过解释器(parser)和优化器(optimizing compiler)尽可能 对 JS 代码提效。

Shapes

需要特别说明的是,Shapes 并不是 原型链,原型链是面向开发者的概念,而 Shapes 是面向 JS 引擎的概念。

比如如下代码:

const a = {};
const b = {};
const c = {};

显然对象 abc 之间是没有关联的,但共享一个 Shapes。

另外理解引擎的概念有助于我们站在语法层面对立面的角度思考问题:在 JS 学习阶段,我们会执着于思考如下几种创建对象方式的异同:

const a = {};
const b = new Object();
const c = new f1();
const d = Object.create(null);

比如上面四种情况,我们要理解在什么情况下,用何种方式创建对象性能最优。

但站在 JS 引擎优化角度去考虑,JS 引擎更希望我们都通过 const a = {} 这种看似最没有难度的方式创建对象,因为可以共享 Shape。而与其他方式混合使用,可能在逻辑上做到了优化,但阻碍了 JS 引擎做自动优化,可能会得不偿失。

Inline Caches

对象级别的优化已经很极致了,工程代码中也没有机会帮助 JS 引擎做得更好,值得注意的是不要对数组使用 Object 对象下的方法,尤其是 defineProperty,因为这会让 JS 引擎在存储数组元素时,使用 Dictionary Elements 结构替代 Elements,而 Elements 结构是共享 PropertyDescriptor 的。

但也有难以避免的情况,比如使用 Object.defineProperty 监听数组变化时,就不得不破坏 JS 引擎渲染了。

笔者写 dob 的时候,使用 proxy 监听数组变化,这并不会改变 Elements 的结构,所以这也从另一个侧面证明了使用 proxy 监听对象变化比 Object.defineProperty 更优,因为 Object.defineProperty 会破坏 JS 引擎对数组做的优化。

4 总结

本文主要介绍了 JS 引擎两个概念: ShapesInline Caches,通过认识 JS 引擎的优化方式,在编程中需要注意以下两件事:

  1. 尽量以相同方式初始化对象,因为这样会生成较少的 Shapes
  2. 不要混淆对象的 propertyKey 与数组的下标,虽然都是用类似的结构存储,但 JS 引擎对数组下标做了额外优化。

5 更多讨论

讨论地址是:精读《JS 引擎基础之 Shapes and Inline Caches》 · Issue #91 · dt-fe/weekly

如果你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。

查看原文

赞 22 收藏 18 评论 0

LWXYFER 回答了问题 · 2018-06-14

解决react 頁面換換

手动修改: history 即可。

关注 2 回答 1

LWXYFER 回答了问题 · 2018-06-14

前端的生命周期钩子是什么意思?

一般我们讲生命周期: 如 React 中生命周期,这是由框架定义的。其他框架也有类似定义。 单独讲,钩子函数就是钩子函数。

在前端而言,页面也会有生命周期: contentLoaded,loaded, unloaded 等这些。

关注 3 回答 3

LWXYFER 关注了用户 · 2018-06-14

颜海镜 @yanhaijing

知名技术博主,开源达人,《React状态管理与同构实战》作者,http://yanhaijing.com

关注 892

LWXYFER 赞了文章 · 2018-05-28

聊一聊 cookie

咱们不搞一开始就一大堆理论知识介绍,怕把人讲懵了...... 咱们换一个思维方式——"从现象看本质",先说说我们看到了什么,再从看到的现象中提出问题,最后深入寻找答案。

我们看到的 cookie

我自己创建了一个网站,网址为http://ppsc.sankuai.com。在这个网页中我设置了几个cookieJSSESSIONIDPA_VTIMEskmtutctest

在 chrome 浏览器中打开这个网站,进入开发者模式,点击Resources栏 -> 选择cookies,我们会看到如下图所示的界面:

图片描述

解释一下:左边栏Cookies下方会列举当前网页中设置过cookie的域都有哪些。上图中只有一个域,即“ppsc.sankuai.com”。而右侧区域显示的就是某个域下具体的 cookie 列表,对应上图就是“ppsc.sankuai.com”域下设置的4个cookie

在这个网页中我往http://ppsc.sankuai.com/getList接口发了一个 Ajax 请求,request header如下图所示:

图片描述

从上图中我们会看到request header中自动添加了Cookie字段(我并没有手动添加这个字段哦~),Cookie字段的值其实就是我设置的那4个 cookie。这个请求最终会发送到http://ppsc.sankuai.com这个服务器上,这个服务器就能从接收到的request header中提取那4个cookie

上面两张图展示了cookie的基本通信流程:设置cookie => cookie被自动添加到request header中 => 服务端接收到cookie。这个流程中有几个问题需要好好研究:

  1. 什么样的数据适合放在cookie中?

  2. cookie是怎么设置的?

  3. cookie为什么会自动加到request header中?

  4. cookie怎么增删查改?

我们要带着这几个问题继续往下阅读。

cookie 是怎么工作的?

首先必须明确一点,存储cookie是浏览器提供的功能。cookie 其实是存储在浏览器中的纯文本,浏览器的安装目录下会专门有一个 cookie 文件夹来存放各个域下设置的cookie

当网页要发http请求时,浏览器会先检查是否有相应的cookie,有则自动添加在request header中的cookie字段中。这些是浏览器自动帮我们做的,而且每一次http请求浏览器都会自动帮我们做。这个特点很重要,因为这关系到“什么样的数据适合存储在cookie中”。

存储在cookie中的数据,每次都会被浏览器自动放在http请求中,如果这些数据并不是每个请求都需要发给服务端的数据,浏览器这设置自动处理无疑增加了网络开销;但如果这些数据是每个请求都需要发给服务端的数据(比如身份认证信息),浏览器这设置自动处理就大大免去了重复添加操作。所以对于那设置“每次请求都要携带的信息(最典型的就是身份认证信息)”就特别适合放在cookie中,其他类型的数据就不适合了。

但在 localStorage 出现之前,cookie被滥用当做了存储工具。什么数据都放在cookie中,即使这些数据只在页面中使用而不需要随请求传送到服务端。当然cookie标准还是做了一些限制的:每个域名下的cookie 的大小最大为4KB,每个域名下的cookie数量最多为20个(但很多浏览器厂商在具体实现时支持大于20个)。

cookie 的格式

document.cookie

JS 原生的 API提供了获取cookie的方法:document.cookie(注意,这个方法只能获取非 HttpOnly 类型的cookie)。在 console 中执行这段代码可以看到结果如下图:

图片描述

打印出的结果是一个字符串类型,因为cookie本身就是存储在浏览器中的字符串。但这个字符串是有格式的,由键值对 key=value构成,键值对之间由一个分号和一个空格隔开。

cookie 的属性选项

每个cookie都有一定的属性,如什么时候失效,要发送到哪个域名,哪个路径等等。这些属性是通过cookie选项来设置的,cookie选项包括:expiresdomainpathsecureHttpOnly。在设置任一个cookie时都可以设置相关的这些属性,当然也可以不设置,这时会使用这些属性的默认值。在设置这些属性时,属性之间由一个分号和一个空格隔开。代码示例如下:

"key=name; expires=Thu, 25 Feb 2016 04:18:00 GMT; domain=ppsc.sankuai.com; path=/; secure; HttpOnly"

expires

expires选项用来设置“cookie 什么时间内有效”。expires其实是cookie失效日期,expires必须是 GMT 格式的时间(可以通过 new Date().toGMTString()或者 new Date().toUTCString() 来获得)。

expires=Thu, 25 Feb 2016 04:18:00 GMT表示cookie讲在2016年2月25日4:18分之后失效,对于失效的cookie浏览器会清空。如果没有设置该选项,则默认有效期为session,即会话cookie。这种cookie在浏览器关闭后就没有了。

expires 是 http/1.0协议中的选项,在新的http/1.1协议中expires已经由 max-age 选项代替,两者的作用都是限制cookie 的有效时间。expires的值是一个时间点(cookie失效时刻= expires),而max-age 的值是一个以为单位时间段(cookie失效时刻= 创建时刻+ max-age)。
另外,max-age 的默认值是 -1(即有效期为 session );若max-age有三种可能值:负数、0、正数。负数:有效期session0:删除cookie;正数:有效期为创建时刻+ max-age

domain 和 path

domain是域名,path是路径,两者加起来就构成了 URL,domainpath一起来限制 cookie 能被哪些 URL 访问。

一句话概括:某cookie的 domain为“baidu.com”, path为“/ ”,若请求的URL(URL 可以是js/html/img/css资源请求,但不包括 XHR 请求)的域名是“baidu.com”或其子域如“api.baidu.com”、“dev.api.baidu.com”,且 URL 的路径是“/ ”或子路径“/home”、“/home/login”,则浏览器会将此 cookie 添加到该请求的 cookie 头部中。

所以domainpath2个选项共同决定了cookie何时被浏览器自动添加到请求头部中发送出去。如果没有设置这两个选项,则会使用默认值。domain的默认值为设置该cookie的网页所在的域名,path默认值为设置该cookie的网页所在的目录。

特别说明1:
发生跨域xhr请求时,即使请求URL的域名和路径都满足 cookie 的 domain 和 path,默认情况下cookie也不会自动被添加到请求头部中。若想知道原因请阅读本文最后一节)

特别说明2:
domain是可以设置为页面本身的域名(本域),或页面本身域名的父域,但不能是公共后缀 public suffix。举例说明下:如果页面域名为 www.baidu.com, domain可以设置为“www.baidu.com”,也可以设置为“baidu.com”,但不能设置为“.com”或“com”。

secure

secure选项用来设置cookie只在确保安全的请求中才会发送。当请求是HTTPS或者其他安全协议时,包含 secure 选项的 cookie 才能被发送至服务器。

默认情况下,cookie不会带secure选项(即为空)。所以默认情况下,不管是HTTPS协议还是HTTP协议的请求,cookie 都会被发送至服务端。但要注意一点,secure选项只是限定了在安全情况下才可以传输给服务端,但并不代表你不能看到这个 cookie。

下面我们设置一个 secure类型的 cookie:

document.cookie = "name=huang; secure";

之后你就能在控制台中看到这个 cookie 了,如下图所示:

图片描述

这里有个坑需要注意下:
如果想在客户端即网页中通过 js 去设置secure类型的 cookie,必须保证网页是https协议的。在http协议的网页中是无法设置secure类型cookie的。

httpOnly

这个选项用来设置cookie是否能通过 js 去访问。默认情况下,cookie不会带httpOnly选项(即为空),所以默认情况下,客户端是可以通过js代码去访问(包括读取、修改、删除等)这个cookie的。当cookiehttpOnly选项时,客户端则无法通过js代码去访问(包括读取、修改、删除等)这个cookie

在客户端是不能通过js代码去设置一个httpOnly类型的cookie的,这种类型的cookie只能通过服务端来设置。

那我们在页面中怎么知道哪些cookiehttpOnly类型的呢?看下图:

图片描述

凡是httpOnly类型的cookie,其 HTTP 一列都会打上√,如上图中的PA_VTIME。你通过document.cookie是不能获取的,也不能修改PA_VTIME的。

——httpOnly与安全

从上面介绍中,大家是否会有这样的疑问:为什么我们要限制客户端去访问cookie?其实这样做是为了保障安全。

试想:如果任何 cookie 都能被客户端通过document.cookie获取会发生什么可怕的事情。当我们的网页遭受了 XSS 攻击,有一段恶意的script脚本插到了网页中。这段script脚本做的事情是:通过document.cookie读取了用户身份验证相关的 cookie,并将这些 cookie 发送到了攻击者的服务器。攻击者轻而易举就拿到了用户身份验证信息,于是就可以摇摇大摆地冒充此用户访问你的服务器了(因为攻击者有合法的用户身份验证信息,所以会通过你服务器的验证)。

如何设置 cookie?

知道了cookie的格式,cookie的属性选项,接下来我们就可以设置cookie了。首先得明确一点:cookie既可以由服务端来设置,也可以由客户端来设置。

服务端设置 cookie

不管你是请求一个资源文件(如 html/js/css/图片),还是发送一个ajax请求,服务端都会返回response。而response header中有一项叫set-cookie,是服务端专门用来设置cookie的。如下图所示,服务端返回的response header中有5个set-cookie字段,每个字段对应一个cookie(注意不能将多个cookie放在一个set-cookie字段中),set-cookie字段的值就是普通的字符串,每个cookie还设置了相关属性选项。

图片描述

注意:

  • 一个set-Cookie字段只能设置一个cookie,当你要想设置多个 cookie,需要添加同样多的set-Cookie字段。

  • 服务端可以设置cookie 的所有选项:expiresdomainpathsecureHttpOnly

客户端设置 cookie

在网页即客户端中我们也可以通过js代码来设置cookie。如我当前打开的网址为http://dxw.st.sankuai.com/mp/,在控制台中我们执行了下面代码:

document.cookie = "name=Jonh; ";

查看浏览器 cookie 面板如下图所示,cookie确实设置成功了,而且属性选项 domainpathexpires都用了默认值。

图片描述

再执行下面代码:

document.cookie="age=12; expires=Thu, 26 Feb 2116 11:50:25 GMT; domain=sankuai.com; path=/";

查看浏览器cookie 面板,如下图所示,新的cookie设置成功了,而且属性选项 domainpathexpires都变成了设定的值。

图片描述

注意:

  • 客户端可以设置cookie 的下列选项:expiresdomainpathsecure(有条件:只有在https协议的网页中,客户端设置secure类型的 cookie 才能成功),但无法设置HttpOnly选项。

用 js 如何设置多个 cookie

当要设置多个cookie时, js 代码很自然地我们会这么写:

document.cookie = "name=Jonh; age=12; class=111";

但你会发现这样写只是添加了第一个cookie“name=John”,后面的所有cookie都没有添加成功。所以最简单的设置多个cookie的方法就在重复执行document.cookie = "key=name",如下:

document.cookie = "name=Jonh";
document.cookie = "age=12";
document.cookie = "class=111";

如何修改、删除

修改 cookie

要想修改一个cookie,只需要重新赋值就行,旧的值会被新的值覆盖。但要注意一点,在设置新cookie时,path/domain这几个选项一定要旧cookie 保持一样。否则不会修改旧值,而是添加了一个新的 cookie。

删除 cookie

删除一个cookie 也挺简单,也是重新赋值,只要将这个新cookie的expires 选项设置为一个过去的时间点就行了。但同样要注意,path/domain/这几个选项一定要旧cookie 保持一样。

cookie 编码

cookie其实是个字符串,但这个字符串中逗号、分号、空格被当做了特殊符号。所以当cookie的 key 和 value 中含有这3个特殊字符时,需要对其进行额外编码,一般会用escape进行编码,读取时用unescape进行解码;当然也可以用encodeURIComponent/decodeURIComponent或者encodeURI/decodeURI三者的区别可以参考这篇文章)。

var key = escape("name;value");
var value = escape("this is a value contain , and ;");
document.cookie= key + "=" + value + "; expires=Thu, 26 Feb 2116 11:50:25 GMT; domain=sankuai.com; path=/";

跨域请求中 cookie

之前在介绍 XHR 的一篇文章里面提过:默认情况下,在发生跨域时,cookie 作为一种 credential 信息是不会被传送到服务端的。必须要进行额外设置才可以。具体原因和如何设置可以参考我的这篇文章:你真的会使用XMLHttpRequest吗?

另外,关于跨域资源共享 CORS极力推荐大家阅读阮一峰老师的这篇 跨域资源共享 CORS 详解

其他补充

  1. 什么时候 cookie 会被覆盖:name/domain/path 这3个字段都相同的时候;

  2. 关于domain的补充说明(参考1/参考2):

    1. 如果显式设置了 domain,则设置成什么,浏览器就存成什么;但如果没有显式设置,则浏览器会自动取 url 的 host 作为 domain 值;

    2. 新的规范中,显式设置 domain 时,如果 value 最前面带点,则浏览器处理时会将这个点去掉,所以最后浏览器存的就是没有点的(注意:但目前大多数浏览器并未全部这么实现)

    3. 前面带点‘.’和不带点‘.’有啥区别:

      • 带点:任何 subdomain 都可以访问,包括父 domain

      • 不带点:只有完全一样的域名才能访问,subdomain 不能(但在 IE 下比较特殊,它支持 subdomain 访问)

总结

咱们今天就聊到这里,若有不对之处欢迎各位指正~~
最后附上一些参考资料:

  1. http://www.quirksmode.org/js/...

  2. http://www.tutorialspoint.com...

  3. http://www.allaboutcookies.or...

  4. http://bubkoo.com/2014/04/21/...

查看原文

赞 329 收藏 533 评论 50

LWXYFER 关注了专栏 · 2018-05-17

前端进阶

关注 1142

LWXYFER 关注了用户 · 2018-04-19

justjavac @justjavac

会写点 js 代码

关注 14445

LWXYFER 关注了问题 · 2018-04-10

解决闭包中,seTimeout中alert(i),为什么会乱序?

一个很经典的闭包题

for (var i = 1; i <= 5; i++) {
   (function a(i) {
       setTimeout(function() {
           alert(i);
       }, 1000);
   })(i);
}

按理说,应该弹出1 2 3 4 5 ,但是实际弹出的却是乱序的。
换成console.log(i)就是1 2 3 4 5。
这是为什么呢?

关注 6 回答 1

LWXYFER 赞了文章 · 2018-04-08

React v16.3.0: New lifecycles and context API

几天前,我们写了一篇关于即将到来的对我们的传统生命周期方法的变更的文章,包括逐步迁移策略。在React 16.3.0中,我们添加了一些新的生命周期方法来帮助迁移。我们还引入了新的API,用于长时间请求的特性:一个官方的上下文API、一个ref转发API和一个更语意化的ref API。

请继续阅读,了解更多关于这个版本的信息。

官方认证的 Context API

多年来,React为Context提供了一个实验性的API。虽然它是一个强大的工具,但是由于API中固有的问题,它的使用是不受欢迎的,因此我们打算用一个更好的API来替代这实验性的API。

React 16.3引入了一个新的Context API,它更高效,同时支持静态类型检查和深度更新。

注意
旧的ContextAPI 将继续保留到React 16.x,所以您将有时间迁移。

下面是一个示例,说明如何使用新的上下文API注入“主题”:

## by 司徒正美
const ThemeContext = React.createContext('light');

class ThemeProvider extends React.Component {
  state = {theme: 'light'};

  render() {
    return (
      <ThemeContext.Provider value={this.state.theme}>
        {this.props.children}
      </ThemeContext.Provider>
    );
  }
}

class ThemedButton extends React.Component {
  render() {
    return (
      <ThemeContext.Consumer>
        {theme => <Button theme={theme} />}
      </ThemeContext.Consumer>
    );
  }
}

createRef API

以前,React提供了两种管理refs的方法:字符串ref API和回调ref API。尽管字符串ref API比较方便,但是它有几个缺点,所以我们的官方推荐是使用回调ref。

React 16.3为管理refs提供了一个新的方案,它为字符串ref提供了方便,并且没有任何缺点:

## by 司徒正美

class MyComponent extends React.Component {
  constructor(props) {
    super(props);

    this.inputRef = React.createRef();
  }

  render() {
    return <input type="text" ref={this.inputRef} />;
  }

  componentDidMount() {
    this.inputRef.current.focus();
  }
}
注意

除了新的createRef API外,回调refs将继续得到支持。

您不需要在组件中替换回调refs。它们稍微灵活一些,因此它们将继续作为一个高级特性。

forwardRef API

高阶组件(或HOCs)是在组件之间重用代码的常用方法。基于上面的主题上下文示例,我们可能会创建一个临时对象,将当前的“主题”作为一个属性注入:

## by 司徒正美

function withTheme(Component) {
  return function ThemedComponent(props) {
    return (
      <ThemeContext.Consumer>
        {theme => <Component {...props} theme={theme} />}
      </ThemeContext.Consumer>
    );
  };
}

我们可以使用上述特殊的方式将组件连接到主题上下文,而不必直接使用主题上下文。例如:

## by 司徒正美

class FancyButton extends React.Component {
  buttonRef = React.createRef();

  focus() {
    this.buttonRef.current.focus();
  }

  render() {
    const {label, theme, ...rest} = this.props;
    return (
      <button
        {...rest}
        className={`${theme}-button`}
        ref={this.buttonRef}>

        {label}
      </button>
    );
  }
}

const FancyThemedButton = withTheme(FancyButton);

// We can render FancyThemedButton as if it were a FancyButton
// It will automatically receive the current "theme",
// And the HOC will pass through our other props.
<FancyThemedButton
  label="Click me!"
  onClick={handleClick}
/>;

HOCs通常会将props传递给它们包装的组件。不幸的是,refs没有冲透进去。这意味着如果我们使用FancyThemedButton,我们就不能将ref添加到FancyButton中,因此我们无法调用focus()。

新的代理API通过提供一种方法来拦截一个ref,并将其转发为一个普通的props,从而解决了这个问题:

## by 司徒正美

function withTheme(Component) {
  // Note the second param "ref" provided by React.forwardRef.
  // We can attach this to Component directly.
  function ThemedComponent(props, ref) {
    return (
      <ThemeContext.Consumer>
        {theme => (
          <Component {...props} ref={ref} theme={theme} />
        )}
      </ThemeContext.Consumer>
    );
  }

  // These next lines are not necessary,
  // But they do give the component a better display name in DevTools,
  // e.g. "ForwardRef(withTheme(MyComponent))"
  const name = Component.displayName || Component.name;
  ThemedComponent.displayName = `withTheme(${name})`;

  // Tell React to pass the "ref" to ThemedComponent.
  return React.forwardRef(ThemedComponent);
}

const fancyButtonRef = React.createRef();

// fancyButtonRef will now point to FancyButton
<FancyThemedButton
  label="Click me!"
  onClick={handleClick}
  ref={fancyButtonRef}
/>;

组件生命周期钩子的变化

React的类组件API已经存在多年,几乎没有变化。但是,当我们为更高级的特性(例如错误边界和即将到来的异步渲染模式)添加支持时,我们以它本来没有打算的方式来扩展这个模型。

例如,在当前的API中,用一些非寻常的手段来阻止初始渲染是很容易的。在某种程度上,这是因为有太多的钩子来完成这项既定的任务,而且还不清楚哪一个是最好的。我们已经注意到错误处理的中断行为通常不会被考虑,并且可能导致内存泄漏(这也会影响即将到来的异步渲染模式)。当前的类组件API也使其他的工作变得复杂,比如我们的代码优化器(Prepack)的工作。

componentWillMount, componentWillReceiveProps, componentWillUpdate这些钩子很容易引发问题,并且也严重扰乱React的生命周期。基于这些原因,我们将废弃这些方法,以支持更好的替代方案。

我们认识到这一变化将影响许多现有的组件。因此,迁移路径将尽可能平缓,并提供迁移方案。(在Facebook,我们拥有5万多个React组件。我们也依赖于一个渐进的发布周期!

注意

弃用警告将在React16以后的版本中启用, 一直保留到17发布时。

即使在React17中,仍然可以使用它们,但是它们将添加“UNSAFE_”前缀,以表明它们可能导致问题。我们还准备了一个自动化的脚本,以便现有代码中重命名它们。

除了废弃不安全的生命周期钩子外,我们还增加了一些新的生命周期钩子:

getDerivedStateFromProps 用来componentWillReceiveProps。

getSnapshotBeforeUpdate,用在更新前从DOM中安全地读取属性。

StrictMode 组件

<StrictMode />是一种专门用于暴露潜在问题的工具。与<Fragment />一样,<StrictMode/>将 不会渲染到视图中。它能为其子组件激活额外的检查和警告。

注意

<StrictMode />检查只在开发模式下运行;它们不会影响生产构建。

虽然严格的模式不可能捕获所有的问题(例如某些类型的窜改),但它可以帮助很多人。如果您在严格的模式下看到警告,这些事情很可能会导致异步渲染的错误。

在16.3版本中,StrictMode帮助:

  1. 识别具有不安全生命周期钩子的组件。
  2. 关于遗留字符串ref API用法的警告。
  3. 检测意想不到的副作用
查看原文

赞 21 收藏 29 评论 6

LWXYFER 赞了文章 · 2018-04-03

React16.2的fiber架构(2)

insertUpdateIntoFiber 会根据fiber的状态创建一个或两个列队对象,对象是长成这样的

//by 司徒正美, 加群:370262116 一起研究React与anujs
// https://github.com/RubyLouvre/anu 欢迎加star
function createUpdateQueue(baseState) {//我们现在是丢了一个null做传参
  var queue = {
    baseState: baseState,
    expirationTime: NoWork,//NoWork会被立即执行
    first: null,
    last: null,
    callbackList: null,
    hasForceUpdate: false,
    isInitialized: false
  };

  return queue;
}

scheduleWork是一个奇怪的方法,只是添加一下参数

 function scheduleWork(fiber, expirationTime) {
    return scheduleWorkImpl(fiber, expirationTime, false);
  }

scheduleWorkImpl的最开头有一个recordScheduleUpdate方法,用来记录调度器的执行状态,如注释所示,它现在相当于什么都没有做

function recordScheduleUpdate() {
  if (enableUserTimingAPI) {//全局变量,默认为true
    if (isCommitting) {//全局变量,默认为false, 没有进入分支
      hasScheduledUpdateInCurrentCommit = true;
    }
    //全局变量,默认为null,没有没有进入分支
    if (currentPhase !== null && currentPhase !== 'componentWillMount' && currentPhase !== 'componentWillReceiveProps') {
      hasScheduledUpdateInCurrentPhase = true;
    }
  }
}

scheduleWorkImpl的一些分支非常复杂,我们打一些断点

function computeExpirationForFiber(fiber) {
    var expirationTime = void 0;
    if (expirationContext !== NoWork) {
      // An explicit expiration context was set;
      expirationTime = expirationContext;
    } else if (isWorking) {
      if (isCommitting) {
        // Updates that occur during the commit phase should have sync priority
        // by default.
        expirationTime = Sync;
      } else {
        // Updates during the render phase should expire at the same time as
        // the work that is being rendered.
        expirationTime = nextRenderExpirationTime;
      }
    } else {
      // No explicit expiration context was set, and we're not currently
      // performing work. Calculate a new expiration time.
      if (useSyncScheduling && !(fiber.internalContextTag & AsyncUpdates)) {
        // This is a sync update
        console.log("expirationTime", Sync)
        expirationTime = Sync;//命中这里
      } else {
        // This is an async update
        expirationTime = computeAsyncExpiration();
      }
    }
    return expirationTime;
  }
    function checkRootNeedsClearing(root, fiber, expirationTime) {
    if (!isWorking && root === nextRoot && expirationTime < nextRenderExpirationTime) {
      console.log("checkRootNeedsClearing对nextRoot,nextUnitOfWork,nextRenderExpirationTime进行置空")
      // Restart the root from the top.
      if (nextUnitOfWork !== null) {
        // This is an interruption. (Used for performance tracking.)
        interruptedBy = fiber;
      }
      nextRoot = null;
      nextUnitOfWork = null;
      nextRenderExpirationTime = NoWork;
    }else{
      console.log("checkRootNeedsClearing就是想酱油")
    }
  }

function scheduleWorkImpl(fiber, expirationTime, isErrorRecovery) {
    recordScheduleUpdate();//现在什么也没做

    var node = fiber;
    while (node !== null) {
      // Walk the parent path to the root and update each node's
      // expiration time.
      if (node.expirationTime === NoWork || node.expirationTime > expirationTime) {
        node.expirationTime = expirationTime;//由于默认就是NoWork,因此会被重写 Sync
      }
      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) {//进入这里
          var root = node.stateNode;
          checkRootNeedsClearing(root, fiber, expirationTime);
          console.log("requestWork",root, expirationTime)
          requestWork(root, expirationTime);
          checkRootNeedsClearing(root, fiber, expirationTime);
        } else {
          return;
        }
      }
      node = node['return'];
    }
  }

输出如下

clipboard.png

requestWork也很难理解,里面太多全局变量,觉得不是前端的人搞的。为了帮助理解,我们继续加日志

//by 司徒正美, 加群:370262116 一起研究React与anujs

 // requestWork is called by the scheduler whenever a root receives an update.
  // It's up to the renderer to call renderRoot at some point in the future.
  /*
只要root收到更新(update对象),requestWork就会被调度程序调用。
渲染器在将来的某个时刻调用renderRoot。
  */
  function requestWork(root, expirationTime) {
    if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
      invariant_1(false, 'Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.');
    }

    // Add the root to the schedule.
    // Check if this root is already part of the schedule.
    if (root.nextScheduledRoot === null) {
      // This root is not already scheduled. Add it.
      console.log("设置remainingExpirationTime",expirationTime)
      root.remainingExpirationTime = expirationTime;
      if (lastScheduledRoot === null) {
        console.log("设置firstScheduledRoot, lastScheduledRoot")
        firstScheduledRoot = lastScheduledRoot = root;
        root.nextScheduledRoot = root;
      } else {
        lastScheduledRoot.nextScheduledRoot = root;
        lastScheduledRoot = root;
        lastScheduledRoot.nextScheduledRoot = firstScheduledRoot;
      }
    } else {
      // This root is already scheduled, but its priority may have increased.
      var remainingExpirationTime = root.remainingExpirationTime;
      if (remainingExpirationTime === NoWork || expirationTime < remainingExpirationTime) {
        // Update the priority.
        root.remainingExpirationTime = expirationTime;
      }
    }

    if (isRendering) {
      // Prevent reentrancy. Remaining work will be scheduled at the end of
      // the currently rendering batch.
      return;
    }

    if (isBatchingUpdates) {
      // 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;
        console.log("performWorkOnRoot")
        performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime);
      }
      return;
    }

    // TODO: Get rid of Sync and use current time?
    if (expirationTime === Sync) {
      console.log("进入performWork")
      performWork(Sync, null);
    } else {
      scheduleCallbackWithExpiration(expirationTime);
    }
  }

从日志输出来看,requestWork只是修改了两个全局变量,然后进入performWork。这三个内部方法起名很有意思。scheduleWork意为打算工作,requestWork意为申请工作,performWork意为努力工作(正式上班)

function performWork(minExpirationTime, dl) {
    deadline = dl;

    // Keep working on roots until there's no more work, or until the we reach
    // the deadline.
    //这里会将root设置为highestPriorityRoot
    findHighestPriorityRoot();

    if (enableUserTimingAPI && deadline !== null) {
      var didExpire = nextFlushedExpirationTime < recalculateCurrentTime();
      console.log(didExpire)
      stopRequestCallbackTimer(didExpire);
    }

    while (nextFlushedRoot !== null 
      && nextFlushedExpirationTime !== NoWork 
      && (minExpirationTime === NoWork || nextFlushedExpirationTime <= minExpirationTime) 
      && !deadlineDidExpire) {
      console.log("performWorkOnRoot")
      performWorkOnRoot(highestPriorityRoot, nextFlushedExpirationTime);
      // Find the next highest priority work.
      findHighestPriorityRoot();
    }

    // We're done flushing work. Either we ran out of time in this callback,
    // or there's no more work left with sufficient priority.

    // If we're inside a callback, set this to false since we just completed it.
    if (deadline !== null) {
      callbackExpirationTime = NoWork;
      callbackID = -1;
    }
    // If there's work left over, schedule a new callback.
    if (nextFlushedExpirationTime !== NoWork) {
      console.log("scheduleCallbackWithExpiration")
      scheduleCallbackWithExpiration(nextFlushedExpirationTime);
    }

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

    if (hasUnhandledError) { //如果有没处理的错误则throw
      var _error4 = unhandledError;
      unhandledError = null;
      hasUnhandledError = false;
      throw _error4;
    }
  }

clipboard.png

我们终于进入performWorkOnRoot,performWorkOnRoot的作用是区分同步渲染还是异步渲染,expirationTime等于1,因此进入同步。导步肯定为false

// https://github.com/RubyLouvre/anu 欢迎加star

function performWorkOnRoot(root, expirationTime) {

    isRendering = true;

    // Check if this is async work or sync/expired work.
    // TODO: Pass current time as argument to renderRoot, commitRoot
    if (expirationTime <= recalculateCurrentTime()) {
      // Flush sync work.
     
      var finishedWork = root.finishedWork;
      console.log("Flush sync work.", finishedWork)
      if (finishedWork !== null) {
        // This root is already complete. We can commit it.
        root.finishedWork = null;
        console.log("commitRoot")
        root.remainingExpirationTime = commitRoot(finishedWork);
      } else {
        root.finishedWork = null;
        console.log("renderRoot")
        finishedWork = renderRoot(root, expirationTime);
        if (finishedWork !== null) {
          console.log("继续commitRoot")
          // We've completed the root. Commit it.
          root.remainingExpirationTime = commitRoot(finishedWork);
        }
      }
    } else {
      console.log("Flush async work.")
      // Flush async work.
      // ...略
    }

    isRendering = false;
  }

clipboard.png

renderRoot也是怒长,React16代码的特点是许多巨型类,巨型方法,有JAVA之遗风。renderRoot只有前面几行是可能处理虚拟DOM(或叫fiber),后面都是错误边界的


function renderRoot(root, expirationTime) {
   
    isWorking = true;

    // We're about to mutate the work-in-progress tree. If the root was pending
    // commit, it no longer is: we'll need to complete it again.
    root.isReadyForCommit = false;

    // Check if we're starting from a fresh stack, or if we're resuming from
    // previously yielded work.
    if (root !== nextRoot || expirationTime !== nextRenderExpirationTime || nextUnitOfWork === null) {
      // Reset the stack and start working from the root.
      resetContextStack();
      nextRoot = root;
      nextRenderExpirationTime = expirationTime;
      //可能是用来工作的代码
       console.log("createWorkInProgress")
      nextUnitOfWork = createWorkInProgress(nextRoot.current, null, expirationTime);
    }
    //可能是用来工作的代码
     console.log("startWorkLoopTimer")
    startWorkLoopTimer(nextUnitOfWork);
   // 处理错误边界
    var didError = false;
    var error = null;
    invokeGuardedCallback$1(null, workLoop, null, expirationTime);
    // An error was thrown during the render phase.
    while (didError) {
       console.log("componentDidCatch的相关实现")
      if (didFatal) {
        // This was a fatal error. Don't attempt to recover from it.
        firstUncaughtError = error;
        break;
      }

      var failedWork = nextUnitOfWork;
      if (failedWork === null) {
        // An error was thrown but there's no current unit of work. This can
        // happen during the commit phase if there's a bug in the renderer.
        didFatal = true;
        continue;
      }

      // 处理错误边界
      var boundary = captureError(failedWork, error);
      !(boundary !== null) ? invariant_1(false, 'Should have found an error boundary. This error is likely caused by a bug in React. Please file an issue.') : void 0;

      if (didFatal) {
        // The error we just captured was a fatal error. This happens
        // when the error propagates to the root more than once.
        continue;
      }
       // 处理错误边界
      didError = false;
      error = null;
      // We're finished working. Exit the error loop.
      break;
    }
   // 处理错误边界
    var uncaughtError = firstUncaughtError;

    // We're done performing work. Time to clean up.
    stopWorkLoopTimer(interruptedBy);
    interruptedBy = null;
    isWorking = false;
    didFatal = false;
    firstUncaughtError = null;
     // 处理错误边界
    if (uncaughtError !== null) {
      onUncaughtError(uncaughtError);
    }

    return root.isReadyForCommit ? root.current.alternate : null;
  }

  function resetContextStack() {
    // Reset the stack
    reset$1();
    // Reset the cursors
    resetContext();
    resetHostContainer();
  }

function reset$1() {
  console.log("reset",index)
  while (index > -1) {
    valueStack[index] = null;

    {
      fiberStack[index] = null;
    }

    index--;
  }
}

function resetContext() {
  consoel.log("resetContext")
  previousContext = emptyObject_1;
  contextStackCursor.current = emptyObject_1;
  didPerformWorkStackCursor.current = false;
}

  function resetHostContainer() {
    console.log("resetHostContainer",contextStackCursor, rootInstanceStackCursor, NO_CONTEXT )
    contextStackCursor.current = NO_CONTEXT;
    rootInstanceStackCursor.current = NO_CONTEXT;
  }

clipboard.png

createWorkInProgress就是将根组件的fiber对象再复制一份,变成其alternate属性。因此 将虚拟DOM转换为真实DOM的重任就交给invokeGuardedCallback

var invokeGuardedCallback = function (name, func, context, a, b, c, d, e, f) {
  ReactErrorUtils._hasCaughtError = false;
  ReactErrorUtils._caughtError = null;
  var funcArgs = Array.prototype.slice.call(arguments, 3);
  try {
    func.apply(context, funcArgs);
  } catch (error) {
    ReactErrorUtils._caughtError = error;
    ReactErrorUtils._hasCaughtError = true;
  }
//这下面还有怒长(100-150L )的关于错误边界的处理,略过
};

func为workLoop

//by 司徒正美, 加群:370262116 一起研究React与anujs

 function workLoop(expirationTime) {
    if (capturedErrors !== null) {
      // If there are unhandled errors, switch to the slow work loop.
      // TODO: How to avoid this check in the fast path? Maybe the renderer
      // could keep track of which roots have unhandled errors and call a
      // forked version of renderRoot.
      slowWorkLoopThatChecksForFailedWork(expirationTime);
      return;
    }
    if (nextRenderExpirationTime === NoWork || nextRenderExpirationTime > expirationTime) {
      return;
    }

    if (nextRenderExpirationTime <= mostRecentCurrentTime) {
      // Flush all expired work.
      while (nextUnitOfWork !== null) {
        console.log("performUnitOfWork",nextUnitOfWork)
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
      }
    } else {
      // Flush asynchronous work until the deadline runs out of time.
      while (nextUnitOfWork !== null && !shouldYield()) {
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
      }
    }
  }

clipboard.png
我们终于看到工作的代码了。 这个nextUnitOfWork 是renderRoot生成的
performUnitOfWork与beginWork的代码,里面会根据fiber的tag进入各种操作

//by 司徒正美, 加群:370262116 一起研究React与anujs
// https://github.com/RubyLouvre/anu 欢迎加star

function performUnitOfWork(workInProgress) {
    // 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.
    var current = workInProgress.alternate;

    // See if beginning this work spawns more work.
    startWorkTimer(workInProgress);
    {
      ReactDebugCurrentFiber.setCurrentFiber(workInProgress);
    }
    console.log("beginWork")
    var next = beginWork(current, workInProgress, nextRenderExpirationTime);
    {
      ReactDebugCurrentFiber.resetCurrentFiber();
    }
    if (true && ReactFiberInstrumentation_1.debugTool) {
      ReactFiberInstrumentation_1.debugTool.onBeginWork(workInProgress);
    }

    if (next === null) {
      console.log("next")
      // If this doesn't spawn new work, complete the current work.
      next = completeUnitOfWork(workInProgress);
    }

    ReactCurrentOwner.current = null;

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

    switch (workInProgress.tag) {
      case IndeterminateComponent:
        return mountIndeterminateComponent(current, workInProgress, renderExpirationTime);
      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 CallHandlerPhase:
        // This is a restart. Reset the tag to the initial phase.
        workInProgress.tag = CallComponent;
      // Intentionally fall through since this is now the same.
      case CallComponent:
        return updateCallComponent(current, workInProgress, renderExpirationTime);
      case ReturnComponent:
        // A return component is just a placeholder, we can just run through the
        // next one immediately.
        return null;
      case HostPortal:
        return updatePortalComponent(current, workInProgress, renderExpirationTime);
      case Fragment:
        return updateFragment(current, workInProgress);
      default:
        invariant_1(false, 'Unknown unit of work tag. This error is likely caused by a bug in React. Please file an issue.');
    }
  }

我们再调查一下workInProgress.tag是什么

https://github.com/facebook/r...

这里有全部fiber节点的类型描述,我们创建一个对象


// https://github.com/RubyLouvre/anu 欢迎加star

var mapBeginWork = {
    3: "HostRoot 根组件",
    0: "IndeterminateComponent 只知道type为函数",
    2: "ClassComponent 普通类组件" ,
    5: "HostComponent 元素节点",
    6: "HostText 文本节点"
  }
  function beginWork(current, workInProgress, renderExpirationTime) {
    if (workInProgress.expirationTime === NoWork || workInProgress.expirationTime > renderExpirationTime) {
      return bailoutOnLowPriority(current, workInProgress);
    }
    console.log(workInProgress.tag, mapBeginWork[workInProgress.tag])
     switch (workInProgress.tag) {
     //略
     }
}

clipboard.png

总结一下到现在的所有流程:

clipboard.png

好了,今天就这么多,想必大家都累了,下一篇继续

查看原文

赞 7 收藏 5 评论 1

LWXYFER 关注了用户 · 2018-03-30

司徒正美 @situzhengmei

穿梭于二次元与二进制间的魔法师( ̄(工) ̄) 凸ส้้้้้้้้้้้้้้้้้้้้้้้้้้้้้้้้้้้้้

关注 2128

LWXYFER 赞了文章 · 2018-03-07

ES2018 新特征之:正则表达式 dotAll 模式

ES2018 新特性


“正则表达式的 s (dotAll) flag” 提案 proposal-regexp-dotall-flag 由 Mathias Bynens 负责,目前已经进入 stage 4,并将成为 ES9(ES2018) 的一部分。

1. 概述

在 JavaScript 正则表达式中 . 用来匹配任何单个字符。但是有 2 个例外:

1.1 多字节 emoji 字符

let regex = /^.$/;
regex.test('?');   // false

我们可以通过设置 u 标志来解决,u 的含义是 unicode:

let regex = /^.$/u;
regex.test('?');   // true

1.2 行终结符(line terminator characters)

行终结符包含:

  • U+000A LINE FEED (LF) (\n) - 换行
  • U+000D CARRIAGE RETURN (CR) (\r) - 回车
  • U+2028 LINE SEPARATOR - 行分隔符
  • U+2029 PARAGRAPH SEPARATOR - 段分隔符

还有一些其它字符,也可以作为一行的开始:

  • U+000B VERTICAL TAB (\v)
  • U+000C FORM FEED (\f)
  • U+0085 NEXT LINE

目前 JavaScript 正则表达式的 . 可以匹配其中的一部分:

let regex = /./;

regex.test('\n');       // false
regex.test('\r');       // false
regex.test('\u{2028}'); // false
regex.test('\u{2029}'); // false

regex.test('\v');       // true
regex.test('\f');       // true
regex.test('\u{0085}'); // true

在正则表达式中,用于表示字符串开头和字符串结尾的元字符是 ^$, 因此一个变通的方式是使用 ^ 来匹配。

/foo.bar/.test('foo\nbar');     // false
/foo[^]bar/.test('foo\nbar');   // true

或者使用 \s 来匹配空白字符:

/foo.bar/.test('foo\nbar');     // false
/foo[\s]bar/.test('foo\nbar');   // true

2. 增加 s/dotAll 标志

在最新的 ECMAScript 规范中,为 JavaScript 的正则表达式增加了一个新的标志 s 用来表示 dotAll。以使 . 可以匹配任意字符。

/foo.bar/s.test('foo\nbar');    // true

High-level API

const re = /foo.bar/s;  //  等价于 const re = new RegExp('foo.bar', 's');
re.test('foo\nbar');    // true
re.dotAll;      // true
re.flags;       // "s"

3. 命名由来

既然是为了实现 dotAll 功能,为什么不命名为 d 或者 a。因为在其它语言的正则表达式实现中,已经使用 s 标志了:

  • Java 使用 Pattern.DOTALL
  • C# 和 VB 使用 RegexOptions.Singleline
  • Python 同时支持 re.DOTALLre.S

在支持正则表达式使用 flag 的语言如 Perl、PHP 也使用 s 作为标志。

s 的含义是 singleline 和 dotAll。

singleline(单行)对应的是 multiline(多行)。

m 标志用于指定多行输入字符串应该被视为多个行。如果使用 m 标志,^$ 匹配的开始或结束是字符串中的每一行,而不是整个字符串的开始或结束。

/^java/.test('just\njava\n');   // false
/^java/m.test('just\njava\n');  // true
  • m 标志只影响 ^$
  • s 标志只影响 .

目前在 JavaScript 正则表示中所有修饰符的含义:

  • g → global
  • i → ignoreCase
  • m → multiline
  • y → sticky
  • u → unicode
  • s → dotAll

4. 实现

查看原文

赞 9 收藏 19 评论 0

LWXYFER 关注了专栏 · 2018-01-24

javascript魔法师

涉及我的学习心得与经验

关注 1313

LWXYFER 发布了文章 · 2018-01-19

Weekly 00001011

Weekly 不发出来都没有动力更新, 都变成 Monthly 了???。 每个链接下都是自己的拙见,望各位大佬多多指教。博客链接

2017/12/24 - 2018/1/19

// 这个还是有意思的:
// An object can only be converted if it has a key valueOf and it's function returns any of the above types.
+{
  valueOf: function(){
    return '0xFF'
  }
}
//returns 255
  • ★ JS things I never knew existed

    • 这里作者讲的还都是挺有意思的点。
    • Label Statements
        loop1: // labeling "loop1" 
        for (let i = 0; i < 3; i++) { // "loop1"
            loop2: // labeling "loop2"
            for (let j = 0; j < 3; j++) { // "loop2"
                if (i === 1) {
                    continue loop1; // continues upper "loop1"
                    // break loop1; // breaks out of upper "loop1"
                }
            console.log(`i = ${i}, j = ${j}`);
            }
        }
        
    • Comma Operator
    y = false, true; // returns true in console
    console.log(y); // false (left-most)
    
    z = (false, true); // returns true in console
    console.log(z); // true (right-most)
    • Pipeline Operator: 管道操作符最近在 TC39 的提议中,不知是否会加入特性中,全看大佬们的啊。

      const square = (n) => n * n;
      const increment = (n) => n + 1;
      
      // without pipeline operator
      square(increment(square(2))); // 25
      
      // with pipeline operator
      2 |> square |> increment |> square; // 25
      
  • HTML5.2 New Dialog Element

    • 话说还是增加了些东西的,都还没有看。2333333
    • 原生模态层算是解决了一个大问题的。MDN 参考
    <dialog open>
        Native dialog box!
    </dialog>
查看原文

赞 0 收藏 1 评论 0

LWXYFER 关注了专栏 · 2018-01-09

边城客栈

全栈技术专栏

关注 3132

LWXYFER 回答了问题 · 2017-11-25

解决.gitignore文件书写规则,我想忽略所有的项目配置.idea文件夹,怎么写?

**/.idea

或者

.idea

gitignore 文档

关注 3 回答 2

LWXYFER 回答了问题 · 2017-11-22

如何关闭网页的缩放(快捷键, 多点触摸)功能?

<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no" />

关注 2 回答 1

LWXYFER 回答了问题 · 2017-11-17

浏览器的布局原理是怎样的?

基于 webkit 来说的话,布局(Layout)是再知道对一个元素应用哪些规则之后,浏览器即可开始计算它要占据的空间大小及其在屏幕的位置。说原理的话,不如说是 Webkit/Blink 的处理机制。

给你几个链接吧,自己看下吧:

关注 3 回答 1

LWXYFER 回答了问题 · 2017-11-06

解决chrome开发者工具 查看加载文件列表怎么隐藏来自chrome扩展的js?

ctrl + shift + n 进无痕模式。

关注 2 回答 1