时倾

时倾 查看完整档案

上海编辑中原工学院  |  软件工程 编辑  |  填写所在公司/组织填写个人主网站
编辑

把梦想放在心中

个人动态

时倾 发布了文章 · 4月21日

React Styleguidist 入门

此文章是由个人对该技术的理解和解读官网翻译而来,不免有不太恰当的地方,望多多指教,共同学习。

react-styleguidist 会把所有组件都显示在一个页面上,包含 props 文档和用法示例,以及一个独立开发组件的环境。 在Styleguidist 中,可以在 Markdown 中编写示例,每个代码段都会立即呈现;

原理

Styleguidist会加载组件,使用react-docgen生成文档,可能需要更改代码才能正常工作。

React-docgen会把组件作为静态文本文件读取,然后类似于查找React组件的模式去查找组件(例如类或函数声明)。React-docgen不会运行任何JavaScript代码,因此,如果组件是动态生成,或包装在高阶组件中,或拆分为多个文件,react-docgen可能无法理解。

React-docgen支持通过React.createClass,ES6 classes 和函数组件定义的组件;
React-docgen支持Flow和TypeScript注释;

在某些情况下,可以通过导出两个组件 欺骗 Styleguidist和react-docgen:

  • 作为命名导出的基本组件
  • 作为默认导出的增强组件
import React from 'react'
import CSSModules from 'react-css-modules'
import styles from './Button.css'

// Base component will be used by react-docgen to generate documentation
export function Button({ color, size, children }) {
  /* ... */
}

// Enhanced component will be used when you write <Button /> in your example files
export default CSSModules(Button, styles)

开始

安装 Styleguidist:

// npm
npm install --save react-styleguidist

// yarn
yarn add react-styleguidist

配置package.json脚本

"scripts": {
  "styleguide": "NODE_ENV=development styleguidist server",
  "styleguide:build": "NODE_ENV=production styleguidist build",
}

运行Styleguidist

npm run styleguide  //启动styleguidist开发服务器
npm run styleguide:build  //构建生产HTML版本

组件生成文档

Styleguidist根据组件中的注释,propTypes声明和Readerme.md为组件生成文档。

解析props

默认行为:Styleguidist从propTypes中获取props并生成一个表格,并根据props的注释显示组件的说明,根据defaultProps获取props的默认值。

import React from 'react'
import PropTypes from 'prop-types'
/**
 * General component description in JSDoc format. Markdown is    *supported*.
 */
export default class Button extends React.Component {
  static propTypes = {
    /** Description of prop "foo". */
    foo: PropTypes.number,
    /** Description of prop "baz". */
    baz: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
  }

  static defaultProps = {
    foo: 42
  }

  render() {
    /* ... */
  }
}

修改默认行为

可以使用propsParserresolver更改解析props默认行为;

propsParser: Function
此方法可以重新个从源文件如何解析props。默认是使用react-docgen进行解析。

module.exports = {
  propsParser(filePath, source, resolver, handlers) {
    return require('react-docgen').parse(source, resolver, handlers)
  }
}

resolver
此方法可以确定哪些类/组件需要解析。默认行为是在每个文件中查找所有导出的组件,可以配置它以让所有查找到的组件使用自定义解析方法。

module.exports = {
  resolver: require('react-docgen').resolver
    .findAllComponentDefinitions
}
组件的PropType和文档注释由react-docgen库解析, 可以使用updateDocs函数对其进行修改。有关解析props的更多信息,可参见react-docgen文档。

示例文件查找与编写

默认行为:Styleguidist默认在组件文件夹下查找Readme.md[ComponentName].md的文件并生成示例文件。
带有js,jsx或javascript标记的代码块将被呈现为带有交互式的React组件。为了向后兼容,不带语言标签的代码块也会展示成带有交互式的React组件。

React component example:

​```js
<Button size="large">Push Me</Button>
​```

You can add a custom props to an example wrapper:

​```js { "props": { "className": "checks" } }
<Button>I’m transparent!</Button>
​```

To render an example as highlighted source code add a `static` modifier:

​```jsx static
import React from 'react';
​```

修改默认行为

可以通过getExampleFilename自定义md 文件名字,此方法通过提供的组建文件路径返回新的 md 文件路径。
例如:用 ComponentName.examples.md代替 Readme.md

module.exports = {
  getExampleFilename(componentPath) {
    return componentPath.replace(/\.jsx?$/, '.examples.md')
  }
}

关联其他示例文件

可以使用@example doclet语法将其他示例文件与组件关联。
例子:Button 组件有一个从extra.examples.md文件加载的示例

/**
 * Component is described here.
 *
 * @example ./extra.examples.md
 */
export default class Button extends React.Component {
  // ...
}

写 code

  • 导入依赖项: 通过import导入
  • 管理状态:每个示例都是一个function组件,可以使用useState Hook来处理状态。
// ```jsx inside Markdown
import React from 'react'
import Button from 'rsg-example/components/Button'
import Placeholder from 'rsg-example/components/Placeholder'
;<Button size="large">Push Me</Button>


// ```jsx inside Markdown
const [isOpen, setIsOpen] = React.useState(false)
;<div>
  <button onClick={() => setIsOpen(true)}>Open</button>
  <Modal isOpen={isOpen}>
    <h1>Hallo!</h1>
    <button onClick={() => setIsOpen(false)}>Close</button>
  </Modal>
</div>

moduleAliases
定义模块的别名,可以在示例文件中导入这些别名,以使示例代码更加实际和可复制;

const path = require('path');

module.exports = {
  moduleAliases: {
    'rsg-example': path.resolve(__dirname, 'src')
  }
}
  • Markdown语法都被支持;
  • 如果需要在文档中显示一些不想被呈现为交互式的JavaScript代码,则可以将static修饰符与语言标签一起使用;
  • Styleguidist 通过 Bublé 运行ES6代码, 大多数ES6功能都支持;
  • rsg-example模块是由moduleAliases 选项定义的别名;
  • 如果需要更复杂的演示,通常最好将其定义在一个单独的JavaScript文件中,然后将其导入Markdown中

公共方法

默认情况下,组件所具有的任何方法均被视为私有方法,不会被发布。使用JSDoc @public标记方法,就可以使其成为公共方法并会在文档中发布。

/**
 * @param {string} name
 * @public
 */
getName(name) {
  // ...
}

隐藏props

默认情况下,组件所有的props都是公开并可发布。在某些情况下代码存在某props, 希望文档中不展示这个props,使用JSDoc @ignore标记props,就可以将其从文档中删除。

Button.propTypes = {
  /**
   * A prop that should not be visible in the documentation.
   * @ignore
   */
  hiddenProp: React.PropTypes.string
}

定位组件

查找组件

默认情况下,Styleguidist将使用此模式定位组件:src/components/**/*.{js,jsx,ts,tsx}
例如:

  • src/components/Button.js
  • src/components/Button/Button.js
  • src/components/Button/index.js

但是会忽略 tests 文件:

  • __tests__ 文件夹
  • 文件名里包含.test.js.spec.js (类似于: .jsx, .ts , .tsx)

修改默认查找方式

styleguide.config.js文件里的配置components可以修改组件查找方式来适应不同的项目;

例如:如果组件路径是components/Button/Button.js,为了简化导入在components/Button/index.js中再次导出(export { default } from './Button'),【为了让components/Button替代components/Button/Button】, 这时候需要跳过 index.js。

module.exports = {
  components: 'src/components/**/[A-Z]*.js'
}

使用ignore可将某些文件从样式指南中排除

ignore:String[]

默认值:

[
  '**/__tests__/**', 
  '**/*.test.{js,jsx,ts,tsx}', 
  '**/*.spec.{js,jsx,ts,tsx}', 
  '**/*.d.ts'
]

使用getComponentPathLine更改组件导入路径

getComponentPathLine: Function
默认值: 组件文件名
返回值:返回组件路径

例如: 从components/Button中导入Button,而不是components/Button /Button.js

const path = require('path');
module.exports = {
  getComponentPathLine(componentPath) {
    const name = path.basename(componentPath, '.js')
    const dir = path.dirname(componentPath)
    return `import ${name} from '${dir}';`
  }
}
所有路径都相对于config文件夹;

加载组件

Styleguidist加载组件并暴露在全局以供示例使用。

标识符

Styleguidist默认使用组件的displayName作为标识符。如果它不能理解displayName(displayName动态生成),它将退回到它可以理解的东西。

Sections

Sections: Array[{}]

将组件分组,或将额外的Markdown文档添加到样式指南中。

Sections数组的每一项有:

  • name — Sections 标题;
  • content — 包含概述内容的Markdown文件的位置;
  • components — 可以是glob模式字符串,组件路径数组,glob模式字符串数组,返回一个组件数组的函数或返回glob模式字符串的函数。规则与根选项components相同;
  • sections — 嵌套的Sections数组(可以再嵌套);
  • description — section描述;
  • sectionDepth — 单个页面的小节数,仅在pagePerSection中可用;
  • exampleMode — 代码示例的初始状态,使用 exampleMode.
  • usageMode — props和方法的初始状态, 使用 usageMode.
  • ignore — 需要忽略的文件,值可以是字符串或数组;
  • href - navigate的URL(不是导航到Sections 内容的URL);
  • external - 如果设置,会打开一个新页面;
  • expand - 在常规设置中将tocMode设置为collapse折叠时,确认是否应该被展开;
上述所有字段都是可选的;
// styleguide.config.js

module.exports = {
  sections: [
    {
      name: 'Introduction',
      content: 'docs/introduction.md'
    },
    {
      name: 'Documentation',
      sections: [
        {
          name: 'Installation',
          content: 'docs/installation.md',
          description: 'The description for the installation section'
        },
        {
          name: 'Configuration',
          content: 'docs/configuration.md'
        },
        {
          name: 'Live Demo',
          external: true,
          href: 'http://example.com'
        }
      ]
    },
    {
      name: 'UI Components',
      content: 'docs/ui.md',
      components: 'lib/components/ui/*.js',
      exampleMode: 'expand', // 'hide' | 'collapse' | 'expand'
      usageMode: 'expand' // 'hide' | 'collapse' | 'expand'
    }
  ]
}

配置webpack

Styleguidist依赖webpack,使用webpack确定如何加载项目的文件,但项目也不一定非要配置webpack。
默认情况下,Styleguidist会在项目的根目录中查找webpack.config.js并使用它。

自定义Webpack配置

如果webpack配置在其他位置,则需要手动加载:

module.exports = {
  webpackConfig: require('./configs/webpack.js')
}

也可以merge多个webpack配置:

module.exports = {
  webpackConfig: Object.assign({}, require('./configs/webpack.js'), {
    /* Custom config options */
  })
}
  • 配置entry, externals, output, watch, 和 stats .会被忽略。生产上,devtool 也会被忽略;
  • 插件 CommonsChunkPlugins, HtmlWebpackPlugin, MiniHtmlWebpackPlugin, UglifyJsPlugin, TerserPlugin, HotModuleReplacementPlugin 会被忽略, 因为 Styleguidist 已经引入了他们,再次引入会影响 Styleguidist;
  • 如果loaders 不起作用,请尝试包含和排除绝对路径;

styleguide配置

title: String

serverPort: Number
端口号

require: Array
添加用户自定义的js, CSS 或 polyfills

assetsDir: String
资源文件名

styleguideDir: String
默认值:styleguide
定义styleguidist构建命令生成的静态HTML的文件夹;

getComponentPathLine: Function
获取组件加载路径

template: Object | Function
更改应用程序的HTML。 一个可以添加favicon,meta 标签, 内嵌JavaScript或CSS的对象。

styles: Object | String | Function
自定义Styleguidist组件的样式。

theme: Object | String
配置值可以是对象或导出此类对象的文件的路径实现自定义UI字体,颜色等;
配置的文件路径是相对配置文件或者绝对路径。

sections
设置组件分组;

styleguideComponents: Object
重写被用于渲染到浏览器的React组件;

webpackConfig
自定义webpack配置;

更多配置选项查看官方文档:configuration

查看原文

赞 4 收藏 2 评论 0

时倾 发布了文章 · 4月19日

Create React App 简介及如何拓展webpack配置

Create React App(以下简称 CRA)是一个官方支持的创建 React 单页应用的脚手架,它提供了一个零配置的现代构建设置,将一些复杂工具(比如 webpack, Babel)的配置封装了起来,让使用者不用关心这些工具的具体配置,从而降低了工具的使用难度。

创建方法

npx: npx 来自 npm 5.2+ 或更高版本

npx create-react-app my-app

npm: npm init <initializer> 在 npm 6+ 中可用

npm init react-app my-app

Yarn: yarn create 在 Yarn 0.25+ 中可用

yarn create react-app my-app

Scripts

在新创建的项目中,可以运行一些内置命令:

npm startyarn start

在开发模式下运行应用程序, 默认在浏览器打开http://localhost:3000。如果更改代码,页面将自动重新加载。

npm testyarn test

以交互模式运行测试程序。 默认情况下,运行与上次提交后更改的文件相关的测试。

npm run buildyarn build

将生产环境的应用程序构建到 build 目录。 它能将 React 正确地打包为生产模式中并优化构建以获得最佳性能。构建将被压缩,文件名中将包含哈希。

npm run eject

注意:这是单向操作。一旦 eject ,就不能回去了!

执行完这个命令后会将封装在 CRA 中的配置全部反编译到当前项目,这样开发者完全取得 webpack 文件的控制权,可以自定义修改webpack打包配置。

文件结构

创建后,项目文件结构如下所示:

my-app
  node_modules
  public
    favicon.ico
    index.html // 页面模板
    manifest.json
  src
    App.css
    App.js
    App.test.js
    index.css
    index.js // 项目入口
    logo.svg
    reportWebVitals.js
    setupTests.js
  .gitignore
  package.json
  README.md
  yarn.lock
  • 为了加快重新构建的速度,Webpack 只处理 src 中的文件。 你需要将任何 JS 和 CSS 文件放在 src,否则 Webpack 将发现不了它们。
  • 只能在 public/index.html 中使用 public 中的文件。
  • manifest.js: 目的是将Web应用程序安装到设备的主屏幕,为用户提供更快的访问和更丰富的体验。

项目升级

Create React App 分为两个包:

  • create-react-app 是一个全局命令行实用程序,可用于创建新项目。
  • react-scripts 包含Create React App的脚本与配置

当你运行 create-react-app 时,它始终使用最新版本的 react-scripts 创建项目,新创建的应用会获得所有新功能和改进。

CRA 将所有新特性委托给 react-scripts , 只需要更新react-scripts, 不需要更新 create-react-app 就可以升级CRA 的特性。比如用老版本 CRA 创建了一个项目,这个项目不具备 PWA 功能,但只要项目升级了react-scripts包的版本就可以具备 PWA 的功能,项目本身的代码不需要做任何修改。

如何扩展 Create React App 的 Webpack 配置

Create React App已经封装了webpack 配置,如果想对 webpack 配置做一些修改,这个时候应该怎么办呢?CRA提供了以下几种方式来修改 webpack 的配置:

  • eject 命令
  • 替换 react-scripts 包
  • 使用 react-app-rewired
  • scripts 包 + override 组合

eject 命令

使用 CRA 创建完项目以后,在package.json里面提供了这样一个命令:

{
  ...
  "scripts": {
    "eject": "react-scripts eject"
  },
  ...
}

执行yarn eject后会将封装在 CRA 中的配置全部复制到当前项目。eject 后项目根目录下会新增 config与scripts 文件夹,修改package.json与yarn.lock文件。

config
  jest
  env.js
  getHttpsConfig.js
  modules.js
  paths.js
  pnpTs.js
  webpack.config.js
  webpackDevServer.config.js
scripts
  build.js
  start.js
  test.js

如果使用了eject命令,虽然扩展了 webpack 配置,但是再也享受不到 CRA 升级带来的好处了。因为react-scripts已经是以文件的形式存在于你的项目,而不是以包的形式,所以无法对其升级。

替换 react-scripts 包

react-scripts 是 CRA 的一个核心包,一些脚本和工具的默认配置都集成在里面,使用 CRA 创建项目默认就是使用这个包。但是 CRA 还提供了另外一种方式来创建 CRA 项目,用自定义 scripts 包的方式。

// 默认方式
$ create-react-app my-app

// 自定义 scripts 包方式
$ create-react-app my-app --scripts-version 自定义包

自定义包可以是下面几种形式:

  • react-scripts包的版本号,比如0.8.2,这种形式可以用来安装低版本的react-scripts包。
  • 一个已经发布到 npm 仓库上的包的名字,比如your-scripts,里面包含了修改过的 webpack 配置。
  • 一个 tgz 格式的压缩文件,比如/your/local/scripts.tgz,通常是未发布到 npm 仓库的自定义 scripts 包,可以用 npm pack 命令生成。

这种方式相对于之前的eject是一种更灵活地修改 webpack 配置的方式,而且可以做到和 CRA 一样,通过升级 scrips 包来升级项目特性。

自定义 scripts 包的结构可以参照react-scripts包的结构,只要修改对应的 webpack 配置文件,并安装上所需的 webpack loader 或 plugin 包就可以。

使用 react-app-rewired

react-app-rewired 是 react 社区开源的一个修改 CRA 配置的工具,这种方式让开发者既不用eject项目也不用自己创建 scripts 包就可以拓展webpack。

如何使用 react-app-rewired

1.在 CRA 创建的项目中安装react-app-rewired

npm install react-app-rewired --save-dev

2.在项目根目录下创建config-overrides.js 文件(支持自定义文件路径)

/* config-overrides.js */

module.exports = function override(config, env) {
  // 参数中的 config 就是默认的 webpack config
  
  // 对 config 进行任意修改
  config.mode = 'development';
  
  // 一定要把新的 config 返回
  return config;
}

config-overriders.js 导出的是一个函数,这个函数的签名是 const override = (oldWebpackConfig, env) => newWebpackConfig

3.修改scripts命令

/* package.json */

"scripts": {
-   "start": "react-scripts start",
+   "start": "react-app-rewired start",
-   "build": "react-scripts build",
+   "build": "react-app-rewired build",
-   "test": "react-scripts test",
+   "test": "react-app-rewired test",
    "eject": "react-scripts eject"
}
config-overrides.js 文件

设置自定义路径 通过package.json 的config-overrides-path设置自定义路径:

/* package.json */
{
  ...
  "config-overrides-path": "src/app", // src/app/config-overrides.js
  ...
}

默认情况下,config-overrides.js文件导出一个函数,用于自定义Webpack配置以在开发或生产模式下编译React应用。 也可以改为此文件导出一个对象,该对象最多包含三个字段,每个字段都是一个函数。

module.exports = {
  // The Webpack config
  webpack: function(config, env) {
    // ...add your webpack config
    return config;
  },
  // The Jest config
  jest: function(config) {
    // ...add your jest config customisation...
    return config;
  },
  // create a webpack dev server
  devServer: function(configFunction) {
    return function(proxy, allowedHost) {
      const config = configFunction(proxy, allowedHost);
      const fs = require('fs');
      config.https = {
        key: fs.readFileSync(process.env.REACT_HTTPS_KEY, 'utf8'),
        cert: fs.readFileSync(process.env.REACT_HTTPS_CERT, 'utf8'),
        ca: fs.readFileSync(process.env.REACT_HTTPS_CA, 'utf8'),
        passphrase: process.env.REACT_HTTPS_PASS
      };
      return config;
    };
  },
  paths: function(paths, env) {
    // ...add your paths config
    return paths;
  },
}
实现原理

编译时,react-app-rewired 会先取到 create-react-app 生成的默认的 webpack config,然后调用 override(config) 方法,对 config 进行修改,得到新的 webpack config。webpack 最终会使用这个新的 config 进行打包。

流程大致如下:

const overrides = require('../config-overrides');
const webpackConfigPath = paths.scriptVersion + "/config/webpack.config.prod";

// load original config
const webpackConfig = require(webpackConfigPath);

// override config in memory
require.cache[require.resolve(webpackConfigPath)].exports =
  overrides.webpack(webpackConfig, process.env.NODE_ENV);

// run original script
require(paths.scriptVersion + '/scripts/build');

scripts 包 + override 组合

虽然react-app-rewired的方式已经可以很方便地修改 webpack 的配置了,但也可以在自定义的 script 包中实现类似的功能。

build.js为例,在获取基本 webpack 配置对象和使用 webpack 对象之间加入以下代码:

// override config
const override = require(paths.configOverrides);
const overrideFn = override || ((config, env) => config);
const overrideConfig = overrideFn(config, process.env.NODE_ENV);

overrideConfig就是修改后的 webpack 对象,最后修改调用了 webpack 对象的代码,将原来的 webpack 对象替换成修改后的 webpack 对象。

查看原文

赞 1 收藏 1 评论 0

时倾 关注了用户 · 4月16日

紫与黑 @ziyuhei

关注 1

时倾 发布了文章 · 4月15日

Redux 入门

Redux思想

严格的单向数据流是 Redux 架构的设计核心,使用 Redux 的一个好处就是让 state 的变化过程变的可预知和透明。

Redux约定使用普通对象object tree来描述应用的 state ,如果想更新 state 中的数据,需要先发起一个 action请求来对更改进行描述,然后调用 reducer函数执行state更改。

  • object tree:这个对象就像 “Model”,区别是它并没有 setter。state 可以是普通对象,不可变对象,或者其它类型。
  • Action:一个普通 JavaScript 对象,用来描述发生了什么。
  • Reducer:一些纯函数,它接收先前的 state 和 action,并返回新的 state。

三大原则

  • 单一数据源:整个应用的 state 被储存在一个 object tree中 ,这个 object tree 只存在于唯一一个 store 中。
  • State 是只读的:改变 store 内 state 的惟一途径是对它 dispatch 一个 action。
  • 使用纯函数来执行修改:只能在 reducers 中执行state的修改。

Store

Store 就是用来维持应用所有的 state 的一个对象。

  • Store 不是类, 它只是有几个方法的对象;
  • 应用中应有且仅有一个 store;

Store方法

getState()

返回应用当前的 state 树;

dispatch(action : object)

分发 action 改变 store;

subscribe(listener : Function):

注册监听器, 每当 dispatch action 的时候就会执行; state 树中的一部分可能已经变化, 可以在回调函数里调用 getState()来拿到当前 state。

replaceReducer(nextReducer : Function):

用 nextReducer 替换当前 store 的 reducer。在需要实现代码分隔,而且需要立即加载一些 reducer 的时候才可能会用到它,在实现 Redux 热加载机制的时候也可能会用到。

创建store

创建一个 Redux store 来以存放应用中所有的 state:
createStore(reducer, [preloadedState], enhancer)

参数

reducer(): Function

接收两个参数,分别是当前的 state 树和要处理的action,返回新的state 树。

preloadedState: any

初始化state。 如果使用 combineReducers 创建 reducer,它必须是一个普通对象,与传入的 keys 保持同样的结构;否则可以自由传入任何 reducer 可理解的内容。

enhancer: Function

Store enhancer 是一个组合 store creator 的高阶函数,返回一个新的强化过的 store creator。

返回值

Store: 保存了应用所有 state 的对象。

栗子

import React from 'react'
import { createStore } from 'redux'
import reducer from './reducers'

const store = createStore(reducer);

Action

Action 是把数据从应用传到 store 的有效载荷。它是 store 数据的唯一来源。action 内必须使用一个字符串类型的 type 字段来表示将要执行的动作。

  • 多数情况下,type 会被定义成字符串常量。
  • 当应用规模越来越大时,建议使用单独的模块或文件来存放 action。
{
  type: ADD_TODO,
  text: 'Build my first Redux app'
}

Action Creator

Action Creator 就是生成 action 的方法。

function fetchPosts(url) {
  return {
    type: FETCH_POSTS,
    url
  }
}

调用action

store 里直接通过 store.dispatch()调用 action,但是多数情况下会使用 react-redux 提供的 connect()来调用。bindActionCreators()可以自动把多个 action 创建函数 绑定到 dispatch() 方法上。

一个约定俗成的做法是通过创建函数生成 action 对象,而不是在dispatch 的时候内联生成action。
// 1. 内联生成 action
store.dispatch({
  type: ADD_TODO,
  text: 'Build my first Redux app'
})

// 2. Action Creator 生成 action
function fetchPosts(url) {
  return {
    type: FETCH_POSTS,
    url
  }
}
store.dispatch(fetchPosts(url))

Reducer

Redux 的应用程序中最常见的 state 结构是一个简单的 JavaScript 对象,它最外层的每个 key 中拥有特定域的数据。给这种 state 结构写 reducer 的方式是分拆成多个 reducer,拆分之后的 reducer 都是相同的结构(state, action),并且每个函数独立负责管理该特定切片 state 的更新。多个拆分之后的 reducer 可以响应一个 action,在需要的情况下独立的更新他们自己的切片 state,最后组合成新的 state。

  • 每个reducer要保证是纯函数。只要传入参数相同,返回计算得到的下一个 state 就一定相同。
  • 没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。
function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      });
    default:
      return state;
  }
}

combineReducers

这是一个高阶 Reducer ,combineReducers 接收拆分之后的 reducer 函数组成的对象,并且创建出具有相同键对应状态对象的Reducer函数。目前 combineReducers 只能处理普通的 JavaScript 对象。

如果没有给 createStore 提供预加载 state,输出 state 对象的 key 将由输入的拆分之后 reducer 组成对象的 key 决定。

结合Immutable.js 使用

combineReducers 不解决 Immutable.js,Maps等构建的 state tree,也不会把其余部分的 state 作为额外参数传递给 reducer 或者排列 reducer 的调用顺序,它同样不关心 reducer 如何工作。现在有大量提供类似功能的工具,例如 redux-immutable,这个第三方包实现了一个能够处理 Immutable Map 数据而非普通的 JavaScript 对象的 combineReducers

栗子

1.定义reducers

/**
 * reducers.js
 */
function info(state = {}, action) {
  switch (action.type) {
    case 'UPDATE_NAME':
      return { ...state, name: action.text };
    case 'UPDATE_SCHOOL':
      return { ...state, school: action.text };
    default:
      return state
  }
}

function age(state = {}, action) {
  switch (action.type) {
    case 'INCREMENT_AGE':
      return { ...state, age: state.age + 1 };
    case 'DECREMENT_AGE':
      return { ...state, age: state.age - 1 }
    default:
      return state
  }
}

export { info, age };

2.创建store, 引入combineReducers

/**
 * App.js
 */
import { createStore, combineReducers } from 'redux'
import { info, age } from './reducers.js'

let store = createStore(combineReducers({ info, age }), {
  name: '',
  school: '',
  age: 0
});
console.log(store.getState())
// {
//   info: { name: '', school: '' },
//   age: 0
// }

store.dispatch({
  type: 'UPDATE_NAME',
  text: 'Use Redux'
})
console.log(store.getState())
// {
//   info: { name: 'Use Redux', school: '' },
//   age: 0
// }

中间件Middleware

为什么出现中间件?

同步: Action 发出以后,Reducer 立即算出 State;
异步:Action 发出以后,过一段时间再执行 Reducer;
默认情况下,createStore()所创建的 Redux store 只支持同步数据流, dispatch只能接收一个普通对象。怎么才能在异步操作结束后Reducer自动执行呢?这时就需要使用中间件。

定义

Redux middleware 提供的是 action 被发起之后,到达 reducer 之前的扩展点,在每个 action 对象 dispatch 出去之前,注入一个自定义的逻辑来解释 action 对象。

  • 多个 middleware 可以被组合到一起使用,形成 middleware 链。
  • 每个 middleware 都不需要关心链中它前后的 middleware 的任何信息。

使用

中间件可以进行日志记录、创建崩溃报告、调用异步接口或者路由等等;在applyMiddleware方法里把中间件作为参数传入。此方法是 Redux 的原生方法,作用是将所有中间件组成一个数组,依次执行。

applyMiddleware(...middleware)

参数

...middleware

遵循 Redux middleware API 的函数。每个 middleware 接受 StoredispatchgetState函数作为命名参数,并返回一个函数。该函数会被传入被称为 next 的下一个 middleware 的 dispatch 方法,并返回一个接收 action 的新函数,这个函数可以直接调用 next(action),或者在其他需要的时刻调用,甚至根本不去调用它。调用链中最后一个 middleware 会接受真实的 store 的 dispatch方法作为 next 参数,并借此结束调用链。所以,middleware 的函数签名是 ({ getState, dispatch }) => next => action

返回值

(Function) :一个应用了 middleware 后的 store enhancer。这个 store enhancer 的签名是 createStore => createStore,但是最简单的使用方法就是直接作为最后一个 enhancer 参数传递给 createStore()函数。

栗子

实现请求前后打日志, 请求失败处理异常的中间件:

import { createStore, combineReducers, applyMiddleware } from 'redux';
import reducers from './reducers'

// 请求前后日志记录middleware
const logger = store => next => action => {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}

// 请求异常记录middleware
const crashReporter = store => next => action => {
  try {
    return next(action)
  } catch (err) {
    console.error('Caught an exception!', err)
    Raven.captureException(err, {
      extra: {
        action,
        state: store.getState()
      }
    })
    throw err
  }
}

// createStore引入自己创建的middleware
const store = createStore(
  combineReducers(reducers),
  preloadedState,
  applyMiddleware(logger, crashReporter)
)

compose

从右到左来组合多个函数: compose(...functions)

compose(funcA, funcB, funcC) 形象为 compose(funcA(funcB(funcC())))

参数
(arguments): 需要合成的多个函数。预计每个函数都接收一个参数。它的返回值将作为一个参数提供给它左边的函数,以此类推

返回值
(Function): 从右到左把接收到的函数合成后的最终函数。

React-redux

react-redux是使用redux开发react时使用的一个插件。react-redux提供了两个重要的API:Provider、connect

<Provider> API

<Provider> 使组件层级中的 connect() 方法都能够获得 Redux store。
正常情况下,你的根组件应该嵌套在 <Provider> 中才能使用 connect() 方法。如果不想把根组件嵌套在 <Provider> 中,你可以把 store 作为 props 传递到每一个被 connect() 包装的组件。

属性

  • store : 应用程序中唯一的 Redux store 对象;
  • children: ReactElement : 组件层级的根组件;

栗子

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

connect API

连接 React 组件与 Redux store。连接操作不会改变原来的组件类, 返回一个新的已与 Redux store 连接的组件类。
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])

参数

mapStateToProps(state, [ownProps]): Function
stateProps。

  • 如果定义该参数,组件将会监听 Redux store 的变化;如果省略,则组件将不会监听 Redux store的变化;
  • 只要 Redux store 发生改变,mapStateToProps 函数如果定义了就会被调用;
  • 如果定义了第二个参数 ownProps,则该参数的值为传递到组件的 props。只要组件接收新的 props,mapStateToProps 也会被调用。(例如,当父组件重新渲染子组件props改变,那么 ownProps 参数,mapStateToProps 都会被重新计算)

该函数必须返回一个纯对象,这个对象会与组件的 props 合并。

mapDispatchToProps(dispatch, [ownProps]):(Object / Function)
dispatchProps。

  • 如果传递的是一个对象,那么每个定义在该对象的函数都将被当作 Redux action creator,对象所定义的方法名将作为属性名。每个方法将返回一个新的函数,函数中dispatch方法会将 action creator 的返回值作为参数执行。这些属性会被合并到组件的 props 中。
  • 如果传递的是一个函数,该函数参数默认是 dispatch 函数,返回值是一个对象。返回对象通过 dispatch 函数与 action creator 以某种方式绑定在一起。
  • 如果省略 mapDispatchToProps ,默认情况下,dispatch 会注入到你的组件 props 中。
  • 如果指定了该回调函数中第二个参数 ownProps,该参数的值为传递到组件的 props,而且只要组件接收到新 props,mapDispatchToProps 也会被调用。

mergeProps(stateProps, dispatchProps, ownProps): Function]
props。

  • 如果指定了这个参数,mapStateToProps()mapDispatchToProps() 的执行结果和组件自身的 props 将传入到这个回调函数中。
  • 该回调函数返回的对象将作为 props 传递到被包装的组件中。
  • 如果你省略这个参数,默认情况下返回 (Object.assign({}, ownProps, stateProps, dispatchProps) 的结果。

options: Object
如果指定这个参数,可以定制 connect 的行为;

  • [pure = true] (Boolean): 如果为 true,connector 将执行 shouldComponentUpdate 并且浅对比 mergeProps 的结果,避免不必要的更新,前提是当前组件是一个“纯”组件,它不依赖于任何的输入或 state 而只依赖于 props 和 Redux store 的 state。默认值为 true
  • [withRef = false] (Boolean): 如果为 true,connector 会保存一个对被包含的组件实例的引用,该引用通过 getWrappedInstance() 方法获得。默认值为 false

返回值
根据配置信息,返回一个注入了 state 和 action creator 的 React 组件。

redux-thunk 中间件

redux-thunk是redux解决异步的中间件, 可以让 Action Creator 返回函数(普通的 Action Creator 默认返回一个对象)。如果action creator 返回的是一个函数,就执行它,如果不是,就按照原来的next(action)执行。

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducers';

const store = createStore(
  reducer,
  applyMiddleware(thunk)
);

redux-thunkexport default的是createThunkMiddleware(),这个函数返回的是一个柯里化过的函数。

function createThunkMiddleware(extraArgument) {
  return function({ dispatch, getState }) {
    return function(next){
      return function(action){
        if (typeof action === 'function') {
          return action(dispatch, getState, extraArgument);
        }
        return next(action);
      };
    }
  }
}

栗子

  • fetchPosts返回了一个函数,而普通的 Action Creator 默认返回一个对象。
  • 返回函数的参数是dispatchgetState,而普通的 Action Creator 的参数是 Action 的内容。
  • 在返回的函数之中,发出一个 ActionrequestPosts(title),表示操作开始。
  • 异步操作结束之后,发出一个 ActionreceivePosts(title, json)`,表示操作结束。
const fetchPosts = title => (dispatch, getState) => {
  dispatch(requestPosts(title));
  return fetch(url)
    .then(response => response.json())
    .then(json => dispatch(receivePosts(title, json)));
  };
};

store.dispatch(fetchPosts('test'));

redux-promise 中间件

既然使用redux-thunk中间件可以让 Action Creator 返回函数,当然也可以返回其他值。另一种异步操作的解决方案是让 Action Creator 返回一个 Promise 对象。这就需要使用redux-promise中间件。

import { createStore, applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise';
import reducer from './reducers';

const store = createStore(
  reducer,
  applyMiddleware(promiseMiddleware)
); 

栗子

const fetchPosts = (dispatch, title) => new Promise((resolve, reject)=> {
  dispatch(requestPosts(title));
  return fetch(url).then(response => {
      type: 'FETCH_POSTS',
      payload: response.json()
  });
});

redux-actions

Redux 让状态管理变得很冗长,大量的action、actionCreator、reducer让开发者不断在写重复的代码。redux-actions就解决了这个问题,让编写redux状态管理变得简单起来。

主要API有createAction(s)handleAction(s)combineActions

createAction(s) Api

创建一个action: createAction(type)

  • type必须实现toString()。
import { createAction } from 'redux-actions';

export const increment = createAction('INCREMENT');
export const decrement = createAction('DECREMENT');

increment(); // { type: 'INCREMENT' }
decrement(); // { type: 'DECREMENT' }
increment(10); // { type: 'INCREMENT', payload: 10 }
decrement([1, 42]); // { type: 'DECREMENT', payload: [1, 42] }

创建多个action: createActions(actionMap, ...identityActions[, options])

  • 第一个参数 actionMap 是一个对象,以 action type 为键名,键值value有三种形式:

    1. 函数,该函数参数传入的是action创建的时候传入的参数,返回结果会作为到生成的actionpayload的value。
    2. 数组[payload, meta],payload如上函数, meta 是必需的。
    3. 一个 actionMap 对象;
  • 第二个参数identityActions,可选参数;
createActions({
  ADD_TODO: todo => ({ todo }), // payload creator
  REMOVE_TODO: [
    todo => ({ todo }), // payload creator
    (todo, warn) => ({ todo, warn }) // meta
  ]
});

handleAction(s) API

处理action,返回一个reducer,处理一种类型的action type

处理一个action: handleAction(type, reducer, defaultState)

  • 如果传递了reducer函数,则该函数将用于处理正常操作和失败操作。
  • 如果未定义reducer参数(reducer),那么将使用identity函数。
  • 第三个参数defaultState是必需的,并且在将undefined传递给reducer时使用。
import { handleAction } from 'redux-actions';

handleAction(
  'APP/COUNTER/INCREMENT',
  (state, action) => ({
    counter: state.counter + action.payload.amount
  }),
  defaultState
);

处理多个action: handleActions(reducerMap, defaultState[, options])

  • defaultState是必需的,将undefined传递给reducer 时使用;
const reducer = handleActions(
  {
    INCREMENT: (state, action) => ({
      counter: state.counter + action.payload
    }),

    DECREMENT: (state, action) => ({
      counter: state.counter - action.payload
    })
  },
  { counter: 0 }
);

禁止Redux DevTools

React Developer Tools、Redux DevTools 可以给开发人员在研发阶段调试程序带来极大的方便。 但是上了生产环境后,应该将禁止 DevTools。

Redux DevTools的作者已经给出了标准的解决方案。具体实现步骤如下:

  • 设置process.env.NODE_ENV = JSON.stringify('production')
  • 使用redux-devtools-extension/developmentOnly引入方法
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly';

const store = createStore(
  rootReducer,
  composeWithDevTools(middlewareEnhancer)
);
查看原文

赞 1 收藏 1 评论 0

时倾 发布了文章 · 3月25日

位运算

位运算

js中的位运算只对整数起作用,因为位操作都有ToInt32这一步,从而舍弃小数部分

十六进制与二进制转换规则

十六进制二进制
00000
10001
20010
30011
40100
50101
60110
70111
81000
91001
A1010
B1011
C1100
D1101
E1110
F1111

数的存储

计算机中数是以二进制补码进行存储的,正数的原码、反码、补码都是一样,负数的补码是原码的反码再加1,这样可以减法运算可以使用加法器实现,符号位也参与运算二进制的最高位为符号位0为正,1为负,以8位来算,最高位为符号位,其余7位表示数值),取反码与符号位无关。

int类型的数占用4字节(32位)。5转换成二进制是101,不满32位会在前面填充0。那么5在计算机中表示为:00000000 00000000 00000000 00000101

原码,反码与补码

原码:一个整数,按照绝对值大小转换成的二进制数;
反码:将二进制数按位取反【1变0,0变1】;
补码:反码加 1;

负数的二进制

如十进制: -5

        原码: 00000000 00000000 00000000 00000101
        反码:11111111 11111111 11111111 11111010
补码(反码加一):11111111 11111111 11111111 11111011

所以 -5 的二进制是 11111111 11111111 11111111 11111011,转换为十六进制:0xFFFFFFFB

二进制求整

如补码是:11111111 11111111 11111111 11110010

        补码: 11111111 11111111 11111111 11110010
反码(补码减一):11111111 11111111 11111111 11110001
按位取反,原码:00000000 00000000 00000000 00001110

原码00000000 00000000 00000000 00001110即14, 然后取反就是 -14。

<<左移

运算规则: 按二进制形式把所有的数字向左移动对应的位数,高位移出(舍弃),低位的空位补零。
语法格式: 需要移位的数字 << 移位的次数。
数学意义: 如果是10进制向左移动一位相当于乘10倍,移两位乘10的2次方倍,所以在数字没有溢出的前提下,对于正数和负数,二进制左移n位就相当于乘以2的n次方。

/** 3 << 2 **/
3转化为二进制: 00000011
    移动补位:00001100
 转化为十进制:12

3 * 2 ^ 2 = 3 * 4 = 12

为什么没有无符号左移<<<?
因为左位移是填补右边空出的位,符号位不影响它的值。

>>带符号右移

运算规则: 按二进制形式把所有的数字向右移动对应的位数,低位移出(舍弃),高位的空位补符号位,即正数补零,负数补1。【补的位数全部是符号位】
语法格式: 需要移位的数字 >> 移位的次数
数学意义: 右移一位相当于除2,右移n位相当于除以2的n次方。商若为小数,取整即可。

/** 11 >> 2 **/     
11转化为二进制: 0000 1011
     移动补位:0000 0010
  转化为十进制:2

11 / 2^2 = 11 / 4 = 2

负数右移

例如: -100 >> 4【-100带符号右移4位】

        -100原码:00000000 00000000 00000000 01100100
        -100反码:11111111 11111111 11111111 10011011
        -100补码:11111111 11111111 11111111 10011100
右移4位,在高位补1:11111111 11111111 11111111 11111001

补码形式的移位完成后,结果不是移位后的结果,要根据补码写出原码才是最后的结果。

    减一:11111111 11111111 11111111 11111000
 按位取反:00000000 00000000 00000000 00000111
添加符号位:10000000 00000000 00000000 00000111
     结果:-7

>>>无符号右移

>>>运算符执行无符号右移位运算,它把无符号的 32 位整数所有数位整体右移。最左侧空位不再用符号位的值来填充,而是用 0 来填充。

// 对于无符号数或正数,无符号右移与有符号右移运算结果相同。
console.log(1000 >> 8);  // 3
console.log(1000 >>> 8);  // 3

console.log(-1000 >> 8);  // -4
console.log(-1000 >>> 8);  // 16777212

~

运算规则: 操作数被转换为32位二进制表示(0和1)。超过32位的数字将丢弃其最高有效位。
语法格式: ~ 操作数。
数学意义: 任何数 x 的运算结果都是-(x + 1)~-5运算结果为`4;

/** ~ 5 **/
5转化为二进制: 00000000 00000000 00000000 00000101
    位数取反: 11111111 11111111 11111111 11111010 【补码】
       反码:11111111 11111111 11111111 11111001
       原码:00000000 00000000 00000000 00000110
  添加符号位:10000000 00000000 00000000 00000110
 转化为十进制: -6

const a = 5;
console.log(~a); // -6

&

运算规则: 第一个操作数的的第n位与第二个操作数的第n位对比,如果都是1,那么第n位的结果为1,否则为0;同真为真,一假为假

5转换为二进制:00000000 00000000 00000000 00000101
3转换为二进制:00000000 00000000 00000000 00000011
 5 & 3 结果:00000000 00000000 00000000 00000001

5 & 3 结果:00000000 00000000 00000000 00000001, 转化为二进制是1;

|

运算规则: 第一个操作数的的第n位与第二个操作数的第n位对比,只要有一个是1,那么第n位的结果为1,否则为0;一真为真,同假为假

5转换为二进制:00000000 00000000 00000000 00000101
3转换为二进制:00000000 00000000 00000000 00000011
  5 | 3 结果:00000000 00000000 00000000 00000111

5 | 3 结果:00000000 00000000 00000000 00000111, 转化为二进制是7;

^异或

运算规则: 第一个操作数的的第n位与第二个操作数的第n位对比,如果相反那么第n位结果的为1,否则为0;同为假,异为真

5转换为二进制:00000000 00000000 00000000 00000101
3转换为二进制:00000000 00000000 00000000 00000011
  5 ^ 3 结果:00000000 00000000 00000000 00000110

5 ^ 3 结果:00000000 00000000 00000000 00000110, 转化为二进制是6;

实际应用

  • 判断奇偶
a & 1 == 0; // 偶数
a & 1 == 1; // 奇数
  • 交换两个数的值

异或运算有如下特性:a ^ b ^ a = b; a ^ b ^ b = a

x ^= y;
y ^= x;
x ^= y;
查看原文

赞 0 收藏 0 评论 0

时倾 发布了文章 · 2月22日

前端基础知识总结(四)- webpack

Webpack几个概念:

  • module: 模块,在webpack眼里,任何可以被导入导出的文件都是一个模块。
  • chunk: chunk是webpack拆分出来的:

    • entry chunk:每个入口文件都是一个chunk
    • 通过 import、require 引入的代码
    • children chunk:通过 splitChunks 拆分出来的代码
    • commons chunk: 通过CommonsChunkPlugin创建出来的文件
  • bundle: webpack打包出来的文件,也可以理解为就是对chunk编译压缩打包等处理后的产出。

webpack 优化

问题分析:

  • 核心问题:多页应用打包后代码冗余,文件体积大。
  • 根本原因:相同模块在不同入口之间没有得到复用,bundle之间比较独立。

解决思路:

  1. 解决代码冗余。把不同入口之间,共同引用的模块,抽离出来,放到一个公共模块中。这样不管这个模块被多少个入口引用,都只会在最终打包结果中出现一次。
  2. 减小文件体积。当把这些共同引用的模块都堆在一个模块中,这个文件可能异常巨大,也是不利于网络请求和页面加载的。所以我们需要把这个公共模块再按照一定规则进一步拆分成几个模块文件。

如何拆分,方式因人而异,因项目而异。拆分原则有:

  • 业务代码和第三方库分离打包,实现代码分割;
  • 业务代码中的公共业务模块提取打包到一个模块;
  • 第三方库最好也不要全部打包到一个文件中,因为第三方库加起来通常会很大。可以把特别大的库独立打包,剩下的加起来如果还很大,就把它按照一定大小切割成若干模块。

提取公共模块

通过将公共模块拆出来,最终合成的文件在最开始的时候加载一次,便存到缓存中供后续使用。这个带来速度上的提升,因为浏览器会迅速将公共的代码从缓存中取出来,而不是每次访问一个新页面时,再去加载一个更大的文件。

webpack提供了一个非常好的内置插件帮我们实现这一需求:CommonsChunkPlugin。不过在 webpack4 中CommonsChunkPlugin被删除,取而代之的是optimization.splitChunks

CommonsChunkPlugin

CommonsChunkPlugin 插件,是一个可选的用于建立一个独立文件(又称作 chunk)的功能,这个文件包括多个入口 chunk 的公共模块。

配置选项:
  • name:可以是已经存在的chunk(一般指入口文件)对应的name,那么就会把公共模块代码合并到这个chunk上;否则,会创建名字为name的commons chunk进行合并
  • filename:指定commons chunk的文件名
  • chunks:指定source chunk,即指定从哪些chunk当中去找公共模块,省略该选项的时候,默认就是entry chunks
  • children

    • 指定为true的时候,就代表source chunks是通过entry chunks(入口文件)进行code split出来的children chunks
    • children和chunks不能同时设置,因为它们都是指定source chunks的
    • children 可以用来把 entry chunk 创建的 children chunks 的共用模块合并到自身,但这会导致初始加载时间较长
  • async:即解决children:true时合并到entry chunks自身时初始加载时间过长的问题。async设为true时,commons chunk 将不会合并到自身,而是使用一个新的异步的commons chunk。当这个children chunk 被下载时,自动并行下载该commons chunk
  • minChunks:既可以是数字,也可以是函数,还可以是Infinity, 默认值是2
minChunks含义:
数字:模块被多少个chunk公共引用才被抽取出来成为commons chunk
函数:接受 (module, count) 两个参数,返回一个布尔值,你可以在函数内进行你规定好的逻辑来决定某个模块是否提取成为commons chunk
Infinity:只有当入口文件(entry chunks) >= 3 才生效,用来在第三方库中分离自定义的公共模块
基本使用

1. 分离出第三方库、自定义公共模块、webpack运行文件, 放在同一个文件中

修改webpack.config.js新增一个入口文件vendor, 使用CommonsChunkPlugin插件进行公共模块的提取:

const path = require("path");
const webpack = require("webpack");
const packageJson = require("./package.json");

module.exports = {
  entry: {
    first: './src/first.js',
    second: './src/second.js',
    // 新增一个入口文件vendor
    vendor: Object.keys(packageJson.dependencies)
  },
  output: {
    path: path.resolve(__dirname,'./dist'),
    filename: '[name].js'
  },
  plugins: [
    // 使用CommonsChunkPlugin插件进行公共模块的提取
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      filename: '[name].js'
    }),
  ]
}

生成dist文件夹下文件有:first.js, second.js, vendor.js。

通过查看vendor.js文件,发现first.js和second.js文件中依赖的第三方库和自定义公共模块都被打包进vendor.js中,同时还有webpack的运行文件。

2. 单独分离出第三方库、自定义公共模块、webpack运行文件

plugins: [
  // 抽离第三方库与webpack运行文件
  new webpack.optimize.CommonsChunkPlugin({
    name: ['vendor','runtime'], 
    // 创建runtime.js进行webpack运行文件的抽离,其中source chunks是vendor.js
    filename: '[name].js',
    minChunks: Infinity
  }),
  // 抽离自定义公共模块
  new webpack.optimize.CommonsChunkPlugin({
    name: 'common',
    filename: '[name].js',
    chunks: ['first','second'],// 从first.js和second.js中抽取commons chunk
  }),
]

生成dist文件夹下文件有:first.js, second.js, vendor.js, runtime.js, common.js

splitChunks

属性
  • cacheGroups: cacheGroupssplitChunks配置的核心,在cacheGroups缓存组里配置代码的拆分规则。缓存组的每一个属性都是一个配置规则, 例如配置default属性,属性名可以不叫default可以自己定。属性的值是一个对象。
  • name: 提取出来的公共模块将会以这个来命名,可以不配置,如果不配置,就会生成默认的文件名,大致格式是index/a.js这样的。
  • chunks: 指定哪些类型的chunk参与拆分,值可以是string可以是函数。如果是string,可以是这个三个值之一:all,async,initialall代表所有模块,async代表异步加载的模块, initial代表初始化时就能获取的模块。如果是函数,则可以根据chunk参数的name等属性进行更细致的筛选。
  • minChunks:splitChunks是自带默认配置的,而缓存组默认会继承这些配置,其中有个minChunks属性:

    1. 它控制的是每个模块什么时候被抽离出去:当模块被不同entry引用的次数大于等于这个配置值时,才会被抽离出去。
    2. 它的默认值是1。也就是任何模块都会被抽离出去(入口模块其实也会被webpack引入一次)。
  • minSize

    minSize设置生成文件的最小大小,单位是字节。如果一个模块符合之前所说的拆分规则,但是如果提取出来最后生成文件大小比minSize要小,那它不会被提取出来。这个属性可以在每个缓存组属性中设置,也可以在splitChunks属性中设置,在每个缓存组都会继承这个配置。

  • priority

    priority设置拆分规则的优先级,属性值为数字,可以为负数。当某个模块同时符合一个以上的规则时,通过优先级属性priority来决定使用哪个拆分规则。优先级高者执行。

  • test

    test设置缓存组选择的模块,与chunks属性的作用有一点像,但是维度不一样。test的值可以是一个正则表达式,也可以是一个函数。它可以匹配模块的绝对资源路径或chunk名称,匹配chunk名称时,将选择chunk中的所有模块。

实例

1. 实现代码分离:

//webpack.config.js
optimization: {
  splitChunks: {
    cacheGroups: {
      default: {
        name: 'common',
        chunks: 'initial',
        minChunks: 2, //模块被引用2次以上的才抽离
      }
    }
  }
}

进入dist目录查看:
common.js: 包含引用2次以上的所有模块

2. 分离第三方库与自定义组件库

//webpack.config.js
optimization: {
  splitChunks: {
    minSize: 300,  //提取出的chunk的最小大小
    cacheGroups: {
      default: {
        name: 'common',
        chunks: 'initial',
        minChunks: 2, //模块被引用2次以上的才抽离
        priority: -20,
      },
      // 拆分第三方库(通过npm|yarn安装的库)
      vendors: {
        test: /[\\/]node_modules[\\/]/,
        name: 'vendors',
        chunks: 'initial',
        priority: -10,
      },
      // 拆分指定文件
      locallib: {
         test: /(src\/locallib\.js)$/,
         name: 'locallib',
         chunks: 'initial',
         priority: -9
       }
    }
  }
}

进入dist目录查看:
common.jsvendor.js包含第三方库代码,locallib.js包含locallib模块的代码。

分离动态库

什么是DLL

DLL(Dynamic Link Library)文件为动态链接库文件。在Windows中,许多应用程序并不是一个完整的可执行文件,它们被分割成一些相对独立的动态链接库,即DLL文件,放置于系统中。当我们执行某一个程序时,相应的DLL文件就会被调用。

为什么使用DLL

通常来说,我们的代码都可以至少简单区分成业务代码和第三方库。如果不做处理,每次构建时都需要把所有的代码重新构建一次,耗费大量的时间。然后大部分情况下,很多第三方库的代码并不会发生变更(除非是版本升级),这时就可以用到dll:把复用性较高的第三方模块打包到动态链接库中,在不升级这些库的情况下,动态库不需要重新打包,每次构建只重新打包业务代码。

如何使用

DllPluginDllReferencePlugin 用某种方法实现了拆分 bundles,大幅度提升了构建的速度。使用DLL时,可以把构建过程分成dll构建过程和主构建过程,所以需要两个构建配置文件,例如叫做webpack.config.jswebpack.dll.config.js

1. 使用DLLPlugin打包需要分离到动态库的模块

DllPluginwebpack内置的插件,不需要额外安装,直接配置webpack.dll.config.js文件。此插件用于在单独的 webpack 配置中创建一个 dll-only-bundle,会生成一个名为 manifest.json 的文件,这个文件是用于让 DllReferencePlugin 能够映射到相应的依赖上。

  • context(可选): manifest 文件中请求的 context (默认值为 webpack 的 context)
  • format (boolean = false):如果为 true,则 manifest json 文件 (输出文件) 将被格式化。
  • name:暴露出的 DLL 的函数名(TemplatePaths[fullhash] & [name]
  • path:manifest.json 文件的 绝对路径(输出文件)
  • entryOnly (boolean = true):如果为 true,则仅暴露入口
  • type:dll bundle 的类型
我们建议 DllPlugin 只在 entryOnly: true 时使用,否则 DLL 中的 tree shaking 将无法工作,因为所有 exports 均可使用。
// webpack.dll.config.js

module.exports = {
  entry: {
    // 第三方库
    react: ['react', 'react-dom', 'react-redux']
  },
  output: {
    // 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称,
    filename: '[name].dll.js',
    path: resolve('dist/dll'),
    // library必须和后面dllplugin中的name一致
    library: '[name]_dll_[hash]'
  },
  plugins: [
    new webpack.DllPlugin({
      // 动态链接库的全局变量名称,需要和 output.library 中保持一致
      // 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值
      name: '[name]_dll_[hash]',
      // 描述动态链接库的 manifest.json 文件输出时的文件名称
      path: path.join(__dirname, 'dist/dll', '[name].manifest.json')
    }),
  ]
}

2. 在主构建配置文件使用DllReferencePlugin引用动态库文件

webpack.config.js中使用dll要用到DllReferencePlugin, 此插件会把 dll-only-bundles 引用到需要的预编译的依赖中。

  • context:(绝对路径) manifest (或者是内容属性)中请求的上下文
  • extensions:用于解析 dll bundle 中模块的扩展名 (仅在使用 'scope' 时使用)。
  • manifest :包含 contentname 的对象,或者是一个字符串 —— 编译时用于加载 JSON manifest 的绝对路径
  • content (可选): 请求到模块 id 的映射(默认值为 manifest.content
  • name (可选):dll 暴露地方的名称(默认值为 manifest.name)(可参考externals
  • scope (可选):dll 中内容的前缀
  • sourceType (可选):dll 是如何暴露的 (libraryTarget)

通过引用 dll 的 manifest 文件来把依赖的名称映射到模块的 id 上,之后再在需要的时候通过内置的 __webpack_require__ 函数来 require 对应的模块。

new webpack.DllReferencePlugin({
  context: __dirname,
  manifest: require('./dist/dll/react.manifest.json')
}),

第一步产出的manifest文件就用在这里,给主构建流程作为查找dll的依据:DllReferencePlugin去 manifest.json 文件读取 name 字段的值,把值的内容作为在从全局变量中获取动态链接库中内容时的全局变量名,因此:在 webpack.dll.config.js 文件中,DllPlugin 中的 name 参数必须和 output.library 中保持一致。

3. 在入口文件引入dll文件

生成的dll暴露出的是全局函数,因此还需要在入口文件里面引入对应的dll文件。

<body>
  <div id="app"></div>
  <!--引用dll文件-->
  <script data-original="../../dist/dll/react.dll.js"></script>
</body>

使用DLL作用

1.分离代码,业务代码和第三方模块可以被打包到不同的文件里,这个有几个好处:

  • 避免打包出单个文件的大小太大,不利于调试
  • 将单个大文件拆成多个小文件之后,一定情况下有利于加载(不超出浏览器一次性请求的文件数情况下,并行下载肯定比串行快)

2.提升构建速度。第三方库没有变更时,由于我们只构建业务相关代码,相比全部重新构建自然要快的多。

移除不必要的文件

moment.js日期处理库,占用很大的体积, 因为所有的locale文件都被引入,而这些文件在整个库的体积中占了大部分,因此当webpack打包时移除这部分内容会让打包文件的体积有所减小。

webpack自带的两个库可以实现这个功能:

  • IgnorePlugin
  • ContextReplacementPlugin

IgnorePlugin的使用方法如下:

// 插件配置
plugins: [
  // 忽略moment.js中所有的locale文件
  new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
],
  
// 使用方式
const moment = require('moment');
// 引入zh-cn locale文件
require('moment/locale/zh-cn');
moment.locale('zh-cn');

ContextReplacementPlugin的使用方法如下:

// 插件配置
plugins: [
  // 只加载locale zh-cn文件
  new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /zh-cn/),
],
  
// 使用方式
const moment = require('moment');
moment.locale('zh-cn');

模块化引入

在项目中使用了lodash这个很常用的工具库,然而在使用这类工具库的时候往往只使用到了其中的很少的一部分功能,但却把整个库都引入了。因此这里也可以进一步优化,只引用需要的部分。

import { chain, cloneDeep } from 'lodash';
// 或者
import chain from 'lodash/chain';
import cloneDeep from 'lodash/cloneDeep';

压缩混淆代码

我们平常也会对代码进行压缩混淆,可以通过UglifyJS等工具来对js代码进行压缩,同时可以去掉不必要的空格、注释、console信息等,也可以有效的减小代码体积。

webpack 基本功能

webpack hash区别

hash一般是结合CDN缓存来使用,通过webpack构建之后,生成对应文件名自动带上对应的MD5值。如果文件内容改变的话,那么对应文件哈希值也会改变,对应的HTML引用的URL地址也会改变,触发CDN服务器从源服务器上拉取对应数据,进而更新本地缓存。

  • hash

hash是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash值都会更改。同一次构建过程中生成的哈希都是一样的。

output:{
  path:path.join(__dirname, '/dist'),
  filename: 'bundle.[name].[hash].js',
}
  • chunkhash

根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值。把一些公共库和程序入口文件区分开,单独打包构建,接着我们采用chunkhash的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响。

output:{
  path:path.join(__dirname, '/dist/js'),
  filename: 'bundle.[name].[chunkhash].js',
}

采用chunkhash,项目主入口文件Index.js及其对应的依赖文件Index.css由于被打包在同一个模块,共用相同的chunkhash。由于公共库是不同的模块,有单独的chunkhash。所以Index文件的更改不会影响公共库。如果index.js更改了代码,css未改变,由于该模块发生了改变,导致css文件会重复构建。

  • contenthash

根据文件内容创建出唯一 hash。当文件内容发生变化时,[contenthash] 才会发生变化。

output: {
  filename: '[name].[contenthash].js',
  chunkFilename: '[name].[contenthash].js',
  path: path.resolve(__dirname, '../dist'),
}

模块热更新

模块热替换(hot module replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新所有类型的模块,而无需完全刷新。

实现

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');

{
  devServer: {
    contentBase: './dist',
    hot: true, // DevServer开启模块热替换模式
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'Hot Module Replacement',
    }),
  ],
}

filename 和 chunkFilename 的区别

filename列在entry 中,打包后输出的文件的名称。

chunkFilename未列在entry 中,却又需要被打包出来的文件的名称。默认使用 [id].js 或从 output.filename 中推断出的值([name] 会被预先替换为 [id] 或 [id].)。

// webpack.config.js

module.exports =  {
  entry: './src/index.js',
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
    chunkFileName: '[name].bundle.js'
  }
}

CSS Modules 模块化

现状

因为 css 不是编程语言,所以不能声明变量、函数,不能做判断、循环和计算,也不能嵌套。为了解决这个问题,衍生了两种拓展语言 lesssass,它们兼容 css,并且拓展了编程的功能,主要是带来了以下的特性:

  • 可以声明变量、函数,可以进行一些简单的计算、判断、循环;
  • 可以嵌套选择器,这样节省了书写的内容,也更具阅读性;
  • @import 避免重复导入问题,因此可以放心大胆的导入其他文件。

从模块化的角度来讲,lesssass 只是扩充了 css 的功能,但并没有在语言的层面做模块化,因为全局命名冲突的问题依然还在。

实现模块化

想要让 css 具备模块化功能,暂时还不能从语言的层面来考虑,所以只能从工具的角度来实现。目前比较好的方式是使用 js 来加载 css 文件,并将 css 的内容导出为一个对象,使用 js 来渲染整个 dom 树和匹配相应的样式到对应的元素。

css文件建议遵循如下原则

  • 不使用选择器与id,只使用 class 名来定义样式(因为只有 .class 才能导出为对象的属性)
  • 不层叠多个 class,只使用一个 class 把所有样式定义好
  • 所有样式通过 composes 组合来实现复用
  • 不嵌套
  • 推荐用 .className 书写,而非 .class-name(前者可以通过 styles.className 访问,后者需要通过 styles['class-name'] 才能访问)。

实例

/* dialog.css */
.root {}
.confirm {}
.disabledConfirm {}

js文件引入dialog.css, 使用 classnames 库来操作 class 名:

/* dialog.jsx */
import classNames from 'classnames';
import styles from './dialog.css';

export default class Dialog extends React.Component {
  render() {
    const cx = classNames({
      [styles.confirm]: !this.state.disabled,
      [styles.disabledConfirm]: this.state.disabled
    });

    return <div className={styles.root}>
      <a className={cx}>Confirm</a>
      ...
    </div>
  }
}
如果你不想频繁的输入 styles.**,可以试一下 react-css-modules,它通过高阶函数的形式来避免重复输入 styles.**

依赖webpack: css-loader

这个功能需要构建工具的支持,如果使用 webpack ,可以使用 css-loader,并设置 options.modulestrue, 便可使用模块化的功能了。
css-loader 解析@import和 url() ,会 import/require() 后再解析(resolve)它们。
css-loader配置项:

名称类型默认值描述
rootStringroot值将被添加到 URL 前面,然后再进行转译。因为对于以 / 开头的 URL,默认行为是不转译。
urlBooleantrue启用/禁用解析 url()
aliasObject{}给url创建别名。用别名重写你的 URL,在难以改变输入文件的url 路径时,这会很有帮助。
importBooleantrue启用/禁用 @import 处理
minimizeBoolean\Objectfalse启用/禁用 压缩
sourceMapBooleanfalse启用/禁用 Sourcemap
importLoadersNumber0在 css-loader 前应用的 loader 的数量
modulesBooleanfalse启用/禁用 CSS 模块
camelCaseBoolean\Stringfalse是否以驼峰化式命名导出类名
localIdentNameString[hash:base64]配置生成的类名标识符(ident)
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/i,
        loader: "css-loader",
        options: {
          modules: true,
        },
      },
    ],
  },
};

loader 和 plugin 的区别

loader

是一个转换器,将A文件进行编译成B文件,比如:将A.less转换为A.css,单纯的文件转换过程。
是一个导出为function的node模块。可以将匹配到的文件进行一次转换,同时loader可以链式传递。

plugin

是一个扩展器,通过钩子可以涉及整个构建流程,可以做一些在构建范围内的事情。
它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务。

常用loader

  • 样式:style-loader、css-loader、less-loader、sass-loader等
  • 文件:raw-loader、file-loader 、url-loader等
  • 编译:babel-loader、coffee-loader 、ts-loader等
  • 校验测试:mocha-loader、jshint-loader 、eslint-loader等

sass-loader转化sass为css文件,并且包一层module.exports成为一个js module。

css-loader解析@import和 url() 。

style-loader将创建一个style标签将css文件嵌入到html中。

vue-loader、coffee-loader、babel-loader等可以将特定文件格式转成js模块、将其他语言转化为js语言和编译下一代js语言。

file-loader可以处理资源,file-loader可以复制和放置资源位置,并可以指定文件名模板,用hash命名更好利用缓存。

url-loader可以处理资源, 将小于配置limit大小的文件转换成内敛Data Url的方式,减少请求。

raw-loader可以将文件以字符串的形式返回

imports-loader、exports-loader可以向模块注入变量或者提供导出模块功能。

常用Plugin

  • webpack内置UglifyJsPlugin,压缩和混淆代码。
  • webpack内置CommonsChunkPlugin,将指定的模块或公用模块打包出来,减少主bundle文件的体积,配合缓存策略,加快应用访问速度。
  • webpack 内置DllPluginDllReferencePlugin相互配合,前置第三方包的构建,只构建业务代码,同时能解决Externals多次引用问题。DllReferencePlugin引用DllPlugin配置生成的manifest.json文件,manifest.json包含了依赖模块和module id的映射关系
  • html-webpack-plugin可以根据模板自动生成html代码,并自动引用css和js文件
  • extract-text-webpack-plugin 将js文件中引用的样式单独抽离成css文件
  • HotModuleReplacementPlugin 热更新
  • optimize-css-assets-webpack-plugin 不同组件中重复的css可以快速去重
  • webpack-bundle-analyzer 一个webpack的bundle文件分析工具,将bundle文件以可交互缩放的treemap的形式展示。
  • compression-webpack-plugin 生产环境可采用gzip压缩JS和CSS
  • happypack:通过多进程模型,来加速代码构建
  • clean-wenpack-plugin 清理每次打包后没有使用的文件

webpack文件分离思想

现状

为什么要分离第三方库?

第三方库是比较稳定,不会轻易改变的,利用浏览器缓存后,用户再次加载页面会减少服务器请求,提高速度优化体验。提取多个应用(入口)公共模块的作用和他类似,公共部分会被缓存,所有应用都可以利用缓存内容从而提高性能。

分离第三方库就能利用浏览器换缓存了么?

答案是否定的。导致无法利用缓存的因素有很多,比如每次分离的库文件重新打包都会得到不同的名称,后台的同事给js文件设置的缓存过期时间为0,只要文件是完全不变的,包括修改时间,文件内容等,依然会利用缓存。

浏览器缓存机制是什么样的?

HTTP1.1给的策略是使用Cache-control配合Etag。
Apache中,ETag的值默认是对文件的索引节(INode),大小(Size)和最后修改时间(MTime)进行Hash后得到的。如果Etag相同,依然不会请求新资源,而会使用以前的文件。

文件分离插件

CommonsChunkPlugin与SplitChunksPlugin

作用

将公共模块抽离。每次打包的时候都会重新打包,还是会去处理一些第三方依赖库,只是它能把第三方库文件和我们的代码分开掉,生成一个独立的 js 文件。但是它还是不能提高打包的速度

自 webpack 4.0 上线之后,CommonsChunkPlugin 已被替换成 SplitChunksPlugin,旨在优化 chunk 的拆分。

CommonsChunkPlugin

设计思路:满足 minChunks 的引用次数时,都会将对应的模块抽离如一个新的 chunk 文件中,这个文件为所有的业务文件的父级。

这种设计思路带来了会造成模块打包冗余。总的来说会造成这么几个问题:

  • 产出的 chunk 在引入时,会包含重复的代码;
  • 无法优化异步 chunk;
  • 高优的 chunk 产出需要的 minchunks 配置比较复杂。

SplitChunksPlugin

SplitChunksPlugin 优化了 webpack 的打包策略,使用自动重复算法,会自动计算出各页面公共的包引用以及部分页面公共的包引用,当然,对于那些部分共有但是阈值过小的文件其不会创建单独的输出文件,因为其大小不值得去新开一个请求。(缓存策略配置在 cacheGroup 中)

SplitChunksPlugin 默认的分包策略基于以下 4 个条件:

  1. 新代码块可以被共享引用,或这些模块都是来自 node_modules;
  2. 新产出的 vendor-chunk 的大小得大于 30kb;
  3. 按需加载的代码块(vendor-chunk)并行请求的数量不多于 5 次;
  4. 初始加载的代码块,并行请求的数量不多于 3 次。
  • SplitChunksPlugin 配合使用 RuntimeChunk 对运行时的 hash 变动做优化(相当于 CommonsChunkPlugin 的两次使用)
  • 减少 maxInitial/AsyncRequest 会加大 module 的冗余,但是会进一步的减少请求。
DllPlugin与DllReferencePlugin

使用

DLLPlugin 这个插件是在一个额外独立的 webpack 设置中创建一个只有 dll 的 bundle,也就是说,除了 webpack.config.js,项目中还会新建一个 webpack.dll.config.js 文件来配置 dll 的打包。webpack.dll.config.js 作用是把所有的第三方库依赖打包到一个 bundle 的 dll 文件里面,还会生成一个名为 manifest.json 文件。该 manifest.json 的作用是用来让 DllReferencePlugin 映射到相关的依赖上去的。(可类比 CommonsChunkPlugin 的两次打包或者 RuntimeChunk 的运行包配置)

设计思路

DLLPlugin 是提前将公共的包构建出来,使得在 build 时过滤掉这些构建过的包,使得在正是构建时的速度缩短。所以其相对来说打包速度会更快

推荐使用策略

  • 如果是单页应用,只用DllPlugin打包库文件即可,业务代码一个包搞定。
  • 如果是多页应用,DllPlugin打包库文件,如果有很多公共的业务代码而且可能随时变动,就需要使用CommonsChunkPlugin提取公共业务代码。在页面间切换时,公共部分还是会被缓存的。

参考:
webpack 文件分离思想

webpack运行机制

webpack的运行过程可以简单概述为如下流程:

初始化配置参数 -> 绑定事件钩子回调 -> 确定Entry逐一遍历 -> 使用loader编译文件 -> 输出文件

webpack事件流

什么是webpack事件流?

Webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。Webpack 通过 Tapable 来组织这条复杂的生产线。 Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 Webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。 -- 吴浩麟《深入浅出webpack》

我们将webpack事件流理解为webpack构建过程中的一系列事件,他们分别表示着不同的构建周期和状态,我们可以像在浏览器上监听click事件一样监听事件流上的事件,并且为它们挂载事件回调。我们也可以自定义事件并在合适时机进行广播,这一切都是使用了webpack自带的模块 Tapable 进行管理的。我们不需要自行安装 Tapable ,在webpack被安装的同时它也会一并被安装,如需使用,我们只需要在文件里直接 require 即可。

Tapable的原理

Tapable的原理其实就是我们在前端进阶过程中都会经历的EventEmit,通过发布者-订阅者模式实现,它的部分核心代码可以概括成下面这样:

class SyncHook{
    constructor(){
        this.hooks = [];
    }

    // 订阅事件
    tap(name, fn){
        this.hooks.push(fn);
    }

    // 发布
    call(){
        this.hooks.forEach(hook => hook(...arguments));
    }
}

webpack运行流程详解

  • 首先,webpack会读取你在命令行传入的配置以及项目里的 webpack.config.js 文件,初始化本次构建的配置参数,并且执行配置文件中的插件实例化语句,生成Compiler传入plugin的apply方法,为webpack事件流挂上自定义钩子。
  • 接下来到了entryOption阶段,webpack开始读取配置的Entries,递归遍历所有的入口文件
  • Webpack进入其中一个入口文件,开始compilation过程。先使用用户配置好的loader对文件内容进行编译(buildModule),我们可以从传入事件回调的compilation上拿到module的resource(资源路径)、loaders(经过的loaders)等信息;之后,再将编译好的文件内容使用acorn解析生成AST静态语法树(normalModuleLoader),分析文件的依赖关系逐个拉取依赖模块并重复上述过程,最后将所有模块中的require语法替换成__webpack_require__来模拟模块化操作。
  • emit阶段,所有文件的编译及转化都已经完成,包含了最终输出的资源,我们可以在传入事件回调的compilation.assets 上拿到所需数据,其中包括即将输出的资源、代码块Chunk等等信息。

参考:
Webpack揭秘——走向高阶前端的必经之路

查看原文

赞 3 收藏 3 评论 1

时倾 发布了文章 · 2月8日

前端基础知识总结(三)

react 生命周期

React v16.0前的生命周期

初始化(initialization)阶段

此阶段只有一个生命周期方法:constructor。

constructor()

用来做一些组件的初始化工作,如定义this.state的初始内容。如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。

为什么必须先调用super(props)?

因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。

class Checkbox extends React.Component {
  constructor(props) {
    // 🔴 这时候还不能使用this
    super(props);
    // ✅ 现在开始可以使用this
    console.log(props);      // ✅ {}
    console.log(this.props); // ✅ {}
    this.state = {}; 
  }
}

为什么super要传 props?

props 传进 super 是必要的,这使得基类 React.Component 可以初始化 this.props。然而,即便在调用 super() 时没有传入 props 参数,你依然能够在 render 和其它方法中访问 this.props。其实是 React 在调用你的构造函数之后,马上又给实例设置了一遍 props

// React 内部
class Component {
  constructor(props) {
    this.props = props; // 初始化 this.props
    // ...
  }
}

// React 内部
const instance = new Button(props);
instance.props = props; // 给实例设置 props

// Button类组件
class Button extends React.Component {
  constructor(props) {
    super(); // 😬 我们忘了传入 props
    console.log(props);      // ✅ {}
    console.log(this.props); // 😬 undefined
  }
}

挂载(Mounting)阶段

此阶段生命周期方法:componentWillMount => render => componentDidMount

1. componentWillMount():

在组件挂载到DOM前调用,且只会被调用一次。
每一个子组件render之前立即调用;
在此方法调用this.setState不会引起组件重新渲染,也可以把写在这边的内容提前到constructor()中。

2. render(): class 组件唯一必须实现的方法

render 被调用时,它会检查 this.propsthis.state 的变化并返回以下类型之一:

  • React 元素。通常通过 JSX 创建。例如,<div /> 会被 React 渲染为 DOM 节点,<MyComponent /> 会被 React 渲染为自定义组件,无论是 <div /> 还是 <MyComponent /> 均为 React 元素。
  • 数组或 fragments。 使得 render 方法可以返回多个元素。Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点。
  • Portals。可以渲染子节点到不同的 DOM 子树中。Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。
  • 字符串或数值类型。它们在 DOM 中会被渲染为文本节点。
  • 布尔类型或 null。什么都不渲染。

render() 函数应该为纯函数,这意味着在不修改组件 state 的情况下,每次调用时都返回相同的结果,并且它不会直接与浏览器交互。不能在里面执行this.setState,会有改变组件状态的副作用。

3. componentDidMount

会在组件挂载后(插入 DOM 树中)立即调用, 且只会被调用一次。依赖于 DOM 节点的初始化应该放在这里。
render之后并不会立即调用,而是所有的子组件都render完之后才会调用。

更新(update)阶段

此阶段生命周期方法:componentWillReceiveProps => shouldComponentUpdate => componentWillUpdate => render => componentDidUpdate。

react组件更新机制

setState引起的state更新或父组件重新render引起的props更新,更新后的state和props相对之前无论是否有变化,都将引起子组件的重新render。

1. 父组件重新render

  1. 直接重新渲染。每当父组件重新render导致的重传props,子组件将直接跟着重新渲染,无论props是否有变化。可通过shouldComponentUpdate方法优化。
  2. 更新state再渲染。在componentWillReceiveProps方法中,将props转换成自己的state,调用 this.setState() 将不会引起第二次渲染。

    因为componentWillReceiveProps中判断props是否变化了,若变化了,this.setState将引起state变化,从而引起render,此时就没必要再做第二次因重传props引起的render了,不然重复做一样的渲染了。

2. 自身setState

组件本身调用setState,无论state有没有变化。可通过shouldComponentUpdate方法优化。

生命周期分析

1. componentWillReceiveProps(nextProps)

此方法只调用于props引起的组件更新过程中,响应 Props 变化之后进行更新的唯一方式。
参数nextProps是父组件传给当前组件的新props。根据nextProps和this.props来判断重传的props是否改变,以及做相应的处理。

2. shouldComponentUpdate(nextProps, nextState)

根据 shouldComponentUpdate() 的返回值,判断 React 组件的输出是否受当前 state 或 props 更改的影响。默认行为是 state 每次发生变化组件都会重新渲染。

当 props 或 state 发生变化时,shouldComponentUpdate() 会在渲染执行之前被调用。返回值默认为 true。首次渲染或使用 forceUpdate() 时不会调用该方法。

此方法可以将 this.propsnextProps 以及 this.statenextState 进行比较,返回true时当前组件将继续执行更新过程,返回false则跳过更新,以此可用来减少组件的不必要渲染,优化组件性能。请注意,返回 false 并不会阻止子组件在 state 更改时重新渲染。

  • 如果在componentWillReceiveProps()中执行了this.setState,更新state,但在render前(如shouldComponentUpdate,componentWillUpdate),this.state依然指向更新前的state,不然nextState及当前组件的this.state的对比就一直是true了。
  • 应该考虑使用内置的 PureComponent 组件,而不是手动编写 shouldComponentUpdate()PureComponent 会对 props 和 state 进行浅层比较,并减少了跳过必要更新的可能性。

3. componentWillUpdate(nextProps, nextState)

此方法在调用render方法前执行,在这边可执行一些组件更新发生前的工作,一般较少用。

4. render

render同上

5. componentDidUpdate(prevProps, prevState)

此方法在组件更新后立即调用,可以操作组件更新的DOM。
prevProps和prevState这两个参数指的是组件更新前的props和state。

卸载阶段

此阶段只有一个生命周期方法:componentWillUnmount

componentWillUnmount

此方法在组件被卸载前调用,可以在这里执行一些清理工作,比如清楚组件中使用的定时器,清楚componentDidMount中手动创建的DOM元素等,以避免引起内存泄漏。
componentWillUnmount()不应调用 setState(),因为该组件将永远不会重新渲染。组件实例卸载后,将永远不会再挂载它。

React v16.0 后的生命周期

React v16.0刚推出的时候,增加了一个componentDidCatch生命周期函数,这只是一个增量式修改,完全不影响原有生命周期函数;

React v16.3,引入了两个新的生命周期:getDerivedStateFromProps,getSnapshotBeforeUpdate, 废弃掉componentWillMount、componentWillReceiveProps 以及 componentWillUpdate 三个周期(直到React 17前还可以使用,只是会有一个警告)。

为什么要更改生命周期?

生命周期函数的更改是因为 16.3 采用了 Fiber 架构,在新的 Fiber 架构中,组件的更新分为了两个阶段:

  1. render phase:这个阶段决定究竟哪些组件会被更新。
  2. commit phase:这个阶段是 React 开始执行更新(比如插入,移动,删除节点)。

commit phase 的执行很快,但是真实 DOM 的更新很慢,所以 React 在更新的时候会暂停再恢复组件的更新以免长时间的阻塞浏览器,这就意味着 render phase 可能会被执行多次(因为有可能被打断再重新执行)。

  • constructor
  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate
  • getDerivedStateFromProps
  • shouldComponentUpdate
  • render

这些生命周期都属于 render phase,render phase 可能被多次执行,所以要避免在 render phase 中的生命周期函数中引入副作用。在 16.3 之前的生命周期很容易引入副作用,所以 16.3 之后引入新的生命周期来限制开发者引入副作用。

getDerivedStateFromProps(nextProps, prevState)

React v16.3中,static getDerivedStateFromProps只在组件创建和由父组件引发的更新中调用。如果不是由父组件引发,那么getDerivedStateFromProps也不会被调用,如自身setState引发或者forceUpdate引发。

在React v16.4中改正了这一点,static getDerivedStateFromProps会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。

特点:

  • 无副作用 。因为是处于 Fiber 的 render 阶段,所以有可能会被多次执行。所以 API 被设计为了静态函数,无法访问到实例的方法,也没有 ref 来操作 DOM,这就避免了实例方法带来副作用的可能性。但是依旧可以从 props 中获得方法触发副作用,所以在执行可能触发副作用的函数前要三思。
  • 只用来更新 state 。其这个生命周期唯一的作用就是从 nextProps 和 prevState 中衍生出一个新的 state。它应返回一个对象来更新 state,或者返回null来不更新任何内容。
  • getDerivedStateFromProps前面要加上static保留字,声明为静态方法,不然会被react忽略掉。
  • getDerivedStateFromProps里面的this为undefined。

    static静态方法只能Class来调用,而实例是不能调用,所以React Class组件中,静态方法getDerivedStateFromProps无权访问Class实例的this,即this为undefined。

getSnapshotBeforeUpdate()

getSnapshotBeforeUpdate() 只会调用一次,在最近一次渲染输出(提交到 DOM 节点)之前调用,,所以在这个生命周期能够获取这一次更新前的 DOM 的信息。此生命周期的任何返回值将作为 componentDidUpdate() 的第三个参数 “snapshot” 参数传递, 否则componentDidUpdate的第三个参数将为 undefined。应返回 snapshot 的值(或 null)。

错误处理

当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用如下方法:

  • static getDerivedStateFromError():此生命周期会在后代组件抛出错误后被调用。 它将抛出的错误作为参数,并返回一个值以更新 state
  • componentDidCatch():此生命周期在后代组件抛出错误后被调用,它应该用于记录错误之类的情况。

    它接收两个参数:

    1. error —— 抛出的错误。
    2. info —— 带有 componentStack key 的对象

生命周期比较

16.0 前生命周期

image

16.0 后生命周期:
image

参考:
浅析 React v16.3 新生命周期函数

react 16做了哪些更新

  1. react作为一个ui库,将前端编程由传统的命令式编程转变为声明式编程,即所谓的数据驱动视图。如果直接更新真实dom,比如将生成的html直接采用innerHtml替换,会带来重绘重排之类的性能问题。为了尽量提高性能,React团队引入了虚拟dom,即采用js对象来描述dom树,通过对比前后两次的虚拟对象,来找到最小的dom操作(vdom diff),以此提高性能。
  2. 上面提到的reactDom diff,在react 16之前,这个过程我们称之为stack reconciler,它是一个递归的过程,在树很深的时候,单次diff时间过长会造成JS线程持续被占用,用户交互响应迟滞,页面渲染会出现明显的卡顿,这在现代前端是一个致命的问题。所以为了解决这种问题,react 团队对整个架构进行了调整,引入了fiber架构,将以前的stack reconciler替换为fiber reconciler。采用增量式渲染。引入了任务优先级(expiration)requestIdleCallback的循环调度算法,简单来说就是将以前的一根筋diff更新,首先拆分成两个阶段:reconciliationcommit;第一个reconciliation阶段是可打断的,被拆分成一个个的小任务(fiber),在每一侦的渲染空闲期做小任务diff。然后是commit阶段,这个阶段是不拆分且不能打断的,将diff节点的effectTag一口气更新到页面上。
  3. 由于reconciliation是可以被打断的,且存在任务优先级的问题,所以会导致commit前的一些生命周期函数多次被执行, 如componentWillMount、componentWillReceiveProps 和 componetWillUpdate,但react官方已声明,在React17中将会移除这三个生命周期函数。
  4. 由于每次唤起更新是从根节点(RootFiber)开始,为了更好的节点复用与性能优化。在react中始终存workInprogressTree(future vdom) 与 oldTree(current vdom)两个链表,两个链表相互引用。这无形中又解决了另一个问题,当workInprogressTree生成报错时,这时也不会导致页面渲染崩溃,而只是更新失败,页面仍然还在。

React hooks原理

在React 16前,函数式组件不能拥有状态管理?因为16以前只有类组件有对应的实例,而16以后Fiber 架构的出现,让每一个节点都拥有对应的实例,也就拥有了保存状态的能力。

Hooks的本质就是闭包两级链表

闭包是指有权访问另一个函数作用域中变量或方法的函数,创建闭包的方式就是在一个函数内创建闭包函数,通过闭包函数访问这个函数的局部变量, 利用闭包可以突破作用链域的特性,将函数内部的变量和方法传递到外部。

hooks 链表

一个组件包含的hooks 以链表的形式存储在fiber节点的memoizedState属性上,currentHook链表就是当前正在遍历的fiber节点的。nextCurrentHook 就是即将被添加到正在遍历fiber节点的hooks的新链表。

let currentHook: Hook | null = null;
let nextCurrentHook: Hook | null = null;

type Hooks = {
  memoizedState: any, // 指向当前渲染节点 Fiber
  baseState: any, // 初始化 initialState, 最新的state
  baseUpdate: Update<any> | null,
  // 当前需要更新的 Update ,每次更新完之后,会赋值上一个 update,方便 react 在渲染错误的边缘,数据回溯
  queue: UpdateQueue<any> | null,// 可以让state变化的,即update或dispach产生的update
  next: Hook | null, // link 到下一个 hooks
}

state

其实state不是hooks独有的,类操作的setState也存在。

memoizedState,cursor 是存在哪里的?如何和每个函数组件一一对应的?
react 会生成一棵组件树(或Fiber 单链表),树中每个节点对应了一个组件,hooks 的数据就作为组件的一个信息,存储在这些节点上,伴随组件一起出生,一起死亡。

为什么只能在函数最外层调用 Hook?
memoizedState 是按 hook定义的顺序来放置数据的,如果 hook 顺序变化,memoizedState 并不会感知到。

自定义的 Hook 是如何影响使用它的函数组件的?
共享同一个 memoizedState,共享同一个顺序。

“Capture Value” 特性是如何产生的?
每一次 ReRender 的时候,都是重新去执行函数组件了,对于之前已经执行过的函数组件,并不会做任何操作。

react setState 异步更新

setState 实现原理

setState 通过一个队列机制来实现 state 更新,当执行 setState() 时,会将需要更新的 state 浅合并后放入 状态队列,而不会立即更新 state,队列机制可以高效的批量更新 state。如果不通过setState,直接修改this.state 的值,则不会放入状态队列,当下一次调用 setState 对状态队列进行合并时,之前对 this.state 的修改将会被忽略,造成无法预知的错误。

setState()有的同步有的异步?

在React中, 如果是由React引发的事件处理(比如通过onClick引发的事件处理),调用setState不会同步更新this.state,除此之外的setState调用会同步执行this.state 。所谓“除此之外”,指的是绕过React通过addEventListener直接添加的事件处理函数,还有通过setTimeout/setInterval产生的异步调用。

原因: 在React的setState函数实现中,会根据一个变量isBatchingUpdates判断是直接更新this.state还是放到队列中回头再说,而isBatchingUpdates默认是false,也就表示setState会同步更新this.state,但是,有一个函数batchedUpdates,这个函数会把isBatchingUpdates修改为true,而当React在调用事件处理函数之前就会调用这个batchedUpdates,造成的后果,就是由React控制的事件处理过程setState不会同步更新this.state

setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。

调用风险

当调用 setState 时,实际上是会执行 enqueueSetState 方法,并会对 partialState_pendingStateQueue 队列进行合并操作,最终通过 enqueueUpdate 执行 state 更新。

performUpdateIfNecessary 获取 _pendingElement _pendingStateQueue_pendingForceUpdate,并调用 reaciveComponentupdateComponent 来进行组件更新。

但,如果在 shouldComponentUpdatecomponentWillUpdate 方法里调用 this.setState 方法,就会造成崩溃。 这是因为在 shouldComponentUpdatecomponentWillUpdate 方法里调用 this.setState 时,this._pendingStateQueue!=null,则 performUpdateIfNecessary 方法就会调用 updateComponent 方法进行组件更新,而 updateComponent 方法又会调用 shouldComponentUpdatecomponentWillUpdate 方法,因此造成循环调用,使得浏览器内存占满后崩溃。

React Fiber

掉帧:在页面元素很多,且需要频繁刷新的场景下,React 15 会出现掉帧的现象,其根本原因,是大量的同步计算任务阻塞了浏览器的 UI 渲染。默认情况下,JS 运算、页面布局和页面绘制都是运行在浏览器的主线程当中,他们之间是互斥的关系。如果 JS 运算持续占用主线程,页面就没法得到及时的更新。当我们调用setState更新页面的时候,React 会遍历应用的所有节点,计算出差异,然后再更新 UI,整个过程不能被打断。如果页面元素很多,整个过程占用的时机就可能超过 16 毫秒,就容易出现掉帧的现象。

如何解决主线程长时间被 JS 运算?将JS运算切割为多个步骤,分批完成。在完成一部分任务之后,将控制权交回给浏览器,让浏览器有时间进行页面的渲染。等浏览器忙完之后,再继续之前未完成的任务。

React 15 及以下版本通过递归的方式进行渲染,使用的是 JS 引擎自身的函数调用栈,它会一直执行到栈空为止。而Fiber实现了自己的组件调用栈,它以链表的形式遍历组件树,可以灵活的暂停、继续和丢弃执行的任务。实现方式是使用了浏览器的requestIdleCallback

window.requestIdleCallback()会在浏览器空闲时期依次调用函数,这就可以让开发者在主事件循环中执行后台或低优先级的任务,而且不会对像动画和用户交互这些延迟触发但关键的事件产生影响。函数一般会按先进先调用的顺序执行,除非函数在浏览器调用它之前就到了它的超时时间。

React 框架内部的运作可以分为 3 层:

  • Virtual DOM 层,描述页面长什么样。
  • Reconciler 层,负责调用组件生命周期方法,进行 Diff 运算等。
  • Renderer 层,根据不同的平台,渲染出相应的页面,比较常见的是 ReactDOM 和 ReactNative。

Fiber 表征reconciliation阶段所能拆分的最小工作单元,其实指的是一种链表树,它可以用一个纯 JS 对象来表示:

const fiber = {
  stateNode: {},    // 节点实例
  child: {},        // 子节点
  sibling: {},      // 兄弟节点
  return: {},       // 表示处理完成后返回结果所要合并的目标,通常指向父节点
};

Reconciler区别

  • 以前的 Reconciler 被命名为Stack Reconciler。Stack Reconciler 运作的过程是不能被打断的,必须一条道走到黑;
  • Fiber Reconciler 每执行一段时间,都会将控制权交回给浏览器,可以分段执行;

Stack ReconcilerFiber Reconciler,源码层面其实就是干了一件递归改循环的事情。

scheduling(调度)

scheduling(调度)是fiber reconciliation的一个过程,主要是进行任务分配,达到分段执行。任务的优先级有六种:

  • synchronous,与之前的Stack Reconciler操作一样,同步执行
  • task,在next tick之前执行
  • animation,下一帧之前执行
  • high,在不久的将来立即执行
  • low,稍微延迟执行也没关系
  • offscreen,下一次render时或scroll时才执行

优先级高的任务(如键盘输入)可以打断优先级低的任务(如Diff)的执行,从而更快的生效。

Fiber Reconciler 在执行过程中,会分为 2 个阶段:

  • 阶段一,生成 Fiber 树,得出需要更新的节点信息。这一步是一个渐进的过程,可以被打断。
  • 阶段二,将需要更新的节点一次过批量更新,这个过程不能被打断。

阶段一可被打断的特性,让优先级更高的任务先执行,从框架层面大大降低了页面掉帧的概率。

参考:
React Fiber 原理介绍
React Fiber

HOC 与render props区别

Render Props: 把将要包裹的组件作为props属性传入,然后容器组件调用这个属性,并向其传参。

实现方式:

1.通过props.children(props),props.children返回的是UI元素。<RenderProps> JSX 标签中的所有内容都会作为一个 children prop 传递给 RenderProps组件。因为 RenderProps{props.children} 渲染在一个 <div> 中,被传递的这些子组件最终都会出现在输出结果中。

// 定义
const RenderProps = props => <div>
   {props.children(props)}
</div>

// 调用
<RenderProps>
    {() => <>Hello RenderProps</>}
</RenderProps>

2.通过props中的任何函数, 自行定义传入内容

// 定义
const LoginForm = props => {
  const flag = false;
  const allProps = { flag, ...props };
  
  if (flag) {
    return <>{props.login(allProps)}</>
  } else {
    return <>{props.notLogin(allProps)}</>
  }
}

// 调用
<LoginForm
  login={() => <h1>LOGIN</h1>}
  noLogin={() => <h1>NOT LOGIN</h1>}
/>
优点:
1、支持ES6
2、不用担心props命名问题,在render函数中只取需要的state
3、不会产生无用的组件加深层级
4、render props模式的构建都是动态的,所有的改变都在render中触发,可以更好的利用组件内的生命周期。

HOC: 接受一个组件作为参数,返回一个新的组件的函数。

class Home extends React.Component {
  // UI
}

export default Connect()(Home);

高阶组件由于每次都会返回一个新的组件,对于react来说,这是不利于diff和状态复用的,所以高阶组件的包装不能在render 方法中进行,而只能像上面那样在组件声明时包裹,这样也就不利于动态传参。

优点:
1、支持ES6
2、复用性强,HOC为纯函数且返回值为组件,可以多层嵌套
3、支持传入多个参数,增强了适用范围
缺点:
1、当多个HOC一起使用时,无法直接判断子组件的props是哪个HOC负责传递的
2、多个组件嵌套,容易产生同样名称的props
3、HOC可能会产生许多无用的组件,加深了组件的层级

总的来说,render props其实和高阶组件类似,就是在puru component上增加state,响应react的生命周期。

React 通信

react的数据流是单向的,最常见的就是通过props由父组件向子组件传值。

  • 父向子通信: 传入props
  • 子向父通信:父组件向子组件传一个函数,然后通过这个函数的回调,拿到子组件传过来的值
  • 父向孙通信:利用context传值。React.createContext()
  • 兄弟间通信:

​ 1、找一个相同的父组件,既可以用props传递数据,也可以用context的方式来传递数据。
​ 2、用一些全局机制去实现通信,比如redux等
​ 3、发布订阅模式

react合成事件

React 合成事件(SyntheticEvent)是 React 模拟原生 DOM 事件所有能力的一个事件对象,即浏览器原生事件的跨浏览器包装器。

为什么要使用合成事件?

  1. 进行浏览器兼容,实现更好的跨平台
    React 采用的是顶层事件代理机制,能够保证冒泡一致性,可以跨浏览器执行。React 提供的合成事件用来抹平不同浏览器事件对象之间的差异,将不同平台事件模拟合成事件。
  2. 避免垃圾回收
    事件对象可能会被频繁创建和回收,因此 React 引入事件池,在事件池中获取或释放事件对象。即 React 事件对象不会被释放掉,而是存放进一个数组中,当事件触发,就从这个数组中弹出,避免频繁地去创建和销毁(垃圾回收)
  3. 方便事件统一管理和事务机制

实现原理
在 React 中,“合成事件”会以事件委托方式绑定在 document 对象上,并在组件卸载(unmount)阶段自动销毁绑定的事件。

合成事件和原生事件
当真实 DOM 元素触发事件,会冒泡到 document 对象后,再处理 React 事件;所以会先执行原生事件,然后处理 React 事件;最后真正执行 document 上挂载的事件。
合成事件和原生事件最好不要混用。 原生事件中如果执行了stopPropagation方法,则会导致其他React事件失效。因为所有元素的事件将无法冒泡到document上,所有的 React 事件都将无法被注册。

合成事件的事件池
合成事件对象池,是 React 事件系统提供的一种性能优化方式合成事件对象在事件池统一管理不同类型的合成事件具有不同的事件池

react 虚拟dom

什么是虚拟dom?

在 React 中,render 执行的结果得到的并不是真正的 DOM 节点,而是轻量级的 JavaScript 对象,我们称之为 virtual DOM。它通过JS的Object对象模拟DOM中的节点,然后再通过特定的render方法将其渲染成真实的DOM节点。

虚拟 DOM 是 React 的一大亮点,具有batching(批处理) 和高效的 Diff 算法。batching 把所有的 DOM 操作搜集起来,一次性提交给真实的 DOM。diff 算法时间复杂度也从标准的的 Diff 算法的 O(n^3) 降到了 O(n)。

batching(批处理)
主要思想是,无论setState您在React事件处理程序同步生命周期方法中进行多少次调用,它都将被批处理成一个更新, 最终只有一次重新渲染。

虚拟 DOM 与 原生 DOM

如果没有 Virtual DOM,就需要直接操作原生 DOM。在一个大型列表所有数据都变了的情况下,直接重置 innerHTML还算合理,但是,只有一行数据发生变化时,它也需要重置整个 innerHTML,这就造成了大量浪费。

innerHTML 和 Virtual DOM 的重绘性能消耗:
innerHTML: render html string + 重新创建所有 DOM 元素
Virtual DOM: render Virtual DOM + diff + 必要的 DOM 更新

Virtual DOM render + diff 显然比渲染 html 字符串要慢,但是它依然是纯 js 层面的计算,比起后面的 DOM 操作来说,依然便宜了太多。innerHTML 的总计算量不管是 js 计算还是 DOM 操作都是和整个界面的大小相关,但 Virtual DOM 的计算量只有 js 计算和界面大小相关,DOM 操作是和数据的变动量相关。

Real DOMVirtual DOM
1. 更新缓慢。1. 更新更快。
2. 可以直接更新 HTML。2. 无法直接更新 HTML。
3. 如果元素更新,则创建新DOM。3. 如果元素更新,则更新 JSX 。
4. DOM操作代价很高。4. DOM 操作非常简单。
5. 消耗的内存较多。5. 很少的内存消耗。

虚拟 DOM 与 MVVM

相比起 React,其他 MVVM 系框架比如 Angular, Knockout , Vue ,Avalon 采用的都是数据绑定。通过 Directive/Binding 对象,观察数据变化并保留对实际 DOM 元素的引用,当有数据变化时进行对应的操作。MVVM 的变化检查是数据层面的,而 React 的检查是 DOM 结构层面的。

MVVM 的性能也根据变动检测的实现原理有所不同:Angular 依赖于脏检查;Knockout/Vue/Avalon 采用了依赖收集。

  • 脏检查:scope digest(watcher count) ) + 必要 DOM 更新
  • 依赖收集:重新收集依赖(data change) ) + 必要 DOM 更新

Angular 最不效率的地方在于任何小变动都有的和 watcher 数量相关的性能代价,当所有数据都变了的时候,Angular更有效。依赖收集在初始化和数据变化的时候都需要重新收集依赖,这个代价在小量更新的时候几乎可以忽略,但在数据量庞大的时候也会产生一定的消耗。

性能比较

在比较性能的时候,要分清楚初始渲染、小量数据更新、大量数据更新这些不同的场合。Virtual DOM、脏检查 MVVM、数据收集 MVVM 在不同场合各有不同的表现和不同的优化需求。Virtual DOM 为了提升小量数据更新时的性能,也需要针对性的优化,比如 shouldComponentUpdate 或是 immutable data。

  • 初始渲染:Virtual DOM > 脏检查 >= 依赖收集
  • 小量数据更新:依赖收集 >> Virtual DOM + 优化 > 脏检查(无法优化)> Virtual DOM 无优化
  • 大量数据更新:脏检查 + 优化 >= 依赖收集 + 优化 > Virtual DOM(无法/无需优化)>> MVVM 无优化

diff 算法

传统 diff 算法通过循环递归对节点进行依次对比,算法复杂度达到 O(n^3),其中 n 是树中节点的总数。O(n^3) 意味着如果要展示1000个节点,就要依次执行上十亿次的比较, 这是无法满足现代前端性能要求的。

diff 算法主要包括几个步骤:

  • 用 JS 对象的方式来表示 DOM 树的结构,然后根据这个对象构建出真实的 DOM 树,插到文档中。
  • 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树的差异, 最后把所记录的差异应用到所构建的真正的DOM树上,视图更新。

diff 策略

React 通过制定大胆的diff策略,将diff算法复杂度从 O(n^3) 转换成 O(n)

  • React 通过分层求异的策略,对 tree diff 进行算法优化;
  • React 通过相同类生成相似树形结构,不同类生成不同树形结构的策略,对 component diff 进行算法优化;
  • React 通过设置唯一 key的策略,对 element diff 进行算法优化;

tree diff(层级比较)

React 对树进行分层比较,两棵树只会对同一层次的节点进行比较。
当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会进行进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。
当出现节点跨层级移动时,并不会出现移动操作,而是以该节点为根节点的树被重新创建,这是一种影响 React 性能的操作,因此 React 官方建议不要进行 DOM 节点跨层级的操作

  • 先进行树结构的层级比较,对同一个父节点下的所有子节点进行比较;
  • 接着看节点是什么类型的,是组件就做 Component Diff;
  • 如果节点是标签或者元素,就做 Element Diff;
注意:在开发组件时,保持稳定的 DOM 结构会有助于性能的提升。例如,可以通过 CSS 隐藏或显示节点,而不是真的移除或添加 DOM 节点。

component diff(组件比较)

  • 如果是同一类型的组件,按照原策略继续比较 virtual DOM tree。
  • 如果不是,则将该组件判断为 dirty component,替换整个组件下的所有子节点。举个例子,当一个元素从 <Article> 变成 <Comment>会触发一个完整的重建流程。

对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间。因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。

对于两个不同类型但结构相似的组件,不会比较二者的结构,而且替换整个组件的所有内容。不同类型的 component 是很少存在相似 DOM tree 的机会,因此这种极端因素很难在实现开发过程中造成重大影响的。

element diff (元素比较)

当节点处于同一层级时,React diff 提供了三种节点操作,分别为:INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)。

  • INSERT_MARKUP,新的 component 类型不在老集合里, 即是全新的节点,需要对新节点执行插入操作。
  • MOVE_EXISTING,在老集合有新 component 类型,且 element 是可更新的类型,这种情况下需要做移动操作,可以复用以前的 DOM 节点。
  • REMOVE_NODE,老 component 类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者老 component 不在新集合里的,也需要执行删除操作。
<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

React 并不会意识到应该保留<li>Duke</li><li>Villanova</li>,而是会重建每一个子元素,不会进行移动 DOM 操作。

key 优化

为了解决上述问题,React 引入了 key 属性, 对同一层级的同组子节点,添加唯一 key 进行区分。

当子元素拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素。如果有相同的节点,无需进行节点删除和创建,只需要将老集合中节点的位置进行移动,更新为新集合中节点的位置。

  • 在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。
  • key 不需要全局唯一,但在列表中需要保持唯一。
  • Key 应该具有稳定,可预测,以及列表内唯一的特质。不稳定的 key(比如通过 Math.random() 生成的)会导致许多组件实例和 DOM 节点被不必要地重新创建,这可能导致性能下降和子组件中的状态丢失。

react与vue区别

1. 监听数据变化的实现原理不同

Vue通过 getter/setter以及一些函数的劫持,能精确知道数据变化。
React默认是通过比较引用的方式(diff)进行的,如果不优化可能导致大量不必要的VDOM的重新渲染。

2. 数据流不同

Vue1.0中可以实现两种双向绑定:父子组件之间props可以双向绑定;组件与DOM之间可以通过v-model双向绑定。
Vue2.x中父子组件之间不能双向绑定了(但是提供了一个语法糖自动帮你通过事件的方式修改)。
React一直不支持双向绑定,提倡的是单向数据流,称之为onChange/setState()模式。

3. HoC和mixins

Vue组合不同功能的方式是通过mixin,Vue中组件是一个被包装的函数,并不简单的就是我们定义组件的时候传入的对象或者函数。
React组合不同功能的方式是通过HoC(高阶组件)。

4. 模板渲染方式的不同

模板的语法不同,React是通过JSX渲染模板, Vue是通过一种拓展的HTML语法进行渲染。
模板的原理不同,React通过原生JS实现模板中的常见语法,比如插值,条件,循环等。而Vue是在和组件JS代码分离的单独的模板中,通过指令来实现的,比如 v-if 。

举个例子,说明React的好处:react中render函数是支持闭包特性的,所以我们import的组件在render中可以直接调用。但是在Vue中,由于模板中使用的数据都必须挂在 this 上进行一次中转,所以我们import 一个组件完了之后,还需要在 components 中再声明下。

5. 渲染过程不同

Vue可以更快地计算出Virtual DOM的差异,这是由于它会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。
React当状态被改变时,全部子组件都会重新渲染。通过shouldComponentUpdate这个生命周期方法可以进行控制,但Vue将此视为默认的优化。

6. 框架本质不同

Vue本质是MVVM框架,由MVC发展而来;
React是前端组件化框架,由后端组件化发展而来。

性能优化

1. 静态资源使用 CDN

CDN是一组分布在多个不同地理位置的 Web 服务器。当服务器离用户越远时,延迟越高。

2. 无阻塞

头部内联的样式和脚本会阻塞页面的渲染,样式放在头部并使用link方式引入,脚本放在尾部并使用异步方式加载

3. 压缩文件

压缩文件可以减少文件下载时间。

  1. 在 webpack 可以使用如下插件进行压缩:
    JavaScript:UglifyPlugin
    CSS :MiniCssExtractPlugin
    HTML:HtmlWebpackPlugin
  2. 使用 gzip 压缩。通过向 HTTP 请求头中的 Accept-Encoding 头添加 gzip 标识来开启这一功能。

4. 图片优化

  1. 图片懒加载
  2. 响应式图片:浏览器根据屏幕大小自动加载合适的图片。
  3. 降低图片质量:方法有两种,一是通过 webpack 插件 image-webpack-loader,二是通过在线网站进行压缩。

5. 减少重绘重排

  • 降低 CSS 选择器的复杂性
  • 使用 transform 和 opacity 属性更改来实现动画
  • 用 JavaScript 修改样式时,最好不要直接写样式,而是替换 class 来改变样式。
  • 如果要对 DOM 元素执行一系列操作,可以将 DOM 元素脱离文档流,修改完成后,再将它带回文档。推荐使用隐藏元素(display:none)或文档碎片(DocumentFragement),都能很好的实现这个方案。

6. 使用 requestAnimationFrame 来实现视觉变化

window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行

7. webpack 打包, 添加文件缓存

index.html 设置成 no-cache,这样每次请求的时候都会比对一下 index.html 文件有没变化,如果没变化就使用缓存,有变化就使用新的 index.html 文件。
其他所有文件一律使用长缓存,例如设置成缓存一年 maxAge: 1000 * 60 * 60 * 24 * 365
前端代码使用 webpack 打包,根据文件内容生成对应的文件名,每次重新打包时只有内容发生了变化,文件名才会发生变化。

  • max-age: 设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。在这个时间前,浏览器读取文件不会发出新请求,而是直接使用缓存。
  • 指定 no-cache 表示客户端可以缓存资源,每次使用缓存资源前都必须重新验证其有效性

输入url后发生了什么

  1. DNS域名解析;
  2. 建立TCP连接(三次握手);
  3. 发送HTTP请求;
  4. 服务器处理请求;
  5. 返回响应结果;
  6. 关闭TCP连接(四次握手);
  7. 浏览器解析HTML;
  8. 浏览器布局渲染;

1. DNS域名解析: 拿到服务器ip

客户端收到你输入的域名地址后,它首先去找本地的hosts文件,检查在该文件中是否有相应的域名、IP对应关系,如果有,则向其IP地址发送请求,如果没有,再去找DNS服务器。

2. 建立TCP链接: 客户端链接服务器

TCP提供了一种可靠、面向连接、字节流、传输层的服务。对于客户端与服务器的TCP链接,必然要说的就是『三次握手』。“3次握手”的作用就是双方都能明确自己和对方的收、发能力是正常的

客户端发送一个带有SYN标志的数据包给服务端,服务端收到后,回传一个带有SYN/ACK标志的数据包以示传达确认信息,最后客户端再回传一个带ACK标志的数据包,代表握手结束,连接成功。

SYN —— 用于初如化一个连接的序列号。
ACK —— 确认,使得确认号有效。
RST —— 重置连接。
FIN —— 该报文段的发送方已经结束向对方发送数据。

客户端:“你好,在家不。” -- SYN
服务端:“在的,你来吧。” -- SYN + ACK
客户端:“好嘞。” -- ACK

3. 发送HTTP请求

4. 服务器处理请求

5. 返回响应结果

6. 关闭TCP连接(需要4次握手)

为了避免服务器与客户端双方的资源占用和损耗,当双方没有请求或响应传递时,任意一方都可以发起关闭请求。

关闭连接时,服务器收到对方的FIN报文时,仅仅表示客户端不再发送数据了但是还能接收数据,而服务器也未必全部数据都发送给客户端,所以服务器可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。

客户端:“兄弟,我这边没数据要传了,咱关闭连接吧。” -- FIN + seq
服务端:“收到,我看看我这边有木有数据了。” -- ACK + seq + ack
服务端:“兄弟,我这边也没数据要传你了,咱可以关闭连接了。” - FIN + ACK + seq + ack
客户端:“好嘞。” -- ACK + seq + ack

7. 浏览器解析HTML

浏览器需要加载解析的不仅仅是HTML,还包括CSS、JS,以及还要加载图片、视频等其他媒体资源。

浏览器通过解析HTML,生成DOM树,解析CSS,生成CSSOM树,然后通过DOM树和CSSPOM树生成渲染树。渲染树与DOM树不同,渲染树中并没有head、display为none等不必显示的节点。

浏览器的解析过程并非是串连进行的,比如在解析CSS的同时,可以继续加载解析HTML,但在解析执行JS脚本时,会停止解析后续HTML,会出现阻塞问题。

8. 浏览器渲染页面

根据渲染树布局,计算CSS样式,即每个节点在页面中的大小和位置等几何信息。HTML默认是流式布局的,CSS和js会打破这种布局,改变DOM的外观样式以及大小和位置。最后浏览器绘制各个节点,将页面展示给用户。

replaint:屏幕的一部分重画,不影响整体布局,比如某个CSS的背景色变了,但元素的几何尺寸和位置不变。

reflow: 意味着元素的几何尺寸变了,需要重新计算渲染树。

参考:
细说浏览器输入URL后发生了什么
浏览器输入 URL 后发生了什么?

前端路由

什么是路由

路由是用来跟后端服务器进行交互的一种方式,通过不同的路径请求不同的资源。
路由这概念最开始是在后端出现, 在前后端不分离的时期, 由后端来控制路由, 服务器接收客户端的请求,解析对应的url路径, 并返回对应的页面/资源。

前端路由

Ajax,全称 Asynchronous JavaScript And XML,是浏览器用来实现异步加载的一种技术方案。

在Ajax没有出现时期,大多数的网页都是通过直接返回 HTML,用户的每次更新操作都需要重新刷新页面,及其影响交互体验。为了解决这个问题,提出了Ajax(异步加载方案), 有了 Ajax 后,用户交互就不用每次都刷新页面。后来出现SPA单页应用。

SPA 中用户的交互是通过 JS 改变 HTML 内容来实现的,页面本身的 url 并没有变化,这导致了两个问题:

  • SPA 无法记住用户的操作记录,无论是刷新、前进还是后退,都无法展示用户真实的期望内容。
  • SPA 中虽然由于业务的不同会有多种页面展示形式,但只有一个 url,对 SEO 不友好,不方便搜索引擎进行收录。

前端路由就是为了解决上述问题而出现的。

前端路由的实现方式

前端路由的实现实际上是检测 url 的变化,截获 url 地址,解析来匹配路由规则。有下面两种实现方式:

1. Hash模式

hash 就是指 url 后的 # 号以及后面的字符。 #后面 hash 值的变化,并不会导致浏览器向服务器发出请求,浏览器不发请求,也就不会刷新页面。

hash 的改变会触发 hashchange 事件,可以用onhashchange事件来监听hash值的改变。

// 监听hash变化,点击浏览器的前进后退会触发
window.onhashchange = function() { ... }

window.addEventListener('hashchange', function(event) { ...}, false);

2.History 模式

在 HTML5 之前,浏览器就已经有了 history 对象。但在早期的 history 中只能用于多页面的跳转:

history.go(-1);       // 后退一页
history.go(2);        // 前进两页
history.forward();    // 前进一页
history.back();       // 后退一页

在 HTML5 的规范中,history 新增了几个 API:

history.pushState();   // 向当前浏览器会话的历史堆栈中添加一个状态
history.replaceState();// 修改了当前的历史记录项(不是新建一个)
history.state          // 返回一个表示历史堆栈顶部的状态的值

由于 history.pushState() 和 history.replaceState() 可以改变 url 同时,不会刷新页面,所以在 HTML5 中的 histroy 具备了实现前端路由的能力。
window对象提供了onpopstate事件来监听历史栈的改变,一旦历史栈信息发生改变, 便会触发该事件。

调用history.pushState()或history.replaceState()不会触发popstate事件。只有在做出浏览器动作时,才会触发该事件,例如执行history.back()history.forward()后触发 window.onpopstate事件。
// 历史栈改变
window.onpopstate = function() { ... }

注意: pushState() 不会造成 hashchange 事件调用, 即使新的URL和之前的URL只是锚的数据不同。

两种模式对比

对比HashHistory
路径#, 路径丑正常路径
兼容性>=ie8>=ie10
实用性直接使用,无需服务端配合处理。需服务端配合处理
命名空间同一document同源
锚点导致锚点功能失效锚点功能正常

前端路由实践

vue-router/react-router 都是基于前端路由的原理实现的~
react-router常用的 history 有三种形式:

  • browserHistory: 使用浏览器中的History API 用于处理 URL。history 在 DOM 上的实现,用于支持 HTML5 history API 的浏览器。
  • hashHistory: 使用 URL 中的 hash(#)部分去创建路由。history 在 DOM 上的实现,用于旧版浏览器。
  • createMemoryHistory: 不会在地址栏被操作或读取,history 在内存上的实现,用于测试或非 DOM 环境(例如 React Native)。

Babel Plugin与preset区别

Babel是代码转换器,比如将ES6转成ES5,或者将JSX转成JS等。借助Babel,开发者可以提前用上新的JS特性。

原始代码 --> [Babel Plugin] --> 转换后的代码

Plugin

实现Babel代码转换功能的核心,就是Babel插件(plugin)。Babel插件一般尽可能拆成小的力度,开发者可以按需引进, 既提高了性能,也提高了扩展性。比如对ES6转ES5的功能,Babel官方拆成了20+个插件。开发者想要体验ES6的箭头函数特性,那只需要引入transform-es2015-arrow-functions插件就可以,而不是加载ES6全家桶。

Preset

可以简单的把Babel Preset视为Babel Plugin的集合。想要将所有ES6的代码转成ES5,逐个插件引入的效率比较低下, 就可以采用Babel Preset。比如babel-preset-es2015就包含了所有跟ES6转换有关的插件。

Plugin与Preset执行顺序

可以同时使用多个Plugin和Preset,此时,它们的执行顺序非常重要。

  1. 先执行完所有Plugin,再执行Preset。
  2. 多个Plugin,按照声明次序顺序执行。
  3. 多个Preset,按照声明次序逆序执行。

比如.babelrc配置如下,那么执行的顺序为:

  1. Plugin:transform-react-jsx、transform-async-to-generator
  2. Preset:es2016、es2015
{
  "presets": [ 
    "es2015",
    "es2016"    
  ],
  "plugins": [ 
    "transform-react-jsx",
    "transform-async-to-generator"
  ]
}

怎样开发和部署前端代码

为了进一步提升网站性能,会把静态资源和动态网页分集群部署,静态资源会被部署到CDN节点上,网页中引用的资源也会变成对应的部署路径。当需要更新静态资源的时候,同时也会更新html中的引用。

如果同时改了页面结构和样式,也更新了静态资源对应的url地址,现在要发布代码上线,是先上线页面,还是先上线静态资源?

  1. 先部署页面,再部署资源:在二者部署的时间间隔内,如果有用户访问页面,就会在新的页面结构中加载旧的资源,并且把这个旧版本的资源当做新版本缓存起来,其结果就是:用户访问到了一个样式错乱的页面,除非手动刷新,否则在资源缓存过期之前,页面会一直执行错误。
  2. 先部署资源,再部署页面:在部署时间间隔之内,有旧版本资源本地缓存的用户访问网站,由于请求的页面是旧版本的,资源引用没有改变,浏览器将直接使用本地缓存,这种情况下页面展现正常;但没有本地缓存或者缓存过期的用户访问网站,就会出现旧版本页面加载新版本资源的情况,导致页面执行错误,但当页面完成部署,这部分用户再次访问页面又会恢复正常了。

这个奇葩问题,起源于资源的 覆盖式发布,用 待发布资源 覆盖 已发布资源,就有这种问题。解决它也好办,就是实现 非覆盖式发布。用文件的摘要信息来对资源文件进行重命名,把摘要信息放到资源文件发布路径中,这样,内容有修改的资源就变成了一个新的文件发布到线上,不会覆盖已有的资源文件。上线过程中,先全量部署静态资源,再灰度部署页面,整个问题就比较完美的解决了。

大公司的静态资源优化方案,基本上要实现这么几个东西:

  1. 配置超长时间的本地缓存 —— 节省带宽,提高性能
  2. 采用内容摘要作为缓存更新依据 —— 精确的缓存控制
  3. 静态资源CDN部署 —— 优化网络请求
  4. 更改资源发布路径实现非覆盖式发布 —— 平滑升级

大数相加

function add(a, b){
   const maxLength = Math.max(a.length, b.length);
   a = a.padStart(maxLength, 0);
   b = b.padStart(maxLength, 0);
   let t = 0;
   let f = 0; 
   let sum = "";
  
   for (let i = maxLength - 1; i >= 0; i--) {
      t = parseInt(a[i]) + parseInt(b[i]) + f;
      f = Math.floor(t / 10);
      sum = `${t % 10}${sum}`;
   }
   if (f === 1){
      sum = "1" + sum;
   }
   return sum;
}

斐波那契数列求和

function fib(n) {
    if (n <= 0) {
        return 0;
    }
    let n1 = 1;
    let n2 = 1;
    let sum = 1;
    for(let i = 3; i <= n; i++) {
        [n1, n2] = [n2, sum];
        sum = n1 + n2;
    }
    return sum;
};
查看原文

赞 7 收藏 5 评论 0

时倾 发布了文章 · 2月8日

React 组件通信之发布订阅模式

React 通信

react的数据流是单向的, react 通信有以下几种方式:

  • 父向子通信: 传入props
  • 子向父通信:父组件向子组件传一个函数,然后通过这个函数的回调,拿到子组件传过来的值
  • 父向孙通信:利用context传值。React.createContext()
  • 兄弟间通信:

​ 1、找一个相同的父组件,既可以用props传递数据,也可以用context的方式来传递数据。
​ 2、用一些全局机制去实现通信,比如redux等
​ 3、发布订阅模式

兄弟间通信 - 发布订阅模式

组件间通信需要引用一个类的实例,使用单例模式实现。

发布/订阅模式

在 发布/订阅模式 有 发布者订阅者,它们通过信道链接到一起。

其主要包含三个对象:

  • 发布者:消息的发布者,往信道中投递消息的对象。
  • 订阅者:订阅一个或者多个信道消息的对象。
  • 信道:每个信道都有一个名字,信道的实现细节对用户代码来说是隐藏的。

优点

  1. 松耦合:发布者和订阅者的通信是在用户代码之外处理的,通过信道降低了发布者和订阅者的耦合性
  2. 可扩展性:发布/订阅模式可以让系统在无论什么时候都可以扩展
  3. 灵活性:不需要担心不同的组件是如何组合在一起的

缺点

  1. 无法知道消息传送是成功的还是失败的,信道不会通知系统消息传送的状态
  2. 随着订阅者和发布者数量的增加,不断增加的消息传送回导致架构的不稳定,容易在负载大的时候出问题

单例模式

确保一个类仅有一个实例,并提供一个访问它的全局访问点。

代码实现

定义发布对象:

class SingletonPublish {
  constructor() {
    this.listenList = {};
    this.instance = null;
  }

  static getInstance() {
    if (!this.instance) {
      this.instance = new SingletonPublish();
    }
    return this.instance;
  }

  // 订阅者添加订阅事件
  addListen(key, fn) {
    if (!this.listenList[key]) {
      this.listenList[key] = [];
    }
    this.listenList[key].push(fn);
  }

  // 发布者发布消息,执行订阅者订阅事件
  trigger() {
    const key = Array.from(arguments).shift();
    const fns = this.listenList[key];
    if (!fns || fns.length === 0) {
      return false;
    }

    fns.forEach((fn) => {
      fn.apply(this, arguments);
    });
  }

  // 移除订阅事件
  remove(key, fn) {
    const fns = this.listenList[key];
    if (!fns || fns.length === 0) return;

    if (!fn) {
      this.listenList[key] = [];
    } else {
      for (let l = fns.length - 1; l >= 0; l--) {
        if (fn === fns[l]) {
          fns.splice(l, 1);
        }
      }
    }
  }
}

export default SingletonPublish.getInstance();

订阅者订阅一个back事件:

import SingletonPublish from '../singleton-publish';

// ...
SingletonPublish.addListen('back', () => {
  console.log('get -- back');
  SingletonPublish.remove('back', hasExitAndVisible);
});
// ...

发布者发布一个back消息:

import SingletonPublish from '../singleton-publish';

// ...
SingletonPublish.trigger('back');
//... 

观察者模式

在这种模式中,一个目标对象(被观察者)管理所有的依赖于它的对象(观察者),并且在它本身的状态发生变化的时候主动发出通知。

其主要包含两个对象:

  • 被观察者
  • 观察者

缺点

  1. 耦合问题: 每个观察者必须和被观察对象绑定在一起,这引入了耦合
  2. 性能问题:在最基本的实现中观察对象必须同步地通知观察者。这可能会导致性能瓶颈。
查看原文

赞 4 收藏 3 评论 0

时倾 发布了文章 · 2月8日

用React实现上下跑马灯

基本思想:

  1. 利用transform:translateY()实现上下滑动,transition控制滑动时间,
  2. 把原数组拼接0下标的数据用于展示,当滑动到最后一个,马上定位到第一个。(最后一个与第一个相同)

代码示例:

// index.jsx
import React, { useRef, useState } from 'react';
import { useInterval } from 'ahooks';
import './style.scss';

function Marquee() {
  const marqueeRef = useRef(null);
  const content = [
    '鸣谢生命有你参与,笑纳我的邀请',
    '青春兵荒马乱,我们潦草地离散',
    '我对你付出的青春 这么多年',
    '我的心事,蒸发成云,再下场雨却舍不得淋湿你',
  ];
  useInterval(() => {
    if (!marqueeRef || !marqueeRef.current) return;

    const { height } = marqueeRef.current.getBoundingClientRect();
    const translateYItem = Math.floor(height / (content.length + 1));
    const nextIndex = index + 1;
    marqueeRef.current.style.transform = `translateY(-${
      translateYItem * nextIndex
    }px)`;
    marqueeRef.current.style.transition = 'transform 1s';

    if (index >= content.length - 1) {
      // 最后一个
      setTimeout(() => {
        setIndex(0);
        marqueeRef.current.style.transform = 'translateY(0px)';
        marqueeRef.current.style.transition = 'transform 0s';
      }, 1000);
    } else {
      setIndex(nextIndex);
    }
  }, 2000);

  
  return (
    <div className="marquee-container">
      <div className="marquee-wrapper">
        <div className="marquee-content" ref={marqueeRef}>
          {
            content.concat(content[0]).map((itm, i) => (
              <div key={`${itm}-${i}`} className="marquee-content-itm">
                {itm}
              </div>
            ))
          }
        </div>
      </div>
    </div>
  );
}
// style.scss
.marquee-container {
  width: 100%;

  .marquee-wrapper {
    height: 17px;
    overflow: hidden;

    .marquee-content {
      transform: translateY(0);
      transition: transform 1s;
      display: flex;
      flex-direction: column;
      align-items: flex-start;

      .marquee-content-itm {
        line-height: 17px;
        font-size: 12px;
        color: #5e1400;
      }
    }
  }
}
查看原文

赞 2 收藏 2 评论 0

时倾 发布了文章 · 1月28日

前端基础知识总结(一)

HTML页面的生命周期:

HTML页面的生命周期有以下三个重要事件:

  • DOMContentLoaded —— 浏览器已经完全加载了 HTML,DOM 树已经构建完毕,但是像是 <img> 和样式表等外部资源可能并没有下载完毕。
  • load —— 浏览器已经加载了所有的资源(图像,样式表等)。
  • beforeunload —— 当用户即将离开当前页面(刷新或关闭)时触发。正要去服务器读取新的页面时调用,此时还没开始读取;
  • unload —— 在用户离开页面后触发。从服务器上读到了需要加载的新的页面,在即将替换掉当前页面时调用。

每个事件都有特定的用途:

  • DOMContentLoaded —— DOM 加载完毕,所以 JS 可以访问所有 DOM 节点,初始化界面。
  • load —— 附加资源已经加载完毕,可以在此事件触发时获得图像的大小(如果没有被在 HTML/CSS 中指定)
  • beforeunload —— 该事件可用于弹出对话框,提示用户是继续浏览页面还是离开当前页面。
  • unload —— 删除本地数据localstorage等

DOMContentLoaded

DOMContentLoadeddocument 对象触发。使用 addEventListener 来监听它:

document.addEventListener("DOMContentLoaded", () => {});

DOMContentLoaded 和脚本

当浏览器在解析 HTML 页面时遇到了 <script>...</script> 标签,将无法继续构建DOM树(UI 渲染线程与 JS 引擎是互斥的,当 JS 引擎执行时 UI 线程会被挂起),必须立即执行脚本。所以 DOMContentLoaded 有可能在所有脚本执行完毕后触发。

外部脚本(带 src 的)的加载和解析也会暂停DOM树构建,所以 DOMContentLoaded 也会等待外部脚本。带 async 的外部脚本,可能会在DOMContentLoaded事件之前或之后执行。带 defer 的脚本肯定会在在DOMContentLoaded事件之前执行。

DOMContentLoaded 与样式表

外部样式表并不会阻塞 DOM 的解析,所以 DOMContentLoaded 并不会被它们影响。

load

window 对象上的 load 事件在所有文件包括样式表,图片和其他资源下载完毕后触发。

window.addEventListener('load', function(e) {...});

window.onload = function(e) { ... };

beforeunload

当窗口即将被卸载(关闭)时, 会触发该事件。此时页面文档依然可见, 且该事件的默认动作可以被取消。beforeunload在unload之前执行,它还可以阻止unload的执行。

// 推荐使用
window.addEventListener('beforeunload', (event) => {
  // Cancel the event as stated by the standard.
  event.preventDefault();
  // Chrome requires returnValue to be set.
  event.returnValue = '关闭提示';
});


window.onbeforeunload = function (e) {
  e = e || window.event;
  // 兼容IE8和Firefox 4之前的版本
  if (e) {
    e.returnValue = '关闭提示';
  }
  // Chrome, Safari, Firefox 4+, Opera 12+ , IE 9+
  return '关闭提示';
};

unload

用户离开页面的时候,window 对象上的 unload 事件会被触发,无法阻止用户转移到另一个页面上。

// 推荐使用
window.addEventListener("unload", function(event) { ... });
                                                   
window.onunload = function(event) { ... };

readyState

document.readyState 表示页面的加载状态,有三个值:

  • loading 加载 —— document仍在加载。
  • interactive 互动 —— 文档已经完成加载,文档已被解析,但是诸如图像,样式表和框架之类的子资源仍在加载。
  • complete —— 文档和所有子资源已完成加载。 load 事件即将被触发。

可以在 readystatechange 中追踪页面的变化状态:

document.addEventListener('readystatechange', () => {
  console.log(document.readyState);
});

Script标签:向html插入js的方法

属性描述
asyncasync立即下载脚本(仅适用于外部脚本)。
charsetcharset表示通过src属性指定的代码的字符集
deferdefer表示脚本可以延迟到文档完全被解析和显示之后再执行(仅适用于外部脚本)。
languagescript(已废弃)表示编写代码使用的脚本语言。用 type 属性代替它。
srcURL规定外部脚本文件的 URL。
xml:spacepreserve规定是否保留代码中的空白。
typetext/xxxlanguage的替换属性,表示编写代码使用的脚本语言的内容类型,也称为MIME属性。

没有 deferasync,所有<script>元素会按照在页面出现的先后顺序依次被解析,浏览器会立即加载并执行指定的脚本, 只有解析完前面的script元素的内容后,才会解析后面的代码。
asyncdefer 属性仅仅对外部脚本起作用,在 src 不存在时会被自动忽略。

使用<script>的两种方式

1.页面中嵌入script代码, 只需指定type属性

<script type="text/javascript">
  function sayHi() {
    console.log('hihihi');
    // 内部不能出现'</script>'字符串,如果必须出现,必须使用转义标签‘\’
    alert('<\/script>');
  }
</script>

包含在<script>元素内的代码会从上而下依次解释,在解释器对<script>元素内的所有代码求值完毕之前,页面中的其余内容都不会被浏览器加载或显示

2.包含外部js文件, src属性是必须的。

<script data-original="example.js"></script>
// 带有src属性的元素不应该在标签之间包含额外的js代码,即使包含,只会下载并执行外部文件,内部代码也会被忽略。

与嵌入式js代码一样, 在解析外部js文件时,页面的处理会暂时停止。

改变脚本行为的方法

1. defer: 立即下载,延迟执行

加载和渲染后续文档元素的过程将和脚本的加载并行进行(异步),但是脚本的执行会在所有元素解析完成之后。 脚本总会按照声明顺序执行。
在DOMContentLoaded事件之前执行。

<script defer="defer" data-original="example.js"></script>

2. async: 异步脚本

加载和渲染后续文档元素的过程将和脚本的加载与执行并行进行(异步)。但是async 在下载完毕后的执行会阻塞HTML的解析。脚本加载后马上执行,不能保证异步脚本按照他们在页面中出现的顺序执行。
一定会在load事件之前执行,可能会在DOMContentLoaded事件之前或之后执行。

<script async="async" data-original="example.js"></script>

区别:

deferasync
执行时机在所有元素解析完成之后加载后即执行
执行顺序按照声明顺序加载和执行脚本不管声明的顺序如何,只要加载完了就会立刻执行
DOMContentLoaded脚本会在页面加载和解析完毕后执行,在 DOMContentLoaded之前执行。(脚本下载并执行完,才触发了 DOMContentLoaded可能会在DOMContentLoaded事件之前或之后执行
loadload之前执行load之前执行

meta

META标签是HTML标记HEAD区的一个关键标签,它提供的信息虽然用户不可见,但却是文档的最基本的元信息。<meta>除了提供文档字符集、使用语言、作者等网页相关信息外,还可以设置信息给搜索引擎,目的是为了SEO(搜索引擎优化)。

HTML <meta> 元素表示那些不能由其它 HTML 元相关(meta-related)元素((<base>、<link>, <script>、<style> 或 <title>)之一表示的任何元数据信息。

属性

name
设置元数据的名称。namecontent 属性可以一起使用,以名-值对的方式给文档提供元数据,content 作为元数据的值。

content
设置与 http-equiv 或 name 属性相关的元信息。

charset
声明了文档的字符编码。如果使用了这个属性,其值必须是与ASCII大小写无关(ASCII case-insensitive)的"utf-8"。

http-equiv
定义了一个编译指示指令,其作用类似于http协议, 告诉浏览器一些关于字符设定,页面刷新,cookie,缓存等等相关信息。属性名叫做 http-equiv 是因为所有允许的值都是HTTP头部的名称。可设置的值有:

  1. content-security-policy:它允许页面作者定义当前页的内容策略。 内容策略主要指定允许的服务器源和脚本端点,这有助于防止跨站点脚本攻击。
  2. Expires:可以用于设定网页的到期时间,一旦过期则必须到服务器上重新调用。content必须使用GMT时间格式;
  3. content-type:如果使用这个属性,其值必须是"text/html; charset=utf-8"。注意:该属性只能用于 MIME type为 text/html 的文档,不能用于MIME类型为XML的文档。
  4. default-style:设置默认CSS 样式表组的名称。
  5. refresh:定时让网页在指定的时间n内,刷新或跳转;
    如果 content 只包含一个正整数,则是n秒后, 页面刷新。
    如果 content 包含一个正整数,并且后面跟着字符串 ';url=' 和一个合法的 URL,则是重定向到指定链接的时间间隔(秒)。

meta 元素定义的元数据的类型包括以下几种:

  • 如果设置了 name 属性,meta 元素提供的是文档级别(document-level)的元数据,应用于整个页面。
  • 如果设置了 http-equiv 属性,meta 元素则是编译指令,提供的信息与类似命名的HTTP头部相同。
  • 如果设置了 charset 属性,meta 元素是一个字符集声明,告诉文档使用哪种字符编码。
  • 如果设置了 itemprop 属性,meta 元素提供用户定义的元数据。
注意: 全局属性 name 在 <meta> 元素中具有特殊的语义;另外, 在同一个 <meta> 标签中,name, http-equiv 或者 charset 三者中任何一个属性存在时,itemprop 属性不能被使用。

使用
content值里有多个属性通过,隔开,同时设置多个属性。

/* name */
// 适配移动设备
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" />
// 检测html格式:禁止把数字转化为拨号链接
<meta name="format-detection" content="telephone=no" /> 

/* charset */
<meta charset="utf-8">
  
/* http-equiv */
<meta http-equiv="refresh" content="3;url=https://www.mozilla.org">
<meta http-equiv="Expires" content="Mon,12 May 2001 00:20:00 GMT">

meta viewport元信息

什么是 viewport?

viewport 是浏览器的可视区域,可视区域的大小是浏览器自己设置的。它可能大于移动设备可视区域,也可能小于移动设备可视区域。一般来讲,移动设备上的viewport都是大于移动设备可视区域。在控制台输出window.innerWidth查看Viewport大小。

相关概念
设备像素:设备屏幕分辨率。 iphone6p 的分辨率是 1334*750;
设备独立像素:设备上程序用来描绘数据的一个个的“点”, 在控制台用 screen.width/height查看。 iphone6p 的设备独立像素是375*667;
设备像素比(DPR):设备像素(宽)/设备独立像素(宽),DPR越高渲染越精致。在控制台输出window.devicePixelRatio查看设备像素比。iphone6s 的设备像素比就是 750 / 375 = 2;
CSS像素:浏览器使用的单位,用来精确度量网页上的内容。在一般情况下(页面缩放比为 1),1 个 CSS 像素等于 1 个设备独立像素。
屏幕尺寸:屏幕对角线的长度,以英尺为单位。
像素密度(PPI):每英寸屏幕拥有的像素数。

为什么要使用meta viewport?

通常情况下,移动设备上的浏览器都会把viewport设为980px或1024px,此时页面会出现横向滚动条,因为移动设备可视区域宽度是比这个默认的viewport的宽度要小。所以出现了meta 标签设置viewport 元始性进行移动端网页优化。

meta viewport 属性

  • width:控制 viewport 的大小,可以给它指定一个值(正整数),或者是一个特殊的值(如:device-width 设备独立像素宽度,单位缩放为 1 时);
  • initial-scale:初始缩放比例,即当页面第一次加载时的缩放比例,为一个数字(可以带小数);
  • maximum-scale:允许用户缩放到的最大比例,为一个数字(可以带小数);
  • minimum-scale:允许用户缩放到的最小比例,为一个数字(可以带小数);
  • user-scalable:是否允许用户手动缩放,值为 "no"(不允许) 或 "yes"(允许);
  • height:与 width 相对应(很少使用)。

基本类型和引用类型

基本类型

基本类型:undefined、null、string、number、boolean、symbol
特点
1.基本类型的值是不可变得

// 任何方法都无法改变一个基本类型的值
 let name = 'jay';
 name.toUpperCase(); // 输出 'JAY'
 console.log(name); // 输出  'jay'

2.基本类型的比较是值的比较

// 只有在它们的值相等的时候它们才相等
let a = 1;
let b = true;
console.log(a == b); //true
// 用==比较两个不同类型的变量时会进行一些类型转换。
// 先会把true转换为数字1再和数字1进行比较,结果就是true了

3.基本类型的变量是存放在栈区的(栈区指内存里的栈内存)

引用类型

引用类型:Object、Array、RegExp、Date、Function等
引用类型也可以说是对象。对象是属性和方法的集合,也就是说引用类型可以拥有属性和方法,属性又可以包含基本类型和引用类型。
特点
1.引用类型的值是可变的

// 我们可为为引用类型添加属性和方法,也可以删除其属性和方法
let person = { name: 'pig' };
person.age = 22;
person.sayName = () => console.log(person.name); 
person.sayName(); // 'pig'
delete person.name;

2.引用类型的比较是引用的比较

let person1 = '{}';
let person2 = '{}';
console.log(person1 == person2); // 字符串值相同,true

let person1 = {};
let person2 = {};
console.log(person1 == person2); // 两个对象的堆内存中的地址不同,false

3.引用类型的值是同时保存在栈内存和堆内存中的对象
javascript和其他语言不同,其不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间。实际上,是操作对象的引用,所以引用类型的值是按引用访问的。准确地说,引用类型的存储需要内存的栈区和堆区(堆区是指内存里的堆内存)共同完成,栈区内存保存变量标识符和指向堆内存中该对象的指针,也可以说是该对象在堆内存的地址。

作用域和执行上下文

JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。

  • 编译阶段:由编译器完成,将代码翻译成可执行代码。这个阶段作用域规则会确定。
  • 执行阶段:由引擎完成,主要任务是执行可执行代码。执行上下文在这个阶段创建。

作用域

简单来说作用域就是一个区域,没有变量。作用域可以嵌套。作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。作用域在函数定义时就已经确定了,不是在函数调用确定。

ES6 之前 JavaScript 只有全局作用域和函数作用域。ES6 后,增加了块级作用域(最近大括号的作用范围), 通过let 和 const 声明的变量。

作用域其实由两部分组成:

  1. 记录作用域内变量信息(假设变量,常量,函数等统称为变量)和代码结构信息的东西,称之为 Environment Record。
  2. 一个引用 __outer__,这个引用指向当前作用域的父作用域。全局作用域的 __outer__ 为 null。

词法作用域

JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。

所谓词法(代码)作用域,就是代码在编写过程中体现出来的作用范围,代码一旦写好了,没有运行之前(不用执行),作用范围就已经确定好了,这个就是所谓的词法作用域。

词法作用域的规则:

  1. 函数允许访问函数外部的数据
  2. 整个代码结构中只有函数才能限定作用域
  3. 作用规则首先使用变量提升规则分析
  4. 如果当前作用规则里面有该名字,则不考虑外面的外面的名字
var a = 1;
function out() {
  var a = 2;
  inner();
}

function inner() {
  console.log(a)
}
out();  //====>  1

作用域链

当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的指针链表就叫做作用域链。作用域链本质上是一个指向当前环境与上层环境的一系列变量对象的指针列表(它只引用但不实际包含变量对象),作用域链保证了当前执行环境对符合访问权限的变量和函数的有序访问。

例子:
用一个数组scopeChain来表示作用域链,数组的第一项scopeChain[0]为作用域链的最前端,而数组的最后一项,为作用域链的最末端,所有的最末端都为全局变量对象。

var a = 1;             
function out() {
    var b = 2;
    function inner() {
        var c = 3;
        console.log(a + b + c);
    }
    inner();          
}
out();

首先,代码开始运行时就创建了全局上下文环境,接着运行到out()时创建 out函数的执行上下文,最后运行到inner()时创建 inner函数的执行上下文,我们设定他们的变量对象分别为VO(global)VO(out), VO(inner)

当函数创建时,执行上下文为:

// 全局上下文环境
globalEC = {
  VO: {
    out: <out reference>,  // 表示 out 的地址引用
    a: undefined
  },
  scopeChain: [VO(global)], // 作用域链
}

// out 函数的执行上下文
outEC = {
  VO: {
    arguments: {...},
    inner: <inner reference>,  // 表示 inner 的地址引用
    b: undefined
  },
  scopeChain: [VO(out), VO(global)], // 作用域链
}
  
// inner 函数的执行上下文
innerEC = {
  VO: {
    arguments: {...},  
    c: undefined,
  }, 
  scopeChain: [VO(inner), VO(out), VO(global)], // 作用域链
}

执行上下文

简单来说,当在代码执行阶段执行到一个函数的时候,就会进行准备工作,这里的“准备工作”,就叫做"执行上下文(EC)",也叫执行上下文环境,也叫执行环境。js引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文。

当调用一个函数时,一个新的执行上下文就会被创建。而一个执行上下文的生命周期可以分为两个阶段:

  • 创建阶段:在这个阶段,执行上下文会分别创建变量对象,建立作用域链,以及确定this的指向。
  • 代码执行阶段:开始执行代码,会完成变量赋值,函数引用,以及执行其他代码。

特点

  1. 处于活动状态的执行上下文环境只有一个, 只有栈顶的上下文处于活动状态,执行其中的代码。
  2. 函数每调用一次,都会产生一个新的执行上下文环境。
  3. 全局上下文在代码开始执行时就创建,只有唯一的一个,永远在栈底,浏览器窗口关闭时出栈。
  4. 函数被调用的时候创建上下文环境。

变量对象

变量对象的创建过程

  1. 建立arguments对象。检查当前上下文中的参数,建立该对象下的属性与属性值。
  2. 检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖。
  3. 检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined如果该变量名的属性已经存在,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。

活动对象

变量对象与活动对象其实都是同一个对象,只是处于执行上下文的不同生命周期。不过只有处于函数调用栈栈顶的执行上下文中的变量对象,才会变成活动对象。

执行上下文栈

执行上下文可以理解为当前代码的执行环境,JavaScript中的运行环境大概包括三种情况:

  • 全局环境JavaScript代码运行起来会首先进入该环境
  • 函数环境:当函数被调用执行时,会进入当前函数中执行代码
  • eval

在代码开始执行时,首先会产生一个全局执行上下文环境,调用函数时,会产生函数执行上下文环境,函数调用完成后,它的执行上下文环境以及其中的数据都会被销毁,重新回到全局执行环境,网页关闭后全局执行环境也会销毁。其实这是一个压栈出栈的过程,全局上下文环境永远在栈底,而当前正在执行的函数上下文在栈顶

var a = 1;             // 1.进入全局上下文环境
function out() {
    var b = 2;
    function inner() {
        var c = 3;
        console.log(a + b + c);
    }
    inner();          // 3. 进入inner函数上下文环境
}
out(); // 2. 进入out函数上下文环境

以上代码的执行会经历以下过程:

  1. 当代码开始执行时就创建全局执行上下文环境,全局上下文入栈
  2. 全局上下文入栈后,其中的代码开始执行,进行赋值、函数调用等操作,执行到out()时,激活函数out创建自己的执行上下文环境,out函数上下文入栈
  3. out函数上下文入栈后,其中的代码开始执行,进行赋值、函数调用等操作,执行到inner()时,激活函数inner创建自己的执行上下文环境,inner函数上下文入栈
  4. inner函数上下文入栈后,其中的代码开始执行,进行赋值、函数调用、打印等操作,由于里面没有可以生成其他执行上下文的需要,所有代码执行完毕后,inner函数上下文出栈
  5. inner函数上下文出栈,又回到了out函数执行上下文环境,接着执行out函数中后面剩下的代码,由于后面没有可以生成其他执行上下文的需要,所有代码执行完毕后,out函数上下文出栈
  6. out函数上下文出栈后,又回到了全局执行上下文环境,直到浏览器窗口关闭,全局上下文出栈

作用域与执行上下文区别

作用域只是一个“地盘”,其中没有变量。变量是通过作用域对应的执行上下文环境中的变量对象来实现的。所以作用域是静态观念的,而执行上下文环境是动态上的。有闭包存在时,一个作用域存在两个上下文环境也是有的。

同一个作用域下,对同一个函数的不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值,所以,作用域中变量的值是在执行过程中确定的,而作用域是在函数创建时就确定的。

如果要查找一个作用域下某个变量的值,就需要找到这个作用域对应的执行上下文环境,再在其中找到变量的值。

变量提升

在Javascript中,函数及变量的声明都将被提升到函数的最顶部,提升的仅仅是变量的声明,变量的赋值并不会被提升。函数的声明与变量的声明是不一样的,函数表达式和变量表达式只是其声明被提升,函数声明是函数的声明和实现都被提升。

function foo() {  
  console.log("global foo");  
}  

function bar() {  
   console.log("global bar");  
}  

//定义全局变量  
var v = "global var";  

function hoistMe() {
   // var bar; 被提升到顶部,并未实现
   // var v;
   console.log(typeof foo); //function  
   console.log(typeof bar); //undefined  
   console.log(v); //undefined  
  
   // 函数里面定义了同名的函数和变量,无论在函数的任何位置定义这些函数和和变量,它们都将被提升到函数的最顶部。  

   foo(); //local foo  
   bar(); //报错,TypeError "bar is not a function"

    //函数声明,变量foo以及其实现被提升到hoistMe函数顶部  
   function foo() {  
     alert("local foo");  
   }  

   //函数表达式,仅变量bar被提升到函数顶部,实现没有被提升  
   var bar = function() {  
       alert("local bar");  
   };  

   //定义局部变量  
   var v = "local";  
}  

let 变量提升

console.log(a); // Uncaught ReferenceError: a is not defined
let a = "I am a";

let b = "I am outside B";
if(true){
    console.log(b); // Uncaught ReferenceError: b is not defined
    let b = " I am inside B";
}

如果b没有变量提升,执行到console.log时应该是输出全局作用域中的b,而不是出现错误。

我们可以推知,这里确实出现了变量提升,而我们不能够访问的原因事实上是因为let的死区设计:当前作用域顶部到该变量声明位置中间的部分,都是该let变量的死区,在死区中,禁止访问该变量。由此,我们给出结论,let声明的变量存在变量提升, 但是由于死区我们无法在声明前访问这个变量。

var let 区别

  1. var声明的变量,只有函数才能为它创建新的作用域;
    let声明的变量,支持块级作用域,花括号就能为它创建新的作用域;
  2. 相同作用域,var可以反复声明相同标识符的变量,而let是不允许的;
  3. let声明的变量禁止在声明前访问
// 全局变量
var i = 0 ;
// 定义外部函数
function outer(){
    // 访问全局变量
    console.log(i); // 0
  
    function inner1(){
        console.log(i); // 0
    }

    function inner2(){
        console.log(i); // undefined
        var i = 1;
        console.log(i); // 1
    }
  
    inner1();
    inner2();
    console.log(i); // 0
}

闭包

闭包就是指有权访问另一个函数作用域中的变量的函数。

官方解释:闭包是由函数以及创建该函数的词法环境组合而成。这个环境包含了这个闭包创建时所能访问的所有局部变量。(词法作用域)
通俗解释:闭包的关键在于:外部函数调用之后其变量对象本应该被销毁,但闭包的存在使我们仍然可以访问外部函数的变量对象。

当某个函数被掉用的时候,会创建一个执行环境及相应的作用域链。然后使用arguments和其他命名参数的值来初始化函数的活动对象。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位...直至作为作用域链终点的全局执行环境。

作用域链本质上是一个指向变量对象的指针列表,他只引用但不实际包含变量对象。

无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相同名字的变量,一般来讲,当函数执行完毕,局部活动对象就会被销毁,内存中仅保存全部作用域的活动对象。但是,闭包不同。

创建闭包: 在一个函数内部创建另一个函数

function add() {
  let a = 1;
  let b = 3;
  function closure() {
     b++;
     return a + b;
  }
  return closure;
}
// 闭包的作用域链包含着它自己的作用域,以及包含它的函数的作用域和全局作用域。

生命周期

通常,函数的作用域及其所有变量都会在函数执行结束后被销毁。但是,在创建了一个闭包以后,这个函数的作用域就会一直保存到闭包不存在为止。

当闭包中的函数closureadd中返回后,它的作用域链被初始化为包含add函数的活动对象和全局变量对象。这样closure就可以访问在add中定义的所有变量。更重要的是,add函数在执行完毕后,也不会销毁,因为closure函数的作用域链仍然在引用这个活动对象。换句话说,当add返回后,其执行环境的作用域链被销毁,但它的活动对象仍然在内存中,直至closure被销毁。

function add(x) {
  function closure(y) {
     return x + y;
  }
  return closure;
}

let add2 = add(2);
let add5 = add(5);
// add2 和 add5 共享相同的函数定义,但是保存了不同的环境
// 在add2的环境中,x为5。而在add5中,x则为10
console.log(add2(3)); // 5
console.log(add5(10)); // 15

// 释放闭包的引用
add2 = null;
add5 = null;

闭包中的this对象

var name = 'window';
var obj = {
  name: 'object',
  getName: () => {
    return () => {
      return this.name;
    }
  }
}
console.log(obj.getName()()); // window

obj.getName()()是在全局作用域中调用了匿名函数,this指向了window。
函数名与函数功能是分割开的,不要认为函数在哪里,其内部的this就指向哪里。
window才是匿名函数功能执行的环境。

使用注意点

1)由于闭包会让包含函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

使用

  1. 模仿块级作用域
  2. 私有变量
  3. 模块模式

在循环中创建闭包:一个常见错误

function show(i) {
  console.log(i);
}

function showCallback(i) {
  return () => {
    show(i);
  };
}

// 测试1【3,3,3】
const testFunc1 = () => {
  // var i;
  for (var i = 0; i < 3; i++) {
    setTimeout(() => show(i), 300);
  }
}

// 测试2 【0,1,2】
const testFunc2 = () => {
  for (var i = 0; i < 3; i++) {
    setTimeout(showCallback(i), 300);
  }
}

// 测试3【0,1, 2】 闭包,立即执行函数
// 在闭包函数内部形成了局部作用域,每循环一次,形成一个自己的局部作用域
const testFunc3 = () => {
  for (var i = 0; i < 3; i++) {
    (() => {
       setTimeout(() => show(i), 300);
    })(i);
  }
}

// 测试4【0,1, 2】let
const testFunc4 = () => {
  for (let i = 0; i < 3; i++) {
    setTimeout(() => show(i), 300);
  }
}

setTimeout()函数回调属于异步任务,会出现在宏任务队列中,被压到了任务队列的最后,在这段代码应该是for循环这个同步任务执行完成后才会轮到它

测试1错误原因:赋值给 setTimeout 的是闭包。这些闭包是由他们的函数定义和在 testFunc1 作用域中捕获的环境所组成的。这三个闭包在循环中被创建,但他们共享了同一个词法作用域,在这个作用域中存在一个变量i。这是因为变量i使用var进行声明,由于变量提升,所以具有函数作用域。当onfocus的回调执行时,i的值被决定。由于循环在事件触发之前早已执行完毕,变量对象i(被三个闭包所共享)已经指向了i的最后一个值。

测试2正确原因: 所有的回调不再共享同一个环境, showCallback 函数为每一个回调创建一个新的词法环境。在这些环境中,i 指向数组中对应的下标。

测试4正确原因:JS中的for循环体比较特殊,每次执行都是一个全新的独立的块作用域,用let声明的变量传入到 for循环体的作用域后,不会发生改变,不受外界的影响。

this 指向问题

this 就是一个指针,指向我们调用函数的对象。
执行上下文: 是语言规范中的一个概念,用通俗的话讲,大致等同于函数的执行“环境”。具体的有:变量作用域(和 作用域链条,闭包里面来自外部作用域的变量),函数参数,以及 this 对象的值。

找出 this 的指向

this 的值并不是由函数定义放在哪个对象里面决定,而是函数执行时由谁来唤起决定。

var name = "Jay Global";
var person = {
    name: 'Jay Person',
    details: {
        name: 'Jay Details',
        print: function() {
            return this.name;
        }
    },
    print: function() {
        return this.name;
    }
};

console.log(person.details.print());  // 【details对象调用的print】Jay Details
console.log(person.print());          // 【person对象调用的print】Jay Person

var name1 = person.print;
var name2 = person.details;

console.log(name1()); // 【name1前面没有调用对象,所以是window】Jay Global
console.log(name2.print()) // 【name2对象调用的print】Jay Details

this和箭头函数

箭头函数按词法作用域来绑定它的上下文,所以 this 实际上会引用到原来的上下文。箭头函数保持它当前执行上下文的词法作用域不变,而普通函数则不会。换句话说,箭头函数从包含它的词法作用域中继承到了 this 的值。
匿名函数,它不会作为某个对象的方法被调用, 因此,this 关键词指向了全局 window 对象。

var object = {
    data: [1,2,3],
    dataDouble: [1,2,3],
    double: function() {
        console.log(this); // object
        return this.data.map(function(item) { // this是当前object,object调用的double
            console.log(this);   // 传给map()的那个匿名函数没有被任一对象调用,所以是window
            return item * 2;
        });
    },
    doubleArrow: function() {
        console.log(this); // object
        return this.dataDouble.map(item => { // this是当前object,object调用的doubleArrow
            console.log(this);      // doubleArrow是object调用的,这就是上下文,所以是window
            return item * 2;
        });
    }
};
object.double();
object.doubleArrow();

明确设置执行上下文

在 JavaScript 中通过使用内置的特性开发者就可以直接操作执行上下文了。这些特性包括:

  • bind():不需要执行函数就可以将 this 的值准确设置到你选择的一个对象上。通过逗号隔开传递多个参数。 设置好 this 关键词后不会立刻执行函数。
  • apply():将 this 的值准确设置到你选择的一个对象上。apply(thisObj, argArray)接收两个参数,thisObj是函数运行的作用域(this),argArray是参数数组,数组的每一项是你希望传递给函数的参数。如果没有提供argArray和thisObj任何一个参数,那么Global对象将用作thisObj。最后,会立刻执行函数。
  • call():将 this 的值准确设置到你选择的一个对象上。然后像bind 一样通过逗号分隔传递多个参数给函数。语法:call(thisObj,arg1,arg2,..., argn);,如果没有提供thisObj参数,那么Global对象被用于thisObj。最后,会立刻执行函数。

this 和 bind

var bobObj = {
    name: "Bob"
};
function print() {
    return this.name;
}
var printNameBob = print.bind(bobObj);
console.log(printNameBob()); // Bob

this 和 call

function add(a, b) { 
    return a + b; 
}
function sum() {
    return Array.prototype.reduce.call(arguments, add);
}
console.log(sum(1,2,3,4)); // 10

this 和 apply

apply 就是接受数组版本的call。

Math.min(1,2,3,4); // 返回 1
Math.min([1,2,3,4]); // 返回 NaN。只接受数字
Math.min.apply(null, [1,2,3,4]); // 返回 1

function Person(name, age){  
  this.name = name;  
  this.age = age;  
}  

function Student(name, age, grade) {  
  Person.apply(this, arguments);  //Person.call(this, name, age);
  this.grade = grade;  
}
var student = new Student("sansan", 21, "一年级");  
 
console.log("student:", student); // {name: 'sansan'; age: '21', grade: '一年级'}
如果你的参数本来就存在一个数组中,那自然就用 apply,如果参数比较散乱相互之间没什么关联,就用 call。

对象属性类型

数据属性

数据属性包含一个数据值的位置,在这个位置可以读取和写入值,数据属性有4个描述其行为的特性:

  • Configurable: 表示是否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。默认值是true
  • Enumerable: 表示能否通过for-in循环返回属性。默认值是true
  • Writable: 表述能否修改属性。默认值是true
  • Value: 包含这个属性的数据值。默认值是true
访问器属性

函数式编程

函数式编程是一种编程范式,是一种构建计算机程序结构和元素的风格,它把计算看作是对数学函数的评估,避免了状态的变化和数据的可变。

纯函数

纯函数是稳定的、一致的和可预测的。给定相同的参数,纯函数总是返回相同的结果。

特性

1. 如果给定相同的参数,则得到相同的结果

我们想要实现一个计算圆的面积的函数。
不是纯函数会这样做:

  let PI = 3.14;    
  const calculateArea = (radius) => radius * radius * PI; 
  // 它使用了一个没有作为参数传递给函数的全局对象
  calculateArea(10); // returns 314.0

纯函数:

 let PI = 3.14;    
 const calculateArea = (radius, pi) => radius * radius * pi; 
 // 现在把 PI 的值作为参数传递给函数,这样就没有外部对象引入。
 calculateArea(10, PI); // returns 314.0

2. 无明显副作用

纯函数不会引起任何可观察到的副作用。可见副作用的例子包括修改全局对象或通过引用传递的参数。

现在,实现一个函数,接收一个整数并返对该整数进行加1操作且返回:

let counter = 1;
function increaseCounter(value) {      
  counter = value + 1;   
}    
increaseCounter(counter);   
console.log(counter); // 2

该非纯函数接收该值并重新分配counter,使其值增加1
函数式编程不鼓励可变性(修改全局对象)。

 let counter = 1;
 const increaseCounter = (value) => value + 1;   // 函数返回递增的值,而不改变变量的值
 increaseCounter(counter); // 2    
 console.log(counter); // 1

3. 引用透明性

如果一个函数对于相同的输入始终产生相同的结果,那么它可以看作透明的。
实现一个square 函数:

const square = (n) => n * n;
square(2); // 4 将2作为square函数的参数传递始终会返回4

可以把square(2)换成4,我们的函数就是引用透明的。

纯函数使用

单元测试

纯函数代码肯定更容易测试,不需要 mock 任何东西。因此我们可以使用不同的上下文对纯函数进行单元测试。
一个简单的例子是接收一组数字,并对每个数进行加 1 :

let list = [1, 2, 3, 4, 5];    
const incrementNumbers = (list) => list.map(number => number + 1);
incrementNumbers(list); // [2, 3, 4, 5, 6]

对于输入[1,2,3,4,5],预期输出是[2,3,4,5,6]

纯函数也可以被看作成值并用作数据使用

  • 从常量和变量中引用它。
  • 将其作为参数传递给其他函数。
  • 作为其他函数的结果返回它。

其思想是将函数视为值,并将函数作为数据传递。通过这种方式,我们可以组合不同的函数来创建具有新行为的新函数。

假如我们有一个函数,它对两个值求和,然后将值加倍,如下所示:

 const doubleSum = (a, b) => (a + b) * 2;

对应两个值求差,然后将值加倍:

  const doubleSubtraction = (a, b) => (a - b) * 2

这些函数具有相似的逻辑,但区别在于运算符的功能。如果我们可以将函数视为值并将它们作为参数传递,我们可以构建一个接收运算符函数并在函数内部使用它的函数。

 const sum = (a, b) => a + b;    
 const subtraction = (a, b) => a - b;    
 const doubleOperator = (f, a, b) => f(a, b) * 2;    
 doubleOperator(sum, 3, 1); // 8    
 doubleOperator(subtraction, 3, 1); // 4

Promise

Promise 必须为以下三种状态之一:等待态(Pending)、执行态(Fulfilled)和拒绝态(Rejected)。一旦Promise 被 resolve 或 reject,不能再迁移至其他任何状态(即状态 immutable)。

基本过程:

  1. 初始化 Promise 状态(pending)
  2. 执行 then(..) 注册回调处理数组(then 方法可被同一个 promise 调用多次)
  3. 立即执行 Promise 中传入的 fn 函数,将Promise 内部 resolve、reject 函数作为参数传递给 fn ,按事件机制时机处理
  4. Promise中要保证,then方法传入的参数 onFulfilled 和 onRejected,必须在then方法被调用的那一轮事件循环之后的新执行栈中执行。

真正的链式Promise是指在当前promise达到fulfilled状态后,即开始进行下一个promise.

跨域

因为浏览器的同源策略导致了跨域。同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。

所谓同源是指"协议+域名+端口"三者相同。不同协议,不同域名,不同端口都会构成跨域。

跨域解决方案

 1. jsonp: 需要服务器配合一个callback函数
 2. CORS: 需要服务器设置header :Access-Control-Allow-Origin
 3. window.name + iframe: 需要目标服务器响应window.name。
 4. document.domain : 仅限主域相同,子域不同的跨域应用场景。
 5. html5的 postMessage + iframe: 需要服务器或者目标页面写一个postMessage,主要侧重于前端通讯。
 6. nginx反向代理: 不用服务器配合,需要搭建一个中转nginx服务器,用于转发请求。

jsonp跨域

在HTML标签里,一些标签比如script、img这样的获取资源的标签是没有跨域限制的。通过动态创建script,再请求一个带参网址实现跨域通信。

  • 需要前后端配合使用。一般后端设置callback ,前端给后台接口中传一个callback 即可。
  • 只能实现get一种请求。

栗子

前端代码:

<script>
    var script = document.createElement('script');
    script.type = 'text/javascript';

    // 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
    script.src = 'http://xxxxxxx:8080/login?callback=handleCallback';
    document.head.appendChild(script);

    function handleCallback(res) {
        alert(JSON.stringify(res));
    }
 </script>

后台代码:

<?php
  $callback = $_GET['callback'];//得到回调函数名
  $data = array('a','b','c');//要返回的数据
  echo $callback.'('.json_encode($data).')';//输出
?>

CORS - 跨域资源共享

CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。

CORS有两种请求,简单请求和非简单请求。只要同时满足以下两大条件,就属于简单请求。

  1. 请求方法是以下三种方法之一: HEAD,GET,POST
  2. HTTP的头信息不超出以下几种字段:Accept,Accept-Language,Content-Language,Last-Event-ID,Content-Type【只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain】,没有自定义的HTTP头部。

简单请求

  1. 浏览器:把客户端脚本所在的域填充到Origin header里,向其他域的服务器请求资源。
  2. 服务器:根据资源权限配置,在响应头中添加Access-Control-Allow-Origin Header,返回结果。
  3. 浏览器:比较服务器返回的Access-Control-Allow-Origin Header和请求域的Origin。如果当前域已经得到授权,则将结果返回给页面。否则浏览器忽略此次响应。
  4. 网页:收到返回结果或者浏览器的错误提示。

对于简单的跨域请求,只要服务器设置的Access-Control-Allow-Origin Header和请求来源匹配,浏览器就允许跨域服务器端设置的`Access-Control-Allow-MethodsAccess-Control-Allow-Headers对简单跨域没有作用。

非简单请求

  1. 浏览器:先向服务器发送一个OPTIONS预检请求,检测服务器端是否支持真实请求进行跨域资源访问,浏览器会在发送OPTIONS请求时会自动添加Origin Header 、Access-Control-Request-Method Header和Access-Control-Request-Headers Header。
  2. 服务器:响应OPTIONS请求,会在responseHead里添加Access-Control-Allow-Methods head。这其中的method的值是服务器给的默认值,可能不同的服务器添加的值不一样。服务器还会添加Access-Control-Allow-Origin Header和Access-Control-Allow-Headers Header。这些取决于服务器对OPTIONS请求具体如何做出响应。如果服务器对OPTIONS响应不合你的要求,你可以手动在服务器配置OPTIONS响应,以应对带预检的跨域请求。在配置服务器OPTIONS的响应时,可以添加Access-Control-Max-Age head告诉浏览器在一定时间内无需再次发送预检请求,但是如果浏览器禁用缓存则无效。
  3. 浏览器:接到OPTIONS的响应,比较真实请求的method是否属于返回的Access-Control-Allow-Methods head的值之一,还有origin, head也会进行比较是否匹配。如果通过,浏览器就继续向服务器发送真实请求, 否则就会报预检错误:请求来源不被options响应允许,请求方法不被options响应允许或请求中有自定义header不被options响应允许。
  4. 服务器:响应真实请求,在响应头中放入Access-Control-Allow-Origin Header、Access-Control-Allow-MethodsAccess-Control-Allow-Headers Header,分别表示允许跨域资源请求的域、请求方法和请求头,并返回数据。
  5. 浏览器:接受服务器对真实请求的返回结果,返回给网页
  6. 网页:收到返回结果或者浏览器的错误提示。

Access-Control-Allow-Origin在响应options请求和响应真实请求时都是有作用的,两者必须同时包含要跨域的源。 Access-Control-Allow-MethodsAccess-Control-Allow-Headers只在响应options请求时有作用。

携带cookie

在 CORS 跨域中,浏览器并不会自动发送 Cookie。对于普通跨域请求只需服务端设置,而带cookie跨域请求前后端都需要设置。

浏览器,对于跨域请求,需要设置withCredentials 属性为 true。服务端的响应中必须携带 Access-Control-Allow-Credentials: true

除了Access-Control-Allow-Credentials之外,跨域发送 Cookie 还要求 Access-Control-Allow-Origin不允许使用通配符。 否则浏览器将会抛出The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' 错误。事实上不仅不允许通配符,而且只能指定单一域名。

计算 Access-Control-Allow-Origin

既然Access-Control-Allow-Origin只允许单一域名, 服务器可能需要维护一个接受 Cookie 的 Origin 列表, 验证 Origin 请求头字段后直接将其设置为Access-Control-Allow-Origin的值。 在 CORS 请求被重定向后 Origin 头字段会被置为 null, 此时可以选择从Referer头字段计算得到Origin

具体实现

服务器端的响应头配置
Access-Control-Allow-Origin 可以设置为* ,表示可以与任意域进行数据共享。

// 设置服务器接受跨域的域名
"Access-Control-Allow-Origin": "http://127.0.0.1:8080",
// 设置服务器接受跨域的请求方法
'Access-Control-Allow-Methods': 'OPTIONS,HEAD,DELETE,GET,PUT,POST',
// 设置服务器接受跨域的headers
'Access-Control-Allow-Headers': 'x-requested-with, accept, origin, content-type',
// 设置服务器不用再次预检请求时间
'Access-Control-Max-Age': 10000,
// 设置服务器接受跨域发送Cookie
'Access-Control-Allow-Credentials': true

document.domain

此方案仅限主域相同,子域不同的跨域应用场景。

实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

栗子:

在父页面 http://xxx.com/a.html 中设置document.domain

<iframe id = "iframe" data-original="http://xxx.com/b.html" onload = "test()"></iframe>
<script type="text/javascript">
    document.domain = 'xxx.com';//设置成主域
    function test(){
       alert(document.getElementById('iframe').contentWindow);
       //contentWindow 可取得子窗口的 window 对象
    }
</script>

在子页面http://xxx.com/b.html 中设置document.domain

<script type="text/javascript">
    document.domain = 'xxx.com';
    //在iframe载入这个页面也设置document.domain,使之与主页面的document.domain相同
</script>

window.postMessage

window.postMessage是html5的功能,是客户端和客户端直接的数据传递,既可以跨域传递,也可以同域传递。

postMessage(data, origin)方法接受两个参数:

  • data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。
  • origin: 协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。

栗子:
假如有一个页面,页面中拿到部分用户信息,点击进入另外一个页面,另外的页面默认是取不到用户信息的,你可以通过window.postMessage把部分用户信息传到这个页面中。(需要考虑安全性等方面。)

发送消息:

// 弹出一个新窗口
var domain = 'http://haorooms.com';
var myPopup = window.open(`${domain}/windowPostMessageListener.html`,'myWindow');

// 发送消息
setTimeout(function(){
  var message = {name:"站点",sex:"男"};
  console.log('传递的数据是  ' + message);
  myPopup.postMessage(message, domain);
}, 1000);

接收消息:

// 监听消息反馈
window.addEventListener('message', function(event) {
  // 判断域名是否正确
  if (event.origin !== 'http://haorooms.com') return;
  console.log('received response: ', event.data);
}, false);

如下图,接受页面得到数据

enter image description here

如果是使用iframe,代码应该这样写:

// 捕获iframe
var domain = 'http://haorooms.com';
var iframe = document.getElementById('myIFrame').contentWindow;

// 发送消息
setTimeout(function(){ 
    var message = {name:"站点",sex:"男"};
    console.log('传递的数据是:  ' + message);
    iframe.postMessage(message, domain); 
},1000);

接收数据并反馈信息:

// 响应事件
window.addEventListener('message',function(event) {
    if(event.origin !== 'http://haorooms.com') return;
    console.log('message received:  ' + event.data, event);
    event.source.postMessage(event.origin);
}, false);

几个比较重要的事件属性:
source – 消息源,消息的发送窗口/iframe。
origin – 消息源的URI(可能包含协议、域名和端口),用来验证数据源。
data – 发送方发送给接收方的数据。

window.name

原理:
window对象有个name属性,该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个window.name,每个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的所有页面中的。

栗子:
在子页面(b.com/data.html) 设置window.name:

/* b.com/data.html */
<script type="text/javascript">
   window.name = 'I was there!';    
   // 这里是要传输的数据,大小一般为2M,IE和firefox下可以大至32M左右
   // 数据格式可以自定义,如json、字符串
</script>

在父页面(a.com/app.html)中创建一个iframe,把其src指向子页面。在父页面监听iframe的onload事件,获取子页面数据:

/* a.com/app.html */
<script type="text/javascript">
    var iframe = document.createElement('iframe');
    iframe.src = 'http://b.com/data.html';
    function iframelLoadFn() {
      var data = iframe.contentWindow.name; 
      console.log(data);
      // 获取数据以后销毁iframe,释放内存;这也保证了安全(不被其他域frame js访问)。
      iframeDestoryFn();
    }

    function iframeDestoryFn() {
      iframe.contentWindow.document.write('');
      iframe.contentWindow.close();
      document.body.removeChild(iframe);
    }

    if (iframe.attachEvent) {
        iframe.attachEvent('onload', iframelLoadFn);
    } else {
        iframe.onload = iframelLoadFn;
    }
    document.body.appendChild(iframe);
</script>

http-proxy-middleware

http-proxy-middleware用于把请求代理转发到其他服务器的中间件。
安装:
npm install http-proxy-middleware --save-dev
配置如下:

module.exports = {
  devServer: {
    contentBase: path.resolve(__dirname, 'dev'),
    publicPath: '/',
    historyApiFallback: true,
    proxy: {
      // 请求到 '/device' 下的请求都会被代理到target:http://target.com中
      '/device/*': {
        target: 'http://target.com',
        secure: false, // 接受运行在https上的服务
        changeOrigin: true
      }
    }
  }
}

使用如下:

fetch('/device/space').then(res => {
  // 被代理到 http://target.com/device/space
  return res.json();
});

// 使用的url 必须以/开始 否则不会代理到指定地址
fetch('device/space').then(res => {
  // http://localhost:8080/device/space 访问本地服务
  return res.json();
});

nginx反向代理

反向代理(Reverse Proxy)方式是指以代理服务器来接受客户端的连接请求,然后将请求转发给内部网络上的服务器;并将从服务器上得到的结果返回给客户端,此时代理服务器对外就表现为一个服务器。

反向代理服务器对于客户端而言它就像是原始服务器,并且客户端不需要进行任何特别的设置。客户端向反向代理 的命名空间(name-space)中的内容发送普通请求,接着反向代理将判断向何处(原始服务器)转交请求,并将获得的内容返回给客户端,就像这些内容 原本就是它自己的一样。

模块化

AMD/CMD/CommonJs都是JS模块化开发的标准,目前对应的实现是RequireJS,SeaJs, nodeJs;

CommonJS:服务端js

CommonJS 是以在浏览器环境之外构建 javaScript 生态系统为目标而产生的写一套规范,主要是为了解决 javaScript 的作用域问题而定义的模块形式,可以使每个模块它自身的命名空间中执行。

实现方法:模块必须通过 module.exports 导出对外的变量或者接口,通过 require() 来导入其他模块的输出到当前模块的作用域中;

主要针对服务端(同步加载文件)和桌面环境中,node.js 遵循的是 CommonJS 的规范;CommonJS 加载模块是同步的,所以只有加载完成才能执行后面的操作。

  • require()用来引入外部模块;
  • exports对象用于导出当前模块的方法或变量,唯一的导出口;
  • module对象就代表模块本身。
// 定义一个module.js文件
var A = () => console.log('我是定义的模块');

// 1.第一种返回方式
module.exports = A; 
// 2.第二种返回方式 
module.exports.test = A
// 3.第三种返回方式 
exports.test = A;

// 定义一个test.js文件【这两个文件在同一个目录下】
var module = require("./module");

//调用这个模块,不同的返回方式用不同的方式调用
// 1.第一种调用方式
module();
// 2.第二种调用方式 
module.test();
// 3.第三种调用方式 
module.test();

// 执行文件
node test.js

AMD: 异步模块定义【浏览器端js】

AMD 是 Asynchronous Module Definition 的缩写,意思是异步模块定义;采用的是异步的方式进行模块的加载,在加载模块的时候不影响后边语句的运行。主要是为前端 js 的表现指定的一套规范。

实现方法:通过define方法去定义模块,通过require方法去加载模块。
define(id?,dependencies?,factory): 它要在声明模块的时候制定所有的依赖(dep),并且还要当做形参传到factory中。没什么依赖,就定义简单的模块(或者叫独立的模块)
require([modules], callback): 第一个参数[modules],是需加载的模块名数组;第二个参数callback,是模块加载成功之后的回调函数

主要针对浏览器js,requireJs遵循的是 AMD 的规范;

// module1.js文件, 定义独立的模块
define({
    methodA: () => console.log('我是module1的methodA');
    methodB: () => console.log('我是module1的methodB');
});

// module2.js文件, 另一种定义独立模块的方式
define(() => {
    return {
        methodA: () => console.log('我是module2的methodA');
        methodB: () => console.log('我是module2的methodB');
    };
});

// module3.js文件, 定义非独立的模块(这个模块依赖其他模块)
define(['module1', 'module2'], (m1, m2) => {
    return {
        methodC: () => {
            m1.methodA();
            m2.methodB();
        }
    };
});


//定义一个main.js,去加载这些个模块
require(['module3'], (m3) => {
    m3.methodC();
});


// 为避免造成网页失去响应,解决办法有两个,一个是把它放在网页底部加载,另一个是写成下面这样:
<script data-original="js/require.js" defer async="true" ></script>
// async属性表明这个文件需要异步加载,避免网页失去响应。
// IE不支持这个属性,只支持defer,所以把defer也写上。

// data-main属性: 指定网页程序的主模块
<script data-main="main" data-original="js/require.js"></script>

// 控制台输出结果
我是module1的methodA
我是module2的methodB

CMD: 通用模块定义【浏览器端js】

CMD 是 Common Module Definition 的缩写,通过异步的方式进行模块的加载的,在加载的时候会把模块变为字符串解析一遍才知道依赖了哪个模块;

主要针对浏览器端(异步加载文件),按需加载文件。对应的实现是seajs

AMD和CMD的区别

  1. 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible(尽可能的懒加载,也称为延迟加载,即在需要的时候才加载)。
  2. CMD 推崇依赖就近,AMD 推崇依赖前置。
// CMD
define(function(require, exports, module) {
    var a = require('./a');
    a.doSomething();
    // ...
    var b = require('./b');   // 依赖可以就近书写
    b.doSomething();
    // ... 
})

// AMD
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
    a.doSomething();
    // ...
    b.doSomething();
    //...
}) 

import和require区别

import和require都是被模块化使用。

  • require是CommonJs的语法(AMD规范引入方式),CommonJs的模块是对象。import是es6的一个语法标准(浏览器不支持,本质是使用node中的babel将es6转码为es5再执行,import会被转码为require),es6模块不是对象。
  • require是运行时加载整个模块(即模块中所有方法),生成一个对象,再从对象上读取它的方法(只有运行时才能得到这个对象,不能在编译时做到静态化),理论上可以用在代码的任何地方。import是编译时调用,确定模块的依赖关系,输入变量(es6模块不是对象,而是通过export命令指定输出代码,再通过import输入,只加载import中导的方法,其他方法不加载),import具有提升效果,会提升到模块的头部(编译时执行)

​export和import可以位于模块中的任何位置,但是必须是在模块顶层,如果在其他作用域内,会报错(es6这样的设计可以提高编译器效率,但没法实现运行时加载)。

  • require是赋值过程,把require的结果(对象,数字,函数等),默认是export的一个对象,赋给某个变量(复制或浅拷贝)。import是解构过程(需要谁,加载谁)。

require/exports:

// require: 真正被require出来的是来自module.exports指向的内存块内容
const a = require('a') //

// exports: 只是 module.exports的引用,辅助module.exports操作内存中的数据
exports.a = a 
module.exports = a

import/export:

// import
import a from 'a';
import { default as a  } from 'a';
import  *  as a  from 'a';
import { fun1,fun2 } from 'a';
// export
export default a;
export const a = 1;
export functon a { ... };
export { fun1, fun2 };

http和https

Http:超文本传输协议(Http,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。设计Http最初的目的是为了提供一种发布和接收HTML页面的方法。它可以使浏览器更加高效。Http协议是以明文方式发送信息的,如果黑客截取了Web浏览器和服务器之间的传输报文,就可以直接获得其中的信息。

Https:是以安全为目标的Http通道,是Http的安全版。Https的安全基础是SSL。SSL协议位于TCP/IP协议与各种应用层协议之间,为数据通讯提供安全支持。SSL协议可分为两层:SSL记录协议(SSL Record Protocol),它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。SSL握手协议(SSL Handshake Protocol),它建立在SSL记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等。

HTTP与HTTPS的区别

1、HTTP是超文本传输协议,信息是明文传输,HTTPS是具有安全性的SSL加密传输协议。
2、HTTPS协议需要ca申请证书,一般免费证书少,因而需要一定费用。
3、HTTP和HTTPS使用的是完全不同的连接方式,用的端口也不一样。前者是80,后者是443。
4、HTTP连接是无状态的,HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,安全性高于HTTP协议。

https的优点

尽管HTTPS并非绝对安全,掌握根证书的机构、掌握加密算法的组织同样可以进行中间人形式的攻击,但HTTPS仍是现行架构下最安全的解决方案,主要有以下几个好处:

1)使用HTTPS协议可认证用户和服务器,确保数据发送到正确的客户机和服务器;
2)HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全,可防止数据在传输过程中不被窃取、改变,确保数据的完整性。
3)HTTPS是现行架构下最安全的解决方案,虽然不是绝对安全,但它大幅增加了中间人攻击的成本。
4)谷歌曾在2014年8月份调整搜索引擎算法,并称“比起同等HTTP网站,采用HTTPS加密的网站在搜索结果中的排名将会更高”。

Https的缺点

1)Https协议握手阶段比较费时,会使页面的加载时间延长近。
2)Https连接缓存不如Http高效,会增加数据开销,甚至已有的安全措施也会因此而受到影响;
3)SSL证书通常需要绑定IP,不能在同一IP上绑定多个域名,IPv4资源不可能支撑这个消耗。
4)Https协议的加密范围也比较有限。最关键的,SSL证书的信用链体系并不安全,特别是在某些国家可以控制CA根证书的情况下,中间人攻击一样可行。

遍历方法

for

在for循环中,循环取得数组或是数组类似对象的值,譬如arguments和HTMLCollection对象。

不足:

  • 在于每次循环的时候数组的长度都要去获取;
  • 终止条件要明确;

foreach(),map()

两个方法都可以遍历到数组的每个元素,而且参数一致;
forEach(): 对数组的每个元素执行一次提供的函数, 总是返回undefined;
map(): 创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。 返回值是一个新的数组;

var array1 = [1,2,3,4,5];
 
var x = array1.forEach((value,index) => {
    console.log(value);
    return value + 10;
});
console.log(x);   // undefined
 
var y = array1.map((value,index) => {
    console.log(value);
    return value + 10;
});
console.log(y);   // [11, 12, 13, 14, 15] 

for in

经常用来迭代对象的属性或数组的每个元素,它包含当前属性的名称或当前数组元素的索引。
当遍历一个对象的时候,变量 i 是循环计数器 为 对象的属性名, 以任意顺序遍历一个对象的可枚举属性。对于每个不同的属性,语句都会被执行。
当遍历一个数组的时候,变量 i 是循环计数器 为 当前数组元素的索引

不足:

for..in循环会把某个类型的原型(prototype)中方法与属性给遍历出来.

const array = ["admin","manager","db"]; 
array.color = 'red';
array.prototype.name= "zhangshan"; 
for(var i in array){
    if(array.hasOwnProperty(i)){ 
        console.log(array[i]);  // admin,manager,db,color
    }
}
// hasOwnProperty(): 对象的属性或方法是非继承的,返回true

for … of

迭代循环可迭代对象(包括Array,Map,Set,String,TypedArray,arguments 对象)等等。不能遍历对象。只循环集合本身的元素

var a = ['A', 'B', 'C'];
var s = new Set(['A', 'B', 'C']);
var m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
a.name = 'array';
for (var x of a) {
 console.log(x); //'A', 'B', 'C'
}
for (var x of s) {
 console.log(x);//'A', 'B', 'C'
}
for (var x of m) {
 console.log(x[0] + '=' + x[1]);//1='x',2='y',3='z'
}

继承

// 定义一个动物类
function Animal(name) {
  // 属性
  this.name = name || 'Animal';
  // 实例方法
  this.sleep = function(){
    console.log(this.name + '正在睡觉!');
  }
}
// 原型方法
Animal.prototype.eat = function(food) {
  console.log(this.name + '正在吃:' + food);
};

原型链继承

核心: 将父类的实例作为子类的原型。

function Dog(age) {
  this.age = age;
}
Dog.protoType = New Animal();
Dog.prototype.name = 'dog';

const dog = new Dog(12);
console.log(dog.name);
console.log(dog.eat('age'));
console.log(dog instanceof Animal); //true 
console.log(dog instanceof Dog); //true

new 创建新实例对象经过了以下几步:

1.创建一个新对象
2.将新对象的_proto_指向构造函数的prototype对象
3.将构造函数的作用域赋值给新对象 (也就是this指向新对象)
4.执行构造函数中的代码(为这个新对象添加属性)
5.返回新的对象

// 1. 创建一个新对象
var Obj = {};
// 2. 将新对象的_proto_指向构造函数的prototype对象
Obj._proto_ =  Animal.prototype();
// 3. 执行构造函数中的代码(为这个新对象添加属性) 
Animal.call(Obj);
// 4. 返回新的对象
return Obj;

特点:
1.实例可继承的属性有:实例的构造函数的属性,父类构造函数属性,父类原型的属性
2.非常纯粹的继承关系,实例是子类的实例,也是父类的实例
3.父类新增原型方法/原型属性,子类都能访问到

缺点:
1.新实例无法向父类构造函数传参。
2.继承单一。
3.所有新实例都会共享父类实例的属性。(原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改!)
4.要想为子类新增原型上的属性和方法,必须要在new Animal()这样的语句之后执行,不能放到构造器中

构造函数继承

核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)

function Dog(name) {
  Animal.apply(this, 'dog');
  this.name = name;
}

const dog = new Dog();
console.log(dog.name);
console.log(dog.eat('age'));
console.log(dog instanceof Animal); //false 
console.log(dog instanceof Dog); //true

重点:用.call()和.apply()将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行(复制))

特点:
1.只继承了父类构造函数的属性,没有继承父类原型的属性。
2.解决了原型链继承缺点1、2、3。
3.可以实现多继承,继承多个构造函数属性(call多个)。
4.在子实例中可向父实例传参。

缺点:
1.能继承父类构造函数的属性。
2.无法实现构造函数的复用。(每次用每次都要重新调用)
3.每个新实例都有父类构造函数的副本,臃肿。
4.实例并不是父类的实例,只是子类的实例

组合继承(原型链继承和构造函数继承)(常用)

核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用

function Cat(name){
  Animal.call(this, name);
  this.name = name;
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true

重点:结合了两种模式的优点,传参和复用

特点:
1.可以继承父类原型上的属性,可以传参,可复用。
2.每个新实例引入的构造函数属性是私有的。
3.既是子类的实例,也是父类的实例

缺点:
调用了两次父类构造函数(耗内存),子类的构造函数会代替原型上的那个父类构造函数。

原型式继承

     img

重点:用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象。object.create()就是这个原理。

特点:
类似于复制一个对象,用函数来包装。

缺点:
1.所有实例都会继承原型上的属性。
2.无法实现复用。(新实例属性都是后面添加的)

寄生式继承

   img
  
重点:就是给原型式继承外面套了个壳子。
优点:没有创建自定义类型,因为只是套了个壳子返回对象(这个),这个函数顺理成章就成了创建的新对象。
缺点:没用到原型,无法复用。

寄生组合式继承(常用)

寄生:在函数内返回对象然后调用
组合:
1、函数的原型等于另一个实例。
2、在函数中用apply或者call引入另一个构造函数,可传参。

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
(function(){
  // 创建一个没有实例方法的类
  var Super = function(){};
  Super.prototype = Animal.prototype;
  //将实例作为子类的原型
  Cat.prototype = new Super();
})();

var cat = new Cat();
Cat.prototype.constructor = Cat; // 需要修复下构造函数
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true

重点:修复了组合继承的问题。

查看原文

赞 26 收藏 20 评论 0

认证与成就

  • 获得 86 次点赞
  • 获得 0 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 0 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-03-11
个人主页被 3.4k 人浏览