Code Monkey home page Code Monkey logo

blog's People

Contributors

mbaxszy7 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

Forkers

jl917

blog's Issues

CSS格式化上下文

整理一下CSS格式化上下文

CSS格式化上下文主要有这么几种: BFC块级格式化上下文、IFC内联格式化上下文、GFC网格(grid)格式化上下文、FFC弹性(flex)格式上下文

GFC网格(grid)格式化上下文

创建规则:display:grid/inline-grid

FFC弹性(flex)格式上下文

创建规则: display:flex/inline-flex

下面主要整理一下BFC块级格式化上下文 和 IFC内联格式化上下文

BFC块级格式化上下文

  • 创建规则:
  1. 根元素html
  2. 浮动元素
  3. 绝对定位元素(position为absolute或fixed)
  4. display: inline-block,table-caption, table-cell
  5. overflow 不为visible
    注意: 是这些元素创建了块格式化上下文,它们本身可以不是块格式化上下文
  如果一个元素具有 BFC,内部子元素再怎么倒腾,都不会影响外部的元素。
  所以,
  1. BFC 元素是不可能发生 margin 重叠的,因为 margin 重叠是会影响外部的元素的;
  2. BFC 元素也可以用来清除浮动的影响,因为如果不清除,子元素浮动则父元素高度塌陷,必然会影 
      响后面元素布局和定位,这显然有违 BFC 元素的子元素不会影响外部元素的设定

所以BFC可以用来防止外边距合并,清除内部浮动

IFC内联格式化上下文

  • 创建规则: IFC 只有在一个块元素中仅包含内联级别元素(inline-level elements)时才会生成

  • 内联级元素(inline-level elements)在一行中一个挨一个地排列,一旦当前行放不下了,就在它下方创建一个新行,所有这些行都是所谓的行盒(line box),用来包住这一行的所有内容。

  • inline-level element (内联级元素)。内联级元素包括 display属性计算值为:

    1. inline 内联元素一般是用来包裹文本的元素,比如span、strong、em标签等。所有 display:inline 的非替换元素生成的盒是行内盒(Inline-level boxes
    2. inline-block 内联-块元素(内嵌的块元素)可以在一行中排列显示,以具有width,height(也有 可能是通过其内容确定的)和padding,border及margin。比如img、input标签等
  • 使用IFC,可以实现内联级元素的垂直和水平居中

    1. 使用一个块元素来包含一个内联元素,这样会生成一个IFC来规定如何渲染行内元素。按照IFC行内框的布局规则,其水平位置将由text-align属性来确定,所以设置text-align:center将把行内框居中
    2. 不同大小的的元素意味着不等高的行盒(line box)。在每一个line box中,我们都可以使用vertical-align来对齐line box之中的元素。
    3. line box 中vertical-align的运用:
      • vertical-align 用来指定行内元素(inline)或表格单元格(table-cell)元素的垂直对齐方式。
        vertical-align属性可被用于两种环境:
        1. 使行内元素盒模型与其行内元素容器垂直对齐。例如,用于垂直对齐一行文本的内的图片
        2. 垂直对齐表格单元内容
      • 如果有子元素超过了父元素的高度,那么父元素的高度就是被撑高的高度, 且始终保持最高元素 的对齐方式是正确的

CSS盒模型

记录一下CSS盒模型

盒模型

content-box:W3C 标准盒模型

  • width,height只包含内容content区域,不包含border和padding

border-box: IE 盒模型

  • width 和 height 包含content+padding+border

从计算上看,border-box更符合人的直觉,比如实际生活中的纸盒子.

项目中设置盒模型

如果由第三方库不兼容box-sizing: border-box,则可以如下设置

  : root { box-sizing: border-box; }
  *,
  ::before,
  ::after {
    box-sizing: inherit;
  }
// 这样的话不会破坏第三方组件可能改变的box-sizing
// 可以在必要时选中第三方组件的顶级容器,将其恢复为 content-box
.third-party-component { box-sizing: content-box; }

CSS in Depth

react 的两个主要阶段:render阶段和commit阶段

先来看一下这张图
image
图片来自https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/

图中介绍了React 16.4以后的生命周期函数的执行时机,牵扯到了React 的两个主要阶段:render阶段和commit阶段。下面来简单说一下这两个阶段React做了什么事。

render阶段

在render阶段,React将更新应用于通过setState或render方法触发的组件,并确定需要在用户屏幕上做哪些更新--哪些节点需要插入,更新或删除,哪些组件需要调用其生命周期方法。最终的这些更新信息被保存在一个叫effect list的fiber 节点树上(关于fiber的内容,在这篇文章中有简述react中的fiber)。当然,在首次渲染时,React不需要产生任何更新信息,而是会给每个从render方法返回的element生成一个fiber节点,最终生成一个fiber节点树, 后续的更新也是复用了这棵fiber树。

在上图中, render阶段被标记为纯的、没有副作用的,可能会被React暂停、终止或者重新执行。也就是说,React会根据产生的任务的优先级,安排任务的调度(schedule)。利用类似requestIdleCallback的原理在浏览器空闲阶段进行更新计算,而不会阻塞动画,事件等的执行。

commit阶段

在这个阶段时,React内部会有三个fiber树:

current fiber tree: 在首次渲染时,React不需要产生任何更新信息,而是会给每个从render方法返回的element生成一个fiber节点,最终生成一个fiber节点树, 后续的更新也是复用了这棵fiber树。

workInProgress fiber tree: 
所有的更新计算工作都在workInProgress tree的fiber上执行。当React 遍历current fiber tree时,它为每个current fiber 创建一个替代(alternate)节点,这样的alternate节点构成了workInProgress tree

effect list fiber tree: workInProgress fiber tree 的子树,这个树的作用串联了标记具有更新的节点

commit阶段会遍历effect list,把所有更新都commit到DOM树上。具体的,首先会有一个pre-commit阶段,主要是执行getSnapshotBeforeUpdate方法,可以获取当前DOM的快照(snap)。然后给需要卸载的组件执行componentWillUnmount方法。接着会把current fiber tree 替换为workInProgress fiber tree。最后执行DOM的插入、更新和删除,给更新的组件执行componentDidUpdate,给插入的组件执行componentDidMount。

重点要注意的是,这一阶段是同步执行的,不能中止。

webpack 的 scope hoisting

下面比较简单的来总结一下webpack 的 scope hoisting

什么是scope hoisting

scope hoisting 翻译过来就是作用域提升。在webpack中,这个特性被用来检测引用链(import chaining)是否可以被内联,从而减少没有必要的module。

webpack中的scope hoisting

要了解webpack中的scope hoisting,首选需要知道webpack打包出来的代码。这一部分已经在我的前面一篇webpack是如何实现动态导入的中有详细的讲述,下面来简单提一下。webpack打包后的代码框架:

(function(modules) {
  . . .
  // cache
  var installedModules = {};

  function __webpack_require__(moduleId) {
     // check cache
    if (installedModules[id]) {
      return installedModules[id].exports;
    }

     // create new module and cache it
     var module = installedModules[id] = {
       id: id,
       exports: {}
     };

     // execute the module function
     modules[id].call(module.exports, module, module.exports, __webpack_require__);
  }

  // load entry module and return exports
   return __webpack_require__(0);
})({
  "hello.js": function() {},
  "app.js": function() {},
  0:  function() {}
});

可以看到匿名函数的参数modules对象是一个个我们项目中的模块。如果模块很多那modules将会很大,毫无疑问会有大量的函数声明和内存开销。所以webpack通过scope hoisting来检测模块间的引用链(import chaining),从而来展平引用链,并把他们内联到一个函数中,达到“压缩”modules的效果。

怎么开启scope hoisting

在webpack中开启ModuleConcatenationPlugin插件可以开启scope hoisting。此插件只在 production mode生产环境中默认开启。
new webpack.optimize.ModuleConcatenationPlugin();

一些不会产生scope hoisting的情况

webpack attempts to achieve partial scope hoisting. It will merge modules into a single scope but cannot do so in every case. If webpack cannot merge a module, the two alternatives are Prevent and Root. Prevent means the module must be in its own scope. Root means a new module group will be created. The following conditions determine the outcome

大致翻译如下: webpack尝试完成部分scope hoisting。也就是说webpack不会在每种情况下都把modules合并到同一个作用域。如果合并失败会有以下两种情况替代: Prevent and Root。

- Prevent: 模块必须待在自己的作用域中。比如非es6模块、使用eval()、export * from "cjs-module"
- Root:一个新的模块会被开启。比如动态import

具体情况如下,可以在官方文档中查阅。

react中的fiber 和 React Fiber 架构

记录一下自己对react fiber 的理解

React element

React 的JXS语法最终会被编译为ReactReact.createElement:

class Demo extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }
  render() {
    return <span>{this.state.count}</span>;
  }
}

// 编译后
class Demo {
...
  render() {
    return React.createElement("span", null, this.state.count);
  }
}

在render方法中的React.createElement 会把span转变为如下的数据结构:

{
  $$typeof: Symbol(react.element),
  type: 'span',
  key:null
  props: {
      children: 0
  }
}

React 用$$typeof标识了一个 React element, 而每个这样的element对应了一个fiber。

什么是fiber

  • 一个fiber就是一个对象结构,包含了一系列要完成的任务。
  • react 的每一个element都对应了一个fiber(一棵elements树就对应了一棵fiber节点树)。
  • 一个fiber不会在每次render中重新创建,相反,它是一个可以被操作改变的数据结构,保留了组件的状态和dom。所以操作在每个fiber上任务(更新,删除等)都可以映射到对应的element。

为什么要用fiber

上面提到用fiber就是要完成一系列的任务,这些任务具体可以概括为:

  • 暂停任务,并且可以稍后继续
  • 为不同的任务标记优先级
  • 重用之前完成的任务
  • 终止不再需要的任务

这一些列的工作运用在一整棵fiber树(Fiber上下文)上也最终体现了react 的Fiber架构。让React有了优先级调度(schedule)的能力。也让React能把reconciliation(计算哪一部分的element 树需要被更新,计算更新的这一步也被分为很多unit,防止阻塞主线程)和render(使用那些计算好的更新信息,把更新渲染到用户屏幕上)分开,使得reconciliation可以重用在不同的平台上(React Native 、React DOM)

一个fiber的结构

几个重要属性如下:

 {
    type: React.createElement 对应的type,表明这个fiber 节点对应的element
    tag: 表明fiber 的类型
    pendingProps: 已经是被更新的props,需要被运用到子组件或者dom 元素上
    key: 对应prop 上的key
    stateNode:  dom节点(HostComponent) / 类组件的实例 (ClassComponent) / fn() (FunctionComponent) 
    nextEffect:  指向下一个**effect list**中的节点 (effect list:一个workInProgress(finishedWork)的子树,是在render阶段 最终需要决定被执行更新 的产物,会在commit阶段被处理)
    effectTag:  当前fiber需要执行的副作用类型
    alternate:  用于构成**workInProgress**(从当前fiber树构建而来,反应了需要被更新渲染到用户屏幕的状态树)
    return: 指向父fiber节点
    sibling: 指向兄弟fiber节点
    child: 指向child fiber节点
  }

推荐一篇很棒的介绍React Fiber的文章 完全理解React Fiber

React Fiber Architecture
React源码揭秘1 架构设计与首屏渲染

CSS 相对长度

整理一下 CSS 相对长度

em

  • 1em 等于当前元素的字号,其准确值取决于作用的元素
  • 当设置 padding、height、width、border-radius 等属性时,使用 em 会很方便。这是 因为当元素继承了不同的字号,或者用户改变了字体设置时,这些属性会跟着元素均匀地缩放
  • 使用 em 定义字号时, font-size 是根据继承的字号(父元素)来计算的
<style>
  .par {
    font-size: 16px;
  }
  .chi {
    font-size: 2em;
    padding: 2em;
  }
</style>

<div class="par">
  cdcdcdcdcdc
  <div class="chi">
    dcdcdc
  </div>
</div>

div.chi computed:
WeChat2b572ea4e182a3806e2e75e2f4766852

rem

  • rem 是 root em 的缩写。rem 不是相对于当前元素,而是相对于根元素的单位。不管在文档的什么位置使用 rem,1.2rem 都会有相同的计算值:1.2 乘以根元素的字号
  • 拿不准的时候,用 rem 设置字号,用 px 设置边框,用 em 设置其他大部分属性

视口的相对单位

  • vh:视口高度的 1/100。(ios safari 100vh bug, 用js 的 window.innerHeight 解决
  • vw:视口宽度的 1/100。
  • vmin:视口宽、高中较小的一方的 1/100。
  • vmax:视口宽、高中较大的一方的 1/100。
  • 如何使用 vw 定义字号:

如果给一个元素加上 font-size: 2vw 会发生什么?
在一个 1200px 的桌面显示器上,计算值为 24px(1200 的 2%)。在一个 768px 宽的平板上,计算值约为 15px(768 的 2%)。这样做的好处在于元素能够在这两种大小之间平滑地过渡,这意味着不会在某个断点突然改变。当视口大小改变时,元素会逐渐过渡 。
不幸的是,24px 在大屏上来说太大了。更糟糕的是,在 iPhone 6 上会缩小到只有 7.5px
0.5em 保证了最小字号,1vw 则确保 了字体会随着视口缩放。这段代码保证基础字号从 iPhone 6 里的 11.75px 一直过渡到 1200px 的浏 览器窗口里的 20px。可以按照自己的喜好调整这个值:
:root { font-size: calc(0.5em + 1vw); }

https://www.manning.com/books/css-in-depth

CSS 层叠值的计算和CSS的继承

整理一下CSS层叠值的计算和CSS的继承

层叠值的计算

层叠值: 浏览器遵循三个步骤,即来源、优先级、源码顺序,来解析网页上每个元素的每个属性

!!!不推荐使用!important,因为!important没有层叠值可言。

当在一个样式声明中使用一个 !important 规则时,此声明将覆盖任何其他声明

  1. 来源: 样式是从哪里来的,包括你书写的样式和用户代理默认样式等
  2. 优先级
 [0,0,0,0] -> [行内,id, class, 标签]
根据以上对应关系统计每个选择器的个数作为指数,然后再给一个很大的基数,结果值就是相加值。栗子如下(基数取1000):

div#a.b .c[id=x]
属性选择器[id=x] 跟 class选择器 .b .c 权重一致
[0, 1, 3, 1]
1000^0 + 1000^1 + 1000^3 + 1000^1

#a:not(#b)
:not 不参与权重计算
[0, 2, 0, 0]
1000^0 + 1000^2 + 1000^0 + 1000^0

*.a
通用选择器*  不参与权重计算
[0, 0, 1, 0]
1000^0 + 1000^0 + 1000^1 + 1000^0

div.a
[0, 0, 1, 1]
1000^0 + 1000^0 + 1000^1 + 1000^1

伪类选择器(如 :hover )和属性选择器(如 [type="input"] )与一个类选择 器的优先级相同。
通用选择器( * )和组合器( > 、 + 、 ~ )对优先级没有影响(不参与权重计算)。

通常最好让优先级尽可能低,这样当需要覆盖一些样式时,才能有选择空间

  1. 源码顺序: 样式在样式表里的声明顺序
    如果两个声明的来源和优先级相同,其中一个声明在样式表中出现较晚,或者位于页面较晚引入的样式表中,则该声明胜出。

CSS的继承(继承属性 (inherited property))

当没有给元素的继承属性指定一个值的时候,该属性会取父元素的同属性的计算值 computed value。
但不是所有的属性都能被继承。默认情况下,只有特定的一些属性能被继承,通常是我们希望被继承的那些。它们主要是跟文本相关的属性:color、font、font-family、font-size、 font-weight、font-variant、font-style、line-height、letter-spacing、text-align、 text-indent、text-transform、white-space 以及 word-spacing。

编写一个类似webpack的bundler

看了webpack打包出来后的代码,觉得很精妙,想尝试写一个极其简易版的js bundler
项目地址:https://github.com/mbaxszy7/make-bundler
运行: node ./bundler.js

项目的模块

入口模块: index.js
index模块依赖的模块: hello.js ,console.js
hello模块依赖的模块:world.js

bundler 实现的要点

  1. 需要有一个模块的分析器,能分析模块的其他模块依赖,产生依赖图谱
  2. 转化import,类似webpack中打包后的_webpack_require_
  3. 产出bundle.js作为打包结果,可以在浏览器上直接运行

模块分析器的实现

模块分析器主要是来转换import语句,分析模块的import chanining和收集这些import依赖。从源码层面来分析代码结构就需要用到抽象语法树ast, 这里使用了@babel/parser。然后需要遍历分析ast的节点,来收集import依赖,我们需要使用@babel/traverse这个库。收集到的import依赖的文件路径是相对于entry 文件的路径,需要处理一下变为相对于根目录的文件路径。最后,需要把ast转换为实际的代码。具体代码如下:

  // 相对于entry 文件的路径 -> 相对于根目录的路径
const makeSrcPath = (fileName, moduleSrc) => {
  const dirName = path.join(path.dirname(fileName), moduleSrc);
  return `./${dirName}`;
};

const moduleAnalysis = (fileName) => {
  const content = fs.readFileSync(fileName, "utf-8");
  const ast = parser.parse(content, {
    // parse in strict mode and allow module declarations
    sourceType: "module",
  });
  const dependencies = {};
  traverse(ast, {
    ImportDeclaration({ node }) {
      const moduleSrc = node.source.value;
      const path = makeSrcPath(fileName, moduleSrc);
      dependencies[moduleSrc] = path;
    },
  });
  const { code } = babel.transformFromAst(ast, null);
  return {
    fileName,
    dependencies,
    code,
  };
};

产出的dependencies结构如下:

// {
//   相对于entry 文件的路径:  相对于根目录的路径
// }

dependencies =  {
  './hello.js': './src/hello.js',
  './console.js': './src/console.js'
}

这样的结构便于我们下一步遍历每个模块的dependencies,产出整个dependencies。

产出dependencies

为了收集全部模块的dependencies,需要遍历每个模块的import依赖。分析这个项目的模块依赖,我们可以把它简化为一个多叉树。那么遍历依赖就变成了广度优先的遍历方式,其中为了简单处理循环引用,在dependencies result中已经存在的模块就不再次遍历。代码如下:

 const generateDependenciesGraph = (entry) => {
  const entryModule = moduleAnalysis(entry);
  console.log(entryModule);
  // 构造队列,处理广度优先遍历
  const queue = [entryModule];
  const ret = {
    [entry]: {
      dependencies: entryModule.dependencies,
      code: entryModule.code,
    },
  };

  while (queue.length) {
    const item = queue.shift();

    const { dependencies } = item;
    if (dependencies) {
      for (const [k, v] of Object.entries(dependencies)) {
       // 如果不在dependencies result中
        if (!ret[v]) {
          const res = moduleAnalysis(v);
          // 把依赖推入queue
          queue.push(res);
          const { dependencies, code } = res;
          ret[v] = {
            dependencies,
            code,
          };
        }
      }
    }
  }
  return ret;
};

最后返回的ret:

{
  "./src/index.js": {
    dependencies: {
      "./hello.js": "./src/hello.js",
      "./console.js": "./src/console.js",
    },
    code:
      '"use strict";\n\nvar _hello = _interopRequireDefault(require("./hello.js"));\n\nvar _console = require("./console.js");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\ndocument.getElementById("root").innerText = _hello.default;\n(0, _console.log)(_hello.default);',
  },
  "./src/hello.js": {
    dependencies: { "./world.js": "./src/world.js" },
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.default = void 0;\n\nvar _world = _interopRequireDefault(require("./world.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nconst hello = `Hello. ${_world.default}`;\nvar _default = hello;\nexports.default = _default;',
  },
  "./src/console.js": {
    dependencies: {},
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.log = void 0;\n\nconst log = (...props) => console.log(...props);\n\nexports.log = log;',
  },
  "./src/world.js": {
    dependencies: {},
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.default = void 0;\nconst world = "A brave new world";\nvar _default = world;\nexports.default = _default;',
  },
}

生成可运行的代码

在上一阶段产出的dependencies中每个dependency的code就是对应模块的源码(index.js 为例):

var _hello = _interopRequireDefault(require("./hello.js"));
var _console = require("./console.js");
function _interopRequireDefault(obj) {
  return obj && obj.__esModule ? obj : { default: obj };
}
document.getElementById("root").innerText = _hello.default;
(0, _console.log)(_hello.default);

但是这段代码在浏览器上运行不来,因为我们没有实现代码里的require函数,下面就来分析实现这段代码里的require函数。

代码里用require函数是这么运行的:require("./console.js"),所以要用./console.js得到./src/hello.js, 也就是我们之前构造好的
dependencies。然后为了传入require函数运行模块代码和防止模块变量污染外部的变量,我们需要把代码放在一个闭包中运行。最后,生成可运行的代码的函数返回的也是一个立即执行函数,接受的modules参数就是上一步的ret:

const generateCode = (entry) => {
  const modules = JSON.stringify(generateDependenciesGraph(entry));
  return `
    (
      function(modules) {
        function _bundler_require_(module) {
          function _relative_require_(relativePath) {
            return _bundler_require_(modules[module].dependencies[relativePath])
          }

          var exports = {};
          (
            function(require, exports, code) {
              eval(code)
            }
          )(_relative_require_, exports, modules[module].code)

          return exports
        }

        _bundler_require_('${entry}')
      }
    )(${modules})
  `;
};

这个立即执行函数参考的就是webpack打包出来的那个立即执行函数。

输出dist目录

代码如下:

 fs.writeFile("./dist/bundle.js", generateCode("./src/index.js"), (error) => {
  console.error(error);
});

后续ToDo

  • 实现动态import
  • 实现babel的polyfill

React 的 setState

记录下这么一个问题:React 中 setState 什么时候是同步的,什么时候是异步的?

这里的异步并不是异步执行,而是React会把多个setState合并更新

现象

直接上代码

class Test extends Component<
  Record<string, unknown>,
  { [propName: string]: number }
> {
  constructor(props: Record<string, unknown>) {
    super(props)
    this.state = {
      num: 1,
      number: 1,
      batchNumber: 1,
      clickNum: 1,
      callbackNum: 1
    }
  }

  componentDidMount(): void {
     // componentDidMount中正常的多次setState
    this.setState({
      num: this.state.num + 1
    })
    console.log(`num: ${this.state.num}`)
    this.setState({
      num: this.state.num + 2
    })
    console.log(`num: ${this.state.num}`)
    this.setState({
      num: this.state.num + 3
    })
    console.log(`num: ${this.state.num}`)

      // componentDidMount中在setTimeout内多次setState
    setTimeout(() => {
      this.setState({
        number: this.state.number + 1
      })
      console.log(`number: ${this.state.number}`)
      this.setState({
        number: this.state.number + 2
      })
      console.log(`number: ${this.state.number}`)
      this.setState({
        number: this.state.number + 3
      })
      console.log(`number: ${this.state.number}`)
    }, 0)

     // componentDidMount中在unstable_batchedUpdates内多次setState
    batchedUpdates(() => {
      this.setState({
        batchNumber: this.state.batchNumber + 1
      })
      console.log(`batchNumber: ${this.state.batchNumber}`)

      this.setState({
        batchNumber: this.state.batchNumber + 2
      })
      console.log(`batchNumber: ${this.state.batchNumber}`)
      this.setState({
        batchNumber: this.state.batchNumber + 3
      })
      console.log(`batchNumber: ${this.state.batchNumber}`)
    })

    // componentDidMount中在使用setState传入函数的多次setState
    this.setState((state) => ({
      callbackNum: state.callbackNum + 1
    }))
    console.log(`callbackNum: ${this.state.callbackNum}`)
    this.setState((state) => ({
      callbackNum: state.callbackNum + 2
    }))
    console.log(`callbackNum: ${this.state.callbackNum}`)
    this.setState((state) => ({
      callbackNum: state.callbackNum + 3
    }))
    console.log(`callbackNum: ${this.state.callbackNum}`)
  }

   // 在onClick 中多次setState
  handleTestClick: () => void = () => {
    this.setState((preState) => ({
      clickNum: preState.clickNum + 1
    }))
    console.log(`clickNum: ${this.state.clickNum}`)
    this.setState((preState) => ({
      clickNum: preState.clickNum + 2
    }))
    console.log(`clickNum: ${this.state.clickNum}`)
    this.setState((preState) => ({
      clickNum: preState.clickNum + 3
    }))
    console.log(`clickNum: ${this.state.clickNum}`)
  }

  render(): React.ReactElement {
    return (
      <>
        <h1 onClick={this.handleTestClick}> Test </h1>
        <p>clickNum {this.state.clickNum}</p>
        <p>callbackNum {this.state.callbackNum}</p>
        <p>batchNumber {this.state.batchNumber}</p>
        <p>number {this.state.number}</p>
        <p>num {this.state.num}</p>
      </>
    )
  }
}

上面代码注释有五种setState。下面来看一下log:
WeChatfc78152e23149c48c913843bd9746350
WeChatef96856db49289dc34c7d19a934e0a3e
五种setState在每次调用this.setState后的console.log中,只有setTimeout中的this.setState是每次都可以拿到最新的state的,其余都是原始的state值。

再来看一下屏幕上render的结果:
WeChat7102af3ef0d3773d8991654740c151ae
只有在setState中使用function 和 在setTimeout中的setState 才会根据上一次的state来产生state,其余都是用了原始值作为base state

原因

出现上面现象的原因主要有:

  1. 在setTimeout中setState不是批量更新的(所谓批量更新就是React通过一个queue来实现 state 更新,当执行 setState() 时,会将需要更新的 state 浅合并后放入 queue,而不会立即更新 state,队列机制可以高效的批量更新 state)
  2. 函数式 setState() 可以在传入的函数中拿到上一步的state

什么是React batched update 机制

在之前版本的React中会有一个isBatchingUpdates变量,当isBatchingUpdates 为true的时候会产生批量更新的效果(放到队列中),当isBatchingUpdates为false的时候会直接产生更新。比如在之前版本的React的unstable_batchedUpdates实现

function batchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
  const previousIsBatchingUpdates = isBatchingUpdates;
  // isBatchingUpdates 设置为true
  isBatchingUpdates = true;
  try {
    return fn(a);
  } finally {
    // 复原isBatchingUpdates 
    isBatchingUpdates = previousIsBatchingUpdates;
    if (!isBatchingUpdates && !isRendering) {
      performSyncWork();
    }
  }
}

在当前最新版本的React(16.13.1)中unstable_batchedUpdates的实现:

function batchedUpdates<A, R>(fn: A => R, a: A): R {
  const prevExecutionContext = executionContext;
  executionContext |= BatchedContext;
  try {
    return fn(a);
  } finally {
    executionContext = prevExecutionContext;
    if (executionContext === NoContext) {
      // Flush the immediate callbacks that were scheduled during this batch
      flushSyncCallbackQueue();
    }
  }
}

可以看到React已经去掉了isBatchingUpdates,换成了executionContext这个枚举值

然后再看当在setTimeout中setState的时候executionContext会变成0 (枚举值NoContext):

// scheduleUpdateOnFiber也就是scheduleWork (在enqueueSetState中调用)
function scheduleUpdateOnFiber(
  fiber: Fiber,
  lane: Lane,
  eventTime: number,
) {
  checkForNestedUpdates();
  warnAboutRenderPhaseUpdatesInDEV(fiber);

  const root = markUpdateLaneFromFiberToRoot(fiber, lane);
  if (root === null) {
    warnAboutUpdateOnUnmountedFiberInDEV(fiber);
    return null;
  }

  // TODO: requestUpdateLanePriority also reads the priority. Pass the
  // priority as an argument to that function and this one.
  const priorityLevel = getCurrentPriorityLevel();

  if (lane === SyncLane) {
    if (
      // 是否在unbatched update中
      (executionContext & LegacyUnbatchedContext) !== NoContext &&
      (executionContext & (RenderContext | CommitContext)) === NoContext
    ) {
      schedulePendingInteractions(root, lane);
      performSyncWorkOnRoot(root);
    } else {
      ensureRootIsScheduled(root, eventTime);
      schedulePendingInteractions(root, lane);
      // setTimeout命中,触发flushSyncCallbackQueue
      if (executionContext === NoContext) {
        flushSyncCallbackQueue();
      }
    }
  } else {
    if (
      (executionContext & DiscreteEventContext) !== NoContext &&
      (priorityLevel === UserBlockingSchedulerPriority ||
        priorityLevel === ImmediateSchedulerPriority)
    ) {
      if (rootsWithPendingDiscreteUpdates === null) {
        rootsWithPendingDiscreteUpdates = new Set([root]);
      } else {
        rootsWithPendingDiscreteUpdates.add(root);
      }
    }
    ensureRootIsScheduled(root, eventTime);
    schedulePendingInteractions(root, lane);
  }
  mostRecentlyUpdatedRoot = root;
}

总结setState合并执行

  • 在当前最新的React版本(16.13.1)中,如下情况会有setState batched的情况:
  1. React 组件的合成事件回调
  2. ReactDOM.unstable_batchedUpdates
  3. componentDidMount 和 useEffect
  • 没有setState batched的情况:
  1. 异步函数,如setTimeout, setInterval,async/await等
  2. addEventListener 内
  3. 异步回调

CSS渲染

CSS In Depth

布局

  • 在布局阶段中,浏览器需要计算每个元素将在屏幕上占多大空间。因为文档流的工作方式,所以一个元素的大小和位置可以影响页面上无数其他元素的大小和位置。这个阶段会解决这个问题。
  • 任何时候改变一个元素的宽度或高度,或者调整位置属性(比如 top 或者 left),元素的布局都会重新计算。如果使用 JavaScript 在 DOM 中插入或者移除元素,也会重新计算布局。一旦布局发生改变,浏览器就必须重排(reflow)页面,重新计算所有其他被移动或者缩放的元素的布局

绘制

  • 这个过程就是填充像素:描绘文本,着色图片、边框和阴影。这不会真正显示在屏幕上,而是在内存中绘制。页面各部分生成了很多的图层(layers)。
  • 如果改变某个元素的背景颜色,就必须重新绘制它。但因为更改背景颜色不会影响到页面上 任何元素的位置和大小,所以这种变化不需要重新计算布局。改变背景颜色比改变元素大小需要 的计算操作要少。
  • 某些条件下,页面元素会被提取到自己的图层。这时候,它会从页面的其他图层中独立出来单独绘制。浏览器把这个图层发送到计算机的图形处理器(graphics processing unit,GPU)进行绘制,而不是像主图层那样使用主 CPU 绘制。这样安排是有好处的,因为 GPU 经过了充分的优化,比较适合做这类计算。
  • 这就是我们经常提到的硬件加速(hardware acceleration),因为需要依赖于计算机上的某些 硬件来推进渲染速度。多个图层就意味着需要消耗更多的内存,但好处是可以加快渲染。

合成

  • 在合成(composite)阶段,浏览器收集所有绘制完成的图层,并把它们提取为最终显示在屏幕上的图像。合成过程需要按照特定顺序进行,以确保图层出现重叠时,正确的图层显示在其他 图层之上
  • opacity 和 transform 这两个属性如果发生改变,需要的渲染时间就会非常少。当我们修 改元素的这两个属性之一时,浏览器就会把元素提升到其自己的绘制图层并使用 GPU 加速。因 为元素存在于自己的图层,所以整个图像变化过程中主图层将不会发生变化,也无须重复的重绘
  • 如果只是对页面做一次性修改,那么通常不会感觉出这种优化可以带来明显的差异。但如果 修改的是动画的一部分,屏幕需要在一秒内发生多达几十次的更新,这种情况下渲染速度就很重 要了。大部分的屏幕每秒钟会刷新 60 次。理想情况下,动画中每次变化所需的重新计算也要至 少这么快,才能在屏幕上生成最流畅的运动轨迹。浏览器在每次重新计算的时候需要做的事情越多,越难达到这种速度

使用 will-change 控制绘制图层
will-change 的属性对渲染图层添加控制。这个属性可以提前告知浏览器,元素的特定属性将改变。这通常意味着元素将被提升到自己的绘制图层。例如,设置了 will-change: transform 就表示我们将要改变元素的 transform属性。
除非遇到性能问题,否则不要盲目添加该属性到页面,因为它会占用很多的系统资源

只有 tranfrorm 3D 变换会提升元素到自己的图层,现在已经不是这样了,最新的浏览器对 2D 变换也可以使用 GPU 加速

CSS 选择器

CSS 选择器整理

基础选择器

  1. 标签选择器
  2. 类选择器
  3. ID 选择器
  4. 通用选择器

组合器

  1. 子组合器(>)——匹配的目标元素是其他元素的直接后代。例如:.parent > .child
  2. 相邻兄弟组合器(+)——匹配的目标元素紧跟在其他元素后面。例如:p + h2
  3. 通用兄弟组合器(~)——匹配所有跟随在指定元素之后的兄弟元素。注意,它不会选中目标元素之前的兄弟元素。例如:li.active ~ li

复合选择器

多个基础选择器可以连起来(不使用空格或者其他组合器)组成一个复合(compound)选择器(例如:h1.page-header)。 复合选择器选中的元素将匹配其全部基础选择器。 例如,.dropdown.is-active 能够选中<div class="dropdown is-active">,但是无法选中<div class="dropdown">

伪类选择器

比如:

:first-child
:last-child
:disabled——匹配已禁用的元素,包括 input、select 以及 button 元素

伪元素选择器

比如:

::first-line
::first-letter
::before
::after

属性选择器

  1. [attr]—— 匹 配 的 元 素 拥 有 指 定 属 性 attr ,无论属性值是 什 么 , 例 如 input[disabled]
  2. [attr="value"]——匹配的元素拥有指定属性 attr,且属性值等于指定的字符串值。例如:input[type="radio"]
  3. [attr^="value"]——“开头”属性选择器。该选择器匹配的元素拥有指定属性 attr,且属性值的开头是指定的字符串值,例如:a[href^="https"]
  4. [attr$="value"]——“结尾”属性选择器。该选择器匹配的元素拥有指定属性 attr,且属性值的结尾是指定的字符串值,例如:a[href$= ".pdf"]
  5. [attr*="value"]——“包含”属性选择器。该选择器匹配的元素拥有指定属性 attr,且属性值包含指定的字符串值,例如:[class*="sprite-"]
  6. [attr~="value"]——“空格分隔的列表”属性选择器。该选择器匹配的元素拥有指定属性 attr,且属性值是一个空格分隔的值列表,列表中的某个值等于指定的字符串值, 例如:a[rel="author"]
  7. [attr|="value"]——匹配的元素拥有指定属性 attr,且属性值要么等于指定的字符 串值,要么以该字符串开头且紧跟着一个连字符(-)。

CSS in Depth

mobx依赖收集和依赖更新原理浅析

先看下面的代码:

class Demo {
  @observable
  public test = 1

  log: () => void = autorun(() => {
    console.log(`test input onChange: ${this.test}`)
  })
}

修改test的值,会触发log函数自动执行。相当于传入autorun的方法,会自动收集依赖到的 observable值的变化。个人猜测autorun函数的工作方式是这样的

function autorun (fn) { 
   // 依赖收集的准备工作
   fn() // 触发observable属性的get方法
   // 清理依赖收集
}

下面来简单看一下autorun源码 (5.15.4)

function autorun(
    view: (r: IReactionPublic) => any,
    opts: IAutorunOptions = EMPTY_OBJECT
): IReactionDisposer {
 
    const name: string = (opts && opts.name) || (view as any).name || "Autorun@" + getNextId()
    const runSync = !opts.scheduler && !opts.delay
    let reaction: Reaction
    
    // 只看同步的autorun,异步是根据传入的delay setTimeout
    if (runSync) {
        // normal autorun
        reaction = new Reaction(
            name,
            // reaction的onInvalidate, 用track调用reactionRunner, 也就是view(reaction), (重新)收集依赖
            function(this: Reaction) {
                this.track(reactionRunner)
            },
            opts.onError,
            opts.requiresObservable
        )
    } else {
       // ... 处理异步
    }

    function reactionRunner() {
        view(reaction)
    }
    // 将 reaction 放进全局 globalState.pendingReactions 队列,里面会执行runReactions
    reaction.schedule()
    // 返回取消订阅
    return reaction.getDisposer()
}

再来看看runReactions,runReactions是依赖收集启动方法

let reactionScheduler: (fn: () => void) => void = f => f();

function runReactions() {
  // 不在事务中并且没有正在执行的reaction
  if (globalState.inBatch > 0 || globalState.isRunningReactions) return
  // 核心的调用runReactionsHelper
  reactionScheduler(runReactionsHelper)
}

runReactionsHelper:

function runReactionsHelper() {
    globalState.isRunningReactions = true
    const allReactions = globalState.pendingReactions
    let iterations = 0

    // 遍历所有globalState.pendingReactions中的reaction,并执行每个对象的runReaction
    while (allReactions.length > 0) {
        if (++iterations === MAX_REACTION_ITERATIONS) {
            console.error(
                `Reaction doesn't converge to a stable state after ${MAX_REACTION_ITERATIONS} iterations.` +
                    ` Probably there is a cycle in the reactive function: ${allReactions[0]}`
            )
            allReactions.splice(0) // clear reactions
        }
        let remainingReactions = allReactions.splice(0)
        for (let i = 0, l = remainingReactions.length; i < l; i++)
            remainingReactions[i].runReaction()
    }
    globalState.isRunningReactions = false
}

runReaction关键就是触发onInvalidate参数函数, 也就是用track包裹的view函数(autorun的传入函数)

    // fn 就是view 函数
    track(fn: () => void) {
   
        startBatch()
        ....
        this._isRunning = true
        // trackDerivedFunction是核心, 把fn传入了trackDerivedFunction,
        const result = trackDerivedFunction(this, fn, undefined)
        this._isRunning = false
        ....
        endBatch()
    }

终于到trackDerivedFunction了,trackDerivedFunction就是最终调用autorun的传入函数的方法。至此就完成了触发observable属性的get方法。后面就是监听observable属性的get方法的调用, 最终完成依赖收集。例如在mobx ObservableValue类中有一个get方法,这个方法就是trap了observable属性的get方法:

  public get(): T {
        // reportObserved将 observable 上报给正在收集依赖的 derivation (reaction)
        //  derivation 从globalState.trackingDerivation中获取,globalState.trackingDerivation在上面提到的最终触发autorun的传入 
       //    函数 的 trackDerivedFunction中设置的
        this.reportObserved()
        return this.dehanceValue(this.value)
    }

至此完成了依赖收集。
下面来简单的说一下obserable属性的set方法,触发set方法,如果值改变,mobx会通知此obserable属性的依赖:

    public set(newValue: T) {
        const oldValue = this.value
        newValue = this.prepareNewValue(newValue) as any
        if (newValue !== globalState.UNCHANGED) {
             ...
            //值改变, 触发setNewValue, 最终会触发Reaction 中的onBecomeStale, 而onBecomeStale调用的就是 this.schedule(),这个就合前面的依赖收集重合了
            this.setNewValue(newValue)
            if (notifySpy && process.env.NODE_ENV !== "production") spyReportEnd()
        }
    }

最后,我们也可以看到mobx抽离Reaction这一层,设计的很巧妙,不仅抽离了依赖收集的逻辑,管理全局的依赖管理,也抽离了不同依赖管理阶段side effect,在初始化依赖收集的时候可以设置track get 方法,在依赖更新的时候也可以将Reaction用作他处,比如React,进行组件的更新。

分析 webpack 动态import的实现

要搞懂webpack 动态import的实现需要先搞懂webpack打包后产生的代码

webpack输出代码分析

  1. 代码框架分析
  • webpack打包后的代码其实是一个自执行函数,该函数的modules参数就是一个对象,该对象所有的key是文件的路径,对应的value是该文件转换后的代码。以index.js为例,对应的代码片段是:

carbon (2)

从以上代码可以看出,webpack替换了import 和 index中依赖的模块Hello。

  • 再看自执行函数的匿名函数部分

carbon

简化以后的代码如下:

carbon (1)

所以这个自执行函数会运行__webpack_require__(0)(第0项内容就是去加载 src/index.js 中的代码)

  1. 具体函数分析
  • webpack_require
    carbon (2)

__webpack_require__接受一个moduleId,就是modules参数的key。__webpack_require__内执行的模块的代码就是key对应的value。

  • 在此项目中, 当运行到index模块的代码时,会去加载hello模块的代码:webpack_require(/*! ./hello */ “./src/hello.js”)。hello.js打包后的代码如下:

carbon (3)

这里多了执行__webpack_require__.r 和 webpack_require.e。可以看到打包后的hello.js中有这样有个promise链:

__webpack_require__.e(/*! import() */ 0)
.then(__webpack_require__.bind(null, /*! ./async */ "./src/async.js"))

先去加载了bundle 0 也就是0.bundle.js, 也就是webpack code splitting 出的async.js。然后(then)执行async.js的代码。

  • webpack_require.r
    carbon (4)

  • webpack_require.e
    先来看一下__webpack_require__.e内部的代码:
    carbon (4)

大致概括一下__webpack_require__.e的代码: 根据传入的chunkId,先检查是否已经加载过了,再检查是否正在加载, 否则的话创建加载script的promise,然后去加载chunk script。如果script加载失败(超时也算),那么执行chunk promise的reject。

从这步可以看到动态import的实现已经初露端倪。要完整的理解,还需要思考一个问题:上面script加载成功的chunk promise的resolve在什么时候执行?下面来看一下项目async.js打包出来的代码(去掉了一些注释):
carbon (5)

这里重点要看window[“webpackJsonp”]。window[“webpackJsonp”]
在之前的自执行函数的匿名函数中有定义。并且window[“webpackJsonp”]
的push方法已经被重写为webpackJsonpCallback。下面就来看一下webpackJsonpCallback这个函数。

  • webpackJsonpCallback
    carbon (6)

webpackJsonpCallback大致就是在做:将传入的chunkid(个人以为叫bundleid更合理)标记为已加载,并将传入的模块挂在到installedChunks对象上(缓存),最终执行 webpack_require.e 函数返回的promise的resolve,注意resolve也是从__webpack_require__.e 函数处理过的installedChunks上取的(installedChunks[chunkId] = [resolve, reject])

总结

实现动态import的主要代码:

async () => {
    const res = await import("./async");
    console.log(res.default());
}

对应打包后的代码:

async () => {
  const res = await __webpack_require__.e(/*! import() */ 0)
  .then(__webpack_require__.bind(null, /*! ./async */ "./src/    
           async.js"));
    console.log(res.default());
}
  • 执行流程:
  1. 先执行__webpack_require__.e(0),把动态import的promise挂载到installedChunks上, 创建script 加载0.bundle.js, 返回promise

  2. 如果加载(超时也是)失败,在onScriptComplete中reject;如果加载成功,则执行加载过来的js:执行 window[webpackJsonp] 上的push方法(已经被重写为webpackJsonpCallback),将动态加载的模块(0.bundle.j)标记为已加载,并将模块(async.js)对应的代码挂载到modules参数上,最后resolve异步加载的模块。

  3. 在then后执行(webpack_require)挂载到modules参数上对应的模块(async.js)的代码

通过分析 webpack打包后的模板代码,可以看到webpack用IIFE的形式将所有模块作为modules参数传入,从entry模块开始依次执行modules,巧妙的处理了动态加载的模块。用 webpack_require 抹平了import和require之间的差异。

react Diff 算法

react Diff 算法

react Diff 操作在哪个阶段?

react Diff 操作发生在render阶段产出workInProgress fiber节点的时候,会根据current fiber tree 和本次要更新节点进行diff,同时会标记effect tag(在effect list fiber tree 上)。

    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderExpirationTime,
    );

reconcileChildFibers就是diff算法的入口函数

react Diff 是如何做优化的?

  • 正常的两棵树diff的时间复杂度是O(n^3) ,在React官网文档Reconciliation上有提到。
    O(n^3) 的大致由来: 两棵树嵌套循环寻找不同的节点:O(n^2),寻找到不同的节点后,需要再遍历得到最小的转换消耗,最终得 到O(n^3)

  • React基于两个假设实现了一种启发式O(n)算法

  1. 不同类型的两个元素将产生不同的树。
  2. 开发人员可以使用key prop 提示哪些子元素在不同的渲染中可能是稳定的。
  • 最终React Diff 算法的优化
  1. 只对同层节点进行比较
  2. 不同类型的元素直接删除,然后重新创建新的元素
  3. 相同类型的DOM元素,React会查看两者的属性,仅更新更改的属性
  4. 相同类型的组件元素,组件实例保持不变。React会更新组件的props
  5. 多个子元素的情况下,React引入key prop,使用key将原始树中的子代与后一棵树中的子代进行匹配,对子元素进行重用。

按照这样的优化,最终只需一层遍历O(n)即可完成

react Diff 源码浅析

 function reconcileChildFibers(
    // 父节点fiber
    returnFiber: Fiber,
    // 父节点fiber的第一个child fiber
    currentFirstChild: Fiber | null,
    // 新产生的节点信息
    newChild: any,
    expirationTime: ExpirationTime,
  ): Fiber | null {
    // 处理<>{[...]}</> and <>...</> 直接取fragment的children
    const isUnkeyedTopLevelFragment =
      typeof newChild === 'object' &&
      newChild !== null &&
      newChild.type === REACT_FRAGMENT_TYPE &&
      newChild.key === null;
    if (isUnkeyedTopLevelFragment) {
      newChild = newChild.props.children;
    }

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

    if (isObject) {
      switch (newChild.$$typeof) {
        // React.createElement
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(
            reconcileSingleElement(
              returnFiber,
              currentFirstChild,
              newChild,
              expirationTime,
            ),
          );
        // ReactDOM.createPortal
        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)) {
      // generator
      return reconcileChildrenIterator(
        returnFiber,
        currentFirstChild,
        newChild,
        expirationTime,
      );
    }

   ......

    // 删除没有匹配的子节点
    return deleteRemainingChildren(returnFiber, currentFirstChild);
  }

通过判断新节点(newChild)的类型,匹配不同的diff操作。

  1. REACT_ELEMENT_TYPE。由React.createElement 产生,是单子元素节点,diff操作reconcileSingleElement
  2. REACT_PORTAL_TYPE。由ReactDOM.createPortal 产生,是单子元素节点,diff操作reconcileSinglePortal
  3. "string" "number"。文本节点,是单子元素节点,diff操作reconcileSingleTextNode
  4. 数组子元素。diff操作reconcileChildrenArray
  5. 可迭代子元素(比如generator)。diff操作reconcileChildrenIterator

最后对没有匹配的到newChild节点的old子节点进行删除操作

reconcileSingleElement

详细注释

function reconcileSingleElement(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    element: ReactElement,
    expirationTime: ExpirationTime,
  ): Fiber {
    // 要更新的child的key
    const key = element.key;
    // current child
    let child = currentFirstChild;
    // 取第一个不为null的current child
    while (child !== null) {
      // 判断current child 和 要更新的child的key 是否相等
      if (child.key === key) {
        switch (child.tag) {
          case Fragment: {
            if (element.type === REACT_FRAGMENT_TYPE) {
              // "删除" current child 的 sibling
              deleteRemainingChildren(returnFiber, child.sibling);
              // 复用current child, 传入要更新的child的props
              const existing = useFiber(child, element.props.children);
              existing.return = returnFiber;
              if (__DEV__) {
                existing._debugSource = element._source;
                existing._debugOwner = element._owner;
              }
              return existing;
            }
            break;
          }
          case Block:
            if (enableBlocksAPI) {
              if (
                element.type.$$typeof === REACT_BLOCK_TYPE &&
                element.type.render === child.type.render
              ) {
                deleteRemainingChildren(returnFiber, child.sibling);
                // 复用current child, 传入要更新的child的props
                const existing = useFiber(child, element.props);
                existing.type = element.type;
                existing.return = returnFiber;
                if (__DEV__) {
                  existing._debugSource = element._source;
                  existing._debugOwner = element._owner;
                }
                return existing;
              }
            }

          default: {
            // type 相同
            if (
              child.elementType === element.type ||
              (__DEV__
                ? isCompatibleFamilyForHotReloading(child, element)
                : false)
            ) {
              deleteRemainingChildren(returnFiber, child.sibling);
              // 复用current child, 传入要更新的child的props
              const existing = useFiber(child, element.props);
              existing.ref = coerceRef(returnFiber, child, element);
              existing.return = returnFiber;
              if (__DEV__) {
                existing._debugSource = element._source;
                existing._debugOwner = element._owner;
              }
              return existing;
            }
            break;
          }
        }
        // "删除" 不能复用的节点
        deleteRemainingChildren(returnFiber, child);
        break;
      } else {
        // key不相等,“删除” current child (标记child的effectTag为Deletion)
        deleteChild(returnFiber, child);
      }
      child = child.sibling;
    }

    // 最终没有可复用的节点,创建节点
    if (element.type === REACT_FRAGMENT_TYPE) {
      const created = createFiberFromFragment(
        element.props.children,
        returnFiber.mode,
        expirationTime,
        element.key,
      );
      created.return = returnFiber;
      return created;
    } else {
      const created = createFiberFromElement(
        element,
        returnFiber.mode,
        expirationTime,
      );
      created.ref = coerceRef(returnFiber, currentFirstChild, element);
      created.return = returnFiber;
      return created;
    }
  }

reconcileChildrenArray

详细注释

 function reconcileChildrenArray(
    // current fiber 父节点
    returnFiber: Fiber,
    // current fiber 的第一个child, 其余child用sibling指针链接 (父节点没有指向非首个子节点的指针)
    currentFirstChild: Fiber | null,
    // 当前要更新的child 数组
    newChildren: Array<*>,
    expirationTime: ExpirationTime,
  ): Fiber | null {
    // 需要返回的结果,当返回时,已经是匹配复用好了的fiber first child,其余child用sibling指针链接
    let resultingFirstChild: Fiber | null = null;
    let previousNewFiber: Fiber | null = null;

    let oldFiber = currentFirstChild;
    // 最后一个可复用节点的index, index指的是current fiber child(oldFiber)对应的index
    let lastPlacedIndex = 0;
    let newIdx = 0;
    let nextOldFiber = null;
    // newChildren第一次循环
    // 处理的是key相同,并且是按顺序的,也就是newChildren 的节点没有改变位置
    // 这一次的循环处理完后,有如下结果
    // 1. newChildren没有遍历完,oldFiber也没有遍历完,待后续处理
    // 2. newChildren 数据已经匹配处理完成, 那就要标记删除oldFiber的节点
    // 3. 没有oldFiber可以复用,那就要新建插入fiber
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber;
        oldFiber = null;
      } else {
        nextOldFiber = oldFiber.sibling;
      }
      // key相等就复用oldFiber
      const newFiber = updateSlot(
        returnFiber,
        oldFiber,
        newChildren[newIdx],
        expirationTime,
      );
      if (newFiber === null) {
        if (oldFiber === null) {
          oldFiber = nextOldFiber;
        }
        break;
      }
      // shouldTrackSideEffects 非首屏渲染为true, 表示需要标记 workInProgress child的effectTag
      if (shouldTrackSideEffects) {
        // newFiber.alternate ===null 匹配完成
        if (oldFiber && newFiber.alternate === null) {
          //标记删除其余的old fiber child
          deleteChild(returnFiber, oldFiber);
        }
      }
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      // 记录能匹配到的最后一个newFiber child
      previousNewFiber = newFiber;
      // 记录能匹配到的最后一个oldFiber child
      oldFiber = nextOldFiber;
    }
     
    // newChildren 数据已经匹配处理完成
    if (newIdx === newChildren.length) {
      // 标记删除其余的old fiber child
      deleteRemainingChildren(returnFiber, oldFiber);
      return resultingFirstChild;
    }

    if (oldFiber === null) {
      // 没有oldFiber可以复用,新建插入fiber
      for (; newIdx < newChildren.length; newIdx++) {
        const newFiber = createChild(
          returnFiber,
          newChildren[newIdx],
          expirationTime,
        );
        if (newFiber === null) {
          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 {
          // 用sibling串联newFiber
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
      return resultingFirstChild;
    }

    // Add all children to a key map for quick lookups.
    // 处理oldFiber, 转换为一个map, key为oldFiber节点的key, value为oldFiber child
    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

    // newChildren第二次循环开始
    // 处理的是newChildren的节点位置换了
    // 这里的逻辑有点绕,需要举🌰
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = updateFromMap(
        existingChildren,
        returnFiber,
        newIdx,
        newChildren[newIdx],
        expirationTime,
      );
      if (newFiber !== null) {
        if (shouldTrackSideEffects) {
          if (newFiber.alternate !== null) {
            // The new fiber is a work in progress, but if there exists a
            // current, that means that we reused the fiber. We need to delete
            // it from the child list so that we don't add it to the deletion
            // list.
            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.
      existingChildren.forEach((child) => deleteChild(returnFiber, child));
    }

    return resultingFirstChild;
  }

第二次循环的例子(🌰来源

// 之前
abcd

// 之后
acdb

===第一轮遍历开始===
a(之后)vs a(之前)  
key不变,可复用
此时 a 对应的oldFiber(之前的a)在之前的数组(abcd)中索引为0
所以 lastPlacedIndex = 0;

继续第一轮遍历...

c(之后)vs b(之前)  
key改变,不能复用,跳出第一轮遍历
此时 lastPlacedIndex === 0;
===第一轮遍历结束===

===第二轮遍历开始===
newChildren === cdb,没用完,不需要执行删除旧节点
oldFiber === bcd,没用完,不需要执行插入新节点

将剩余oldFiber(bcd)保存为map

// 当前oldFiber:bcd
// 当前newChildren:cdb

继续遍历剩余newChildren

key === c  oldFiber中存在
const oldIndex = c(之前).index;
 oldIndex 代表当前可复用节点(c)在上一次更新时的位置索引
此时 oldIndex === 2;  // 之前节点为 abcd,所以c.index === 2
比较 oldIndex  lastPlacedIndex;

如果 oldIndex >= lastPlacedIndex 代表该可复用节点不需要移动
并将 lastPlacedIndex = oldIndex;
如果 oldIndex < lastplacedIndex 该可复用节点之前插入的位置索引小于这次更新需要插入的位置索引,代表该节点需要向右移动

在例子中,oldIndex 2 > lastPlacedIndex 0
 lastPlacedIndex = 2;
c节点位置不变

继续遍历剩余newChildren

// 当前oldFiber:bd
// 当前newChildren:db

key === d  oldFiber中存在
const oldIndex = d(之前).index;
oldIndex 3 > lastPlacedIndex 2 // 之前节点为 abcd,所以d.index === 3
 lastPlacedIndex = 3;
d节点位置不变

继续遍历剩余newChildren

// 当前oldFiber:b
// 当前newChildren:b

key === b  oldFiber中存在
const oldIndex = b(之前).index;
oldIndex 1 < lastPlacedIndex 3 // 之前节点为 abcd,所以b.index === 1
 b节点需要向右移动
===第二轮遍历结束===

最终acd 3个节点都没有移动,b节点被标记为移动

webpack几个基础概念

整理一下webpack几个基础概念

什么是bundle, chunck, module

  • bundel: 打包后的文件
  • chunck: 在进行模块依赖分析,代码分割出来的代码块
  • module: 开发中的单个模块

什么是loader 什么是plugin

  • loader: 用来告知webpack如何转化处理某一些类型的文件,并且引入到打包出的文件中
  • plugin:是用来自定义webpack打包过程的方式, 是一个含有apply方法的对象,通过这个方法可以参与到整个webpack打包的各个流程

定位和层叠上下文

总结一下CSS中的定位和层叠上下文

定位

  • fixed固定定位
    固定定位让元素相对视口定位,此时视口被称作元素的包含块(containing block)。

  • absolute绝对定位
    绝对定位不是相对视口,而是相对最近 的祖先定位元素。如果祖先元素都没有定位, 那么绝对定位的元素会基于初始包含块(initial containing block)来定位。 初始包含块跟视口一样大,固定在网页的顶部。

  • relative相对定位
    相对定位的元素以及它周围的所有元素,都还保持着原来的位置。

  • sticky定位
    它是相对定位和固定定位的结合体:正常情况下,元素会随着页面滚动,当到达屏幕的特定位置时,如果用户继续滚动,它就会 “锁定”在这个位置。

层叠上下文

理解层叠上下文

一个层叠上下文包含一个元素或者由浏览器一起绘制的一组元素。其中一个元素会作为层叠 上下文的根,比如给一个定位元素加上 z-index 的时候,它就变成了一个新的层叠上下文的根。 所有后代元素就是这个层叠上下文的一部分。

创建层叠上下文

  1. 文档根节点 ( html)会给整个页面创建一个顶级的层叠上下文
  2. 给一个定位元素加z-index属性
  3. CSS3的几个新属性: 小于 1 的 opacity 属性,不为none的transform 属性、 不为none的filter 属性, will-change为opacity、transform、filter中的一个

层叠上下文和 z-index

  1. z-index 只在定位元素上生效,不能用它控制静态元素
  2. 给一个定位元素加上 z-index 可以创建层叠上下文

在一个独立的层叠上下文中,元素如何排列?

由下到上:
1. 层叠上下文的根元素
2. z-index为负值的已定位元素(包括它们的子元素 )
3. 未定位元素
     1. block块级元素
     2. float浮动元素
     3. inline/inline-block元素
4. z-index为auto/0的已定位元素(包括它们的子元素)
5. z-index为正值的已定位元素(包括它们的子元素)

CSS in Depth
张鑫旭-《深入理解CSS中的层叠上下文和层叠顺序》

webpack tree shaking

之前对tree shaking的认识仅仅停留在无用代码剔除上。今天来深究总结一下。

什么是tree shaking

Tree shaking is a term commonly used in the JavaScript context for dead-code elimination

Tree shaking是一个用在js中删除无用代码的术语。

在webpack 2版本中webpack内置支持了ES2015 modules,并且也支持了无用模块的导出检测。webpack 4版本在此功能上进行了扩展,并通过在package.json 中添加“ sideEffects” 属性向编译器提供提示,以标示项目中的哪些文件是“纯”的,从而可以安全的移除。

背后依据的原理

所以tree shaking背后依据的原理肯定和es module息息相关— 依赖的是ES6模块的静态分析能力,体现在:

  • Export和import 只能出现在文件代码的顶层
  • 使用 import 导入的变量是readonly的,类似const

demo实验

下面的例子都是用最新版的webpack

一个tree shaking可行的demo

// test.js:
export const one = function (value) {
  console.log("this is one");
  return value
};
export const two = function (value) {
  console.log("this is two");
  return value;
};

// index.js
import { one, two } from "./test";
console.log(two("test"));

在生产环境下打包(下同),发现函数one没有被打包进去:

WeChat340d8eb3ba34b562aec8e66085b46ee7

一个tree shaking不可行的demo

// test-one.js
window.testOne = () => {
  console.log("this is window test one")
}

export const anotherTest = () => {
  console.log("another test")
}

// test.js
import { anotherTest } from "./test-one.js";

export const one = function (value) {
  console.log("this is one");
  return value;
};
export const two = function (value) {
  console.log("this is two");
  return value;
};

// index.js
import { one, two } from "./test";
console.log(two("test"))

打包后:

WeChat97ab32591ccdc1d089b2cdbf04833e95

可以看到test-one.js 中的anotherTest虽然已经exported,因为没有被使用,所以被tree shaking了。但是,test-one.js中的window.testOne代码却被打包了。如果我想要的效果是:test-one.js export 的内容没有用到就不需要打包该怎么办呢?下面来介绍一下package.json中可以添加的一个属性sideEffects。

sideEffects

sideEffects 顾名思义是side effects,也就是副作用(函数式编程中的一个名词)。在webpack4中,通过在package.json 中添加"sideEffects": false , 可以向webpack指明整个项目是没用副作用的,可以安全的 tree-shaking 。sideEffects 也可以接受一个数组,数组的每一项是文件路径,用于保留这些文件的副作用:

"sideEffects":["./src/global.config.js"]

用sideEffects解决第二个demo的问题

在package.json中配置"sideEffects": false 后,我们再来打包看一下结果:
WeChat67144c387c48827c058d21959fddffdb
我们可以看到window.testOne确实没有被打包了

sideEffects 的注意点

有时候在项目中我们确实想“引入一些有副作用的文件”,比如我们想在window对象上定义一些js函数,供native端调用 (js bridge):

 // global.js
window.NativeBridge = {
  share: () => {
    console.log("this is native share")
  }
}

此时我们在index.js中调用global.js

import { one, two } from "./test";
import "./global.js"

console.log(two("test"));

打包后发现global.js根本没有被打包进文件。导致这种错误的原因是:我们是通过import "./global.js" 引入文件的,webpack 会把所有import "xxx" 看做是引入了文件,但是没有使用的。 如果此时在package.json中配置"sideEffects": false ”, 那么就global.js会被 tree-shaking 。

解决中类似import "xxx" 的方法是在`"sideEffects”中表明有副作用的文件:

"sideEffects":["./src/global.js"]

Tree-shaking 目前的局限

直接上代码:

  // test.js
import { isDate } from "lodash-es"

export const one = function (value) {
  console.log("this is one");
  return isDate(value);
};

export const two = function (value) {
  console.log("this is two");
  return value;
};

// index.js
import { one, two } from "./test";
import "./global.js"

console.log(two("test"));

打包后:
WeChat24790fee69486bc61652b58b3fc0d7d3
可以看到虽然函数one没有没使用,但是lodash-es的部分代码还是被打包了。

解决方法:

  1. 把one函数修改为纯函数,用到的lodash的isDate方法作为依赖注入:
export const one = function (value, isDate ) {
  console.log("this is one");
  return isDate(value);
};
  1. 使用第三方webpack插件: webpack-deep-scope-analysis-plugin

关于babel

为了确保babel不会将代码编译为commonjs,配置 Babel preset @babel/preset-env 的modules属性为false

总结

想要在webpack 中更好的使用tree shaking,那么

  1. 需要使用ES6 模块
  2. 在使用babel的时候,不能编译转化为非es6模块
  3. 合理的加sideEffects。其实sideEffects就是来通知webpack可以安全的进行tree-shaking的,如果有些包真的是有副作用, 那么也可以在sideEffects中配置。
  4. 在写代码时尽量考虑到副作用的产生,合理避免。

Webpack4: Tree-shaking 深度解析
Tree Shaking

Redux原理解析

Redux概念

Redux的核心本质就是一个发布订阅模式。
view

Redux的三个基本原则:

  1. 单一数据源
    在Redux中一般只有一个store,用来存放数据。
  2. 只读的state
    更改状态的唯一方法是触发一个动作action,action 描述了这次修改行为的相关信息的对象。由于action只是简单的对象,因此可以将它们记录,序列化,存储并在以后进行调试。
  3. 使用纯函数进行操作更改state
    为了描述 action 如何修改状态,需要使用reducer 函数。reducer 函数接收前一次的 state 和 action,返回新的 state, 而不是改变先前的state。只要传入相同 的state 和 action,无论reducer被调用多少次,那么就一定返回相同的结果。

Redux源码浅析

createStore

function createStore(reducer, preloadedState, enhancer) {
  // 删除了一些参数矫正的代码

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }
    // store enhancer的使用,也就是使用传入的中间件
    // enhancer就是后续会提到的applyMiddleware函数
    return enhancer(createStore)(reducer, preloadedState)
  }


  let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false

  // 这个方法用于保持nextListeners和currentListeners的同步
  // nextListeners 是从currentListeners浅拷贝来的。
  // 订阅的时候是操作nextListeners, 防止在dispatch的时候执行subscribe/unsubscribe带来的bug
  // 在dispatch 的时候会同步 currentListeners 和 nextListeners (currentListeners = nextListeners)
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  // 获取当前currentState
  function getState() {
    if (isDispatching) {
      throw new Error(
        'You may not call store.getState() while the reducer is executing. ' +
          'The reducer has already received the state as an argument. ' +
          'Pass it down from the top reducer instead of reading it from the store.'
      )
    }

    return currentState
  }

  // 订阅一个listener,是操作在nextListeners上的
  // 返回的是一个函数,用于删除这个订阅的listener,也是在nextListeners上操作的
  // 删除订阅不会在当前的dispatch中生效,而是会在下一次dispatch的时候生效
  function subscribe(listener) {
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.')
    }

    if (isDispatching) {
      throw new Error(
        'You may not call store.subscribe() while the reducer is executing. ' +
          'If you would like to be notified after the store has been updated, subscribe from a ' +
          'component and invoke store.getState() in the callback to access the latest state. ' +
          'See https://redux.js.org/api-reference/store#subscribelistener for more details.'
      )
    }

    let isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

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

      isSubscribed = false

      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
      currentListeners = null
    }
  }

 
   // 触发action
   // 1. 执行reducer,更新state
   // 2. 执行订阅的listener
   // 返回的是传入的action, 用于调试记录
  function dispatch(action) {
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
          'Use custom middleware for async actions.'
      )
    }

    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
          'Have you misspelled a constant?'
      )
    }

    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }

  // 替换当前的reducer
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }

    currentReducer = nextReducer
    // 触发一个内部的REPLACE action
    dispatch({ type: ActionTypes.REPLACE })
  }

  // 用于 observable/reactive libraries
  function observable() {
    const outerSubscribe = subscribe
    return {
      subscribe(observer) {
        if (typeof observer !== 'object' || observer === null) {
          throw new TypeError('Expected the observer to be an object.')
        }

        function observeState() {
          if (observer.next) {
            observer.next(getState())
          }
        }

        observeState()
        const unsubscribe = outerSubscribe(observeState)
        return { unsubscribe }
      },

      [$$observable]() {
        return this
      }
    }
  }

  // 触发一个INIT action,用于创建初始state树
  dispatch({ type: ActionTypes.INIT })

  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
}

applyMiddleware 详解

function applyMiddleware(...middlewares) {
  // 返回的函数就是在createStore中调用的那个enhancer
  return (createStore) => (...args) => {
    // 创建store, 可以获取dispatch和getState
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
          'Other middleware would not be applied to this dispatch.'
      )
    }
    
    // 封装传入中间件的api
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args),
    }
    // 给中间件们绑定能dispatch和getState的api
    const chain = middlewares.map((middleware) => middleware(middlewareAPI))

    // 从这里就可以看出middleware就是来加强dispatch的,在dispatch了一个action后搞点事情
    // compose的本质就是用于串联执行中间件
    // 这个dispatch也重写个middlewareAPI调用的dispatch
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch,
    }
  }
}

详解 dispatch = compose(...chain)(store.dispatch)

compose的本质,组合多个函数, 用于串联执行中间件

 const compose =(...middlewares) =>  middlewares.reduce((f1, f2) => (...args) => f1(f2(...args)))

先看看标准的middleware

({ dispatch, getState }) => (next) => (action) => {
   // 搞点事情
   const state =  next(action)
   // 搞点事情
   return state
  }

去掉getState和dispatch的实现dispatch = compose(...chain)(store.dispatch)中的chain可以理解为:

const middlewares = [
  (next) => (action) => {
    console.log('middleware 1')
    next(action)
    console.log('middleware 1 after')
  },
  (next) => (action) => {
    console.log('middleware 2')
    next(action)
    console.log('middleware 2 after')
  },
  (next) => (action) => {
    console.log('middleware 3')
    next(action)
    console.log('middleware 3 after')
  },
]

调用上面的chain

const compose = middlewares.reduce((f1, f2) => (...args) => f1(f2(...args)))
// compose好后传入middleware 1 的next就是middleware 2, 传入middleware 2 的next就是middleware3
// 而middleware 3 的next就是调用compose后的传入的原始dispatch
const dispatch = compose((action) => {
  console.log('origin dispatch', action)
})
// 最终执行绑定好后的dispatch,就相当于最终执行每个middleware,每个middleware会传递action参数给原始的dispatch
// redux-thunk 的核心原理就是:检测到传入的action如果是函数类型,就执行这个函数
dispatch({ a: 3 })
// log 如下
middleware 1
middleware 2
middleware 3
origin dispatch {a: 3}
middleware 3 after
middleware 2 after
middleware 1 after

applyMiddleware的本质就是把中间件函数参数先一个一个的绑定好,来增强store 的dispatch的执行

combineReducers

combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  // 收集所有传入的 reducer 函数
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]
    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  const finalReducerKeys = Object.keys(finalReducers)

  // 在 dispatch 时会执行 combination 函数,
  // 遍历执行所有 reducer 函数。如果某个 reducer 函数返回了新的 state,就标记hasChanged为true,
  // 所有的 reducer 函数都会被执行一遍
  // hasChanged ? 返回新的state : 返回原来的state
  
  return function combination(state = {}, action) {

    let hasChanged = false
    const nextState = {}
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      const reducer = finalReducers[key]
      const previousStateForKey = state[key]
      const nextStateForKey = reducer(previousStateForKey, action)
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    hasChanged =
      hasChanged || finalReducerKeys.length !== Object.keys(state).length
    return hasChanged ? nextState : state
  }
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.