Git Product home page Git Product logo

blog's People

Contributors

forthealllight avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

blog's Issues

发布一个react组件——react-read-pdf,用于在移动端展示PDF文件

发布一个react组件——react-read-pdf,用于在移动端展示PDF文件


PC端的浏览器对于PDF文件的展示没有太大的问题,给定一个PDF的链接,就可以用浏览器默认的展示样式来展示和渲染PDF文件的内容。比如一个"http://www.baidu.com/test/pdf"。 如何在移动端展示这个文件。为了在移动端展示和渲染PDF文件的内容,本文在pdfjs的基础上实现了一个简单的react组件,用于展示和渲染PDF文件。

  • 将这个react组件,以npm包的形式发布。

  • 这个组件的项目地址为:https://github.com/forthealllight/react-read-pdf
    (如果想看使用的例子,直接下载这个代码或者clone,然后npm install和npm start即可)

React-read-pdf

使用React16.5编写的组件,用于在移动设备和PC端显示和渲染PDF文件

特点

  • Simple: 使用简单方便,仅仅是一个react组件
  • Mobile-friendly: 自适应多种移动端的设备,包括手机,平板和其他的移动办公设备

浏览器支持

  • IE 10+
  • Firefox 3.6+
  • Chrome 6+
  • Safari 6+
  • Opera 11.5+
  • iOS Safari 6.1+
  • Android Browser 3+

快速开始

1. 将 react-read-pdf引入你的react项目中

(在你的项目中比如先引入react,且必须保证React的版本必须在15.0以上)

安装react-read-pdf包

npm install --save react-read-pdf

在PC端建议使用PDFReader:

import React from 'react';
import { PDFReader } from 'react-read-pdf';

在移动端建议使用MobilePDFReader,可以自适应各种移动设备:

import React from 'react';
import { MobilePDFReader } from 'react-read-pdf';

2. 引入之后,再来看简单的使用:

import { MobilePDFReader } from 'react-read-pdf';
export default class Test extends Component{
  render(){
    return <div style={{overflow:'scroll',height:600}}>
            <MobilePDFReader url="http://localhost:3000/test.pdf"/>
           </div>
  }
}
import ReactDOM from 'react-dom';
import Test from './test'
ReactDOM.render(<Test />, document.getElementById('root'));

react-read-pdf 自适配于各种不同的移动设备,包括手机、平板和其他移动办公设备,下图是利用react-read-pdf在iphoneX上展示PDF的一个例子。

文档

react-read-pdf 这个npm包主要包括了两个不同类型的组件 PDFReader 和 MobilePDFReader.

🌱 PDFReader

import { PDFReader } from 'react-read-pdf'

...
<PDFReader url={"http://localhost:3000/test.pdf"} ...>

PDFReader组件中的属性

属性名称 类型 描述
url 字符串或者对象 如果是字符串,那么url表示的是PDF文件的绝对或者相对地址,如果是对象,可以看关于对象属性的具体描述- > url object type
data 字符串 用二进制来描述的PDF文件,在javascript中,我们可以通过“atob”,将base64编码的PDF文件,转化为二进制编码的文件。
page 数字 默认值为1,表示应该渲染PDF文件的第几页
scale 数字 决定渲染的过程中视口的大小
width 数字 决定渲染过程中,视口的宽度
showAllPage 布尔值 默认是false,表示不会一次性渲染,只会渲染page的值所指定的那一页。如果这个值为true,则一次性渲染PDF文件所有的页
onDocumentComplete 函数 将PDF文件加载后,可以通过这个函数输出PDF文件的详细信息。这个函数的具体信息如下所示。 function type

url
**PDFReader**组件的url属性

类型:

  • string : 字符串,表示PDF文件的绝对或者相对地址
  • object : 对象,有下列的属性

属性:

属性名 类型 描述
url 字符串 字符串,表示PDF文件的绝对或者相对地址
withCredentials 布尔值 决定请求是否携带cookie

onDocumentComplete
**PDFReader**的onDocumentComplete属性

Type:

  • function(totalPage)

onDocumentComplete的类型是一个函数, 这个函数的第一个参数表示的是PDF文件的总页数。

注意事项

PDFReader组件的url属性可以是字符串或者是对象。

下面两种方式都是被允许的。

其一是 :

 <MobilePDFReader url="http://localhost:3000/test.pdf"/>

另外一种方式是 :

 <MobilePDFReader url={url:"http://localhost:3000/test.pdf"}/>

🌱 MobilePDFReader

import { MobilePDFReader } from 'react-read-pdf'

...
<MobilePDFReader url={"http://localhost:3000/test.pdf"} ...>

MobilePDFReader组件中的属性

属性名称 类型 描述
url 字符串 如果是字符串,那么url表示的是PDF文件的绝对或者相对地址
page 数字 默认值为1,表示应该渲染PDF文件的第几页
scale 数字或者“auto” 默认值为“auto”,决定渲染的过程中视口的大小,推荐设置成“auto”可以根据移动设备自适应的适配scale
minScale 数字 默认值0.25, scale可取的最小值
maxScale 数字 默认值10, scale可取的最大值
isShowHeader 布尔值 默认值为true,为了生动展示,当值为true,有默认自带的头部样式。设置为false可以去掉这个默认的样式。
isShowFooter 布尔值 默认值为true,为了生动展示,当值为true,有默认自带的尾部样式。设置为false可以去掉这个默认的样式。
onDocumentComplete 函数 将PDF文件加载后,可以通过这个函数输出PDF文件的详细信息。这个函数的具体信息如下所示。function type for details

onDocumentComplete
**MobilePDFReader**的onDocumentComplete属性

类型: 函数

  • function(totalPage,title,otherObj)

函数的参数:

参数名称 类型 描述
totalPage 数字 表示PDF文件的总页数
title 字符串 PDF文件的标题
otherObj 对象 PDF文件的其他扩展或者编码信息

注意事项

scale的默认值为“auto”,强烈推荐将scale的值设置成“auto”,这样可以根据移动设备的大小自适应的改变scale的值。

开发者选项

安装

  1. Clone/download repo
  2. yarn install (or npm install for npm)

使用

Development

yarn run start-dev

  • Build app continuously (HMR enabled)
  • App served @ http://localhost:8080

Production

yarn run start-prod

  • Build app once (HMR disabled)
  • App served @ http://localhost:3000

指令列表

Command Description
yarn run start-dev Build app continuously (HMR enabled) and serve @ http://localhost:8080
yarn run start-prod Build app once (HMR disabled) and serve @ http://localhost:3000
yarn run build Build app to /dist/
yarn run test Run tests
yarn run lint Run Typescript and SASS linter
yarn run lint:ts Run Typescript linter
yarn run lint:sass Run SASS linter
yarn run start (alias of yarn run start-dev)

Note: replace yarn with npm if you use npm.

React16.x中的服务端渲染(SSR)

简要介绍:为了SEO和加快首屏加载速度,React提供了服务端渲染(Server Side Render)。本文结合express,来介绍一下React16.x中的SSR。

本例代码:https://github.com/forthealllight/react16.0-ssr

一、为什么要SSR

单页应用将UI层和内容都由javascript来渲染,搜索引擎或网页爬虫需要完成的HTML结构,因此单页应用如果只在客户端渲染,不利于SEO,此外尽管我们可以通过按需加载的形式来减少首页加载的js,但是通过js来渲染DOM的时候还是会有一定的时间延迟。

因此SSR解决的问题有两个:

  • SEO

  • 加速首屏加载

在React和Vue等前端框架中,SSR的本质就是由服务端执行渲染,直接将渲染结果以HTML结构的形式返回给客户端。也就是将Virtual DOM转化成字符串的形式返回给客户端。

二、React15.x中的SSR

在React15.x中,有两个方法来处理SSR:

  • renderToString

  • renderToStaticMarkup

这两个方法都是在react-dom/server中提供的,用来在服务端将virtual dom渲染成字符串。

(1) 相同点

renderToString和renderToStaticMarkup都接受一个参数,这个参数是react的组件,返回一段HTML字符串。

renderToString(react element):string

renderToStaticMarkup(react element):string

此外react-dom中给浏览器端提供了一个render方法,render方法将react组件,添加到真实的DOM节点中。render实现的就是浏览器端渲染。

归类一下:

服务端渲染:renderToString、renderToStaticMarkup——>string
客户端渲染:render——>HTML结构

(2) SSR实现

下面我们以renderToString为例,通过express来实现一个服务端渲染的例子。

首先node最新版本为8.9.3,还不支持es6语法,同时为了使node支持jsx,我们需要安装babel,本文为了方便,采用了babel-cli。

首先安装babel-cli:

npm install -d babel-cli

接着安装presets:

npm install -d babel-preset-latest babel-preset-stage-0 babel-preset-react

接着我们在script中:

"start":"babel-node ./server/server.js --presets es2015,stage-0,react"

最后,就可以通过 npm run start的方式实现启动server.js,server.js是经过babel处理,可以支持ES6和jsx.

在server.js中,我们利用了express的路由和中间件模块。

let express=require('express');
let app=express();
import React from 'react';
import {renderToString,renderToStaticMarkup} from 'react-dom/server';
import HomePage from '../src/components/homepage/index.js';

var server=app.listen(8080,()=>{
  var host=server.address().address;
  var port=server.address().port;
  console.log('server is start at',host,port);
});
//static
app.use('/dist',express.static('dist'));

app.get('/',(req,res)=>{
  res.write('<!DOCTYPE html><html><head><title>Hello HomePage</title></head><body>');
  res.write('<div id="app">');
  res.write(renderToString(<HomePage/>));
  res.write('</div></body>');
  res.write('<script type="text/javascript" src="../dist/vendor.bundle.js"></script><script type="text/javascript" src="../dist/js/app.js"></script>');
  res.write('</html>');
})

结构很简单,因为返回的html页面要加载静态资源,因此我们在上述的代码中还使用了express内置的静态文件模块express.static.

最后,通过npm start就能启动本地服务器,在浏览器中打开:

http://localhost:8080/ 就能看到我们SSR的例子。

11

(3) renderToString和renderToStaticMarkup的区别

  • renderToString:渲染的结果是带有data-reactid属性的,此时,在服务端的基础上,客户端的render不会重新渲染,只会执行组件componetDidmout中的业务,以及绑定事件等等。

  • renderToStaticMarkup:渲染的结果是不带有data-reactid属性的,此时不管服务端有没有渲染,在客户端中都会重新渲染该组件。

比如在renderToString的关于HomePage的返回HTML字符串结果为:

<h1 data-reactroot>Home Page</h1>

而在renderToStaticMarkup中,关于HomePage的返回HTML字符串结果为:

<h1>Home Page</h1>

三、React16.x中的SSR

(1) hydrate

在React16.x中,在客户端渲染的render的方法的基础上,增加了一个新的方法hydrate.

简单来说,如果在仅在客户端呈现内容,那么使用render方法就已经足够,如果客户端要在服务端的基础上进行渲染,那么可以使用hydrate.
使用的方法和render一样:

import {hydrate} from 'react-dom';
hydrate(<HomePage/>,document.getElementById('app'));

运行后发现提示:

Warning: render(): Calling ReactDOM.render() to hydrate server-rendered markup will stop working in React v17. Replace the ReactDOM.render() call with ReactDOM.hydrate() if you want React to attach to the server HTML.

说明React16.x中,客户端“水合”服务端,是兼容之前的render方法的,之后的版本中会移除render方法,完全用hydrate来代替。

hydrate方法,解决的是如何复用server端,ReactDOMServer的结果。

(2) stream

此外React16.x中,针对renderToString和renderToStaticMarkup提供了stream的方法:

  • renderToNodeStream
  • renderToStaticNodeStream

这两个方法同样接受的参数为react element,但是返回的不是HTML字符串,而是一个可读流。

最后给出完整代码的地址,直接npm start就可以运行:

https://github.com/forthealllight/react16.0-ssr

四、注意事项

如果不用babel-cli的方法,来babel node文件,用webpack的话可能会报一下错误:

RROR in ./node_modules/destroy/index.js
Module not found: Error: Can't resolve 'fs' in 'C:\Users\yuxl\Desktop\react-Scaffold-master\node_modules\destroy'
 @ ./node_modules/destroy/index.js 14:17-30
 @ ./node_modules/send/index.js
 @ ./node_modules/express/lib/response.js
 @ ./node_modules/express/lib/express.js
 @ ./node_modules/express/index.js
 @ ./src/server.js

ERROR in ./node_modules/etag/index.js
Module not found: Error: Can't resolve 'fs' in 'C:\Users\yuxl\Desktop\react-Scaffold-master\node_modules\etag'
 @ ./node_modules/etag/index.js 22:12-25
 @ ./node_modules/express/lib/utils.js
 @ ./node_modules/express/lib/application.js
 @ ./node_modules/express/lib/express.js
 @ ./node_modules/express/index.js
 @ ./src/server.js

ERROR in ./node_modules/express/lib/view.js
Module not found: Error: Can't resolve 'fs' in 'C:\Users\yuxl\Desktop\react-Scaffold-master\node_modules\express\lib'
 @ ./node_modules/express/lib/view.js 18:9-22
 @ ./node_modules/express/lib/application.js
 @ ./node_modules/express/lib/express.js
 @ ./node_modules/express/index.js
 @ ./src/server.js

ERROR in ./node_modules/send/index.js
Module not found: Error: Can't resolve 'fs' in 'C:\Users\yuxl\Desktop\react-Scaffold-master\node_modules\send'
 @ ./node_modules/send/index.js 23:9-22
 @ ./node_modules/express/lib/response.js
 @ ./node_modules/express/lib/express.js
 @ ./node_modules/express/index.js
 @ ./src/server.js

ERROR in ./node_modules/send/node_modules/mime/mime.js
Module not found: Error: Can't resolve 'fs' in 'C:\Users\yuxl\Desktop\react-Scaffold-master\node_modules\send\node_modules\mime'
 @ ./node_modules/send/node_modules/mime/mime.js 2:9-22
 @ ./node_modules/send/index.js
 @ ./node_modules/express/lib/response.js
 @ ./node_modules/express/lib/express.js
 @ ./node_modules/express/index.js

也就是webpack在打包的时候找不到 node自带的模块,比如fs等,解决的方法是在webpack的配置文件里面增加:

target:'node'

Webpack中的sourcemap以及如何在生产和开发环境中合理的设置sourcemap的类型

简要介绍:在webpack的官网,给出了十几种sourcemap,那么每一种sourcemap之间有什么区别,本文在理解sourcemap的基础上,分析在生产和开发环境中,应该采用何种形式的sourcemap

一 、 从Sourcemap和Data URL说起

(1) 什么是Sourcemap?

我们在打包中,将开发环境中源代码经过压缩,去空格,babel编译转化,最终可以得到适用于生产环境的项目代码,这样处理后的项目代码和源代码之间差异性很大,会造成无法debug的问题。

举例来说,如果压缩等处理过的生产环境中的代码出现bug,调试的时候只能定位到压缩处理后的代码的位置,无法定位到开发环境中的源代码。

sourcemap就是为了解决上述代码定位的问题,简单理解,就是构建了处理前的代码和处理后的代码之间的桥梁。主要是方便开发人员的错误定位。这里的处理操作包括:

  • 压缩,减小体积

  • 将多个文件合并成同一个文件

  • 其他语言编译成javascript,比如TypeScript和CoffeeScript等

(2) 什么是DataURL?

DataURL最早是出现在HTML文件img标签中的关于图片的引用,DataURL提供了一种将图片"嵌入"到HTML中的方法。

跟传统的img的src属性指向服务器中某张图片的地址不同,在Data URL协议中,图片被转换成base64编码的字符串形式,并存储在URL中,冠以mime-type。具体通过DataURL引入图片例子如下:

<img src="data:image/gif;base64,R0lGODlhMwAxAIAAAAAAAP///
yH5BAAAAAAALAAAAAAzADEAAAK8jI+pBr0PowytzotTtbm/DTqQ6C3hGX
ElcraA9jIr66ozVpM3nseUvYP1UEHF0FUUHkNJxhLZfEJNvol06tzwrgd
LbXsFZYmSMPnHLB+zNJFbq15+SOf50+6rG7lKOjwV1ibGdhHYRVYVJ9Wn
k2HWtLdIWMSH9lfyODZoZTb4xdnpxQSEF9oyOWIqp6gaI9pI1Qo7BijbF
ZkoaAtEeiiLeKn72xM7vMZofJy8zJys2UxsCT3kO229LH1tXAAAOw==">

DataURL使用于如下的场景

  • 访问外部资源受限

  • 图片体积小,占用一个HTTP会话资源浪费

二 、 Webpack中的Sourcemap

webpack在打包中同样支持Sourcemap,并且提供了十几种的组合。我们以官网给出的为例:

  • eval : 每一个模块都执行eval()过程,并且会追加//@ sourceURL

  • eval-source-map:每一个模块在执行eval()过程之后,并且会为每一个模块生成sourcemap文件,生成的sourcemap文件通过DataURL的方式添加

  • cheap-eval-source-map:跟eval-source-map相同,唯一不同的就是增加了"cheap","cheap"是指忽略了行信息。这个属性同时也不会生成不同loader模块之间的sourcemap。

  • cheap-module-eval-source-map:与cheap-eval-source-map相同,但是包含了不同loader模块之间的sourcemap

官网给出的例子容易让人看懵,因为官网的devtool类型都是以组合形式给出的,实际上webpack中的sourcemap的基本类型包括:eval,cheap,moudule,inline,source-map。其他的类型都是根据这5个基本类型组合而来。我们来具体分析一下这5个基本类型

(1) eval

eval会将每一个module模块,执行eval,执行后不会生成sourcemap文件,仅仅是在每一个模块后,增加sourceURL来关联模块处理前后的对应关系。举例来说:

webpackJsonp([1],[  
function(module,exports,__webpack_require__){    
eval(
      ...      
//# sourceURL=webpack:///./src/js/index.js?'
    )
  },  
function(module,exports,__webpack_require__){    
eval(
      ...      
//# sourceURL=webpack:///./src/static/css/app.less?./~/.npminstall/css-loader/0.23.1/css-loader!./~/.npminstall/postcss-loader/1.1.1/postcss-loader!./~/.npminstall/less-loader/2.2.3/less-loader'
    )
  },  
function(module,exports,__webpack_require__){   
 eval(
      ...      
 //# sourceURL=webpack:///./src/tmpl/appTemplate.tpl?"
    )
  },
...])

上述是一个指定devtool:eval的压缩后的代码,我们发现压缩后的代码的每一个模块后面都增加了一端包含sourceURL的注释,sourceURL的值是压缩前的代码,这样就通过sourceURL关联了压缩前后的代码,并没有为每一个模块生成相应的sourcemap。

因为不需要生成模块的sourcemap,因此打包的速度很快。

(2) soure-map

source-map会为每一个打包后的模块生成独立的soucemap文件,举例来说:

webpackJsonp([1],[  
function(e,t,i){...},  
function(e,t,i){...},  
function(e,t,i){...},  
function(e,t,i){...},
  ...
])//# sourceMappingURL=index.js.map

打包后的模块在模块后面会对应引用一个.map文件,同时在打包好的目录下会针对每一个模块生成相应的.map文件,在上例中会生成一个index.js.map文件,这个文件是一个典型的sourcemap文件,形式如下:

{  
"version":3,  
"sources":[
    "webpack:///js/index.js","webpack:///./src/js/index.js",    
    "webpack:///./~/.npminstall/css-loader/0.23.1/css-loader/lib/css-base.js",
    ...
],  
"names":["webpackJsonp","module","exports"...], 
"mappings":"AAAAA,cAAc,IAER,SAASC...",  
"file":"js/index.js",  
"sourcesContent":[...],  
"sourceRoot":""
}

####( 3) inline

与source-map不同,增加inline属性后,不会生成独立的.map文件,而是将.map文件以dataURL的形式插入。如下所示:

webpackJsonp([1],[  
function(e,t,i){...},  
function(e,t,i){...},  
function(e,t,i){...},  
function(e,t,i){...},
  ...
])
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9...

打包好模块后,在sourceMappingURL中直接将.map文件中的内容以DataURL的方式引入。

(4) cheap

cheap属性在打包后同样会为每一个模块生成.map文件,但是与source-map的区别在于cheap生成的.map文件会忽略原始代码中的列信息。

devtool: 'eval-source-map'

"mappings": "AAAAA,QAAQC,GAAR,CAAY,aAAZ",
devtool: 'cheap-source-map'

"mappings": "AAAA",

对比增加cheap和没有cheap情况下,打包后输出的.map文件,在文件中
使用了VLQ编码,有"逗号"表示包含了列信息,显然增加cheap属性后,.map文件中不包含列信息。

此外增加cheap后也不会有loader模块之间对应的sourcemap,什么是模块之间的sourcemap呢?

因为webpack最终会将所有的非js资源,通过loader的形式转变成js资源。比如jsx语言的操作分为:

jsx——(loader)——js——(压缩等处理)——压缩后的js

如果没有loader之间的sourcemap,那么在debug的时候定义到上图中的压缩前的js处,而不能追踪到jsx中。

(5) module:包含了loader模块之间的sourcemap

这样差不多就理清了webpack中所有的sourcemap类型。

三 、 在不同的环境中如何选择sourcemap的类型

在了解了webpack中所有的sourcemap基本类型后,我们来分析,如何针对开发环境和生产环境,选择合理的sourcemap属性。

(1) 首先在源代码的列信息是没有意义的,只要有行信息就能完整的建立打包前后代码之间的依赖关系。因此不管是开发环境还是生产环境,我们都会选择增加cheap基本类型来忽略模块打包前后的列信息关联。

(2) 其次,不管在生产环境还是开发环境,我们都需要定位debug到最最原始的资源,比如定位错误到jsx,coffeeScript的原始代码处,而不是编译成js的代码处,因此,不能忽略module属性

(3) 再次我们希望通过生成.map文件的形式,因此要增加source-map属性

四、总结:

在开发环境中我们使用:cheap-module-eval-source-map

在生产环境中我们使用:cheap-module-source-map。

这里需要补充说明的是,eval-source-map组合使用是指将.map以DataURL的形式引入到打包好的模块中,类似于inline属性的效果,我们在生产中,使用eval-source-map会使打包后的文件太大,因此在生产环境中不会使用eval-source-map。但是因为eval的rebuild速度快,因此我们可以在本地环境中增加eval属性。

golang学习笔记(一)—— golang基础和相关数据结构

golang学习笔记(一)—— golang基础和相关数据结构


小白前端一枚,最近在研究golang,记录自己学习过程中的一些笔记,以及自己的理解。

  • go中包的依赖管理
  • go中的切片
  • byte 和 string
  • go中的Map
  • go中的struct结构体
  • go中的方法
  • go中的interface接口
  • interface{}

1、go中包的依赖管理

首先要了解的是GOPATH的含义,GOPATH是go命令依赖的重要变量,可以通过:

go env

来查看相应的开发环境中GOPATH的值,也可以通过export GOPATH指定:

export GOPATH = /usr/local/go

指定GOPATH目录后, GOPATH目录包含了3个子目录:

  • src存放源代码(比如.go、.h文件等)
  • pkg编译时生成的中间文件
  • bin编译后生成的可执行文件

此外,go的依赖管理中提供了3个主要的命令go build、go get和 go install。

  • go build: 会编译目录下或者指定的.go文件,得到一个可执行的文件
  • go install: 只能在GOPATH目录下使用,与go build大致相同会编译目录下执行的.go文件,此外go install还会将可执行文件或库文件安装到GOPATH目录下。
  • go install + 远程地址: 会将相应的代码下载到GOPATH同时会编译该远程包
  • go get + 远程地址: 跟go install+远程地址相同,会下载且编译
  • go get u + 远程地址: 下载并更新相应的远程地址的包,但不会自动编译

典型的例子,比如下载一个dep包:

go get -u github.com/golang/dep/cmd/dep

上述的go get和go install + 远程包的方式,不能应用于需要版本管理依赖等场景,可以通过安装dep包,来实现依赖管理。dep提供了几个常用的命令,分别用于安装和更新相应的go包。

  • dep init 初始化一个项目依赖
  • dep ensure 安装项目所依赖的所有包
  • dep ensure -update 更新项目中的所有包
  • dep ensure -add github.com/pkg/errors 为项目添加单个依赖包

此外通过Gopkg.toml里面可以指定所依赖包的git分支,版本号等等,且在dep ensure -add中也可以指定分支和版本号,比如:

dep ensure -add github.com/pkg/foo@^1.0.1

提到包(package),必须补充一句,在go中如果在其他包中引用变量,是通过:

包名.变量名

的形式,在这里变量名必须是大写的,也就是说在go的包中,变量能否导出是根据变量的大小写来确定的,普遍认为如果变量是大写的就是在包内导出的,如果是变量小写的就是默认是包的私有变量。

2、go中的切片

在go的函数调用中,如果传递的参数是一个较大的数组,显然如果直接将数组作为实参传入,在执行函数的过程中,实际上会拷贝一份该数组,会造成内存的浪费等。标准的做法,是传入数组的指针,或者对于数组的部分引用。

这里关于数组的部分引用,就是slice切片

(1)、go中的切片简介

数组和切片之间存在着紧密的联系,slice提供了访问数组子序列的功能。所谓的切片是对于数组的部分引用,slice由三部分组成指针、长度和容量。

  • 指针: 指向第一个slice元素所对应的数组元素的地址
  • 长度: slice中元素的数目
  • 容量: slice中最多可容纳元素的数目

切片的定义方式:

var slice1 []type = make([]type, len, cap)

分别指定切片的类型,长度以及容量。

切片的初始化:

s := [] int { 1,2,3 }

或者通过已经存在的数组来实现切片的初始化,

arr = [10]int {1,2,3,4,5,6,7,8,9,10}
s:=arr[1:5] // arr[startIndex:endIndex]

(2)、go中的切片注意点

go中的slice切片有一个注意点,就是如何判断切片为空,边界情况大致如下所示:

var s []int   //len(s)==0,s==nil
s = nil       //len(s)==0,s==nil
s = []int(nil)//len(s)==0,s==nil
s = []int{}   //len(s)==0,s!=nil

显然如果通过s==nil来判断,不能区别第四种场景,因此判断切片为空的正确方式是len(s)==0.

3、byte 和 string

下述的方法将返回一个byte的切片:

var test:= []byte("hello")

go遍历slice动态删除 map遍历删除安全.

4、go中的Map

map是一个无序的key/value对的集合,其中在每一个map中key是唯一的。go中的map只要坑在于map是无序的。

(1)、Map简介

声明一个map:

var ages map[string]int  //同样初始的情况下,ages = nil
ages == nil // true

如果声明了但是没有赋值,那么尝试插入一对key/value会报错,比如上述声明但没有初始化的情况下:

age["jony"] = 25 // 会panic

解决方法,就是给age定义后赋值:

ages = make(map[string]int)

或者定义的时候同时赋值:

ages := map[string]int{

}

此后插入不存在的key/value就不会报错。

注意:尝试从map中去一个不存在的key,默认的value值为0

(2)、Map无序性

我们从map的遍历结果,来说明map是无序的。比如我们以这么一个map为例:

var ages = map[string]int{
"a":21,
"b":22,
"c":23,
};
for name,age := range ages {
  fmt.Printf("%s\t%d\n",name,age);
}

通过for range可以遍历map对象,分别执行三次遍历后,来看遍历的结果

  • 第一次输出:

    c 23
    a 21
    b 22

  • 第二次输出:
    c 23
    b 22
    a 21

  • 第三次输出:
    a 21
    b 22
    c 23

从上述的结果我们也可以看出map的每次遍历的结果都是不确定的。

注意:Map的value类型不仅仅可以是基本类型,也可以是聚合类型,比如map或者slice。

5 、go中的struct结构体

跟C++中的结构体类似,go中的结构体是一种聚合数据类型,由0个或者多个任意值聚合成实体。

(1)、结构体简介

声明一个结构体很简单,比如我们声明了一个Person结构体:

type Person struct {
   name string
   age int
   salary int
}

然后可以声明一个Person类型的变量:

var person Person

然后可以通过点操作符访问和赋值。

person.age = 25

此外,可以通过取地址符号加点操作符来访问和赋值,下述取地址的方式效果与上述是相同的。

(&person).age = 25

此外,结构体也支持嵌套。

6、go中的方法

在go中没有明确的定义类,但是可以将结构体struct来类比其他语言中的class。

go中的方法与结构体相关,为了说名go中的方法,我们先从go中的函数讲起。

(1)、go中的函数简介

在go中函数声明包括函数名、形参列表、返回值列表(可省略 表示无返回值)以及函数体。

func name (parameter-list)(result-list){


}

比如我们有一个count函数可以如此简单的定义:

func count(x,y int) int {
   return x + y
}

(2)、go中方法简介

在函数定义的基础上我们来介绍一下,如何定义方法。在函数声明时,在函数名前放上一个变量,这个变量称为方法的接收器,一般是结构体类型的。

当然也不一定是结构体,基本类型数值、字符串、slice和map上面都可以作为接收器来定义方法。

声明方法的方式具体可以如下所示:

func (receive Receive) name(parameter-list)(result-list){


}

从上述的声明中也可以看出来只不过在函数的技术上增加了第一个参数接收器,为相应的接收器增加了该名称的方法。比如我们定一个Person结构体,并为其声明sellHello方法:

type Person struct {
   name string
   age int
   salary int
}

func (person Person) sayHello() string{
  return "Hello "+ person.name
}

p := Person{
   name: "Jony",
   age: 25,
   salary:100
}

fmt.Println(p.sayHello());//输出Hello Jony

上述就是在结构体Person上定义了一个sayHello方法,在结构体被初始化后,可以通过p.sayHello()的方式直接调用。

除此之外,我们前面将到定义方法时的接收器不一定是一个结构体,接收器也可以接受基本类型等,比如:

type Mystring string;

func (mystring Mystring)sayHello() string{
  return "Hello"+ string(mystring);
}

var m Mystring
m = "Jony"
fmt.Println(m.sayHello());

上述的例子同样会输出Hello Jony.

甚至nil也可以作为方法的接收器,这里就不具体举例。

(3)、基于指针对象的方法

在函数调用时,是对实参的一个拷贝,如果函数需要更新一个变量,或者传递的参数过大,默认拷贝太为负责,我们经常会使用指针的形式,对于方法而言也同样如此,也就是说方法的接收器可以是指针类型。

对比于上述非指针类型的方法,声明指针类型的方法具体如下所示:

func (receive *Receive) name(parameter-list)(result-list){


}

指针类型的参数作为接收器,可以修改传入参数的实际变量的值。

type Person struct {
  name string
  age int
  salary int
}
func (person *Person) changeAge(newAge int){
  (*person).age = newAge
}
p.changeAge(30);
fmt.Println(p.age); //输出了30,发现age确实发生了改变。

7、go中的interface接口

我们前面也说过go不是一种传统的面向对象的语言,没有类和继承的概念,go里面通过interface接口可以实现很多面向对象的特性。

接口的通俗定义:

接口提供了一种方式来说明对象的行为,接口定义了一组方法,但是不包含实现。

(1)、interface接口简介

可以通过如下格式来定义接口:

type Namer interface {
   Method1(param_list) return_type
   Method2(param_list) return_type
   ...
}

go中的接口都很简短,一般包含了0-3个方法。

同时我们可以通过:

var ai Namer 

来定义一个接口类型的变量,初始值为nil.接口类型的变量是一个指针,声明而未赋值的情况下就为nil。

go中的接口有以下需要注意的点:

  • 一个类型可以实现多个接口
  • 接口类型可以包含一个实例的引用,该实例的类型实现了此接口
  • 即使接口在类型之后定义,二者存在不同的包中,被单独编译,但是只要类型实现了接口中的方法,它就实现了此接口
  • 实现某个接口的类型,除了实现接口方法外,还可以有其他的方法

上述几点都比较好理解,具体第二点,举例来说:

type Person struct {
 name string
 age int
 salary int
}
type Say interface {
  sayHello() string
}
func (person Person) sayHello() string {
  return "Hello "+person.name
}

func main() {
  p := new(Person)
  p.name = "Jony"

  var s Say;
  s = p;
  fmt.Println(s)
}

上述例子中,我们首先new了一个Person结构体类型的变量,并赋值给p,因为Person接口体中实现了Say接口中的所有方法sayHello等。因此我们就说Person实现了Say接口,因此Person的实例p,可以赋值给一个Say接口类型的变量s。

此时的s是个指针,指向Person结构体实例p。

(2)、interface接口类型断言

任何类型只要实现了接口中的所有方法,我们就说该类型实现了该接口。这样一个接口类型的变量varI可以包含任何类型的值,在go中提供了一种安全的方式来检测它的动态类型。

if v,ok := varI.(T);ok {
   Process(v)
   return
}

如果转化合法,那么v是varI转化到类型T的值,ok会是true,否则v是类型T的零值,ok是false。这是一种安全的转化方式不会有错误发生。

我们还是接着上面的代码来讲我们的例子:

type Person struct {
 name string
 age int
 salary int
}

type Say interface {
  sayHello() string
}

func (person Person) sayHello() string {
  return "Hello "+person.name
}

func main() {
  p := new(Person)
  p.name = "Jony"

  var s Say;
  s = p;
  if t,ok := s.(*Person);ok {
    fmt.Printf("The type of s is:%T\n",t);
  }
}

输出的结果为The type of s is:*main.Person。也可以使用特殊的type-switch来判断。

switch t:= s.(*Person){
   case *Person:
      fmt.Printf("The type of s is:%T\n",t);
   case nil:
      ...
   default:
      ...
}

8、interface{}

interface{}是一个空接口,任何类型的值都可以复制给interface{}类型的变量。

比如,我们首先声明一个类型为interface{}的变量:

var test interface{}

任意类型的值都可以复制给test,比如下列基本类型的值复制给test是有效的:

var test interface{}
test = 1
test = true
test ="Hello"

此外,复杂的派生类型也可以赋值给test,我们以指针类型举例:

var test interface{}
var a = 1
test = &a 

interface类型的变量是没有类型的,但是我们可以人为的进行类型转换:

var test interface{}
var a string
test = "hello"
a = test.(string)

上述,可以将test转化成string类型,这样就可以赋值给string类型变量a了。通过.(类型名)的方法可以将interface{}类型的变量转化成任意的类型。

最后举一个简单的例子:

func main() {
   a := make([]interface{},10)
   b :=1
   a[1]=&b
   fmt.Println(*(a[1].(*int)))
}

上述代码发现,将interface{}类型切片中的某一元素的值复制给了int指针类型,然后进行了类型转化,将interface{}类型的变量转换成了int指针类型。

谈谈对16.7.0-alpha中即将引入的hooks的理解

谈谈对react16.7.0-alpha中即将引入的hooks的理解


  为了实现分离业务逻辑代码,实现组件内部相关业务逻辑的复用,在React的迭代中针对类组件中的代码复用依次发布了Mixin、HOC、Render props等几个方案。此外,针对函数组件,在React v16.7.0-alpha 中提出了hooks的概念,在本身无状态的函数组件,引入独立的状态空间,也就是说在函数组件中,也可以引入类组件中的state和组件生命周期,使得函数组件变得丰富多彩起来,此外,hooks也保证了逻辑代码的复用性和独立性。

  本文从针对类组件的复用解决方案开始说起,先后介绍了从Mixin、HOC到Render props的演进,最后介绍了React v16.7.0-alpha 中的 hooks以及自定义一个hooks

  • Mixin
  • HOC
  • Render props
  • React hooks的介绍以及如何自定义一个hooks

一、Mixin

Mixin是最早出现的复用类组件中业务逻辑代码的解决方案,首先来介绍以下如何适应Mixin。下面是一个Mixin的例子:

const someMixins={
  printColor(){
    console.log(this.state.color);
  }
  setColor(newColor){
    this.setState({color:newColor})
  }
  componentDidMount(){
    ..
  }
}

下面是一个使用Mixin的组件:

class Apple extends React.Component{
  //仅仅作为演示,mixins一般是通过React.createClass创建,并且ES6中没有这种写法
  mixins:[someMixins]
  constructor(props){
    super(props);
    this.state={
      color:'red'
    }
    this.printColor=this.printColor.bind(this);
  }
  render(){
    return <div className="m-box" onClick={this.printColor}>
                这是一个苹果
           </div>
  }
}

在类中mixin引入公共业务逻辑:

mixins:[someMixins]

从上面的例子,我们来总结以下mixin的缺点:

  • Mixin是可以存在多个的,是一个数组的形式,且Mixin中的函数是可以调用setState方法组件中的state的,因此如果有多处Mixin的模块中修改了相同的state,会无法确定state的更新来源

  • ES6 classes支持的是继承的模式,而不支持Mixins

  • Mixin会存在覆盖,比如说两个Mixin模块,存在相同生命周期函数或者相同函数名的函数,那么会存在相同函数的覆盖问题。

Mixin已经被废除,具体缺陷可以参考Mixins Considered Harmful

二、HOC

  为了解决Mixin的缺陷,第二种解决方案是高阶组件(high order component,简称HOC)。

1、举例几种HOC的形式

  HOC简单理解就是组件工厂,接受原始组件作为参数,添加完功能与业务后,返回新的组件。下面来介绍HOC参数的几个例子。

(1)参数仅为原始组件

const redApple = withFruit(Apple);

(2)参数为原始组件和一个对象

const redApple = withFruit(Apple,{color:'red',weight:'200g'});

但是这种情况比较少用,如果对象中仅仅传递的是属性,其实完全可以通过组件的props实现值的传递,我们用HOC的主要目的是分离业务,关于UI的展示,以及一些组件中的属性和状态,我们一般通过props来指定比较方便

(3)参数为原始组件和一个函数

const redApp=withFruit(App,()=>{console.log('I am a fruit')})

(4)柯里化

最常见的是仅以一个原始组件作为参数,但是在外层包裹了业务逻辑,比如react-redux的conect函数中:

class Admin extends React.Component{

}
const mapStateToProps=(state)=>{
  return {
  };
}
const mapDispatchToProps=(dispatch)=>{
  return {

  }
}

const connect(mapStateToProps,mapDispatchToProps)(Admin)

2、HOC的缺点

HOC解决了Mixin的一些缺陷,但是HOC本身也有一些缺点:

(1)难以溯源,且存在属性覆盖问题

  如果原始组件A,先后通过工厂函数1,工厂函数2,工厂函数3….构造,最后生成了组件B,我们知道组件B中有很多与A组件不同的props,但是我们仅仅通过组件B,并不能知道哪个组件来自于哪个工厂函数。同时,如果有2个工厂函数同时修改了组件A的某个同名属性,那么会有属性覆盖的问题,会使得前一个工厂函数的修改结果失效。

(2)HOC是静态构建的

  所谓静态构建,也就是说生成的是一个新的组件,并不会马上render,HOC组件工厂是静态构建一个组件,这类似于重新声明一个组件的部分。也就是说,HOC工厂函数里面的声明周期函数,也只有在新组件被渲染的时候才会执行。

(3)会产生无用的空组件

三、Render Prop

  Render Props从名知义,也是一种剥离重复使用的逻辑代码,提升组件复用性的解决方案。在被复用的组件中,通过一个名为“render”(属性名也可以不是render,只要值是一个函数即可)的属性,该属性是一个函数,这个函数接受一个对象并返回一个子组件,会将这个函数参数中的对象作为props传入给新生成的组件。

  这种方法跟直接的在父组件中,将父组件中的state直接传给子组件的区别是,通过Render Props不用写死子组件,可以动态的决定父组件需要渲染哪一个子组件。

或者再概括一点:

Render Props就是一个函数,做为一个属性被赋值给父组件,使得父组件可以根据该属性去渲染子组件。

(1)标准父子组件通信方法

  首先来看常用的在类组件中常用的父子组件,父组件将自己的状态state,通过props传递给子组件。

class Son extends React.Component{
  render(){
  const {feature} = this.props;
   return <div>
             <span>My hair is {feature.hair}</span>
             <span>My nose is {feature.nose}</span>
          </div>
  }
}

class FatherToSon extends React.Component{
   constructor(){
      this.state = {
        hair:'black',
        nose:'high'
      }
   }
  render(){
    return <Son feature = {this.state}>
  }
}

  我们定义了父组件FatherToSon,存在自身的state,并且将自身的state通过props的方式传递给了子组件。

  这种就是常见的利用组件的props父子间传值的方式,这个值可以是变量,对象,也可以是方法,但是仅仅使用只能一次性的给特定的子组件使用。如果现在有个Daughter组件也想复用父组件中的方法或者状态,那么必须新构建一个新组件:

class FatherToDaughter extends React.Component{
   constructor(){
      this.state = {
        hair:'black',
        nose:'high'
      }
   }
  render(){
    return <Daughter feature = {this.state}>
  }
}

从上面的例子可以看出通过标准模式的父子组件的通信方法,虽然能够传递父组件的状态和函数,但是无法实现复用。

(2)Render Props的引出

我们根据Render Props的特点:

Render Props就是一个函数,做为一个属性被赋值给父组件,使得父组件可以根据该属性去渲染子组件。

重新去实现上述的(1)中的例子。

class FatherChild extends React.Component{
   constructor(){
      this.state = {
        hair:'black',
        nose:'high'
      }
   }
  render(){
    <React.Fragment>
      {this.props.render}
    </React.Fragment>
  }
}

此时如果子组件要复用父组件中的属性或者函数,则可以直接使用,比如子组件Son现在可以直接调用:

<FatherChild render={(obj)=>(<Son feature={obj}>)} />

如果子组件Daughter要复用父组件的方法,可以直接调用:

<FatherChild render={(obj)=>(<Daughter feature={obj}>)} />

  从这个例子中可以看出,通过Render Props我们实现同样实现了一个组件工厂,可以实现业务逻辑代码的复用,相比与HOC,Render Props有以下几个优点。

  • 不用担心props的命名问题
  • 可以溯源,子组件的props一定是来自于直接父组件
  • 是动态构建的

Render Props也有一个缺点:

就是无法利用SCU这个生命周期,来实现渲染性能的优化。

四、React hooks的介绍以及如何自定义一个hooks

  hooks概念在React Conf 2018被提出来,并将在未来的版本中被引入,hooks遵循函数式编程的理念,主旨是在函数组件中引入类组件中的状态和生命周期,并且这些状态和生命周期函数也可以被抽离,实现复用的同时,减少函数组件的复杂性和易用性。

  hooks相关的定义还在beta中,可以在React v16.7.0-alpha中体验,为了渲染hooks定义的函数组件,必须执行React-dom的版本也为v16.7.0-alpha,引入hooks必须先安装:

npm i -s React@16.7.0-alpha

npm i -s React-dom@16.7.0-alpha

  hooks主要有三部分组成,State Hooks、Effect Hooks和Custom Hooks,下面分别来一一介绍。

(1)State Hooks

  跟类组件一样,这里的state就是状态的含义,将state引入到函数组件中,同时类组件中更新state的方法为setState,在State Hooks中也有相应的更新状态的方法。

function ExampleWithManyStates() {
  // 声明各种state以及更新相应的state的方法
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
  // ...
}

  上述就声明了3个State hooks,相应的方法为useState,该方法创建一个传入初始值,创建一个state。返回一个标识该state的变量,以及更新该state的方法。

  从上述例子我们来看,一个函数组件是可以通过useState创建多个state的。此外State Hooks的定义必须在函数组件的最高一级,不能在嵌套,循环等语句中使用。

function ExampleWithManyStates() {
  // 声明各种state以及更新相应的state的方法
  if(Math.random()>1){
    const [age, setAge] = useState(42);
    const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
  }else{
    const [fruit, setFruit] = useState('banana');
    const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
  }
  
  // ...
}

  上述的方式是不被允许的,因为一个函数组件可以存在多个State Hooks,并且useState返回的是一个数组,数组的每一个元素是没有标识信息的,完全依靠调用useState的顺序来确定哪个状态对应于哪个变量,所以必须保证使用useState在函数组件的最外层,此外后面要介绍的Effect Hooks的函数useEffect也必须在函数组件的最外层,之后会详细解释。

(2)Effect Hooks

  通过State Hooks来定义组件的状态,同样通过Effect Hooks来引入生命周期,Effect hooks通过一个useEffect的方法,以一种极为简化的方式来引入生命周期。
来看一个更新的例子:

import { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

上述就是一个通过useEffect来实现组件中生命周期的例子,useEffect整合了componentDidMount和componentDidUpdate,也就是说在componentDidMount和componentDidUpdate的时候都会执行一遍useEffect的函数,此外为了实现componentWillUnmount这个生命周期函数,useEffect函数如果返回值是一个函数,这个函数就被定义成在componentWillUnmount这个周期内执行的函数。

useEffect(() => {
    //componentDidMount和componentDidUpdate周期的函数体
    return ()=>{ 
       //componentWillUnmount周期的函数体
    }
});

如果存在多个useState和useEffect时,必须按顺序书写,定义一个useState后,紧接着就使用一个useEffect函数。

useState('Mary')           
useEffect(persistForm)    
useState('Poppins')       
useEffect(updateTitle)

因此通useState一样,useEffect函数也必须位于函数组件的最高一级。

(3)Effect Hooks的补充

上述我们知道useEffect其实包含了componentDidMount和componentDidUpdate,如果我们的方法仅仅是想在componentDidMount的时候被执行,那么必须传递一个空数组作为第二个参数。

useEffect(() => {
  //仅在componentDidMount的时候执行
},[]);

上述的方法会仅仅在componentDidMount,也就是函数组件第一次被渲染的时候执行,此后及时状态更新,也不会执行。

此外,为了减少不必要的状态更新和渲染,可以如下操作:

useEffect(() => {
  //仅在componentDidMount的时候执行
},[stateName]);

在上述的这个例子中,只有stateName的值发生改变,才会去执行useEffect函数。

(4)Custom Hooks自定义hooks

可以将useState和useEffect的状态和生命周期函数抽离,组成一个新的函数,该函数就是一个自定义的封装完毕的hooks。

这是我写的一个hooks ---> dom-location,

可以这样引入:

npm i -s dom-location 

并且可以在函数组件中使用。这个自定义的hooks也很简单,就是封装了状态和生命周期函数。

import { useState, useEffect } from 'react'

const useDomLocation = (element) =>  {
  let [elementlocation,setElementlocation] = useState(getlocation(element));
  useEffect(()=>{
    element.addEventListener('resize',handleResize);
    return ()=>{
      element.removeEventListener('resize', handleResize);
    }
  },[]);
  function handleResize(){
    setElementlocation(getlocation(element));
  }
  function getlocation(E){
    let rect = E.getBoundingClientRect()
    let top = document.documentElement.clientTop
    let left= document.documentElement.clientLeft
    return{
        top    :   rect.top - top,
        bottom :   rect.bottom - top,
        left   :   rect.left - left,
        right  :   rect.right - left
    };
  }
  return elementlocation

}

然后直接在函数中使用:

import useDomLocation from 'dom-location';
function App() {
  ....
  let obj = useDomLocation(element);
  
}

实现一个完美符合Promise/A+规范的Promise

简要介绍:Promise允许我们通过链式调用的方式来解决“回调地狱”的问题,特别是在异步过程中,通过Promise可以保证代码的整洁性和可读性。本文主要解读Promise/A+规范,并在此规范的基础上,自己实现一个Promise.

一、Promise的使用

在了解Promise规范之前,我们知道主流的高版本浏览器已经支持ECMA中的Promise.

创建一个promise实例:

var p=new Promise(function(resolve,reject){
    setTimeout(function(){
       resolve("success")
    },1000);
    console.log("创建一个新的promise");
})
p.then(function(x){
  console.log(x)
})

//输出:
创建一个新的promise
success

上述是一个promise的实例,输出内容为,“创建一个promise”,延迟1000ms后,输出"success"。

从上述的例子可以看出,promise方便处理异步操作。此外promise还可以链式的调用:

var p=new Promise(function(resolve,reject){resolve()});
p.then(...).then(...).then(...)

此外Promise除了then方法外,还提供了Promise.resolve、Promise.all、Promise.race等等方法。

二、Promise/A+规范

Promise/A+规范扩展了早期的Promise/A proposal提案,我们来解读一下Promise/A+规范。

1.术语

(1)"promise"是一个对象或者函数,该对象或者函数有一个then方法

(2)"thenable"是一个对象或者函数,用来定义then方法

(3)"value"是promise状态成功时的值

(4)"reason"是promise状态失败时的值

我们明确术语的目的,是为了在自己实现promise时,保持代码的规范性(也可以跳过此小节)

2.要求

(1)一个promise必须有3个状态,pending,fulfilled(resolved),rejected当处于pending状态的时候,可以转移到fulfilled(resolved)或者rejected状态。当处于fulfilled(resolved)状态或者rejected状态的时候,就不可变。

promise英文译为承诺,也就是说promise的状态一旦发生改变,就永远是不可逆的。

(2)一个promise必须有一个then方法,then方法接受两个参数:

promise.then(onFulfilled,onRejected)

其中onFulfilled方法表示状态从pending——>fulfilled(resolved)时所执行的方法,而onRejected表示状态从pending——>rejected所执行的方法。

(3)为了实现链式调用,then方法必须返回一个promise

promise2=promise1.then(onFulfilled,onRejected)

三、实现一个符合Promise/A+规范的Promise

解读了Promise/A+规范之后,下面我们来看如何实现一个Promise,
首先构造一个myPromise函数,关于所有变量和函数名,应该与规范中保持相同。

1.v1.0 初始版本myPromise

function myPromise(constructor){
    let self=this;
    self.status="pending" //定义状态改变前的初始状态
    self.value=undefined;//定义状态为resolved的时候的状态
    self.reason=undefined;//定义状态为rejected的时候的状态
    function resolve(value){
        //两个==="pending",保证了状态的改变是不可逆的
       if(self.status==="pending"){
          self.value=value;
          self.status="resolved";
       }
    }
    function reject(reason){
        //两个==="pending",保证了状态的改变是不可逆的
       if(self.status==="pending"){
          self.reason=reason;
          self.status="rejected";
       }
    }
    //捕获构造异常
    try{
       constructor(resolve,reject);
    }catch(e){
       reject(e);
    }
}

同时,需要在myPromise的原型上定义链式调用的then方法:

myPromise.prototype.then=function(onFullfilled,onRejected){
   let self=this;
   switch(self.status){
      case "resolved":
        onFullfilled(self.value);
        break;
      case "rejected":
        onRejected(self.reason);
        break;
      default:       
   }
}

上述就是一个初始版本的myPromise,在myPromise里发生状态改变,然后在相应的then方法里面根据不同的状态可以执行不同的操作。

var p=new myPromise(function(resolve,reject){resolve(1)});
p.then(function(x){console.log(x)})
//输出1

但是这里myPromise无法处理异步的resolve.比如:

var p=new myPromise(function(resolve,reject){setTimeout(function(){resolve(1)},1000)});

p.then(function(x){console.log(x)})
//无输出

2.v2.0基于观察模式实现

为了处理异步resolve,我们修改myPromise的定义,用2个数组onFullfilledArray和onRejectedArray来保存异步的方法。在状态发生改变时,一次遍历执行数组中的方法。

function myPromise(constructor){
    let self=this;
    self.status="pending" //定义状态改变前的初始状态
    self.value=undefined;//定义状态为resolved的时候的状态
    self.reason=undefined;//定义状态为rejected的时候的状态
    self.onFullfilledArray=[];
    self.onRejectedArray=[];
    function resolve(value){
       if(self.status==="pending"){
          self.value=value;
          self.status="resolved";
          self.onFullfilledArray.forEach(function(f){
                f(self.value);
                //如果状态从pending变为resolved,
                //那么就遍历执行里面的异步方法
          });
        
       }
    }
    function reject(reason){
       if(self.status==="pending"){
          self.reason=reason;
          self.status="rejected";
          self.onRejectedArray.forEach(function(f){
              f(self.reason);
             //如果状态从pending变为rejected, 
             //那么就遍历执行里面的异步方法
          })
       }
    }
    //捕获构造异常
    try{
       constructor(resolve,reject);
    }catch(e){
       reject(e);
    }
}

对于then方法,状态为pending时,往数组里面添加方法:

myPromise.prototype.then=function(onFullfilled,onRejected){
   let self=this;
   switch(self.status){
      case "pending":
        self.onFullfilledArray.push(function(){
             onFullfilled(self.value)
        });
        self.onRejectedArray.push(function(){
             onRejected(self.reason)
        });
      case "resolved":
        onFullfilled(self.value);
        break;
      case "rejected":
        onRejected(self.reason);
        break;
      default:       
   }
}

这样,通过两个数组,在状态发生改变之后再开始执行,这样可以处理异步resolve无法调用的问题。这个版本的myPromise就能处理所有的异步,那么这样做就完整了吗?

没有,我们做Promise/A+规范的最大的特点就是链式调用,也就是说then方法返回的应该是一个promise。

3.v3.0then方法实现链式调用

要通过then方法实现链式调用,那么也就是说then方法每次调用需要返回一个primise,同时在返回promise的构造体里面,增加错误处理部分,我们来改造then方法

myPromise.prototype.then=function(onFullfilled,onRejected){
    let self=this;
    let promise2;
    switch(self.status){
      case "pending":
        promise2=new myPromise(function(resolve,reject){
             self.onFullfilledArray.push(function(){
                try{
                   let temple=onFullfilled(self.value);
                   resolve(temple)
                }catch(e){
                   reject(e) //error catch
                }
             });
             self.onRejectedArray.push(function(){
                 try{
                   let temple=onRejected(self.reason);
                   reject(temple)
                 }catch(e){
                   reject(e)// error catch
                 }
             });
        })
      case "resolved":
        promise2=new myPromise(function(resolve,reject){
            try{
              let temple=onFullfilled(self.value);
              //将上次一then里面的方法传递进下一个Promise的状态
              resolve(temple);
            }catch(e){
              reject(e);//error catch
            }
        })
        break;
      case "rejected":
        promise2=new myPromise(function(resolve,reject){
            try{
               let temple=onRejected(self.reason);
               //将then里面的方法传递到下一个Promise的状态里
               resolve(temple);   
            }catch(e){
               reject(e);
            }
        })
        break;
      default:       
   }
   return promise2;
}

这样通过then方法返回一个promise就可以实现链式的调用:

p.then(function(x){console.log(x)}).then(function(){console.log("链式调用1")}).then(function(){console.log("链式调用2")})
//输出
1
链式调用1
链式调用2

这样我们虽然实现了then函数的链式调用,但是还有一个问题,就是在Promise/A+规范中then函数里面的onFullfilled方法和onRejected方法的返回值可以是对象,函数,甚至是另一个promise。

4.v4.0 then函数中的onFullfilled和onRejected方法的返回值问题

特别的为了解决onFullfilled和onRejected方法的返回值可能是一个promise的问题。

(1)首先来看promise中对于onFullfilled函数的返回值的要求

I)如果onFullfilled函数返回的是该promise本身,那么会抛出类型错误

II)如果onFullfilled函数返回的是一个不同的promise,那么执行该promise的then函数,在then函数里将这个promise的状态转移给新的promise
III)如果返回的是一个嵌套类型的promsie,那么需要递归。

IV)如果返回的是非promsie的对象或者函数,那么会选择直接将该对象或者函数,给新的promise。

根据上述返回值的要求,我们要重新的定义resolve函数,这里Promise/A+规范里面称为:resolvePromise函数,该函数接受当前的promise、onFullfilled函数或者onRejected函数的返回值、resolve和reject作为参数。

下面我们来看resolvePromise函数的定义:

function resolvePromise(promise,x,resolve,reject){
  if(promise===x){
     throw new TypeError("type error")
  }
  let isUsed;
  if(x!==null&&(typeof x==="object"||typeof x==="function")){
      try{
        let then=x.then;
        if(typeof then==="function"){
           //是一个promise的情况
           then.call(x,function(y){
              if(isUsed)return;
              isUsed=true;
              resolvePromise(promise,y,resolve,reject);
           },function(e){
              if(isUsed)return;
              isUsed=true;
              reject(e);
           })
        }else{
           //仅仅是一个函数或者是对象
           resolve(x)
        }
      }catch(e){
         if(isUsed)return;
         isUsed=true;
         reject(e);
      }
  }else{
    //返回的基本类型,直接resolve
    resolve(x)
  }
}

改变了resolvePromise函数之后,我们在then方法里面的调用也变成了resolvePromise而不是promise。

myPromise.prototype.then=function(onFullfilled,onRejected){
    let self=this;
    let promise2;
    switch(self.status){
      case "pending":
        promise2=new myPromise(function(resolve,reject){
             self.onFullfilledArray.push(function(){
                setTimeout(function(){
                  try{
	                   let temple=onFullfilled(self.value);
	                   resolvePromise(temple)
	                }catch(e){
	                   reject(e) //error catch
	                }
                })
             });
             self.onRejectedArray.push(function(){
                setTimeout(function(){
                   try{
	                   let temple=onRejected(self.reason);
	                   resolvePromise(temple)
	                 }catch(e){
	                   reject(e)// error catch
	               }
                })
             });
        })
      case "resolved":
        promise2=new myPromise(function(resolve,reject){
           setTimeout(function(){
               try{
	              let temple=onFullfilled(self.value);
	              //将上次一then里面的方法传递进下一个Promise状态
	              resolvePromise(temple);
	            }catch(e){
                  reject(e);//error catch
               }
           })
        })
        break;
      case "rejected":
        promise2=new myPromise(function(resolve,reject){
           setTimeout(function(){
             try{
               let temple=onRejected(self.reason);
               //将then里面的方法传递到下一个Promise的状态里
               resolvePromise(temple);   
             }catch(e){
               reject(e);
             }
           })
        })
        break;
      default:       
   }
   return promise2;
}

这样就能处理onFullfilled各种返回值的情况。

var p=new Promise(function(resolve,reject){resolve("初始化promise")})
p.then(function(){return new Promise(function(resolve,reject){resolve("then里面的promise返回值")})}).then(function(x){console.log(x)})

//输出
then里面promise的返回值

到这里可能有点乱,我们再理一理,首先返回值有两个:

  • then函数的返回值——>返回一个新promise,从而实现链式调用

  • then函数中的onFullfilled和onRejected方法——>返回基本值或者新的promise

这两者其实是有关联的,onFullfilled方法的返回值可以决定then函数的返回值。

四、检测是否完全符合Promise/A+规范

npm install -g promises-aplus-tests 

具体用法请看promise test然后

promises-aplus-tests  myPromise.js

结果为:

这里写图片描述
说明我们的实现完全符合Promise/A+规范。

完整代码的地址

https://github.com/forthealllight/promise-achieve

五、最后补充Typescript实现的Promise/A+规范(可以忽略此节)

interface IConstructor{
  (resolve:IResolve,reject:IReject):void
}
interface IResolve {
    (x:any):void
}
interface IReject {
    (x:any):void
}
function myPromise(constructor:IConstructor):void{
  let self:object=this;
  self.status="pending";
  self.value=undefined;//if pending->resolved
  self.reason=undefined;//if pending->rejected
  self.onFullfilledArray=[];//to deal with async(resolved)
  self.onRejectedArray=[];//to deal with async(rejeced)
  let resolve:IResolve;
  resolve=function(value:any):void{
    //pending->resolved
    if(self.status==="pending"){
      self.status="resolved";
      self.value=value;
      self.onFullfilledArray.forEach(function(f){
        f(self.value);
      })
    }
  }
  let reject:IReject;
  reject=function(reason:any):void{
    if(self.status==="pending"){
      self.status="rejected";
      self.reason=reason;
      self.onRejectedArray.forEach(function(f){
        f(self.reason);
      })
    }
  }
  //According to the definition that the function "constructor" accept two parameters
  //error catch
  try {
    constructor(resolve,reject);
  } catch (e) {
    reject(e);
  }
}

单纯的写个工具函数,用ts还是有点影响可读性。

在单页应用中,如何优雅的上报前端性能数据

在单页应用中,如何优雅的上报前端性能数据


  最近在做一个较为通用的前端性能监控平台,区别于前端异常监控,前端的性能监控主要需要上报和展示的是前端的性能数据,包括首页渲染时间、每个页面的白屏时间、每个页面所有资源的加载时间以及每一个页面中所以请求的响应时间等等。

  本文的介绍的是如何设计一个通用的jssdk,可以以较小的侵入性,自动上报前端的性能数据。主要采用的是Performance API以及sendBeacon方法等等。主要参考的是google analytics以及阿里云前端性能监控平台的实践。

  在我的项目中使用nestjs作为后端框架,nestjs是基于express的一款完美支持typescript,类java spring的node后端框架。本文主要侧重与如何上报性能数据,后端处理逻辑比较简单,不会具体介绍,因此不需要了解如何使用nestjs。本文的主要内容包含了:

  • 根据Performance API获取前端性能数据
  • 何时应该上报性能数据
  • 如何上报性能数据

一、根据Performance API 获取前端性能数据

本文上报的前端性能数据包含两部分,一是通过Performance API获得的性能数据,二是自定义的在每个页面应该上报的数据。

首先来看通过Performance API所获取的数据,该数据也包含了两个部分,当前页面的性能相关数据以及当前页面资源加载和异步请求的相关数据。

(1)、Performance API 所提供的性能数据

window.performance.timing会返回一个对象,该对象包含了各种与页面渲染所相关的数据。本文不会具体去介绍该对象,只给出根据该对象计算相关性能数据的方法:

  let times = {};
  let t = window.performance.timing;
  
  //重定向时间
  times.redirectTime = t.redirectEnd - t.redirectStart;
  
  //dns查询耗时
  times.dnsTime = t.domainLookupEnd - t.domainLookupStart;
  
  //TTFB 读取页面第一个字节的时间
  times.ttfbTime = t.responseStart - t.navigationStart;
  
  //DNS 缓存时间
  times.appcacheTime = t.domainLookupStart - t.fetchStart;
  
  //卸载页面的时间
  times.unloadTime = t.unloadEventEnd - t.unloadEventStart;
  
  //tcp连接耗时
  times.tcpTime = t.connectEnd - t.connectStart;
  
  //request请求耗时
  times.reqTime = t.responseEnd - t.responseStart;
  
  //解析dom树耗时
  times.analysisTime = t.domComplete - t.domInteractive;
  
  //白屏时间
  times.blankTime = t.domLoading - t.fetchStart;
  
  //domReadyTime
  times.domReadyTime = t.domContentLoadedEventEnd - t.fetchStart;

在上面的times对象中就包含了性能相关的属性,根据performance.timing中的相关属性计算就可以得到结果。在这里我们认为domReadyTime就是首屏加载的时间,此外也可以自定义的方法上报首屏的时间:

比如有些场景可以认为是dom增量最大的点为首屏渲染完成的时间,也有一些场景可以定义可见的dom在增量最大处为首屏渲染完成的时间。

(2)、Performance API 所提供的资源加载和请求数据

  可以通过window.performance.getEntries()来获取资源的加载和请求相关的数据。每一个页面中,需要去加载很多资源比如js、css等等,同时在页面中还会存在一些异步请求。通过window.performance.getEntries()可以获得这些资源加载和异步请求所相关的数据。我们可以通过如下的方式来获取加载和异步请求的数据:

  let  entryTimesList = [];
  let entryList = window.performance.getEntries();
  entryList.forEach((item,index)=>{
  
     let templeObj = {};
     
     let usefulType = ['navigation','script','css','fetch','xmlhttprequest','link','img'];
     if(usefulType.indexOf(item.initiatorType)>-1){
       templeObj.name = item.name;
       
       templeObj.nextHopProtocol = item.nextHopProtocol;
      
       //dns查询耗时
       templeObj.dnsTime = item.domainLookupEnd - item.domainLookupStart;

       //tcp链接耗时
       templeObj.tcpTime = item.connectEnd - item.connectStart;
       
       //请求时间
       templeObj.reqTime = item.responseEnd - item.responseStart;

       //重定向时间
       templeObj.redirectTime = item.redirectEnd - item.redirectStart;

       entryTimesList.push(templeObj);
     }
  });

我们通过window.performance.getEntries()获得一个带有资源加载和异步请求相关数据的数组,然后根据数组中每一个元素的initiatorType属性来过滤出属性为['navigation','script','css','fetch','xmlhttprequest','link','img']之一的元素数据。

(3)、注意点

  • 通过window.performance.timing所获的的页面渲染所相关的数据,在单页应用中改变了url但不刷新页面的情况下是不会更新的。因此如果仅仅通过该api是无法获得每一个子路由所对应的页面渲染的时间。如果需要上报切换路由情况下每一个子页面重新render的时间,需要自定义上报。

  • 通过window.performance.getEntries()所获取的资源加载和异步请求所相关的数据,在页面切换路由的时候会重新的计算,可以实现自动的上报。

二、何时上报性能数据

  接着来确定应该何时上报性能数据,因为要处理pv(访问量)和uv(独立用户访问量),一般认为一次上报就是一次访问,那么何时上报性能数据呢。在我的系统中选择在一下场景下进行一次前端性能数据的上报:

  • 页面加载和重新刷新
  • 页面切换路由
  • 页面所在的tab标签重新变得可见

针对上述的3种场景,特别是切换路由的情况,如果切换路由是通过改变hash值来实现的,那么只需要监听hashchange事件,如果是通过html5的history api来改变url的,那么需要重新定义pushstate和replacestate事件。具体的做法可以看我的上一篇文章:在单页应用中,如何优雅的监听url的变化

直接给出history实现路由场景下监听url改变的方案:

var _wr = function(type) {
   var orig = history[type];
   return function() {
       var rv = orig.apply(this, arguments);
      var e = new Event(type);
       e.arguments = arguments;
       window.dispatchEvent(e);
       return rv;
   };
};
 history.pushState = _wr('pushState');
 history.replaceState = _wr('replaceState');

然后我们就可以根据上述场景,分别监听相应的事件,从而实现前端性能数据的上报:

addEvent(window,'load',function(e){
    ...deal with something
});
//监控history基础上实现的单页路由中url的变化
addEvent(window,'replaceState', function(e) {
    ...deal with something
});
addEvent(window,'pushState', function(e) {
    ...deal with something
});
//通过hash切换来实现路由的场景
addEvent(window,'hashchange',function(e){
   ...deal with something
});
addEvent('document','visibilitychang',function(e){
   ...deal with something
})

addEvent是一个兼容IE和标准DOM事件流模型的事件。

三、如何上报性能数据

  那么如何上报性能数据呢,我们第一反应就是通过ajax请求的形式来上报前端性能数据。这种方法有一些缺陷,比如必须对跨域做特殊处理以及如果页面销毁后,相应的ajax方法并不一定发送成功等问题。

其中跨域的问题比较好处理,最难解决的问题是第二点:

就是如果页面销毁,那么对应的ajax方法并不一定能成功发送。

  我们可以根据google analytics(GA)中的方法,根据浏览器的兼容性以及url的长度,来采用不同的方法上报性能数据,主要原理是:

通过动态创建img标签的方式,在img.src中拼接url的方式发送请求,不存在跨域限制。如果url太长,则才用sendBeacon的方式发送请求,如果sendBeacon方法不兼容,则发送ajax post同步请求

(1)、sendBeacon方法

  解决在文档卸载或者页面关闭后无法完成异步ajax请求的问题,很多情况下我们会把异步变成同步。在页面卸载的unload或者beforeunload事件中执行同步方法调用。

但是同步方法调用存在一个问题,就是会推迟A页面切换进入B页面的时间。而sendBeacon方法解决了该问题,简单来说:

sendBeacon方法在页面销毁期,可以异步的发送数据,因此不会造成类似同步ajax请求那样的阻塞问题,也不会影响下一个页面的渲染

sendBeacon的调用方式为:

navigator.sendBeacon(url [, data]);

data可以为: ArrayBufferView, Blob, DOMString, 或者 FormData

为了发送参数,我们一般data制定为Blob的形式。此外还要注意的是,在sendBeacon的请求头header中,不支持Content-Type为“application/json; charset=utf-8”。

在sendBeacon的header中,只支持一下3种形式的Content—Type:

  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain

一般制定为application/x-www-form-urlencoded,完整的通过sendBeacon来发送请求的例子如下:

function sendBeacon(url,data){
  //判断支不支持navigator.sendBeacon
  let headers = {
    type: 'application/x-www-form-urlencoded'
  };
  let blob = new Blob([JSON.stringify(data)], headers);
  navigator.sendBeacon(url,blob);
}

后端如何处理sendBeacon请求呢,sendBeacon在的请求头中发送的是一个类似与POST的请求,因此可以类似于处理post一样来处理sendBeacon请求。

一般我们约定ajax请求的content—type为:“application/json; charset=utf-8”,而sendBeacon请求的content-type为:“application/x-www-form-urlencoded”,这样在后端处理中,就可以区别是正常的ajax post请求还是sendBeacon请求。

此外,在处理请求的时候如果存在跨域问题,通过cors跨域的方式来处理,后端需要配置:allow-control-allow-origin等,可以通过express的cors包,来简化配置:

async function bootstrap() {
  const app = await NestFactory.create(ApplicationModule,instance);
  app.use(cors());

  await app.listen(3000)
}
bootstrap();

(2)动态创建img标签的形式

  通过动态创建img标签的形式,指定src属性所指定的url来发送请求,首先不受跨域的限制,其次img标签动态插入,会延迟页面的卸载保证图片的插入,因此可以保证在页面的销毁期,请求可以发生。

下面是一个动态创建img标签的例子:

function imgReport(url, data) {
   if (!url || !data) {
       return;
   }
   let image = document.createElement('img');
   let items = [];
   items = JSON.Parse(data);
   let name = 'img_' + (+new Date());
   image.onload = image.onerror = function () {
      
   };
   let newUrl = url + (url.indexOf('?') < 0 ? '?' : '&') + items.join('&');

   image.src = newUrl;
}

此外,我们在动态创建img标签发送请求的时候,请求的是一张图片,在后端处理的时候,要在末尾将这个图片返回,这样前端的image.onload方法才会被触发。我们以请求的地址为:localhost:8080/1.jpg为例,后端的处理逻辑为:

@Controller('1.jpg')
export class AppUploadController {
  constructor(private readonly appService: AppService) {}
  @Get()
  getUpload(@Req() req,@Res() res): void {
  
    ...deal with some thing
    res.sendFile(join(__dirname, '..', 'public/1.jpg'))
  }
}

在get请求的处理中,我们通过res.sendFile(join(__dirname, '..', 'public/1.jpg'))将图片返回后,这样前端的image的onload方法才会被调用。

(3)同步ajax post请求

  动态创建img标签的方法,拼接url的时候存在一定的问题,因为浏览器对url的长度是有限制的。而sendBeacon方法兼容性不是很好,最后兜底的处理方式就是发送同步的ajax请求,同步的ajax请求前面说过,会在页面销毁期之前执行,虽然会有一定程度的阻塞下一个页面的渲染。

function xmlLoadData(url,data) {
  var client = new XMLHttpRequest();
  client.open("POST", url,false);
  client.setRequestHeader("Content-Type", "application/json; charset=utf-8");
  client.send(JSON.stringify(data));
}

(4)综合解决方案

  一般首先拼接携带参数的完整的url,判断url的长度,如果url的长度小于浏览器允许的最大长度内,那么通过动态创建img标签的形式来发送前端性能数据,如果url太长,则判断浏览器是否支持sendBeacon方法,如果支持,则通过sendBeacon方法来发送请求,否则发送同步的ajax请求。

function dealWithUrl(url,appId){
      let times = performanceInfo(appId);
      let items = decoupling(times);
      let urlLength = (url + (url.indexOf('?') < 0 ? '?' : '&') + items.join('&')).length;
      if(urlLength<2083){
        imgReport(url,times);
      }else if(navigator.sendBeacon){
        sendBeacon(url,times);
      }else{
        xmlLoadData(url,times);
      }
    }

前端开发必备的Atom插件指北

前端开发必备的Atom插件指北


Atom开源且免费,一直用着也蛮舒服的,针对前端开发者介绍一下常用的atom插件。

  • atom通用插件
  • 前端开发常用的atom插件
  • 其他有趣的atom插件
  • 常见问题总结

一、atom通用插件

1.file-icons

 美化文件的图标

2.atom-beautify

 代码格式一键美化

3.minimap

 代码略缩小地图(没啥卵用,看着比较舒服)

4.sync-settings

 插件备份

5.atom-terminal-panel

 内置命令行

6.vim-mode

 支持vim模式

7.platformio-ide-terminal

  命令行

8.platformio-ide-terminal

 填写路径时自动补全

二、前端开发常用的atom插件

1.emmet

快速书写html标签

2.color-picker

在编辑器里面选取颜色

3.language-JavaScript-jsx

支持jsx语法

4.react

react语法支持

5.atom-ternjs

 nodejs、js、es6代码补全

6.jshint

 校验js和jsx语法

7.csslint

 验证css语法是否正确

8.autoprefixer

 为css自动增加前缀(也没有特别有用,用postcss来代替)

9.autoclose-html

 闭合html标签(不是特别好用)

三、其他有趣的atom插件

1.activate-power-mode

 代码输入时有震撼效果

2.power-mode

 随机泡沫

3.atom-miku

  初音

四、常见问题总结

(1)使用Jshint时‘const’ is available in ES6 (use ‘esversion: 6’)

解决方法:在根目录新建一个.jshintrc文件,在该文件中输入:

{
 "esversion": 6
}

或者在每一个需要使用es6语法的文件中,在文件头中输入:

 /* jshint esversion: 6 */ 

React生态,dva源码阅读

React生态,dva源码阅读


  dva的**还是很不错的,大大提升了开发效率,dva集成了Redux以及Redux的中间件Redux-saga,以及React-router等等。得益于Redux的状态管理,以及Redux-saga中通过Task和Effect来处理异步的概念,dva在这些工具的基础上高度封装,只暴露出几个简单的API就可以设计数据模型。

  最近看了一下Redux-saga的源码,结合以及之前在项目中一直采用的是redux-dark模式来将reducers和sagas(generator函数处理异步)拆分到不同的子页面,每一个页面中同一个文件中包含了该页面状态的reducer和saga,这种简单的封装已经可以大大的提升项目的可读性。

  最近看了dva源码,熟悉了dva是在上层如何做封装的。下面会从浅到深,淡淡在阅读dva源码过程中自己的理解。

  • redux-dark模式
  • dva 0.0.12版本的使用和源码理解

一、redux-dark模式

  在使用redux和redux-saga的时候,特别是如何存放reducer函数和saga的generator函数,这两个函数是直接跟如何处理数据挂钩的。

  回顾一下,在redux中使用异步中间件redux-saga后,完整的数据和信息流向:

default

  在存在异步的逻辑下,在UI Component中发出一个plain object的action,然后经过redux-saga这个中间件处理,redux-saga会将这个action传入相应channel,通过redux-saga的effect方法(比如call、put、apply等方法)生成一个描述对象,然后将这个描述对象转化成具有副作用的函数并执行。

  在redux-saga执行具有副作用的函数时,又可以dispatch一个action,这个action也是一个plain object,会直接传入到redux的reducer函数中进行处理,也就是说在redux-saga的task中发出的action,就是同步的action。

简单的概括:从UI组件上发出的action经过了2层的处理,分别是redux-saga中间件和redux的reducer函数。

  redux-dark模式很简单,就是将同一个子页面下的redux-saga处理action的saga函数,以及reducer处理该子页面下的state的reducer函数,放在同一个文件中。

举例来说:

 import { connect } from 'react-redux';
 class Hello extends React.Component{
     componentDidMount(){
       //发出一个action,获取异步数据
       this.props.dispatch({
          type:'async_count'
       })
     }
     
 
 }
 export default connect({})(Hello);
 

从Hello组件中发出一个type = 'async_count'的action,我们用redux-dark模式来将saga和reducer函数放在同一个文件中:

    import { takeEvery } from 'redux-saga/effects';
    
    //saga
    function * asyncCount(){
      console.log('执行了saga异步...')
      //发出一个原始的action
      yield put({
        type:'async'
      });
    }
    function * helloSaga(){
        //接受从UI组件发出的action
        takeEvery('async_count',asyncCount);
    }
    
    //reducer
    function helloReducer(state,action){
       if(action.type === 'count');
       return { ...state,count + 1}
    }

上述就是一个将saga和reducer放在同一个文件里面的例子。redux-dark模式来组织代码,可以显得比较直观,统一了数据的处理层。分拆子页面后,每一个子页面对应一个文件。可读性很高。

二、dva 0.0.12版本的使用和源码理解

  上述的redux-dark模式,就是一种简单的处理,而dva的话是做了更近一步的封装,dva不仅封装了redux和redux-saga,还有react-router-redux、react-router等等。使得我们可以通过很简单的配置,就能使用redux、redux-saga、react-router等。

下面首先以dva的初始版本为例来理解一下dva的源码。

(1)、dva 0.0.12的使用

来看官网给的使用dva 0.0.12的例子:

// 1. Initialize
const app = dva();

// 2. Model
app.model({
  namespace: 'count',
  state: 0,
  effects: {
    ['count/add']: function*() {
      console.log('count/add');
      yield call(delay, 1000);
      yield put({
        type: 'count/minus',
      });
    },
  },
  reducers: {
    ['count/add'  ](count) { return count + 1 },
    ['count/minus'](count) { return count - 1 },
  },
  subscriptions: [
    function(dispatch) {
      //..处理监听等等函数
    }
  ],
  
});

// 3. View
const App = connect(({ count }) => ({
  count
}))(function(props) {
  return (
    <div>
      <h2>{ props.count }</h2>
      <button key="add" onClick={() => { props.dispatch({type: 'count/add'})}}>+</button>
      <button key="minus" onClick={() => { props.dispatch({type: 'count/minus'})}}>-</button>
    </div>
  );
});

// 4. Router
app.router(({ history }) =>
  <Router history={history}>
    <Route path="/" component={App} />
  </Router>
);

// 5. Start
app.start(document.getElementById('root'));

只要三步就完成了一个例子,如何处理action呢,我们以一个图来表示:

default

也就是做UI组件上发出的对象类型的action,先去根据类型匹配=model初始化时候,effects属性中的action type。

  • 如果在effects的属性中有相应的action type的处理函数,那么先执行effects中的相应函数,在执行这个函数里面可以二次发出action,二次发出的action会直接传入到reducer函数中。
  • 如果effects的属性中没有相应的action type的处理函数,那么会直接从reducer中寻找有没有相应类型的处理函数。

在dva初始化过程中的effects属性中的函数,其实就是redux-saga中的saga函数,在该函数中处理直接的异步逻辑,并且该函数可以二次发出同步的action。

此外dva还可以通过router方法初始化路由等。

(2)、dva 0.0.12的源码阅读

下面来直接读读dva 0.0.12的源码,下面的代码是经过我精简后的dva的源码:

//Provider全局注入store
import { Provider } from 'react-redux';
//redux相关的api
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
//redux-saga相关的api,takeEvery和takeLatest监听等等
import createSagaMiddleware, { takeEvery, takeLatest } from 'redux-saga';
//react-router相关的api
import { hashHistory, Router } from 'react-router';
//在react-router4.0之后已经较少使用,将路由的状态存储在store中
import { routerMiddleware, syncHistoryWithStore, routerReducer as routing } from 'react-router-redux';
//redux-actions的api,可以以函数式描述reducer等
import { handleActions } from 'redux-actions';
//redux-saga非阻塞调用effect
import { fork } from 'redux-saga/effects';

function dva() {
  let _routes = null;
  const _models = [];
  //new dva暴露了3个方法
  const app = {
    model,
    router,
    start,
  };
  return app;
  //添加models,一个model对象包含了effects,reducers,subscriptions监听器等等
  function model(model) {
    _models.push(model);
  }
  //添加路由
  function router(routes) {
    _routes = routes;
  }

  
  function start(container) {

    let sagas = {};
    //routing是react-router-redux的routerReducer别名,用于扩展reducer,这样以后扩展后的reducer就可以处理路由变化。
    let reducers = {
      routing
    };
    _models.forEach(model => {
      //对于每一个model,提取其中的reducers和effects,其中reducers用于扩展redux的reducers函数,而effects用于扩展redx-saga的saga处理函数。
      reducers[model.namespace] = handleActions(model.reducers || {}, model.state);
      //扩展saga处理函数,sagas是包含了所有的saga处理函数的对象
      sagas = { ...sagas, ...model.effects }; ---------------------------(1)
    });

    reducers = { ...reducers };
    
    //获取决定使用React-router中的那一个api
    const _history = opts.history || hashHistory;
    //初始化redux-saga
    const sagaMiddleware = createSagaMiddleware();
    //为redux添加中间件,这里添加了处理路由的中间件,以及redux-saga中间件。
    const enhancer = compose(
      applyMiddleware.apply(null, [ routerMiddleware(_history), sagaMiddleware ]),
      window.devToolsExtension ? window.devToolsExtension() : f => f
    );
    const initialState = opts.initialState || {};
    //通过combineReducers来扩展reducers,同时生成扩展后的store实例
    const store = app.store = createStore(
      combineReducers(reducers), initialState, enhancer
    );

    // 执行model中的监听函数,监听函数中传入store.dispatch
    _models.forEach(({ subscriptions }) => {
      if (subscriptions) {
        subscriptions.forEach(sub => {
         store.dispatch, onErrorWrapper);
        });
      }
    });
    
     // 根据rootSaga来启动saga,rootSaga就是redux-saga运行的主task
    sagaMiddleware.run(rootSaga);
    
    
    //创建history实例子,可以监听store中的state的变化。
    let history;
    history = syncHistoryWithStore(_history, store); --------------------------------(2)
    

    // Render and hmr.
    if (container) {
      render();
      apply('onHmr')(render);
    } else {
      const Routes = _routes;
      return () => (
        <Provider store={store}>
          <Routes history={history} />
        </Provider>
      );
    }

    function getWatcher(k, saga) {
      let _saga = saga;
      let _type = 'takeEvery';
      if (Array.isArray(saga)) {
        [ _saga, opts ] = saga;
    
        _type = opts.type;
      }

      function* sagaWithErrorCatch(...arg) {
        try {
          yield _saga(...arg);
        } catch (e) {
          onError(e);
        }
      }

      if (_type === 'watcher') {
        return sagaWithErrorCatch;
      } else if (_type === 'takeEvery') {
        return function*() {
          yield takeEvery(k, sagaWithErrorCatch);
        };
      } else {
        return function*() {
          yield takeLatest(k, sagaWithErrorCatch);
        };
      }
    }

    function* rootSaga() {
      for (let k in sagas) {
        if (sagas.hasOwnProperty(k)) {
          const watcher = getWatcher(k, sagas[k]);
          yield fork(watcher);
        }                      -----------------------------(3)
      }
    }

    function render(routes) {
      const Routes = routes || _routes;
      ReactDOM.render((
        <Provider store={store}>
          <Routes history={history} />
        </Provider>
      ), container);
    }
  }
}

export default dva;

代码的阅读在上面都以注视的方式给出,值得注意的主要有一下3点:

  • 在注释(1)处, handleActions是通过redux-actions封装后的一个API,用于简化reducer函数的书写。下面是一个handleActions的例子:
const reducer = handleActions(
  {
    INCREMENT: (state, action) => ({
      counter: state.counter + action.payload
    }),DECREMENT: (state, action) => ({
      counter: state.counter - action.payload
    })
  },
  { counter: 0 }
);

INCREMENT和DECREMENT属性的函数就可以分别处理,type = "INCREMENT"和type = "DECREMENT"的action。

  • 在注释 (2) 处,通过react-router-redux的api,syncHistoryWithStore可以扩展history,使得history可以监听到store的变化。

  • 在注释(3)处是一个rootSaga, 是redux-saga运行的时候的主Task,在这个Task中我们这样定义:

function* rootSaga() {
  for (let k in sagas) {
    if (sagas.hasOwnProperty(k)) {
      const watcher = getWatcher(k, sagas[k]);
      yield fork(watcher);
    }                     
  }
}

从全局的包含所有saga函数的sagas对象中,获取相应的属性,并fork相应的监听,这里的监听常用的有takeEvery和takeLatest等两个redux-saga的API等。

总结:上面就是dva最早版本的源码,很简洁的使用了redux、redux-saga、react-router、redux-actions、react-router-redux等.其目的也很简单:

简化redux相关生态的繁琐逻辑

参考源码地址:https://github.com/dvajs/dva/tree/0.0.12

在单页应用中,如何优雅的监听url的变化

在单页应用中,如何优雅的监听url的变化


  单页应用的原理从早起的根据url的hash变化,到根据H5的history的变化,实现无刷新条件下的页面重新渲染。那么在单页应用中是如何监听url的变化呢,本文将总结一下,如何在单页页面中优雅的监听url的变化。

  • 单页应用原理
  • 监听url中的hash变化
  • 监听通过history来改变url的事件
  • replaceState和pushState行为的监听

一、单页应用原理

  单页应用的原理,在我们的上一篇文章中React-Router源码阅读已经讲的很详细,这里做一个简单介绍。单页应用使得页面可以在无刷新的条件下重新渲染,通过hash或者html5 Bom对象中的history可以做到改变url,但是不刷新页面。

(1)通过hash来实现单页路由

  早期的前端路由是通过hash来实现的:

  改变url的hash值是不会刷新页面的。

  因此可以通过hash来实现前端路由,从而实现无刷新的效果。hash属性位于location对象中,在当前页面中,可以通过:

window.location.hash='edit'

来实现改变当前url的hash值。执行上述的hash赋值后,页面的url发生改变。

赋值前:http://localhost:3000
赋值后:http://localhost:3000/#edit

在url中多了以#结尾的hash值,但是赋值前后虽然页面的hash值改变导致页面完整的url发生了改变,但是页面是不会刷新的。

此外,除了可以通过window.location.hash来改变当前页面的hash值外,还可以通过html的a标签来实现:

<a href="#edit">edit</a>

(2)通过history实现前端路由

  HTML5的History接口,History对象是一个底层接口,不继承于任何的接口。History接口允许我们操作浏览器会话历史记录。

History提供了一些属性和方法。

History的属性:

  • History.length: 返回在会话历史中有多少条记录,包含了当前会话页面。此外如果打开一个新的Tab,那么这个length的值为1
  • History.state:
    保存了会出发popState事件的方法,所传递过来的属性对象(后面会在pushState和replaceState方法中详细的介绍)

History方法:

  • History.back(): 返回浏览器会话历史中的上一页,跟浏览器的回退按钮功能相同

  • History.forward():指向浏览器会话历史中的下一页,跟浏览器的前进按钮相同

  • History.go(): 可以跳转到浏览器会话历史中的指定的某一个记录页

  • History.pushState():pushState可以将给定的数据压入到浏览器会话历史栈中,该方法接收3个参数,对象,title和一串url。pushState后会改变当前页面url,但是不会伴随着刷新

  • History.replaceState():replaceState将当前的会话页面的url替换成指定的数据,replaceState后也会改变当前页面的url,但是也不会刷新页面。

上面的方法中,pushState和repalce的相同点:

就是都会改变当前页面显示的url,但都不会刷新页面。

不同点:

pushState是压入浏览器的会话历史栈中,会使得History.length加1,而replaceState是替换当前的这条会话历史,因此不会增加History.length.

(3)总结

  通过改变hash值,或者history的repalceState和pushState都可以实现无刷新的改变url。这样还留有一个问题需要解决:

  如何监听url的改变

  因为我们不仅要无刷新的改变url,还要监听到这个url改变的行为,根据该行为去重新渲染视图。在下几章中,重点介绍一下如何监听url的改变。

二、监听url中的hash变化

  通过hash改变了url,会触发hashchange事件,只要监听hashchange事件,就能捕获到通过hash改变url的行为。

window.onhashchange=function(event){
  console.log(event);
}
//或者
window.addEventListener('hashchange',function(event){
   console.log(event);
})

当hash值改变时,输出一个HashChangeEvent。该HashChangeEvent的具体值为:

{isTrusted: true, oldURL: "http://localhost:3000/", newURL:   "http://localhost:3000/#teg", type: "hashchange".....}

有了监听事件,且改变hash页面不刷新,这样我们就可以在监听事件的回调函数中,执行我们展示和隐藏不同UI显示的功能,从而实现前端路由。

三、监听通过history来改变url的事件

在上一章讲到了通过History改变url有以下几种方法:History.back()、History.forward()、History.go()、History.pushState()和History.replaceState()。

同时在history中还支持一个事件,该事件为popstate。第一想法就是如果popstate能够监听所有的history方法所导致的url变化,那么就大功告成了。遗憾的是:

History.back()、History.forward()、History.go()事件是会触发popstate事件的,但是History.pushState()和History.replaceState()不会触发popstate事件。

如果是History.back(),History.forward()、History.go()那么会触发popstate事件,我们只需要:

window.addEventListener('popstate', function(event) {
     console.log(event);
})

就可以监听到相应的行为,手动调用:

window.history.go();
window.history.back();
window.history.forward();

都会触发这个事件,此外,在浏览器中点击后退和前进按钮也会触发popstate事件,这个事件内容为:

PopStateEvent {isTrusted: true, state: null, type: "popstate", target: Window, currentTarget: Window, …}

但是,History.pushState()和History.replaceState()不会触发popstate事件,举例来说:

window.addEventListener('popstate', function(event) {
     console.log(event);
})
window.history.pushState({first:'first'}, "page 2", "/first"})

上述例子中不会有任何的输出,因为并没有监听的popstate事件的发生。

但是History.go和History.back()等,虽然可以触发popstate事件,但是都会刷新页面,我们在单页应用中使用的是replaceState和pushState,因此这里还有一个等待解决的问题:

如何监听replaceState和pushState行为

四、replaceState和pushState行为的监听

  在上面的例子中我们发现History.replaceState和pushState确实不会触发popstate事件,那么如何监听这两个行为呢。可以通过在方法里面主动的去触发popState事件。另一种就是在方法中创建一个新的全局事件。

具体做法为:

var _wr = function(type) {
   var orig = history[type];
   return function() {
       var rv = orig.apply(this, arguments);
      var e = new Event(type);
       e.arguments = arguments;
       window.dispatchEvent(e);
       return rv;
   };
};
 history.pushState = _wr('pushState');
 history.replaceState = _wr('replaceState');

这样就创建了2个全新的事件,事件名为pushState和replaceState,我们就可以在全局监听:

window.addEventListener('replaceState', function(e) {
  console.log('THEY DID IT AGAIN! replaceState 111111');
});
window.addEventListener('pushState', function(e) {
  console.log('THEY DID IT AGAIN! pushState 2222222');
});

这样就可以监听到pushState和replaceState行为。

参考文章:https://stackoverflow.com/questions/4570093/how-to-get-notified-about-changes-of-the-history-via-history-pushstate

发布一个npm包,用于监控页面中的所有API请求的状态和结果

发布一个npm包,用于监控页面中的所有API请求的状态和结果


  在前端监控系统中,或者其他场景下,如果我们需要监控当前页面下所有请求状态。可能通常请求下,我们会选择在请求的回调中去处理。这种做法的缺点就是会侵入具体的业务代码。在通常的监控中,监控部分的代码和业务部分的代码是分离的。此外,如果存在很多的请求需要被监听,通过侵入具体业务代码,为了减少代码的重复,也需要封装监听请求的逻辑。

  本文通过monkey patches的方法实现了一个request-interceptor包,可以按需求监听请求。

  该npm包的项目地址为:https://github.com/forthealllight/request-intercept 欢迎使用。

  • 获取API请求的状态和结果
  • monkey patches实现监控XMLHttpRequest请求
  • monkey patches实现监控fetch请求

一、获取API请求和结果

  获取请求的方式包含了fetch和XMLHttpRequest。比如下面是一个XMLHttpRequest请求的例子:

var client = new XMLHttpRequest();
client.open("POST","http://10.12.72.16:8080/extraInfo" );
client.setRequestHeader("Content-Type", "application/json; charset=utf-8");
client.send(JSON.stringify({}));

  通常我们会通过client上出发的readystatechange来判断请求的状态以及得到请求的响应结果:

client.onreadystatechange = function () {
if (client .readyState==4 &&client.status==200) {
    console.log(client.responseText);//
  }
}

  XMLHttpRequest的prototype除了onreadystatechange事件外还有其他很多事件,比如onabout、onerror、onload、onloadstart等等事件。如果我们要完整的监听一个请求,那么需要实现完整的实现这些事件:

client.onabout = function(){}
client.onerror = function(){}
clinet.onload = function(){}
client.onloadstart = function(){}
 ....

  此外如果当某一个事件发生时,需要按顺序的实行一系列的函数,这样会使得事件函数内部越来越复杂,使得整体项目变的无法维护。

fetch请求也是同理,因此我们需要合理的封装监听请求的逻辑。

二、monkey patches实现监控XMLHttpRequest请求

本文不会具体介绍如何通过monkey patches来封装监听请求的逻辑,该逻辑已经在我的npm包中实现,具体可以参考我的开源项目:

https://github.com/forthealllight/request-intercept

本文只介绍如何使用,如有兴趣,可以读一读具体如何实现这个monkey patches,在目录的source文件夹中,如有疑问,可以提issue。

该npm包的包名为:req-interceptor。首先来看对于XMLHttpRequest请求如何使用:

import { ajaxIntercept } from 'req-interceptor';

//监听
const unregister = ajaxIntercept.register({
  requestAbout: function (xhr) {
      // xhr is real instance of a request
      console.log(xhr)
  },
  requestError: function (xhr) {
      // xhr is real instance of a request
      console.log(xhr)
  },
  requestLoad: function (xhr) {
      // xhr is real instance of a request
      console.log(xhr)
  },
});

//发送请求

var client = new XMLHttpRequest();
client.open("POST","http://10.12.72.16:8080/extraInfo" );
client.setRequestHeader("Content-Type", "application/json; charset=utf-8");
client.send(JSON.stringify({}));

只需要在发送请求前先调用ajaxIntercept.register函数传入监听的对象,该函数会返回一个取消监听的方法。

这样就监听之后的任意请求,在ajaxIntercept.register中的实际参数的对象中,对象的属性是一个函数,参数为xhr,xhr就是一个被监听的XMLHttpRquest,因此我们可以从xhr中拿到请求的具体响应。xhr的一个例子为:

xhr = {
          readyState: 4
          response: "{"success":0}"
          responseText: "{"success":0}"
          responseType: ""
          responseURL: "http://10.12.72.16:8080/extraInfo"
          responseXML: null
          status: 201
          statusText: "Created"
          timeout: 0
     }

如果我们在取消对于某一个请求的监听,则调用该返回的
unregister函数,此后请求不会再被监听。

unregister();

此外我们也可以在某一个请求前添加多个监听函数:

import { ajaxIntercept } from 'req-interceptor';
//监听
const unregister1 = ajaxIntercept.register({...});
const unregister2 = ajaxIntercept.register({...});
const unregister3 = ajaxIntercept.register({...});
//请求
client.open(url,....)

如果我们想要一次性移除所有的对于请求的监听函数,可以直接调用:

ajaxIntercept.clear();

三、monkey patches实现监控fetch请求

对于fetch请求也是一样的。

import { fetchIntercept } from 'req-interceptor';


import { fetchIntercept } from 'req-interceptor';

const unregister = fetchIntercept.register({
    request: function (url, config) {
        // Modify the url or config here
        return [url, config];
    },

    requestError: function (error) {
        // Called when an error occured during another 'request' interceptor call
        return Promise.reject(error);
    },

    response: function (response) {
        // Modify the reponse object
        return response;
    },

    responseError: function (error) {
        // Handle an fetch error
        return Promise.reject(error);
    }
});

// Call fetch to see your interceptors in action.
fetch('http://google.com');

不同的是,fetch不像XMLHttpRequest请求那样,可以监听完整的过程,fetch只有request、requestError、response和responseError这4个属性可以监听,分别映射请求的参数,请求失败,请求返回成功,请求返回失败。

同样的也可以通过返回函数来取消监听,以及通过clear函数来取消所有监听函数。

循序渐进教你实现一个完整的node的EventEmitter模块

循序渐进教你实现一个完整的node的EventEmitter模块


node的事件模块只包含了一个类:EventEmitter。这个类在node的内置模块和第三方模块中大量使用。EventEmitter本质上是一个观察者模式的实现,这种模式可以扩展node在多个进程或网络中运行。本文从node的EventEmitter的使用出发,循序渐进的实现一个完整的EventEmitter模块。

  • EventEmitter模块的基本用法和简单实现
  • node中常用的EventEmitter模块的API
  • EventEmitter模块的异常处理
  • 完整的实现一个EventEmitter模块

一、EventEmitter模块的基本用法和简单实现

(1) EventEmitter模块的基本用法

首先先了解一下EventEmitter模块的基本用法,EventEmitter本质上是一个观察者模式的实现,所谓观察者模式:

它定义了对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知。

因此最基本的EventEmitter功能,包含了一个观察者和一个被监听的对象,对应的实现就是EventEmitter中的on和emit:

var events=require('events');
var eventEmitter=new events.EventEmitter();
eventEmitter.on('say',function(name){
    console.log('Hello',name);
})
eventEmitter.emit('say','Jony yu');

eventEmitter是EventEmitter模块的一个实例,eventEmitter的emit方法,发出say事件,通过eventEmitter的on方法监听,从而执行相应的函数。

(2) 简单实现一个EventEmitter模块

根据上述的例子,我们知道了EventEmitter模块的基础功能emit和on。下面我们实现一个包含emit和on方法的EventEmitter类。

on(eventName,callback)方法传入两个参数,一个是事件名(eventName),另一个是相应的回调函数,我们选择在on的时候针对事件名添加监听函数,用对象来包含所有事件。在这个对象中对象名表示事件名(eventName),而对象的值是一个数组,表示该事件名所对应的执行函数。

emit(eventName,...arg)方法传入的参数,第一个为事件名,其他参数事件对应的执行函数中的实参,emit方法的功能就是从事件对象中,寻找对应key为eventName的属性,执行该属性所对应的数组里面每一个执行函数。

下面来实现一个EventEmitter类

class EventEmitter{
   constructor(){
      this.handler={};
   }
   on(eventName,callback){
      if(!this.handles){
        this.handles={};
      }
      if(!this.handles[eventName]){
        this.handles[eventName]=[];
      }
      this.handles[eventName].push(callback);
   }
   emit(eventName,...arg){
       if(this.handles[eventName]){
     for(var i=0;i<this.handles[eventName].length;i++){
       this.handles[eventName][i](...arg);
     }
   }
   
   }
}

上述就实现了一个简单的EventEmitter类,下面来实例化:

let event=new EventEmitter();
event.on('say',function(str){
   console.log(str);
});
event.emit('say','hello Jony yu');
//输出hello Jony yu

二、node中常用的EventEmitter模块的API

跟在上述简单的EventEmitter模块不同,node的EventEmitter还包含了很多常用的API,我们一一来介绍几个实用的API.

方法名 方法描述
addListener(event, listener) 为指定事件添加一个监听器到监听器数组的尾部。
prependListener(event,listener) 与addListener相对,为指定事件添加一个监听器到监听器数组的头部。
on(event, listener) 其实就是addListener的别名
once(event, listener) 为指定事件注册一个单次监听器,即 监听器最多只会触发一次,触发后立刻解除该监听器。
removeListener(event, listener) 移除指定事件的某个监听器,监听器必须是该事件已经注册过的监听器
off(event, listener) removeListener的别名
removeAllListeners([event]) 移除所有事件的所有监听器, 如果指定事件,则移除指定事件的所有监听器。
setMaxListeners(n) 默认情况下, EventEmitters 如果你添加的监听器超过 10 个就会输出警告信息。 setMaxListeners 函数用于提高监听器的默认限制的数量。
listeners(event) 返回指定事件的监听器数组。
emit(event, [arg1], [arg2], [...]) 按参数的顺序执行每个监听器,如果事件有注册监听返回 true,否则返回 false。

除此之外,还有2个特殊的,不需要手动添加,node的EventEmitter模块自带的特殊事件:

事件名 事件描述
newListener 该事件在添加新事件监听器的时候触发
removeListener 从指定监听器数组中删除一个监听器。需要注意的是,此操作将会改变处于被删监听器之后的那些监听器的索引

上述node的EventEmitter的模块看起来很多很复杂,其实上述的API中包含了一些别名,仔细整理,理解其使用和实现不是很困难,下面一一对比和介绍上述的API。

(1) addListener和removeListener、on和off方法

addListener(eventName,listener)的作用是为指定事件添加一个监听器. 其别名为on

removeListener(eventName,listener)的作用是为移除某个事件的监听器. 其别名为off

再次需要强调的是:addListener的别名是on,removeListener的别名是off

EventEmitter.prototype.on=EventEmitter.prototype.addListener
EventEmitter.prototype.off=EventEmitter.prototype.removeListener

接着我们来看具体的用法:

var events=require('events');
var emitter=new events.EventEmitter();
function hello1(name){
  console.log("hello 1",name);
}
function hello2(name){
  console.log("hello 2",name);
}
emitter.addListener('say',hello1);
emitter.addListener('say',hello2);
emitter.emit('say','Jony');
//输出hello 1 Jony 
//输出hello 2 Jony
emitter.removeListener('say',hello1);
emitter.emit('say','Jony');
//相应的监听say事件的hello1事件被移除
//只输出hello 2 Jony

(2) removeListener和removeAllListeners

removeListener指的是移除一个指定事件的某一个监听器,而removeAllListeners指的是移除某一个指定事件的全部监听器。
这里举例一个removeAllListeners的例子:

var events=require('events');
var emitter=new events.EventEmitter();
function hello1(name){
  console.log("hello 1",name);
}
function hello2(name){
  console.log("hello 2",name);
}
emitter.addListener('say',hello1);
emitter.addListener('say',hello2);
emitter.removeAllListeners('say');
emitter.emit('say','Jony');
//removeAllListeners移除了所有关于say事件的监听
//因此没有任何输出

(3) on和once方法

on和once的区别是:

on的方法对于某一指定事件添加的监听器可以持续不断的监听相应的事件,而once方法添加的监听器,监听一次后,就会被消除。

比如on方法(跟addListener相同):

var events=require('events');
var emitter=new events.EventEmitter();
function hello(name){
  console.log("hello",name);
}
emitter.on('say',hello);
emitter.emit('say','Jony');
emitter.emit('say','yu');
emitter.emit('say','me');
//会一次输出 hello Jony、hello yu、hello me

也就是说on方法监听的事件,可以持续不断的被触发。

(4) 两个特殊的事件newListener和removeListener

我们知道当实例化EventEmitter模块之后,监听对象是一个对象,包含了所有的监听事件,而这两个特殊的方法就是针对监听事件的添加和移除的。

newListener:在添加新事件监听器触发
removeListener:在移除事件监听器时触发

以newListener为例,会在添加新事件监听器的时候触发:

var events=require('events');
var emitter=new events.EventEmitter();

function hello(name){
  console.log("hello",name);
}
emitter.on('newListener',function(eventName,listener){
  console.log(eventName);
  console.log(listener);
});
emitter.addListener('say',hello);
//输出say和[Function: hello]

从上述的例子来看,每当添加新的事件,都会自动的emit一个“newListener”事件,且参数为eventName(新事件的名)和listener(新事件的执行函数)。

同理特殊事件removeListener也是同样的,当事件被移除,会自动emit一个"removeListener"事件。

三、EventEmitter模块的异常处理

(1) node中的try catch异常处理方法

在node中也可以通过try catch方式来捕获和处理异常,比如:

try {
  let x=x;
} catch (e) {
  console.log(e);
}

上述let x=x 赋值语句的错误会被捕获。这里提异常处理,那么跟事件有什么关系呢?

node中有一个特殊的事件error,如果异常没有被捕获,就会触发process的uncaughtException事件抛出,如果你没有注册该事件的监听器(即该事件没有被处理),则 Node.js 会在控制台打印该异常的堆栈信息,并结束进程。

比如:

var events=require('events');
var emitter=new events.EventEmitter();
emitter.emit('error');

在上述代码中没有监听error的事件函数,因此会触发process的uncaughtException事件,从而打印异常堆栈信息,并结束进程。

对于阻塞或者说非异步的异常捕获,try catch是没有问题的,但是:

try catch不能捕获非阻塞或者异步函数里面的异常。

举例来说:

try {
  let x=x;//第二个x在使用前未定义,会抛出异常
} catch (e) {
  console.log('该异常已经被捕获');
  console.log(e);
}

上述代码中,以为try方法里面是同步的,因此可以捕获异常。如果try方法里面有异步的函数:

try {
  process.nextTick(function(){
  	let x=x; //第二个x在使用前未定义,会抛出异常
  });
} catch (e) {
  console.log('该异常已经被捕获');
  console.log(e);
}

因为process.nextTick是异步的,因此在process.nextTick内部的错误不能被捕获,也就是说try catch不能捕获非阻塞函数内的异常。

(2) 通过domains管理异常

node中domain模块能被用来集中地处理多个异常操作,通过node的domain模块可以捕获非阻塞函数内的异常。

var domain=require('domain');
var eventDomain=domain.create();
eventDomain.on('error',function(err){
  console.log('该异常已经被捕获了');
  console.log(err);
});
eventDomain.run(function(){
   process.nextTick(function(){
     let x=x;//抛出异常
   });
});

同样的,即使process.nextTick是一个异步函数,domain.on方法也可以捕获这个异步函数中的异常。

即使更复杂的情况下,比如异步嵌套异步的情况下,domain.on方法也可以捕获异常。

var domain=require('domain');
var eventDomain=domain.create();
eventDomain.on('error',function(err){
  console.log('该异常已经被捕获了');
  console.log(err);
});
eventDomain.run(function(){
   process.nextTick(function(){
     setTimeout(function(){
       setTimeout(function(){
         let x=x;
       },0);
     },0);
   });
});

在上述的情况下,即使异步嵌套很复杂,也能在最外层捕获到异常。

(3) domain模块缺陷

在node最新的文档中,domain被废除(被标记为:Deprecated),domain从诞生之日起就有着缺陷,举例来说:

var domain = require('domain');
var EventEmitter = require('events').EventEmitter;

var e = new EventEmitter();

var timer = setTimeout(function () {
  e.emit('data');
}, 10);

function next() {
  e.once('data', function () {
    throw new Error('something wrong here');
  });
}

var d = domain.create();
d.on('error', function () {
  console.log('cache by domain');
});

d.run(next);

如上述的代码是无法捕获到异常Error的,原因在于发出异常的EventEmitter实例e,以及触发异常的定时函数timer没有被domain包裹。domain模块是通过重写事件循环中的nextTick和_tickCallback来事件将process.domain注入到next包裹的所有异步事件内。

解决上述无法捕获异常的情况,只需要将e或者timer包裹进domain。

d.add(e)或者d.add(timer)

就可以成功的捕获异常。

domain模块已经在node最新的文档中被废除

(4)process.on('uncaughtException')的方法捕获异常

node中提供了一个最外层的兜底的捕获异常的方法。非阻塞或者异步函数中的异常都会抛出到最外层,如果异常没有被捕获,那么会暴露出来,被最外层的process.on('uncaughtException')所捕获。

try {
  process.nextTick(function(){
     let x=x; //第二个x在使用前未定义,会抛出异常
  },0);
} catch (e) {
  console.log('该异常已经被捕获');
  console.log(e);
}
process.on('uncaughtException',function(err){console.log(err)})

这样就能在最外层捕获异步或者说非阻塞函数中的异常。

四、完整的实现一个EventEmitter模块(可选读)

在第二节中我们知道了EventEmitter模块的基本用法,那么根据基本的API我们可以进一步自己去实现一个EventEmitter模块。
每一个EventEmitter实例都有一个包含所有事件的对象_events,
事件的监听和监听事件的触发,以及监听事件的移除等都在这个对象_events的基础上实现。

(1) emit

emit的方法实现的大致功能如下程序流程图所示:

default

从上述的程序图出发,我们开始实现自己的EventEmitter模块。

首先生成一个EventEmitter类,在类的初始化方法中生成这个事件对象_events.

class EventEmitter{
  constructor(){
    if(this._events===undefined){
      this._events=Object.create(null);//定义事件对象
      this._eventsCount=0;
    }
  }
}

_eventsCount用于统计事件的个数,也就是_events对象有多少个属性。

接着我们来实现emit方法,根据框图,我们知道emit所做的事情是在_events对象中取出相应type的属性,并执行属性所对应的函数,我们来实现这个emit方法。

 class EventEmitter{
  constructor(){
    if(this._events===undefined){
      this._events=Object.create(null);//定义事件对象
      this._eventsCount=0;
    }
  }
  emit(type,...args){
    const events=this._events;
    const handler=events[type];
    //判断相应type的执行函数是否为一个函数还是一个数组
    if(typeof handler==='function'){
      Reflect.apply(handler,this,args);
    }else{
      const len=handler.length;
      for(var i=0;li<len;i++){
       Reflect.apply(handler[i],this,args);
      }
    }
    return true;
  }
}

(2) on、addListener和prependListener方法

emit方法是出发事件,并执行相应的方法,而on方法则是对于指定的事件添加监听函数。用程序来说,就是往事件对象中_events添加相应的属性.程序流程图如下所示:

2

接着我们来实现这个方法:

on(type,listener,prepend){
    var m;
    var events;
    var existing;
    events=this._events;
    //添加事件的
    if(events.newListener!==undefined){
       this.emit('newListener',type,listener);
       events=target._events;
    }
    existing=events[type];
    //判断相应的type的方法是否存在
    if(existing===undefined){
      //如果相应的type的方法不存在,这新增一个相应type的事件
      existing=events[type]=listener;
      ++this._eventsCount;
    }else{
      //如果存在相应的type的方法,判断相应的type的方法是一个数组还是仅仅只是一个方法
      //如果仅仅是
      if(typeof existing==='function'){
        //如果仅仅是一个方法,则添加
        existing=events[type]=prepend?[listener,existing]:[existing,listener];
      }else if(prepend){
        existing.unshift(listener);
      }else{
        existing.push(listener);
      }
    }
    //链式调用
    return this;
}
  • 在on方法中为了可以链式的调用我们返回了EventEmitter模块的实例化本身。
  • 且在on方法的参数中,第三个参数用于指定是在相应事件类型属性所对应的数组头部添加还是尾部添加,不传的情况下实在尾部添加,如果指定prepend为true,则相同事件类型的新的监听事件会添加到事件数组的头部。
  • 如果_events存在newListener属性,也就是说_event存在监听newListener监听事件,那么每次on方法添加事件的时候,都会emit出一个‘newListener’方法。

在on方法的基础上可以实现addListener方法和prependListener方法。

addListener方法是on方法的别名:

EventEmitter.prototype.addListener=EventEmitter.prototype.on

prependListener方法相当于在头部添加,指定prepend为true:

EventEmitter.prototype.prependListener =
function prependListener(type, listener) {
  return EventEmitter.prototype.on(type, listener, true);
};

(3) removeListener和removeAllListeners

接着来看移除事件监听的方法removeListener和removeAllListeners,下面我们来看removeListener的程序流程图:

3

接着来看removeListener的代码:

removeListener(type,listener){
 var list,events,position,i,originalListener;
 events=this._events;
 list=events[type];
 //如果相应的事件对象的属性值是一个函数,也就是说事件只被一个函数监听
 if(list===listener){
    if(--this._eventsCount===0){
      this._events=Object.create(null);
    }else{
      delete events[type];
      //如果存在对移除事件removeListener的监听函数,则触发removeListener
      if(events.removeListener)
         this.emit('removeListener',type,listener);
    }
 }else if(typeof list!=='function'){
   //如果相应的事件对象属性值是一个函数数组
   //遍历这个数组,找出listener对应的那个函数,在数组中的位置
   for(i=list.length-1;i>=0;i--){
     if(list[i]===listener){
       position=i;
       break;
     }
   }
   //没有找到这个函数,则返回不做任何改动的对象
   if(position){
     return this;
   }
   //如果数组的第一个函数才是所需要删除的对应listener函数,则直接移除
   if(position===0){
     list.shift();
   }else{
     list.splice(position,1);
   }
   if(list.length===1)
     events[type]=list[0];
   if(events.removeListener!==undefined)
     this.emit('removeListener',type,listener);
   }
   return this;
}
  • 如果在之间设置了对于移除这个特殊事件“removeListener”的监听,那么就会在移除事件时候触发“removeListener”事件。

最后来看removeAllListener,这个与removeListener相似,只要找到传入的type所对应属性的值,没有遍历过程,直接删除这个属性即可。

除此之外,还有其他的类似与once、setMaxListeners、listeners也可以在此基础上实现,就不一一举例。

五、总结

本文从node的EventEmitter模块的使用出发,介绍了EventEmitter提供的常用API,然后介绍了node中基于EventEmitter的异常处理,最后自己实现了一个较为完整的EventEmitter模块。

nodejs中的子进程,深入解析child_process模块和cluster模块

nodejs中的子进程,深入解析child_process模块和cluster模块


  node遵循的是单线程单进程的模式,node的单线程是指js的引擎只有一个实例,且在nodejs的主线程中执行,同时node以事件驱动的方式处理IO等异步操作。node的单线程模式,只维持一个主线程,大大减少了线程间切换的开销。

  但是node的单线程使得在主线程不能进行CPU密集型操作,否则会阻塞主线程。对于CPU密集型操作,在node中通过child_process可以创建独立的子进程,父子进程通过IPC通信,子进程可以是外部应用也可以是node子程序,子进程执行后可以将结果返回给父进程。

  此外,node的单线程,以单一进程运行,因此无法利用多核CPU以及其他资源,为了调度多核CPU等资源,node还提供了cluster模块,利用多核CPU的资源,使得可以通过一串node子进程去处理负载任务,同时保证一定的负载均衡型。本文从node的单线程单进程的理解触发,介绍了child_process模块和cluster模块,本文的结构安排如下:


  • node中的单线程和单进程
  • node中的child_process模块实现多进程
  • node中的cluster模块
  • 总结

一、node中的单线程和单进程

  首先要理解的概念是,node的单线程和单进程的模式。node的单线程于其他语言的多线程模式相比,减小了线程间切换的开销,以及在写node代码的时候不用考虑锁以及线程池的问题。node宣称的单线程模式,比其他语言更加适合IO密集型操作。那么一个经典的问题是:

node是真的单线程的吗?

提到node,我们就可以立刻想到单线程、异步IO、事件驱动等字眼。首先要明确的是node真的是单线程的吗,如果是单线程的,那么异步IO,以及定时事件(setTimeout、setInterval等)又是在哪里被执行的。

严格来说,node并不是单线程的。node中存在着多种线程,包括:

  • js引擎执行的线程
  • 定时器线程(setTimeout, setInterval)
  • 异步http线程(ajax)
    ....

  我们平时所说的单线程是指node中只有一个js引擎在主线程上运行。其他异步IO和事件驱动相关的线程通过libuv来实现内部的线程池和线程调度。libv中存在了一个Event Loop,通过Event Loop来切换实现类似于多线程的效果。简单的来讲Event Loop就是维持一个执行栈和一个事件队列,当前执行栈中的如果发现异步IO以及定时器等函数,就会把这些异步回调函数放入到事件队列中。当前执行栈执行完成后,从事件队列中,按照一定的顺序执行事件队列中的异步回调函数。

default

上图中从执行栈,到事件队列,最后事件队列中按照一定的顺序执行回调函数,整个过程就是一个简化版的Event Loop。此外回调函数执行时,同样会生成一个执行栈,在回调函数里面还有可能嵌套异步的函数,也就是说执行栈存在着嵌套。

也就是说node中的单线程是指js引擎只在唯一的主线程上运行,其他的异步操作,也是有独立的线程去执行,通过libv的Event Loop实现了类似于多线程的上下文切换以及线程池调度。线程是最小的进程,因此node也是单进程的。这样就解释了为什么node是单线程和单进程的。

二、node中的child_process模块实现多进程

  node是单进程的,必然存在一个问题,就是无法充分利用cpu等资源。node提供了child_process模块来实现子进程,从而实现一个广义上的多进程的模式。通过child_process模块,可以实现1个主进程,多个子进程的模式,主进程称为master进程,子进程又称工作进程。在子进程中不仅可以调用其他node程序,也可以执行非node程序以及shell命令等等,执行完子进程后,以流或者回调的形式返回。

1、child_process模块提供的API

child_process提供了4个方法,用于新建子进程,这4个方法分别为spawn、execFile、exec和fork。所有的方法都是异步的,可以用一张图来描述这4个方法的区别。

default

上图可以展示出这4个方法的区别,我们也可以简要介绍这4中方法的不同。

  • spawn : 子进程中执行的是非node程序,提供一组参数后,执行的结果以流的形式返回。

  • execFile:子进程中执行的是非node程序,提供一组参数后,执行的结果以回调的形式返回。

  • exec:子进程执行的是非node程序,传入一串shell命令,执行后结果以回调的形式返回,与execFile
    不同的是exec可以直接执行一串shell命令。

  • fork:子进程执行的是node程序,提供一组参数后,执行的结果以流的形式返回,与spawn不同,fork生成的子进程只能执行node应用。接下来的小节将具体的介绍这一些方法。

2、execFile和exec

我们首先比较execFile和exec的区别,这两个方法的相同点:

执行的是非node应用,且执行后的结果以回调函数的形式返回。

不同点是:

exec是直接执行的一段shell命令,而execFile是执行的一个应用

举例来说,echo是UNIX系统的一个自带命令,我们直接可以在命令行执行:

echo hello world

结果,在命令行中会打印出hello world.

(1) 通过exec来实现

新建一个main.js文件中,如果要使用exec方法,那么则在该文件中写入:

let cp=require('child_process');
cp.exec('echo hello world',function(err,stdout){
  console.log(stdout);
});

执行这个main.js,结果会输出hello world。我们发现exec的第一个参数,跟shell命令完全相似。

(2)通过execFile来实现

let cp=require('child_process');
cp.execFile('echo',['hello','world'],function(err,stdout){
   console.log(stdout);
});

execFile类似于执行了名为echo的应用,然后传入参数。execFlie会在process.env.PATH的路径中依次寻找是否有名为'echo'的应用,找到后就会执行。默认的process.env.PATH路径中包含了'usr/local/bin',而这个'usr/local/bin'目录中就存在了这个名为'echo'的程序,传入hello和world两个参数,执行后返回。

(3)安全性分析

像exec那样,可以直接执行一段shell是极为不安全的,比如有这么一段shell:

echo hello world;rm -rf

通过exec是可以直接执行的,rm -rf会删除当前目录下的文件。exec正如命令行一样,执行的等级很高,执行后会出现安全性的问题,而execFile不同:

execFile('echo',['hello','world',';rm -rf'])

在传入参数的同时,会检测传入实参执行的安全性,如果存在安全性问题,会抛出异常。除了execFile外,spawn和fork也都不能直接执行shell,因此安全性较高。

3、spawn

spawn同样是用于执行非node应用,且不能直接执行shell,与execFile相比,spawn执行应用后的结果并不是执行完成后,一次性的输出的,而是以流的形式输出。对于大批量的数据输出,通过流的形式可以介绍内存的使用。

我们用一个文件的排序和去重来举例:

default

上述图片示意图中,首先读取的input.txt文件中有acba未经排序的文字,通过sort程序后可以实现排序功能,输出为aabc,最后通过uniq程序可以去重,得到abc。我们可以用spawn流形式的输入输出来实现上述功能:

let cp=require('child_process');
let cat=cp.spawn('cat',['input.txt']);
let sort=cp.spawn('sort');
let uniq=cp.spawn('uniq');

cat.stdout.pipe(sort.stdin);
sort.stdout.pipe(uniq.stdin);
uniq.stdout.pipe(process.stdout);
console.log(process.stdout);

执行后,最后的结果将输入到process.stdout中。如果input.txt这个文件较大,那么以流的形式输入输出可以明显减小内存的占用,通过设置缓冲区的形式,减小内存占用的同时也可以提高输入输出的效率。

4、fork

在javascript中,在处理大量计算的任务方面,HTML里面通过web work来实现,使得任务脱离了主线程。在node中使用了一种内置于父进程和子进程之间的通信来处理该问题,降低了大数据运行的压力。node中提供了fork方法,通过fork方法在单独的进程中执行node程序,并且通过父子间的通信,子进程接受父进程的信息,并将执行后的结果返回给父进程。

使用fork方法,可以在父进程和子进程之间开放一个IPC通道,使得不同的node进程间可以进行消息通信。

在子进程中:

通过process.on('message')和process.send()的机制来接收和发送消息。

在父进程中:

通过child.on('message')和process.send()的机制来接收和发送消息。

具体例子,在child.js中:

process.on('message',function(msg){
   process.send(msg)
})

在parent.js中:

let cp=require('child_process');
let child=cp.fork('./child');
child.on('message',function(msg){
  console.log('got a message is',msg);
});
child.send('hello world');

执行parent.js会在命令行输出:got a message is hello world

中断父子间通信的方式,可以通过在父进程中调用:

child.disconnect()

来实现断开父子间IPC通信。

5、同步执行的子进程

exec、execFile、spawn和fork执行的子进程都是默认异步的,子进程的运行不会阻塞主进程。除此之外,child_process模块同样也提供了execFileSync、spawnSync和execSync来实现同步的方式执行子进程。

三、node中的cluster模块

cluster意为集成,集成了两个方面,第一个方面就是集成了child_process.fork方法创建node子进程的方式,第二个方面就是集成了根据多核CPU创建子进程后,自动控制负载均衡的方式。

我们从官网的例子来看:

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  console.log(`主进程 ${process.pid} 正在运行`);

  // 衍生工作进程。
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`工作进程 ${worker.process.pid} 已退出`);
  });
} else {
  // 工作进程可以共享任何 TCP 连接。
  // 在本例子中,共享的是一个 HTTP 服务器。
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('你好世界\n');
  }).listen(8000);

  console.log(`工作进程 ${process.pid} 已启动`);
}

最后输出的结果为:

$ node server.js
主进程 3596 正在运行
工作进程 4324 已启动
工作进程 4520 已启动
工作进程 6056 已启动
工作进程 5644 已启动

我们将master称为主进程,而worker进程称为工作进程,利用cluster模块,使用node封装好的API、IPC通道和调度机可以非常简单的创建包括一个master进程下HTTP代理服务器 + 多个worker进程多个HTTP应用服务器的架构。

四、 总结

本文首先介绍了node的单线程和单进程模式,接着从单线程的缺陷触发,介绍了node中如何实现子进程的方法,对比了child_process模块中几种不同的子进程生成方案,最后简单介绍了内置的可以实现子进程以及CPU进程负载均衡的内置集成模块cluster。

在React项目中,如何优雅的优化长列表

在React项目中,如何优雅的优化长列表


  对于较长的列表,比如1000个数组的数据结构,如果想要同时渲染这1000个数据,生成相应的1000个原生dom,我们知道原生的dom元素是很复杂的,如果长列表通过生成如此多的dom元素来实现,很可能使网页失去响应。

  贯穿React核心的就是"virtual dom",我们同样的可以通过用虚拟列表的方式来优雅的优化长列表

  • 原生dom渲染长列表的缺陷
  • 虚拟列表优化长列表的原理
  • 通过react-virtualized来优化长列表
  • 通过react-tiny-virtual-list来优化长列表

一、原生dom渲染长列表的缺陷

  首先我们尝试在React项目中,未做任何优化一次性渲染1000个dom,每个dom包含一个img标签,原生dom本身是很复杂的对象,加上img标签后。渲染的效果如下图所示:

  可以很明显的看到白屏的时间很长,因为在React中,不做任何优化,直接渲染这么包含1000个图片的dom节点,即使React本身用了虚拟dom,但是在首次渲染的时候,是实实在在的生成了1000个真实的dom,我们可以查看网页中的真实dom情况,如下所示:

  从上图我们可以看出,是确确实实的生成了1000个真实的dom,进入页面后,需要渲染这1000个dom,因此白屏的时间很长。

  此外,在直接渲染1000个dom节点的页面,触发滚动事件,也会使得内存用量增加,具体可以如下图所示:

此外同时渲染很多dom节点,也会造成一下几个问题:

  • 容易失帧,因为渲染很慢,所以无法维持浏览器的帧率,主观上会显得页面卡顿

  • 网页失去响应,事件等无法及时被触发

  上述的效果都是在PC端展示的,对于特定的移动设备,直接无优化的渲染长列表所造成的问题会更加的放大。长列表的渲染在移动端的很多场景会遇到,比如微博,feeds流中等等。合理的优化长列表,可以提升用户体验。

二、虚拟列表优化长列表的原理

优化长列表的原理很简单,基本原理可以一句话概括:

用数组保存所有列表元素的位置,只渲染可视区内的列表元素,当可视区滚动时,根据滚动的offset大小以及所有列表元素的位置,计算在可视区应该渲染哪些元素。

具体实现步骤如下所示:

  1. 首先确定长列表所在父元素的大小,父元素的大小决定了可视区的宽和高
  2. 确定长列表每一个列表元素的宽和高,同时初始的条件下计算好长列表每一个元素相对于父元素的位置,并用一个数组来保存所有列表元素的位置信息
  3. 首次渲染时,只展示相对于父元素可视区内的子列表元素,在滚动时,根据父元素的滚动的offset重新计算应该在可视区内的子列表元素。这样保证了无论如何滚动,真实渲染出的dom节点只有可视区内的列表元素。
  4. 假设可视区内能展示5个子列表元素,及时长列表总共有1000个元素,但是每时每刻,真实渲染出来的dom节点只有5个。
    5.补充说明,这种情况下,父元素一般使用position:relative,子元素的定位一般使用:position:absolute或sticky

通过虚拟列表优化后,同样的显示1000个包含图片的dom,白屏时间会大大的减少。具体效果如下图所示:

对于比无优化的情况,优化后的虚拟列表渲染速度提升很明显。

三、通过react-virtualized来优化长列表

社区实现虚拟列表的React组件很多,较为常用的是react-virtualized和react-tiny-virtual-list,前一个较为全面,第二个较为轻量。接下来会分别来介绍这俩个React组件库。

1、react-virtualized简介

react-virtualized是一个实现虚拟列表较为优秀的组件库,react-virtualized提供了一些基础组件用于实现虚拟列表,虚拟网格,虚拟表格等等,它们都可以减小不必要的dom渲染。此外还提供了几个高阶组件,可以实现动态子元素高度,以及自动填充可视区等等。

react-virtualized的基础组件包含:

  • Grid:用于优化构建任意网状的结构,传入一个二维的数组,渲染出类似棋盘的结构。
  • List:List是基于Grid来实现的,但是是一个维的列表,而不是网状。
  • Table:Table也是基于Grid来实现,表格具有固定的头部,并且可以在垂直方向上滚动
  • Masonry:同样可以在水平方向,也可以在垂直方向滚动,不同于Grid的是可以自定义每个元素的大小,或者子元素的大小也可以是动态变化的
  • Collection:类似于瀑布流的形式,同样可以水平和垂直方向滚动。

值得注意的是这些基础组件都是继承于React中的PureComponent,因此当state变化的时候,只会做一个浅比较来确定重新渲染与否

除了这几个基础组件外,react-virtualized还提供了几个高阶组件,比如ArrowKeyStepper
、AutoSizer、CellMeasurer、InfiniteLoader等,本文具体介绍常用的AutoSizer、CellMeasurer和InfiniteLoader。

  • AutoSizer:使用于一个子元素的情况,通过AutoSizer包含的子元素会根据父元素Resize的变化,自动调节该子元素的可视区的宽度和高度,同时调节的还有该子元素可视区真实渲染的dom元素的数目。
  • CellMeasurer:这个高阶组件可以动态的改变子元素的高度,适用于提前不知道长列表中每一个子元素高度的情况。
  • InfiniteLoader:这个高阶组件用于Table或者List的无限滚动,适用于滚动时异步请求等情况

2、react-virtualized基础组件的使用

下面我们来介绍一下常用的基础组件Grid、List。

(1)Grid

所有基础组件基本上都是基于Grid构成的,一个简单的Grid的例子如下:

import { Grid } from 'react-virtualized';
const list = [
  ['Jony yu', 'Software Engineer', 'Shenzhen', 'CHINA', 'GUANGZHOU'],
  ['Jony yu', 'Software Engineer', 'Shenzhen', 'CHINA', 'GUANGZHOU'],
  ['Jony yu', 'Software Engineer', 'Shenzhen', 'CHINA', 'GUANGZHOU'],
  ['Jony yu', 'Software Engineer', 'Shenzhen', 'CHINA', 'GUANGZHOU'],
  ['Jony yu', 'Software Engineer', 'Shenzhen', 'CHINA', 'GUANGZHOU'],
  ['Jony yu', 'Software Engineer', 'Shenzhen', 'CHINA', 'GUANGZHOU']
];

function cellRenderer ({ columnIndex, key, rowIndex, style }) {
  return (
    <div
      key={key}
      style={style}
    >
      {list[rowIndex][columnIndex]}
    </div>
  )
}
render(
  <Grid
   cellRenderer={cellRenderer}
   columnCount={list[0].length}
   columnWidth={100}
   height={300}
   rowCount={list.length}
   rowHeight={80}
   width={300}
  />,
  rootEl
);

显示的效果如下图所示:

渲染网格也是只渲染可视区的dom节点,有个有趣的现象是滚动条的大小,这里Grid做了一个细节优化,只有滚动的时候才会显示滚动条,停止滚动后会隐藏滚动条。

(2)List

接着来举例说明一下List的使用:

import { List } from 'react-virtualized';
import  loremIpsum from "lorem-ipsum"
const rowCount = 1000;
const list = Array(rowCount).fill().map(()=>{
  return loremIpsum({
        count: 1,
        units: 'sentences',
        sentenceLowerBound: 3,
        sentenceUpperBound: 3
      }
})
function rowRenderer ({
  key,         
  index,      
  isScrolling, 
  isVisible,   
  style      
}) {
  return (
    <div
      key={key}
      style={style}
    >
      {list[index]}
    </div>
  )
}
export default class TestList extends Component{
  render(){
    return <div style={{height:"300px",width:"200px"}}>
            <List
              width={300}
              height={300}
              rowCount={list.length}
              rowHeight={20}
              rowRenderer={rowRenderer}
             />
          </div>
  }
}

List的使用方法也是极简,指定列表总条数rowCount,每一条的高度rowHeight以及每次渲染的函数rowRenderer,就可以构建一个渲染列表。具体的效果如下图所示:

2、react-virtualized高阶组件的使用

结合List来看看react-virtualized高阶组件的使用。

(1)AutoSizer

首先来看使用不使用AutoSizer的缺点,如下图所示,List只能指定固定的大小,如果其所在的父元素的大小resize了,那么List是不会主动填满父元素的可视区的:

从上图可以看出来,List是无法自动填充父元素的。因此我们这里需要使用AutoSizer。AutoSizer的使用也很简单,我们只需要在List的基础上:

class TestList extends Component{
  render(){
    return <div>
             <AutoSizer>
              {({ height, width }) => (
                <List
                  height={height}
                  rowCount={list.length}
                  rowHeight={20}
                  rowRenderer={rowRenderer}
                  width={width}
                />
              )}
            </AutoSizer>
          </div>
  }
}

效果如下图所示:

上述可以看出来增加了AutoSizer可以动态的适应父元素宽度和高度的变化。

但是也存在一个问题:

子元素太长,换行后改变了子元素的高度后无法子适应,也就是说仅仅通过基础的组件List是不支持子元素的高度动态改变的场景

(2)CellMeasurer

为了解决上述的子元素可以动态变化的问题,我们可以利用高阶组件CellMeasurer:

import { List,AutoSizer,CellMeasurer, CellMeasurerCache} from 'react-virtualized';
const cache = new CellMeasurerCache({ defaultHeight: 30,fixedWidth: true});
function cellRenderer ({ index, key, parent, style }) {
  console.log(index)

  return (
    <CellMeasurer
      cache={cache}
      columnIndex={0}
      key={key}
      parent={parent}
      rowIndex={index}
    >
      <div
        style={style}
      >
        {list[index]}
      </div>
    </CellMeasurer>
  );
}

对于需要渲染的List,如下所示:

class TestList extends Component{
  render(){
    return <div>
             <AutoSizer>
              {({ height, width }) => (
                <List
                  height={height}
                  rowCount={list.length}
                  rowHeight={cache.rowHeight}
                  deferredMeasurementCache={cache}
                  rowRenderer={cellRenderer}
                  width={width}
                />
              )}
            </AutoSizer>
          </div>
  }
}

最后的结果如下所示:

上图我们看出来,子列表元素的高度可以动态变化,通过CellMeasurer可以实现子元素的动态高度。

(3)InfiniteLoader

最后我们来考虑这种无限滚动的场景,很多情况下我们可能需要分页加载,就是常见的在可视区内无限滚动的场景。react-virtualized提供了一个高阶组件InfiniteLoader用于实现无限滚动。

InfiniteLoader的使用很简单,只要按着文档来即可,就是分页的去在家下一页,滚动分页所调用的函数为:

function loadMoreRows ({ startIndex, stopIndex }) {
  return new Promise(function(resolve,reject){
    resolve()
  }).then(function(){
    //模拟ajax请求
    let temList = Array(10).fill(1).map(()=>{
      return loremIpsum({
            count: 1,
            units: 'sentences',
            sentenceLowerBound:3,
            sentenceUpperBound:3
        })
    })
    list = list.concat(temList)
  })
}

最后的效果如下:

看起来跟基础组件List一样,其实唯一的区别就是会在滚动的时候自动执行loadMoreRows函数去更新list

(4)总结

通过基础组件Grid、List以及高阶组件AutoSizer、CellMeasurer和InfiniteLoader,已经可以构建出比较复杂的场景,但是有一个缺陷,就是CellMeasurer虽说一定程度上支持动态子元素的高度的变化,其实是一种估算,存在很多边界情况,无法适应于动态元素的场景,特别是文本节点较多导致的高度变化。但是对于图片节点的动态高度支持没有很大的问题。

举例一种边界情况,CellMeasurer无法支持文本动态高度的情况:

从上图可以看到,慢慢缩小的过程中,如果缩的太小,并没有动态的撑大子元素的高度,出现了文字的重叠。

四、通过react-tiny-virtual-list来优化长列表

react-tiny-virtual-list是一个较为轻量的实现虚拟列表的组件,不同于react-virtualized支持网格以及表格等渲染优化。
react-tiny-virtual-list只支持列表,使用方便,其源码也只有700多行。

使用极其简单:

import VirtualList from 'react-tiny-virtual-list';
const data = ['A', 'B', 'C', 'D', 'E', 'F','A', 'B', 'C',
'D', 'E', 'F','A', 'B', 'C', 'D', 'E', 'F',
'A', 'B', 'C', 'D', 'E', 'F'];
export default class TinyVirtual extends Component {
  render(){
    return <VirtualList
            width='100%'
            height={200}
            itemCount={data.length}
            itemSize={50} // Also supports variable heights (array or function getter)
            renderItem={({index, style}) =>
              <div key={index} style={style}>
                // The style property contains the item's absolute position Letter: {data[index]}, Row: #{index}
              </div>
            }
            />
  }
}

最后的渲染结果也是相似的,也可以支持无限滚动等等。

但是react-tiny-virtual-list有一个致命的缺点:

完全不支持子元素的动态高度或者宽度

五、总结

本文介绍了虚拟列表的优化的原理,以及常用的React可以优化虚拟列表的组件库。在接下来的文章中,会具体的介绍react-tiny-virtual-list和react-virtualized的源码,敬请期待。

前端面试总结——基础javascript篇

前端面试总结——基础javascript篇


js是前端开发人员必须熟练掌握的技能,这里概括js的一些必须了解的理论知识:

  • 梳理js基础知识
  • 查漏补缺

1. get请求传参长度的误区

误区:我们经常说get请求参数的大小存在限制,而post请求的参数大小是无限制的。

实际上HTTP 协议从未规定 GET/POST 的请求长度限制是多少。对get请求参数的限制是来源与浏览器或web服务器,浏览器或web服务器限制了url的长度。为了明确这个概念,我们必须再次强调下面几点:

  • HTTP 协议 未规定 GET 和POST的长度限制
  • GET的最大长度显示是因为 浏览器和 web服务器限制了 URI的长度
  • 不同的浏览器和WEB服务器,限制的最大长度不一样
  • 要支持IE,则最大长度为2083byte,若只支持Chrome,则最大长度 8182byte

2. 补充get和post请求在缓存方面的区别

post/get的请求区别,具体不再赘述。

补充补充一个get和post在缓存方面的区别:

  • get请求类似于查找的过程,用户获取数据,可以不用每次都与数据库连接,所以可以使用缓存。
  • post不同,post做的一般是修改和删除的工作,所以必须与数据库交互,所以不能使用缓存。因此get请求适合于请求缓存。

3. 闭包

一句话可以概括:闭包就是能够读取其他函数内部变量的函数,或者子函数在外调用,子函数所在的父函数的作用域不会被释放。

4. 类的创建和继承

(1)类的创建(es5):new一个function,在这个function的prototype里面增加属性和方法。

下面来创建一个Animal类:

// 定义一个动物类
function Animal (name) {
  // 属性
  this.name = name || 'Animal';
  // 实例方法
  this.sleep = function(){
    console.log(this.name + '正在睡觉!');
  }
}
// 原型方法
Animal.prototype.eat = function(food) {
  console.log(this.name + '正在吃:' + food);
};

这样就生成了一个Animal类,实力化生成对象后,有方法和属性。

(2)类的继承——原型链继承

--原型链继承
function Cat(){ }
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.eat('fish'));
console.log(cat.sleep());
console.log(cat instanceof Animal); //true 
console.log(cat instanceof Cat); //true
  • 介绍:在这里我们可以看到new了一个空对象,这个空对象指向Animal并且Cat.prototype指向了这个空对象,这种就是基于原型链的继承。
  • 特点:基于原型链,既是父类的实例,也是子类的实例
  • 缺点:无法实现多继承

(3)构造继承:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
  • 特点:可以实现多继承
  • 缺点:只能继承父类实例的属性和方法,不能继承原型上的属性和方法。

(4)实例继承和拷贝继承

实例继承:为父类实例添加新特性,作为子类实例返回

拷贝继承:拷贝父类元素上的属性和方法

上述两个实用性不强,不一一举例。

(5)组合继承:相当于构造继承和原型链继承的组合体。通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true
  • 特点:可以继承实例属性/方法,也可以继承原型属性/方法
  • 缺点:调用了两次父类构造函数,生成了两份实例

(6)寄生组合继承:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
(function(){
  // 创建一个没有实例方法的类
  var Super = function(){};
  Super.prototype = Animal.prototype;
  //将实例作为子类的原型
  Cat.prototype = new Super();
})();
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true
  • 较为推荐

5. 如何解决异步回调地狱

promise、generator、async/await

6. 说说前端中的事件流

HTML中与javascript交互是通过事件驱动来实现的,例如鼠标点击事件onclick、页面的滚动事件onscroll等等,可以向文档或者文档中的元素添加事件侦听器来预订事件。想要知道这些事件是在什么时候进行调用的,就需要了解一下“事件流”的概念。

什么是事件流:事件流描述的是从页面中接收事件的顺序,DOM2级事件流包括下面几个阶段。

  • 事件捕获阶段
  • 处于目标阶段
  • 事件冒泡阶段

addEventListeneraddEventListener 是DOM2 级事件新增的指定事件处理程序的操作,这个方法接收3个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。最后这个布尔值参数如果是true,表示在捕获阶段调用事件处理程序;如果是false,表示在冒泡阶段调用事件处理程序。

IE只支持事件冒泡

7. 如何让事件先冒泡后捕获

在DOM标准事件模型中,是先捕获后冒泡。但是如果要实现先冒泡后捕获的效果,对于同一个事件,监听捕获和冒泡,分别对应相应的处理函数,监听到捕获事件,先暂缓执行,直到冒泡事件被捕获后再执行捕获之间。

8. 事件委托

  • 简介:事件委托指的是,不在事件的发生地(直接dom)上设置监听函数,而是在其父元素上设置监听函数,通过事件冒泡,父元素可以监听到子元素上事件的触发,通过判断事件发生元素DOM的类型,来做出不同的响应。

  • 举例:最经典的就是ul和li标签的事件监听,比如我们在添加事件时候,采用事件委托机制,不会在li标签上直接添加,而是在ul父元素上添加。

  • 好处:比较合适动态元素的绑定,新添加的子元素也会有监听函数,也可以有事件触发机制。

9. 图片的懒加载和预加载

  • 预加载:提前加载图片,当用户需要查看时可直接从本地缓存中渲染。

  • 懒加载:懒加载的主要目的是作为服务器前端的优化,减少请求数或延迟请求数。

两种技术的本质:两者的行为是相反的,一个是提前加载,一个是迟缓甚至不加载。
懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。

10. mouseover和mouseenter的区别

  • mouseover:当鼠标移入元素或其子元素都会触发事件,所以有一个重复触发,冒泡的过程。对应的移除事件是mouseout

  • mouseenter:当鼠标移除元素本身(不包含元素的子元素)会触发事件,也就是不会冒泡,对应的移除事件是mouseleave

11. js的new操作符做了哪些事情

new 操作符新建了一个空对象,这个对象原型指向构造函数的prototype,执行构造函数后返回这个对象。

12.改变函数内部this指针的指向函数(bind,apply,call的区别)

  • 通过apply和call改变函数的this指向,他们两个函数的第一个参数都是一样的表示要改变指向的那个对象,第二个参数,apply是数组,而call则是arg1,arg2...这种形式。

  • 通过bind改变this作用域会返回一个新的函数,这个函数不会马上执行。

13. js的各种位置,比如clientHeight,scrollHeight,offsetHeight ,以及scrollTop, offsetTop,clientTop的区别?

  • clientHeight:表示的是可视区域的高度,不包含border和滚动条

  • offsetHeight:表示可视区域的高度,包含了border和滚动条

  • scrollHeight:表示了所有区域的高度,包含了因为滚动被隐藏的部分。

  • clientTop:表示边框border的厚度,在未指定的情况下一般为0

  • scrollTop:滚动后被隐藏的高度,获取对象相对于由offsetParent属性指定的父坐标(css定位的元素或body元素)距离顶端的高度。

14. js拖拽功能的实现

  • 首先是三个事件,分别是mousedown,mousemove,mouseup
    当鼠标点击按下的时候,需要一个tag标识此时已经按下,可以执行mousemove里面的具体方法。

  • clientX,clientY标识的是鼠标的坐标,分别标识横坐标和纵坐标,并且我们用offsetX和offsetY来表示元素的元素的初始坐标,移动的举例应该是:

    鼠标移动时候的坐标-鼠标按下去时候的坐标。

    也就是说定位信息为:

    鼠标移动时候的坐标-鼠标按下去时候的坐标+元素初始情况下的offetLeft.

  • 还有一点也是原理性的东西,也就是拖拽的同时是绝对定位,我们改变的是绝对定位条件下的left
    以及top等等值。

补充:也可以通过html5的拖放(Drag 和 drop)来实现

15. 异步加载js的方法

  • defer:只支持IE如果您的脚本不会改变文档的内容,可将 defer 属性加入到<script>标签中,以便加快处理文档的速度。因为浏览器知道它将能够安全地读取文档的剩余部分而不用执行脚本,它将推迟对脚本的解释,直到文档已经显示给用户为止。

  • async,HTML5属性仅适用于外部脚本,并且如果在IE中,同时存在defer和async,那么defer的优先级比较高,脚本将在页面完成时执行。

  • 创建script标签,插入到DOM中

16. Ajax 解决浏览器缓存问题

  • 在ajax发送请求前加上 anyAjaxObj.setRequestHeader("If-Modified-Since","0")。

  • 在ajax发送请求前加上 anyAjaxObj.setRequestHeader("Cache-Control","no-cache")。

  • 在URL后面加上一个随机数: "fresh=" + Math.random()。

  • 在URL后面加上时间搓:"nowtime=" + new Date().getTime()。

  • 如果是使用jQuery,直接这样就可以了 $.ajaxSetup({cache:false})。这样页面的所有ajax都会执行这条语句就是不需要保存缓存记录。

17. js的节流和防抖

http://www.cnblogs.com/coco1s/p/5499469.html

18. JS中的垃圾回收机制

必要性由于字符串、对象和数组没有固定大小,所有当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。

这段话解释了为什么需要系统需要垃圾回收,JS不像C/C++,他有自己的一套垃圾回收机制(Garbage Collection)。JavaScript的解释器可以检测到何时程序不再使用一个对象了,当他确定了一个对象是无用的时候,他就知道不再需要这个对象,可以把它所占用的内存释放掉了。例如:

var a="hello world";
var b="world";
var a=b;
//这时,会释放掉"hello world",释放内存以便再引用

垃圾回收的方法:标记清除、计数引用。

  • 标记清除

这是最常见的垃圾回收方式,当变量进入环境时,就标记这个变量为”进入环境“,从逻辑上讲,永远不能释放进入环境的变量所占的内存,永远不能释放进入环境变量所占用的内存,只要执行流程进入相应的环境,就可能用到他们。当离开环境时,就标记为离开环境。

垃圾回收器在运行的时候会给存储在内存中的变量都加上标记(所有都加),然后去掉环境变量中的变量,以及被环境变量中的变量所引用的变量(条件性去除标记),删除所有被标记的变量,删除的变量无法在环境变量中被访问所以会被删除,最后垃圾回收器,完成了内存的清除工作,并回收他们所占用的内存。

  • 引用计数法

另一种不太常见的方法就是引用计数法,引用计数法的意思就是每个值没引用的次数,当声明了一个变量,并用一个引用类型的值赋值给改变量,则这个值的引用次数为1,;相反的,如果包含了对这个值引用的变量又取得了另外一个值,则原先的引用值引用次数就减1,当这个值的引用次数为0的时候,说明没有办法再访问这个值了,因此就把所占的内存给回收进来,这样垃圾收集器再次运行的时候,就会释放引用次数为0的这些值。

用引用计数法会存在内存泄露,下面来看原因:

function problem() {
    var objA = new Object();
    var objB = new Object();
    objA.someOtherObject = objB;
    objB.anotherObject = objA;
}

在这个例子里面,objA和objB通过各自的属性相互引用,这样的话,两个对象的引用次数都为2,在采用引用计数的策略中,由于函数执行之后,这两个对象都离开了作用域,函数执行完成之后,因为计数不为0,这样的相互引用如果大量存在就会导致内存泄露。

特别是在DOM对象中,也容易存在这种问题:

var element=document.getElementById(’‘);
var myObj=new Object();
myObj.element=element;
element.someObject=myObj;

这样就不会有垃圾回收的过程。

19.eval是做什么的

它的功能是将对应的字符串解析成js并执行,应该避免使用js,因为非常消耗性能(2次,一次解析成js,一次执行)

20.如何理解前端模块化

前端模块化就是复杂的文件编程一个一个独立的模块,比如js文件等等,分成独立的模块有利于重用(复用性)和维护(版本迭代),这样会引来模块之间相互依赖的问题,所以有了commonJS规范,AMD,CMD规范等等,以及用于js打包(编译等处理)的工具webpack

21.Commonjs、 AMD和CMD

一个模块是能实现特定功能的文件,有了模块就可以方便的使用别人的代码,想要什么功能就能加载什么模块。

  • Commonjs:开始于服务器端的模块化,同步定义的模块化,每个模块都是一个单独的作用域,模块输出,modules.exports,模块加载require()引入模块。
  • AMD:中文名异步模块定义的意思。

requireJS实现了AMD规范,主要用于解决下述两个问题。

1.多个文件有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器

2.加载的时候浏览器会停止页面渲染,加载文件越多,页面失去响应的时间越长。

语法:requireJS定义了一个函数define,它是全局变量,用来定义模块。

requireJS的例子:

//定义模块
define(['dependency'], function(){
        var name = 'Byron';
        function printName(){
            console.log(name);
        }
        return {
            printName: printName
        };
   });
//加载模块
require(['myModule'], function (my){
 my.printName();
}

requirejs定义了一个函数define,它是全局变量,用来定义模块:

define(id?dependencies?,factory)

在页面上使用模块加载函数:

require([dependencies],factory);

总结AMD规范:require()函数在加载依赖函数的时候是异步加载的,这样浏览器不会失去响应,它指定的回调函数,只有前面的模块加载成功,才会去执行。
因为网页在加载js的时候会停止渲染,因此我们可以通过异步的方式去加载js,而如果需要依赖某些,也是异步去依赖,依赖后再执行某些方法。

22.对象深度克隆的简单实现

function deepClone(obj){
  var newObj= obj instanceof Array ? []:{};
  for(var item in obj){
    var temple= typeof obj[item] == 'object' ? deepClone(obj[item]):obj[item];
    newObj[item] = temple;
  }
  return newObj;
}

ES5的常用的对象克隆的一种方式。注意数组是对象,但是跟对象又有一定区别,所以我们一开始判断了一些类型,决定newObj是对象还是数组~

23.实现一个once函数,传入函数参数只执行一次

function ones(func){
    var tag=true;
    return function(){
      if(tag==true){
        func.apply(null,arguments);
        tag=false;
      }
      return undefined
    }
}

24.将原生的ajax封装成promise

var  myNewAjax=function(url){
  return new Promise(function(resolve,reject){
      var xhr = new XMLHttpRequest();
      xhr.open('get',url);
      xhr.send(data);
      xhr.onreadystatechange=function(){
           if(xhr.status==200&&readyState==4){
                var json=JSON.parse(xhr.responseText);
                resolve(json)
           }else if(xhr.readyState==4&&xhr.status!=200){
                reject('error');
           }
      }
  })
}

25.js监听对象属性的改变

我们假设这里有一个user对象,

(1)在ES5中可以通过Object.defineProperty来实现已有属性的监听

Object.defineProperty(user,'name',{
    set:function(key,value){
       
    }
})

缺点:如果id不在user对象中,则不能监听id的变化

(2)在ES6中可以通过Proxy来实现

var  user = new Proxy({},{
 set:function(target,key,value,receiver){

  }
})

这样即使有属性在user中不存在,通过user.id来定义也同样可以这样监听这个属性的变化哦~

26.如何实现一个私有变量,用getName方法可以访问,不能直接访

(1)通过defineProperty来实现

obj={
  name:yuxiaoliang,
  getName:function(){
    return this.name
  }
}
object.defineProperty(obj,"name",{
   //不可枚举不可配置
});

(2)通过函数的创建形式

function product(){
    var name='yuxiaoliang';
    this.getName=function(){
      return name;
    }
}
var obj=new product();

27. ==和===、以及Object.is的区别

(1) ==

主要存在:强制转换成number,null==undefined

" "==0  //true
"0"==0  //true
" " !="0" //true
123=="123" //true
null==undefined //true

(2)Object.js

主要的区别就是+0!=-0 而NaN==NaN
(相对比===和==的改进)

28.setTimeout、setInterval和requestAnimationFrame之间的区别

这里有一篇文章讲的是requestAnimationFrame:http://www.cnblogs.com/xiaohuochai/p/5777186.html
与setTimeout和setInterval不同,requestAnimationFrame不需要设置时间间隔,
大多数电脑显示器的刷新频率是60Hz,大概相当于每秒钟重绘60次。大多数浏览器都会对重绘操作加以限制,不超过显示器的重绘频率,因为即使超过那个频率用户体验也不会有提升。因此,最平滑动画的最佳循环间隔是1000ms/60,约等于16.6ms。

RAF采用的是系统时间间隔,不会因为前面的任务,不会影响RAF,但是如果前面的任务多的话,
会响应setTimeout和setInterval真正运行时的时间间隔。

特点:
(1)requestAnimationFrame会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率。

(2)在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这当然就意味着更少的CPU、GPU和内存使用量

(3)requestAnimationFrame是由浏览器专门为动画提供的API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了CPU开销。

从路由原理出发,深入阅读理解react-router 4.0的源码

从路由原理出发,深入阅读理解react-router 4.0的源码


  react-router等前端路由的原理大致相同,可以实现无刷新的条件下切换显示不同的页面。路由的本质就是页面的URL发生改变时,页面的显示结果可以根据URL的变化而变化,但是页面不会刷新。通过前端路由可以实现单页(SPA)应用,本文首先从前端路由的原理出发,详细介绍了前端路由原理的变迁。接着从react-router4.0的源码出发,深入理解react-router4.0是如何实现前端路由的。

  • 通过Hash实现前端路由
  • 通过H5的history实现前端路由
  • React-router4.0的使用
  • React-router4.0源码分析

一、通过Hash实现前端路由

1、hash的原理

  早期的前端路由是通过hash来实现的:

改变url的hash值是不会刷新页面的。

  因此可以通过hash来实现前端路由,从而实现无刷新的效果。hash属性位于location对象中,在当前页面中,可以通过:

window.location.hash='edit'

来实现改变当前url的hash值。执行上述的hash赋值后,页面的url发生改变。

赋值前:http://localhost:3000
赋值后:http://localhost:3000/#edit

在url中多了以#结尾的hash值,但是赋值前后虽然页面的hash值改变导致页面完整的url发生了改变,但是页面是不会刷新的。此外,还有一个名为hashchange的事件,可以监听hash的变化,我们可以通过下面两种方式来监听hash的变化:

window.onhashchange=function(event){
   console.log(event);
}
window.addEventListener('hashchange',function(event){
   console.log(event);
})

当hash值改变时,输出一个HashChangeEvent。该HashChangeEvent的具体值为:

{isTrusted: true, oldURL: "http://localhost:3000/", newURL:   "http://localhost:3000/#teg", type: "hashchange".....}

  有了监听事件,且改变hash页面不刷新,这样我们就可以在监听事件的回调函数中,执行我们展示和隐藏不同UI显示的功能,从而实现前端路由。

此外,除了可以通过window.location.hash来改变当前页面的hash值外,还可以通过html的a标签来实现:

<a href="#edit">edit</a>

2、hash的缺点

hash的兼容性较好,因此在早期的前端路由中大量的采用,但是使用hash也有很多缺点。

  • 搜索引擎对带有hash的页面不友好
  • 带有hash的页面内难以追踪用户行为

二、通过history实现前端路由

HTML5的History接口,History对象是一个底层接口,不继承于任何的接口。History接口允许我们操作浏览器会话历史记录。

(1)History的属性和方法

History提供了一些属性和方法。

History的属性:

  • History.length: 返回在会话历史中有多少条记录,包含了当前会话页面。此外如果打开一个新的Tab,那么这个length的值为1
  • History.state:
    保存了会出发popState事件的方法,所传递过来的属性对象(后面会在pushState和replaceState方法中详细的介绍)

History方法:

  • History.back(): 返回浏览器会话历史中的上一页,跟浏览器的回退按钮功能相同

  • History.forward():指向浏览器会话历史中的下一页,跟浏览器的前进按钮相同

  • History.go(): 可以跳转到浏览器会话历史中的指定的某一个记录页

  • History.pushState():pushState可以将给定的数据压入到浏览器会话历史栈中,该方法接收3个参数,对象,title和一串url。pushState后会改变当前页面url,但是不会伴随着刷新

  • History.replaceState():replaceState将当前的会话页面的url替换成指定的数据,replaceState后也会改变当前页面的url,但是也不会刷新页面。

上面的方法中,pushState和repalce的相同点:

就是都会改变当前页面显示的url,但都不会刷新页面。

不同点:

pushState是压入浏览器的会话历史栈中,会使得History.length加1,而replaceState是替换当前的这条会话历史,因此不会增加History.length.

(2)BOM对象history

history在浏览器的BOM对象模型中的重要属性,history完全继承了History接口,因此拥有History中的所有的属性和方法。

这里我们主要来看看history.length属性以及history.pushState、history.replaceState方法。

  • history.pushState(stateObj,title,url) or history.replaceState(stateObj,title,url)

pushState和replaceState接受3个参数,分别为state对象,title标题,改变的url。

window.history.pushState({foo:'bar'}, "page 2", "bar.html");

此时,当前的url变为:

执行上述方法前:http://localhost:3000
执行上述方法后:http://localhost:3000/bar.html

如果我们输出window.history.state:

console.log(window.history.state);
// {foo:'bar'}

window.history.state就是我们pushState的第一个对象参数。

  • history.replaceState()方法不会改变hitroy的长度

      console.log(window.history.length);
      window.history.replaceState({foo:'bar'}, "page 2", "bar.html");
      console.log(window.history.length);
    

上述前后两次输出的window.history.length是相等的。

此外。

每次触发history.back()或者浏览器的后退按钮等,会触发一个popstate事件,这个事件在后退或者前进的时候发生:

window.onpopstate=function(event){

}

注意:
history.pushState和history.replaceState方法并不会触发popstate事件。

如果用history做为路由的基础,那么需要用到的是history.pushState和history.replaceState,在不刷新的情况下可以改变url的地址,且如果页面发生回退back或者forward时,会触发popstate事件。

hisory为依据来实现路由的优点:

  • 对搜索引擎友好
  • 方便统计用户行为

缺点:

  • 兼容性不如hash
  • 需要后端做相应的配置,否则直接访问子页面会出现404错误

三、React-router4.0的使用

了解了前端路由实现的原理之后,下面来介绍一下React-router4.0。在React-router4.0的代码库中,根据使用场景包含了以下几个独立的包:

  • react-router : react-router4.0的核心代码
  • react-router-dom : 构建网页应用,存在DOM对象场景下的核心包
  • react-router-native : 适用于构建react-native应用
  • react-router-config : 配置静态路由
  • react-router-redux : 结合redux来配置路由,已废弃,不推荐使用。

在react-router4.0中,遵循Just Component的设计理念:

所提供的API都是以组件的形式给出。

比如BrowserRouter、Router、Link、Switch等API都是以组件的形式来使用。

1、React-router-dom常用的组件API

下面我们以React-router4.0中的React-router-dom包来介绍常用的BrowserRouter、HashRouter、Link和Router等。

(1) <BrowserRouter>

用<BrowserRouter> 组件包裹整个App系统后,就是通过html5的history来实现无刷新条件下的前端路由。

<BrowserRouter>组件具有以下几个属性:

  • basename: string 这个属性,是为当前的url再增加名为basename的值的子目录。

      <BrowserRouter basename="test"/>
    

如果设置了basename属性,那么此时的:

http://localhost:3000http://localhost:3000/test 表示的是同一个地址,渲染的内容相同。

  • getUserConfirmation: func 这个属性,用于确认导航的功能。默认使用window.confirm

  • forceRefresh: bool 默认为false,表示改变路由的时候页面不会重新刷新,如果当前浏览器不支持history,那么当forceRefresh设置为true的时候,此时每次去改变url都会重新刷新整个页面。

  • keyLength: number 表示location的key属性的长度,在react-router中每个url下都有为一个location与其对应,并且每一个url的location的key值都不相同,这个属性一般都使用默认值,设置的意义不大。

  • children: node children的属性必须是一个ReactNode节点,表示唯一渲染一个元素。

与<BrowserRouter>对应的是<HashRouter>,<HashRouter>使用url中的hash属性来保证不重新刷新的情况下同时渲染页面。

(2) <Route>

<Route> 组件十分重要,<Route> 做的事情就是匹配相应的location中的地址,匹配成功后渲染对应的组件。下面我们来看<Route>中的属性。

首先来看如何执行匹配,决定<Route>地址匹配的属性:

  • path:当location中的url改变后,会与Route中的path属性做匹配,path决定了与路由或者url相关的渲染效果。

  • exact: 如果有exact,只有url地址完全与path相同,才会匹配。如果没有exact属性,url的地址不完全相同,也会匹配。

举例来说,当exact不设置时:

<Route  path='/home' component={Home}/> 
<Route  path='/home/first' component={First}/> 

此时url地址为:http://localhost:3000/home/first 的时候,不仅仅会匹配到 path='/home/first'时的组件First,同时还会匹配到path='home'时候的Router。

如果设置了exact:

 <Route  path='/home' component={Home}/> 

只有http://localhost:3000/home/first 不会匹配Home组件,只有url地址完全与path相同,只有http://localhost:3000/home才能匹配Home组件成功。

  • strict :与exact不同,strict属性仅仅是对exact属性的一个补充,设置了strict属性后,严格限制了但斜线“/”。

举例来说,当不设置strict的时候:

 <Route  path='/home/' component={Home}/> 

此时http://localhost:3000/homehttp://localhost:3000/home/
都能匹配到组件Home。匹配对于斜线“/”比较宽松。如果设置了strict属性:

<Route  path='/home/' component={Home}/> 

那么此时严格匹配斜线是否存在,http://localhost:3000/home 将无法匹配到Home组件。

当Route组件与某一url匹配成功后,就会继续去渲染。那么什么属性决定去渲染哪个组件或者样式呢,Route的component、render、children决定渲染的内容。

  • component:该属性接受一个React组件,当url匹配成功,就会渲染该组件
  • render:func 该属性接受一个返回React Element的函数,当url匹配成功,渲染覆该返回的元素
  • children:与render相似,接受一个返回React Element的函数,但是不同点是,无论url与当前的Route的path匹配与否,children的内容始终会被渲染出来。

并且这3个属性所接受的方法或者组件,都会有location,match和history这3个参数。如果组件,那么组件的props中会存在从Link传递过来的location,match以及history。

(3) <Link>

<Route>定义了匹配规则和渲染规则,而<Link> 决定的是如何在页面内改变url,从而与相应的<Route>匹配。<Link>类似于html中的a标签,此外<Link>在改变url的时候,可以将一些属性传递给匹配成功的Route,供相应的组件渲染的时候使用。

  • to: string
    to属性的值可以为一个字符串,跟html中的a标签的href一样,即使to属性的值是一个字符串,点击Link标签跳转从而匹配相应path的Route,也会将history,location,match这3个对象传递给Route所对应的组件的props中。

举例来说:

<Link to='/home'>Home</Link>

如上所示,当to接受一个string,跳转到url为'/home'所匹配的Route,并渲染其关联的组件内接受3个对象history,location,match。
这3个对象会在下一小节会详细介绍。

  • to: object
    to属性的值也可以是一个对象,该对象可以包含一下几个属性:pathname、seacth、hash和state,其中前3个参数与如何改变url有关,最后一个state参数是给相应的改变url时,传递一个对象参数。

举例来说:

 <Link to={{pathname:'/home',search:'?sort=name',hash:'#edit',state:{a:1}}}>Home</Link>

在上个例子中,to为一个对象,点击Link标签跳转后,改变后的url为:'/home?sort=name#edit'。 但是在与相应的Route匹配时,只匹配path为'/home'的组件,'/home?sort=name#edit'。在'/home'后所带的参数不作为匹配标准,仅仅是做为参数传递到所匹配到的组件中,此外,state={a:1}也同样做为参数传递到新渲染的组件中。

(4) React-router中传递给组件props的history对象

介绍了 <BrowserRouter> 、 <Route> 和 <Link> 之后,使用这3个组件API就可以构建一个简单的React-router应用。这里我们之前说,每当点击Link标签跳转或者在js中使用React-router的方法跳转,从当前渲染的组件,进入新组件。在新组件被渲染的时候,会接受一个从旧组件传递过来的参数。

我们前面提到,Route匹配到相应的改变后的url,会渲染新组件,该新组件中的props中有history、location、match3个对象属性,其中hisotry对象属性最为关键。

同样以下面的例子来说明:

<Link to={{pathname:'/home',search:'?sort=name',hash:'#edit',state:{a:1}}}>Home</Link>

<Route exact path='/home' component={Home}/>

我们使用了<BrowserRouter>,该组件利用了window.history对象,当点击Link标签跳转后,会渲染新的组件Home,我们可以在Home组件中输出props中的history:

// props中的history
action: "PUSH"
block: ƒ block()
createHref: ƒ createHref(location)
go: ƒ go(n)
goBack: ƒ goBack()
goForward: ƒ goForward()
length: 12
listen: ƒ listen(listener)
location: {pathname: "/home", search: "?sort=name", hash: "#edit", state: {…}, key: "uxs9r5"}
push: ƒ push(path, state)
replace: ƒ replace(path, state)

从上面的属性明细中:

  • push:f 这个方法用于在js中改变url,之前在Link组件中可以类似于HTML标签的形式改变url。push方法映射于window.history中的pushState方法。

  • replace: f 这个方法也是用于在js中改变url,replace方法映射于window.history中的replaceState方法。

  • block:f 这个方法也很有用,比如当用户离开当前页面的时候,给用户一个文字提示,就可以采用history.block("你确定要离开当前页吗?")这样的提示。

  • go / goBack / goForward

在组件props中history的go、goBack、goForward方法,分别window.history.go、window.history.back、window.history.forward对应。

  • action: "PUSH" || "POP"
    action这个属性左右很大,如果是通过Link标签或者在js中通过this.props.push方法来改变当前的url,那么在新组件中的action就是"PUSH",否则就是"POP".

action属性很有用,比如我们在做翻页动画的时候,前进的动画是SlideIn,后退的动画是SlideOut,我们可以根据组件中的action来判断采用何种动画:

function newComponent (props)=>{
   return (
     <ReactCSSTransitionGroup
          transitionAppear={true}
          transitionAppearTimeout={600}
          transitionEnterTimeout={600}
          transitionLeaveTimeout={200}
          transitionName={props.history.action==='PUSH'?'SlideIn':'SlideOut'}
         >
           <Component {...props}/>
    </ReactCSSTransitionGroup>
   )
}
  • location:object
    在新组件的location属性中,就记录了从就组件中传递过来的参数,从上面的例子中,我们看到此时的location的值为:

      hash: "#edit"
      key: "uxs9r5"
      pathname: "/home"
      search: "?sort=name"
      state: {a:1}
    

除了key这个用作唯一表示外,其他的属性都是我们从上一个Link标签中传递过来的参数。

四、React-router4.0源码分析

在第三节中我们介绍了React-router的大致使用方法,读一读React-router4.0的源码。

这里我们主要分析一下React-router4.0中是如何根据window.history来实现前端路由的,因此设计到的组件为BrowserRouter、Router、Route和Link

1、React-router中的history

从上一节的介绍中我们知道,点击Link标签传递给新渲染的组件的props中有一个history对象,这个对象的内容很丰富,比如:action、goBack、go、location、push和replace方法等。

React-router构建了一个History类,用于在window.history的基础上,构建属性更为丰富的实例。该History类实例化后具有action、goBack、location等等方法。

React-router中将这个新的History类的构建方法,独立成一个node包,包名为history。

npm install history -s 

可以通过上述方法来引入,我们来看看这个History类的实现。

const createBrowserHistory = (props = {}) => {
    const globalHistory = window.history;
    ......
    //默认props中属性的值
    const {
      forceRefresh = false,
      getUserConfirmation = getConfirmation,
      keyLength = 6,
      basename = '',
    } = props;
    const history = {
        length: globalHistory.length,
        action: "POP",
        location: initialLocation,
        createHref,
        push,
        replace,
        go,
        goBack,
        goForward,
        block,
        listen
    };                                         ---- (1)
    const basename = props.basename;   
    const canUseHistory = supportsHistory();   ----(2)
            
    const createKey = () =>Math.random().toString(36).substr(2, keyLength);    ----(3)
    
    const transitionManager = createTransitionManager();  ----(4)
    const setState = nextState => {
        Object.assign(history, nextState);
    
        history.length = globalHistory.length;
    
        transitionManager.notifyListeners(history.location, history.action);
    };                                      ----(5)
    
    const handlePopState = event => {
        handlePop(getDOMLocation(event.state));
    };
    const handlePop = location => {
    if (forceNextPop) {
      forceNextPop = false;
      setState();
    } else {
      const action = "POP";
      
      transitionManager.confirmTransitionTo(
            location,
            action,
            getUserConfirmation,
            ok => {
              if (ok) {
                setState({ action, location });
              } else {
                revertPop(location);
              }
            }
          );
        }
    };                                    ------(6)
    const initialLocation = getDOMLocation(getHistoryState());
    let allKeys = [initialLocation.key]; ------(7)
    
  
    // 与pop相对应,类似的push和replace方法
    const push ... replace ...            ------(8)
    
    return history                        ------ (9)
    
}
  • (1) 中指明了新的构建方法History所返回的history对象中所具有的属性。

  • (2)中的supportsHistory的方法判断当前的浏览器对于window.history的兼容性,具体方法如下:

     export const supportsHistory = () => {
       const ua = window.navigator.userAgent;
     
       if (
         (ua.indexOf("Android 2.") !== -1 || ua.indexOf("Android 4.0") !== -1) &&
         ua.indexOf("Mobile Safari") !== -1 &&
         ua.indexOf("Chrome") === -1 &&
         ua.indexOf("Windows Phone") === -1
       )
         return false;
     
       return window.history && "pushState" in window.history;
     };
    

从上述判别式我们可以看出,window.history在chrome、mobile safari和windows phone下是绝对支持的,但不支持安卓2.x以及安卓4.0

  • (3)中用于创建与history中每一个url记录相关联的指定位数的唯一标识key, 默认的keyLength为6位

  • (4)中 createTransitionManager方法,返回一个集成对象,对象中包含了关于history地址或者对象改变时候的监听函数等,具体代码如下:

       const createTransitionManager = () => {
           const setPrompt = nextPrompt => {
             
           };
     
           const confirmTransitionTo = (
             location,
             action,
             getUserConfirmation,
             callback
           ) => {
              if (typeof getUserConfirmation === "function") {
                   getUserConfirmation(result, callback);
                 } else {
                   callback(true);
                 }
               } 
           };
           
           
           let listeners = [];
           const appendListener = fn => {
             let isActive = true;
         
             const listener = (...args) => {
               if (isActive) fn(...args);
             };
         
             listeners.push(listener);
         
             return () => {
               isActive = false;
               listeners = listeners.filter(item => item !== listener);
             };
           };
         
           const notifyListeners = (...args) => {
             listeners.forEach(listener => listener(...args));
           };
         
           return {
             setPrompt,
             confirmTransitionTo,
             appendListener,
             notifyListeners
           };
    

};

setPrompt函数,用于设置url跳转时弹出的文字提示,confirmTransaction函数,会将当前生成新的history对象中的location,action,callback等参数,作用就是在回调的callback方法中,根据要求,改变传入的location和action对象。

接着我们看到有一个listeners数组,保存了一系列与url相关的监听事件数组,通过接下来的appendListener方法,可以往这个数组中增加事件,通过notifyListeners方法可以遍历执行listeners数组中的所有事件。

  • (5) setState方法,发生在history的url或者history的action发生改变的时候,此方法会更新history对象中的属性,同时会触发notifyListeners方法,传入当前的history.location和history.action。遍历并执行所有监听url改变的事件数组listeners。

  • (6)这个getDOMLocation方法就是根据当前在window.state中的值,生成新history的location属性对象,allKeys这是始终保持了在url改变时候的历史url相关联的key,保存在全局,allKeys在执行生“POP”或者“PUSH”、“Repalce”等会改变url的方法时,会保持一个实时的更新。

  • (7) handlePop方法,用于处理“POP”事件,我们知道在window.history中点击后退等会触发“POP”事件,这里也是一样,执行action为“POP”,当后退的时候就会触发该函数。

  • (8)中包含了与pop方法类似的,push和replace方法,push方法同样做的事情就是执行action为“PUSH”(“REPLACE”),该变allKeys数组中的值,唯一不同的是actio为“PUSH”的方法push是往allKeys数组中添加,而action为“REPLACE”的方法replace则是替换掉当前的元素。

  • (9)返回这个新生成的history对象。

2、React-router中Link组件

其实最难弄懂的是React-router中如何重新构建了一个history工厂函数,在第一小节中我们已经详细的介绍了history生成函数createBrowserHistory的源码,接着来看Link组件就很容易了。

首先Link组件类似于HTML中的a标签,目的也很简单,就是去主动触发改变url的方法,主动改变url的方法,从上述的history的介绍中可知为push和replace方法,因此Link组件的源码为:

class Link extends React.Component {

    
   handleClick = event => {
   ...

     const { history } = this.context.router;
     const { replace, to } = this.props;
     if (replace) {
       history.replace(replace);
     } else {
      history.push(to);
     }
   }
  };
  render(){
    const { replace, to, innerRef, ...props } = this.props;
     <a {...props} onClick={this.handleClick}/>
  }
}

上述代码很简单,从React的context API全局对象中拿到history,然后如果传递给Link组件的属性中有replace为true,则执行history.replace(to),to 是一个包含pathname的对象,如果传递给Link组件的replace属性为false,则执行history.push(to)方法。

3、React-router中Route组件

Route组件也很简单,其props中接受一个最主要的属性path,Route做的事情只有一件:

当url改变的时候,将path属性与改变后的url做对比,如果匹配成功,则渲染该组件的componet或者children属性所赋值的那个组件。

具体源码如下:

class Route extends React.Component {


  ....
  constructor(){
  
  
  }
  render() {
    const { match } = this.state;
    const { children, component, render } = this.props;
    const { history, route, staticContext } = this.context.router;
    const location = this.props.location || route.location;
    const props = { match, location, history, staticContext };

    if (component) return match ? React.createElement(component, props) : null;

    if (render) return match ? render(props) : null;

    if (typeof children === "function") return children(props);

    if (children && !isEmptyChildren(children))
      return React.Children.only(children);

    return null;
  }

}

state中的match就是是否匹配的标记,如果匹配当前的Route的path,那么根据优先级顺序component属性、render属性和children属性来渲染其所指向的React组件。

4、React-router中Router组件

Router组件中,是BrowserRouter、HashRouter等组件的底层组件。该组件中,定义了包含匹配规则match函数,以及使用了新history中的listener方法,来监听url的改变,从而,当url改变时,更改Router下不同path组件的isMatch结果。

class Router extends React.Component {
    componentWillMount() {
        const { children, history } = this.props
        
        //调用history.listen监听方法,该方法的返回函数是一个移除监听的函数
        
        this.unlisten = history.listen(() => {
          this.setState({
            match: this.computeMatch(history.location.pathname)
          });
        });
    }
    componentWillUnmount() {
      this.unlisten();
    }
    render() {
    
    }
}

上述首先在组件创建前调用了listener监听方法,来监听url的改变,实时的更新isMatch的结果。

5、总结

本文从前端路由的原理出发,先后介绍了两种前端路由常用的方法,接着介绍了React-router的基本组件API以及用法,详细介绍了React-router的组件中新构建的history对象,最后结合React-router的API阅读了一下React-router的源码。

你真的懂switch吗?聊聊switch语句中的块级作用域

你真的懂switch吗?聊聊switch语句中的块级作用域


  最近在代码中不小心不规范的,在switch里面定义了块级变量,导致页面在某些浏览器中出错,本文讨论以下switch语句中的块级作用域。

  • switch语句中的块级作用域
  • switch语句中的块级作用域可能存在的问题
  • 规范和检测

一、switch语句中的块级作用域

  ES6 或 TS 引入了块级作用域,通过let和const、class等可以定义块级作用域里的变量,块级作用域内的变量不存在变量提升,且存在暂时性死区。常见的if语句,for循环的循环体内都可以定义块级变量。那么switch语句中的块级作用域是什么呢? 先给出结论:

switch语句中的块级作用域,在整个switch语句中,而不是对于每一个case生成一个独立的块级作用域。

下面来举几个例子来说明这个问题:

let number = 1;
switch(number){
  case 1:
    let name = 'Jony';
  default:
    console.log(name)
}

上述的代码会输出jony。

再看一个例子:

let number = 1;
switch(number){
  case 1:
    let name = 'Jony';
    break;
  case 2:
    let name = 'yu';
    break;
  default:
   console.log(name);
}

这样会在重复生命的错误:

Uncaught SyntaxError: Identifier 'name' has already been declared

上述两个例子说明确实switch语句中,整个switch语句构成一个块级作用域。而与case无关,每一个case并不会构成一个独立的块级作用域。

二、switch语句中的块级作用域可能存在的问题

  我们知道了switch语句,整个switch语句的顶层是一个块级作用域,但是还要注意case的特殊性,在case中声明的变量,并不会提升到块级作用域中。

let number = 2;
switch(number){
  case 2:
    name = 'yu';
    break;
}

在这个例子中,name虽然没有声明,但是给name赋值相当于给全局的window对象复制,也就是window.name = 'yu'。不会有任何问题。

有意思的问题来了:

let number = 2;
switch(number){
  case 1:
    let name = 'jony';
    break;
  case 2:
    name = 'yu';
    break;
}

这个例子中,会报错,会报name未定义的错误。

Uncaught ReferenceError: name is not defined

原因的话,这里虽然case里面定义的块级虽然不会存在变量提升,但是会存在暂时性死区,也就是说如果let name = 'jony' 没有执行,也就是name定义的过程没有执行,那么name在整个块级作用域内都是不可用的,都是undefined。

为了证明我们的想法,接着改写上面的例子:

let number = 1;
switch(number){
  case 1:
    let name = 'jony';
    break;
  case 2:
    name = 'yu';
    break;
}

我们把number改成1,我们发现代码不会报任何的错误,因为此时let name的定义和赋值都被执行了。

三、规范和检测

一、什么时候会出现问题

  可能会说为什么在自己的项目中,在ES6或者TS代码中即使有上述的错误使用,也没有报错?

  笔者之前也有这样的问题,要明确的是是否你把ES6或者TS的代码直接转化成了es5,然后再调试或者发布的线上的,当let被编译成es5后,当然就不会存在上述switch中作用域的问题。但是现实中,编译成es5后的js文件可能太大,对于高版本浏览器我们希望直接使用ES6代码(通过type = module来判断浏览器对于ES6的支持性),那么这么上述问题就会出现。

二、如何检测和规避

  那么如何避免这种情况呢,当然最好的方式,就是不要在case中定义块级变量,但是万一不小心写了上述的问题代码如何检测呢。

首先使用typescript,静态编译是不能出现错误提示的,因为这个错误是运行时异常。最好的方式是通过编写eslint的规范来解决上述的非法使用问题。

React内部原理,第二部分: componentWillMount and componentDidMount

React内部原理,第二部分: componentWillMount and componentDidMount


第一部分我们在Feact中实现了一个基础的渲染,实现了一个最重要的生命周期函数render,在本部分中,我们会为组件添加两个生命周期函数componentWillMount和componentDidMount.

这一系列的文章包括:

  • 第一部分:基础渲染
  • 第二部分:componentWillMount and componentDidMount
  • 第三部分:基础更新
  • 第四部分:setState
  • 第三部分:transaction

修改createClass方法

在之间的createClass方法中,仅仅支持render方法,比如:

const Feact = {
    createClass(spec) {
        function Constructor(props) {
            this.props = props;
        }

        // 只使用了构造对象中的render属性,而没有用其他的属性
        Constructor.prototype.render = spec.render;

        return Constructor;
    }
}

我们仅仅需要一个简单的方式,就可以将spec中的其他方法添加到Constructor.prototype中,这样在createClass方式创建的组件不仅可以有render函数,还可以实现其他用户自定义的函数。

const Feact = {
    createClass(spec) {
        function Constructor(props) {
            this.props = props;
        }

        Constructor.prototype =
            Object.assign(Constructor.prototype, spec);

        return Constructor;
    }
    ...
}

聊聊Typescript中的设计模式——装饰器篇(decorators)

聊聊Typescript中的设计模式——装饰器篇(decorators)


  随着Typescript的普及,在KOA2和nestjs等nodejs框架中经常看到类似于java spring中注解的写法。本文从装饰模式出发,聊聊Typescipt中的装饰器和注解。

  • 什么是装饰者模式
  • Typescript中的装饰器
  • Typescript中的注解
  • 总结

一、什么是装饰者模式

  最近在看nestjs等支持Typescript的node框架,经常看到这样一种写法:

import { Controller, Get } from '@nestjs/common';

@Controller('cats')

export class CatsController {
  @Get()
  findAll() {
    return 'This action returns all cats';
  }
}

  上述代码定义了一个处理url为“/cats”的控制器,该控制器对于url为“/cats”的get方法执行findAll()函数,返回相应的字符串。

  在上述的代码中,用@controller('cats')修饰CatsController类,通过@get来修饰类中的findAll方法,这就是典型的装饰者模式。通过@controller('cats')和@get修饰后的类CatsController,简单来说,就是拥有了丰富的“内涵”。

下面看看具体装饰者模式的定义:

我们知道继承模式是丰富子元素“内涵”的一种重要方式,不管是继承接口还是子类继承基类。而装饰者模式可以在不改变继承关系的前提下,包装先有的模块,使其内涵更加丰富,并不会影响到原来的功能。与继承相比,更加的灵活。

javascript中的装饰器处于建议征集的第二阶段,通过babel和Typescrit都可以实现装饰器的语法。

二、Typescript中的装饰器

Typescript中的装饰器与类相关,分别可以修饰类的实例函数和静态函数、类本身、类的属性、类中函数的参数以及类的set/get存取器,下面来意义介绍。

(1)、类方法的装饰器

下面来介绍一下用装饰器来修饰函数,首先来看一个例子:

let temple;
function log(target, key, descriptor) {
  console.log(`${key} was called!`);
  temple = target;
}
class P {
    @log
    foo() {
      console.log('Do something');
    }
}

console.log(P.prototype === temple) //true

上述是实例方法foo中我们用log函数修饰,log函数接受三个参数,通过P.prototype === temple(target)可以判断,在类的实例函数的装饰器函数第一个参数为类的原型,第二个参数为函数名本身,第三个参数为该函数的描述属性。

具体总结如下,对于类的函数的装饰器函数,依次接受的参数为:

  • target:如果修饰的是类的实例函数,那么target就是类的原型。如果修饰的是类的静态函数,那么target就是类本身。
  • key: 该函数的函数名。
  • descriptor:该函数的描述属性,比如 configurable、value、enumerable等。

从上述的例子中我们可以看到,用装饰器来修饰相应的类的函数十分方便:

@log
foo() {
  ...
}

(2)、类的装饰器

装饰函数也可以直接修饰类:

let temple
function foo(target){
   console.log(target);
   temple = target
}
@foo
class P{
   constructor(){
     
   }
}

temple === P //true

当装饰函数直接修饰类的时候,装饰函数接受唯一的参数,这个参数就是该被修饰类本身。上述的例子中,输出的target就是类P的本身。

此外,在修饰类的时候,如果装饰函数有返回值,该返回值会重新定义这个类,也就是说当装饰函数有返回值时,其实是生成了一个新类,该新类通过返回值来定义。

举例来说:

function foo(target){
   return class extends target{
      name = 'Jony';
      sayHello(){
         console.log("Hello "+ this.name)
      }
   }
}
@foo
class P{
   constructor(){
     
   }
}

const p = new P();
p.sayHello(); // 会输出Hello Jony

上面的例子可以看到,当装饰函数foo有返回值时,实际上P类已经被返回值所代表的新类所代替,因此P的实例p拥有sayHello方法。

(3)、类的属性的装饰器

下面我们来看类的属性的装饰器,装饰函数修饰类的属性时,在类实例化的时候调用属性的装饰函数,举例来说:

function foo(target,name){
   console.log("target is",target);
   console.log("name is",name)
}
class P{
   @foo
   name = 'Jony'
}
//会依次输出 target is f P()  name is Jony

这里对于类的属性的装饰器函数接受两个参数,对于静态属性而言,第一个参数是类本身,对于实例属性而言,第一个参数是类的原型,第二个参数是指属性的名字。

(4)、类函数参数的装饰器

接着来看类函数参数的装饰器,类函数的参数装饰器可以修饰类的构建函数中的参数,以及类中其他普通函数中的参数。该装饰器在类的方法被调用的时候执行,下面来看实例:

function foo(target,key,index){
   console.log("target is",target);
   console.log("key is",key);
   console.log("index is",index)
}
class P{
   test(@foo a){
   }
}
// 依次输出 f P() , test , 0 

类函数参数的装饰器函数接受三个参数,依次为类本身,类中该被修饰的函数本身,以及被修饰的参数在参数列表中的索引值。上述的例子中,会依次输出 f P() 、test和0。再次明确一下修饰函数参数的装饰器函数中的参数含义:

  • target: 类本身
  • key:该参数所在的函数的函数名
  • index: 该参数在函数参数列表中的索引值

从上面的Typescrit中在基类中常用的装饰器后,我们发现:

装饰器可以起到分离复杂逻辑的功能,且使用上极其简单方便。与继承相比,也更加灵活,可以从装饰类,到装饰类函数的参数,可以说武装到了“牙齿”。

三、Typescript中的注解

在了解了Typescrit中的装饰器之后,接着我们来看Typescrit中的注解。

什么是注解,所谓注解的定义就是:

为相应的类附加元数据支持

所谓元数据可以简单的解释,就是修饰数据的数据,比如一个人有name,age等数据属性,那么name和age这些字段就是为了修饰数据的数据,可以简单的称为元数据。

元数据简单来说就是可以修饰某些数据的字段。下面给出装饰器和注解的解释和区别:

  • 装饰器:定义劫持,可以对类,类的方法,类的属性以及类的方法的入参进行修改。不提供元数据的支持。
  • 注解:仅提供元数据的支持。

两者之间的联系:

通过注解添加元数据,然后在装饰器中获取这些元数据,完成对类、类的方法等等的修改,可以在装饰器中添加元数据的支持,比如可以可以在装饰器工厂函数以及装饰器函数中添加元数据支持等

(1)、Typescript中的元数据操作

可以通过reflect-metadata包来实现对于元数据的操作。首先我们来看reflect-metadata的使用,首先定义使用元数据的函数:

const formatMetadataKey = Symbol("format");

function format(formatString: string) {
    return Reflect.metadata(formatMetadataKey, formatString);
}

function getFormat(target: any, propertyKey: string) {
    return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}

这里的format可以作为装饰器函数的工厂函数,因为format函数返回的是一个装饰器函数,上述的方法定义了元数据Sysmbol("format"),用Sysmbol的原因是为了防止元数据中的字段重复,而format定义了取元数据中相应字段的功能。

接着我们来在类中使用相应的元数据:

class Greeter {
    @format("Hello, %s")
    name: string;

    constructor(name: string) {
        this.name = message;
    }
    sayHello() {
        let formatString = getFormat(this, "name");
        return formatString.replace("%s", this.name);
    }
}

const g = new Greeter("Jony");
console.log(g.sayHello());

在上述中,我们在name属性的装饰器工厂函数,执行@Format("Hello, %s"),返回一个装饰器函数,且该装饰器函数修饰了Greeter类的name属性,将“name”属性的值写入为"Hello, %s"。

然后再sayHello方法中,通过getFormat(this,"name")取到formatString为“Hello,%s”.

四、总结

通过装饰器,可以方便的修饰类,以及类的方法,类的属性等,相比于继承而言更加灵活,此外,通过注解的方法,可以在Typescript中引入元数据,实现元编程等。特别是在angularjs、nestjs中,大量使用了注解,特别是nestjs构建了类似于java springMVC式的web框架。

golang学习笔记(二)—— 深入golang中的协程


小白一枚,最近在研究golang,记录自己学习过程中的一些笔记,以及自己的理解。

  • go中协程的实现
  • go中协程的sync同步锁
  • go中信道channel
  • go中的range
  • go中的select切换协程
  • go中带缓存的channel
  • go中协程调度

介绍go中的协程之前,首先看以下go中的defer函数,defer函数不是普通的函数,defer函数会在普通函数返回之后执行。defer函数中可以释放函数内部变量、关闭数据库连接等等操作,举例来说:

func print(){
  fmt.Println(2);
}
func main() {
  defer print();
  fmt.Println(1);
}

上述的例子中先输出1后输出2,说明defer确实是在普通函数调用结束之后执行的。

go中使用协程的方式来处理并发,协程可以理解成更小的线程,占用空间小且线程上下文切换的成本少。

可以再为具体的描述以下协程的好处,协程比线程更加轻量,使用4K栈的内存就可以创建它们,可以用很小的内存占用就可以处理大量的任务。

在go中,携程是通过go关键字来调用,从关键字可以看出,golang的一个十分重要的特点就是协程,有句话叫“协程在手,说go就go”。

1、go中协程的实现

下面我们来看一个例子:

func printOne(){
  fmt.Println(1);
}
func printTwo(){
  fmt.Println(2);
}
func printThree(){
  fmt.Println(3);
}

func main() {
  go printOne();
  go printTwo();
  go printThree();
}

执行上述的main函数,我们发现并没有像我们想的那样输出有123的输出,原因在于虽然协程是并发的,但是如果在协程调用前退出了调用协程的函数后,协程会随着程序的消亡而消亡。

因此我们可以在main函数中,将主函数挂起,增加等待协程调用的事件。

func main() {
  go printOne();
  go printTwo();
  go printThree();
  time.Sleep(5 * 1e9);
}

这样会有相应的go关键字修饰的协程函数的调用。我们来看分别执行3次的结果。

  • 第一次
    1
    3
    2
  • 第二次
    3
    2
    1
  • 第三次
    3
    1
    2

我们发现因为协程是并发执行的,我们无法确定其调用的顺序,因此 每次的调用主函数的返回结果都是不确定的。

从协程的上述例子中,我们可以看出使用协程的时候必须还要考虑两个问题:

  • 如何控制协程的调用顺序,特别是当不同的协程同时访问同一个资源。
  • 如何实现不同协程间的通信

问题1,可以通过sync的同步锁来实现,问题2,go中提供了channel来实现不同协程间的通信。

2、go中协程的sync同步锁

go中sync包提供了2个锁,互斥锁sync.Mutex和读写锁sync.RWMutex.我们用互斥锁来解决上述的不同的协程可能同时调度同一个资源的问题,改写上述的例子:

func printOne(m *sync.Mutex){
  m.Lock();
  ... do something use DB or other source
  defer m.Unlock();
}

func printTwo(m *sync.Mutex){
  m.Lock();
  ... the same thing as printOne do something use DB or other source 
  defer m.Unlock();
}

func main() {
  m:= new(sync.Mutex);
  go printOne(m);
  go printTwo(m);
  time.Sleep(5 * 1e9);
}

通过互斥锁,printOne和printTwo不会竞争同一个相同的资源

3、go中信道channel

go中有一种特殊的类型通道channel,可以通过channel来发送类型化的数据,实现在协程之间的通信,通过通道的通信方式也保证了同步性。

channel的声明方式很简单:

var ch1 chan string
ch1 = make(chan string)

我们用ch表示通道,通道的符号包括了流向通道(发送): ch <- int1 和从通道流出(接收) int2 = <- ch。

同时go中也支持声明单向通道:

var ch1 chan int //普通的channel
var ch2 chan <- int //只用于写int数据
var ch3 <- chan int //只用于读int数据

上述定义的都是不带缓存区,或者说长度为1的channel,这种channel的特点就是:

一旦有数据被放入channel,那么该数据必须被取走才能让另一条数据放入,这就是同步的channel,channel的发送者和接受者在同一时间只交流一条数据,然后必须等待另一边完成相应的发送和接受动作。

我们还是用上述的输出123的例子,用同步channel来实现同步的输出。

func printOne(cs chan int){
  fmt.Println(1);
  cs <- 1
}
func printTwo(cs chan int){
  <-cs
  fmt.Println(2);
  defer close(cs);
}

func main() {
  cs := make(chan int);
  go printOne(cs);
  go printTwo(cs);
  time.Sleep(5 * 1e9);
}

上述的例子中会依次输出12,这样我们通过同步channel的方式实现了同步的输出。

我们前面讲到用为了等待go协程执行完成,我们在main函数中用time.sleep来挂起主函数,其实main函数本身也可以看成一个协程,如果使用channel,就不用在main函数中用time.sleep来挂起。

我们改写上述的例子:

func printOne(cs chan int){
  fmt.Println(1);
  cs <- 1
}
func main() {
  cs := make(chan int);
  go printOne(cs);
  <-cs;
  close(cs);
}

上述的例子中,会输出 1 ,我们并没有在主函数中通过time.sleep的方式来挂起,转而用一个等待写入的channel来代替。

注意:通道可以被显式的关闭,当需要告诉接受者不会种子提供新的值的时候,就需要关闭通道。

4、go中的range

上面我们也讲到要及时的关闭channel,但是持续的访问数据源并检查channel是否已经关闭,并不高效。go中提供了range关键字。

range关键字在使用channel的时候,会自动等待channel的动作一直到channel关闭。通俗点将就是可以channel可以自动开关。

同样的来举例:

func input(cs chan int,count int){
  for i:=1;i<=count;i++ {
    cs <- i
  }
}
func output(cs chan int){
  for s:= range cs {
    fmt.Println(s);
  }
}
func main() {
  cs := make(chan int);
  go input(cs,5);
  go output(cs);
  time.Sleep(3*1e9)
}

上述的例子会依次的输出1,2,3,4,5. 通过使用range关键字,当channel被关闭时,接受者的for循环也就自动停止了。

5、go中的select切换协程

从不同的并发执行过程中获取值可以通过关键字select来完成,它和switch控制语句非常相似,也被称为通信开关。

首先要明确select做了什么??

select中存在着一种轮询机制,select监听进入通道的数据,也可以是通道发送值的时候,监听到相应的行为后就执行case里面的操作。

select的声明:

select {
   case u:= <- ch1:
       ...
   case v:= <- ch2;
       ...

}

同样的来看一下具体使用select的例子:

func channel1(cs chan int,count int){
  for i:=1;i<=count;i++ {
    cs <- i
  }
}
func channel2(cs chan int,count int){
  for i:=1;i<=count;i++ {
    cs <- i
  }
}
func selectTest(cs1 ,cs2 chan int){
  for i:=1;i<10;i++ {
    select {
      case u:=<-cs1:
           fmt.Println(u);
      case v:=<-cs2:
           fmt.Println(v);
    }
  }
}
func main() {
  cs1 := make(chan int);
  cs2 := make(chan int);
  go channel1(cs1,5);
  go channel2(cs2,3);
  go selectTest(cs1,cs2);
  time.Sleep(3*1e9)
}

输出结果为:1,2,1,2,3,3,4,5 总共8个数据。且因为没有做同步控制,因此运行几次后的输出结果是不相同的。

6、go中带缓存的channel

前面讲到的都是不带缓存的channel或者说长度为1的channel,实际上channel也是可以带缓存的,我们可以在声明的时候执行channel的长度。

ch = make(chan string,3)

比如上述的例子中,指定了ch这个channel的长度为3,长度不为1的channel,就可以称之为带缓存的channel.

带缓存的channel可以连续写入,直到长度占满为止。

ch <- 1
ch <- 2 
ch <- 3

7、go中协程调度

讲到并发,就要提到go中的协程调度。go中的runtime包,提供了调度器的功能。runtime包提供了以下几个方法:

  • Gosched:让当前线程让出 cpu 以让其它线程运行,它不会挂起当前线程,因此当前线程未来会继续执行
  • NumCPU:返回当前系统的 CPU 核数量
  • GOMAXPROCS:设置最大的可同时使用的 CPU 核数
  • Goexit:退出当前 goroutine(但是defer语句会照常执行)
  • NumGoroutine:返回正在执行和排队的任务总数
  • GOOS:目标操作系统

对于多核CPU的机器,go可以显示的指定编译器将go的协程调度到多个CPU上运行

import "runtime"
...
cpuNum:=runtime.NumCPU;
runtime.GOMAXPROCS(cpuNum)

来聊聊GO中的调度原理,首先定义以下模型的概念:

M:内核中的线程的数目
G:go中的协程,并发的最小单元,在go中通过go关键字来创建
P:处理器,即协程G的上下文,每个P会维护一个本地的协程队列。

接着来看解释GO中协程调度的经典图:

1141545827812_ pic_hd

我们来解释上图:

  • P是处理器的个数,我们经常将调度器的GOMAXPROCS设置成CPU的个数,因此这里P一般来说是机器CPU的个数。
  • M是线程,在P处理器上关联一个线程,P和M的一组配对组成了局部的协程队列
  • G就是协程,需要被添加到由P和M组成的局部队列中依次处理
  • 除了局部的协程外,在全局还维护了一个协程队列。
  • 如果局部协程队列中处理完了所有队列,且没有新队列,那么M线程会取消对于CPU的占用,M线程进入休眠

从promise、process.nextTick、setTimeout出发,谈谈Event Loop中的Job queue

简要介绍:谈谈promise.resove,setTimeout,setImmediate,process.nextTick在EvenLoop队列中的执行顺序

1. 问题的引出

event loop都不陌生,是指主线程从“任务队列”中循环读取任务,比如

例1:

setTimeout(function(){console.log(1)},0);

console.log(2)

//输出2,1

在上述的例子中,我们明白首先执行主线程中的同步任务,当主线程任务执行完毕后,再从event loop中读取任务,因此先输出2,再输出1。

event loop读取任务的先后顺序,取决于任务队列(Job queue)中对于不同任务读取规则的限定。比如下面一个例子:

例2:

setTimeout(function () {
  console.log(3);
}, 0);

Promise.resolve().then(function () {
  console.log(2);
});
console.log(1);
//输出为  1  2 3

先输出1,没有问题,因为是同步任务在主线程中优先执行,这里的问题是setTimeout和Promise.then任务的执行优先级是如何定义的。

2 . Job queue中的执行顺序

在Job queue中的队列分为两种类型:macro-task和microTask。我们举例来看执行顺序的规定,我们设

macro-task队列包含任务: a1, a2 , a3
micro-task队列包含任务: b1, b2 , b3

执行顺序为,首先执行marco-task队列开头的任务,也就是 a1 任务,执行完毕后,在执行micro-task队列里的所有任务,也就是依次执行b1, b2 , b3,执行完后清空micro-task中的任务,接着执行marco-task中的第二个任务,依次循环。

了解完了macro-task和micro-task两种队列的执行顺序之后,我们接着来看,真实场景下这两种类型的队列里真正包含的任务(我们以node V8引擎为例),在node V8中,这两种类型的真实任务顺序如下所示:

macro-task队列真实包含任务:

script(主程序代码),setTimeout, setInterval, setImmediate, I/O, UI rendering

micro-task队列真实包含任务:
process.nextTick, Promises, Object.observe, MutationObserver

由此我们得到的执行顺序应该为:

script(主程序代码)—>process.nextTick—>Promises...——>setTimeout——>setInterval——>setImmediate——> I/O——>UI rendering

在ES6中macro-task队列又称为ScriptJobs,而micro-task又称PromiseJobs

3 . 真实环境中执行顺序的举例

(1) setTimeout和promise

例3:

setTimeout(function () {
  console.log(3);
}, 0);

Promise.resolve().then(function () {
  console.log(2);
});

console.log(1);

我们先以第1小节的例子为例,这里遵循的顺序为:

script(主程序代码)——>promise——>setTimeout
对应的输出依次为:1 ——>2————>3

(2) process.nextTick和promise、setTimeout

例子4:
setTimeout(function(){console.log(1)},0);

new Promise(function(resolve,reject){
   console.log(2);
   resolve();
}).then(function(){console.log(3)
}).then(function(){console.log(4)});

process.nextTick(function(){console.log(5)});

console.log(6);
//输出2,6,5,3,4,1

这个例子就比较复杂了,这里要注意的一点在定义promise的时候,promise构造部分是同步执行的,这样问题就迎刃而解了。

首先分析Job queue的执行顺序:

script(主程序代码)——>process.nextTick——>promise——>setTimeout

I) 主体部分: 定义promise的构造部分是同步的,
因此先输出2 ,主体部分再输出6(同步情况下,就是严格按照定义的先后顺序)

II)process.nextTick: 输出5

III)promise: 这里的promise部分,严格的说其实是promise.then部分,输出的是3,4

IV) setTimeout : 最后输出1

综合的执行顺序就是: 2——>6——>5——>3——>4——>1

(3)更复杂的例子

setTimeout(function(){console.log(1)},0);

new Promise(function(resolve,reject){
   console.log(2);
   setTimeout(function(){resolve()},0)
}).then(function(){console.log(3)
}).then(function(){console.log(4)});

process.nextTick(function(){console.log(5)});

console.log(6);

//输出的是  2 6 5 1 3 4

这种情况跟我们(2)中的例子,区别在于promise的构造中,没有同步的resolve,因此promise.then在当前的执行队列中是不存在的,只有promise从pending转移到resolve,才会有then方法,而这个resolve是在一个setTimout时间中完成的,因此3,4最后输出。

前端面试总结——进阶javascript篇

前端面试总结——进阶javascript篇

1.自己实现一个bind函数

原理:通过apply或者call方法来实现。

(1)初始版本

Function.prototype.bind=function(obj,arg){
  var arg=Array.prototype.slice.call(arguments,1);
  var context=this;
  return function(newArg){
    arg=arg.concat(Array.prototype.slice.call(newArg));
    return context.apply(obj,arg);
  }
}

(2) 考虑到原型链

为什么要考虑?因为在new 一个bind过生成的新函数的时候,必须的条件是要继承原函数的原型

Function.prototype.bind=function(obj,arg){
  var arg=Array.prototype.slice.call(arguments,1);
  var context=this;
  var bound=function(newArg){
    arg=arg.concat(Array.prototype.slice.call(newArg));
    return context.apply(obj,arg);
  }
  var F=function(){}
  //这里需要一个寄生组合继承
  F.prototype=context.prototype;
  bound.prototype=new F();
  return bound;
}

2.用setTimeout来实现setInterval

(1)用setTimeout()方法来模拟setInterval()与setInterval()之间的什么区别?

首先来看setInterval的缺陷,使用setInterval()创建的定时器确保了定时器代码规则地插入队列中。这个问题在于:如果定时器代码在代码再次添加到队列之前还没完成执行,结果就会导致定时器代码连续运行好几次。而之间没有间隔。不过幸运的是:javascript引擎足够聪明,能够避免这个问题。当且仅当没有该定时器的如何代码实例时,才会将定时器代码添加到队列中。这确保了定时器代码加入队列中最小的时间间隔为指定时间。

这种重复定时器的规则有两个问题:1.某些间隔会被跳过 2.多个定时器的代码执行时间可能会比预期小。

下面举例子说明:

假设,某个onclick事件处理程序使用啦setInterval()来设置了一个200ms的重复定时器。如果事件处理程序花了300ms多一点的时间完成。

2018-07-10 11 36 43

这个例子中的第一个定时器是在205ms处添加到队列中,但是要过300ms才能执行。在405ms又添加了一个副本。在一个间隔,605ms处,第一个定时器代码还在执行中,而且队列中已经有了一个定时器实例,结果是605ms的定时器代码不会添加到队列中。结果是在5ms处添加的定时器代码执行结束后,405处的代码立即执行。

function say(){
  //something
  setTimeout(say,200);
}
setTimeout(say,200)

或者

setTimeout(function(){
   //do something
   setTimeout(arguments.callee,200);
},200);

3.js怎么控制一次加载一张图片,加载完后再加载下一张

(1)方法1

<script type="text/javascript">
var obj=new Image();
obj.src="http://www.phpernote.com/uploadfiles/editor/201107240502201179.jpg";
obj.onload=function(){
alert('图片的宽度为:'+obj.width+';图片的高度为:'+obj.height);
document.getElementById("mypic").innnerHTML="<img src='"+this.src+"' />";
}
</script>
<div id="mypic">onloading……</div>

(2)方法2

<script type="text/javascript">
var obj=new Image();
obj.src="http://www.phpernote.com/uploadfiles/editor/201107240502201179.jpg";
obj.onreadystatechange=function(){
if(this.readyState=="complete"){
alert('图片的宽度为:'+obj.width+';图片的高度为:'+obj.height);
document.getElementById("mypic").innnerHTML="<img src='"+this.src+"' />";
}
}
</script>
<div id="mypic">onloading……</div>

3.代码的执行顺序

setTimeout(function(){console.log(1)},0);
new Promise(function(resolve,reject){
   console.log(2);
   resolve();
}).then(function(){console.log(3)
}).then(function(){console.log(4)});

process.nextTick(function(){console.log(5)});

console.log(6);
//输出2,6,5,3,4,1

为什么呢?具体请参考我的文章:
从promise、process.nextTick、setTimeout出发,谈谈Event Loop中的Job queue

4.如何实现sleep的效果(es5或者es6)

(1)while循环的方式

function sleep(ms){
   var start=Date.now(),expire=start+ms;
   while(Date.now()<expire);
   console.log('1111');
   return;
}

执行sleep(1000)之后,休眠了1000ms之后输出了1111。上述循环的方式缺点很明显,容易造成死循环。

(2)通过promise来实现

function sleep(ms){
  var temple=new Promise(
  (resolve)=>{
  console.log(111);setTimeout(resolve,ms)
  });
  return temple
}
sleep(500).then(function(){
   //console.log(222)
})
//先输出了111,延迟500ms后输出222

(3)通过async封装

function sleep(ms){
  return new Promise((resolve)=>setTimeout(resolve,ms));
}
async function test(){
  var temple=await sleep(1000);
  console.log(1111)
  return temple
}
test();
//延迟1000ms输出了1111

####(4).通过generate来实现

function* sleep(ms){
   yield new Promise(function(resolve,reject){
             console.log(111);
             setTimeout(resolve,ms);
        })  
}
sleep(500).next().value.then(function(){console.log(2222)})

5.简单的实现一个promise

首先明确什么是promiseA+规范,参考规范的地址:

primiseA+规范

如何实现一个promise,参考我的文章:

实现一个完美符合Promise/A+规范的Promise

一般不会问的很详细,只要能写出上述文章中的v1.0版本的简单promise即可。

6.Function._proto_(getPrototypeOf)是什么?

获取一个对象的原型,在chrome中可以通过__proto__的形式,或者在ES6中可以通过Object.getPrototypeOf的形式。

那么Function.proto是什么么?也就是说Function由什么对象继承而来,我们来做如下判别。

Function.__proto__==Object.prototype //false
Function.__proto__==Function.prototype//true

我们发现Function的原型也是Function。

我们用图可以来明确这个关系:

2018-07-10 2 38 27

7.实现js中所有对象的深度克隆(包装对象,Date对象,正则对象)

通过递归可以简单实现对象的深度克隆,但是这种方法不管是ES6还是ES5实现,都有同样的缺陷,就是只能实现特定的object的深度复制(比如数组和函数),不能实现包装对象Number,String , Boolean,以及Date对象,RegExp对象的复制。

(1)前文的方法

function deepClone(obj){
    var newObj= obj instanceof Array?[]:{};
    for(var i in obj){
       newObj[i]=typeof obj[i]=='object'?  
       deepClone(obj[i]):obj[i];    
    }
    return newObj;
}

这种方法可以实现一般对象和数组对象的克隆,比如:

var arr=[1,2,3];
var newArr=deepClone(arr);
// newArr->[1,2,3]

var obj={
   x:1,
   y:2
}
var newObj=deepClone(obj);
// newObj={x:1,y:2}

但是不能实现例如包装对象Number,String,Boolean,以及正则对象RegExp和Date对象的克隆,比如:

//Number包装对象
var num=new Number(1);
typeof num // "object"

var newNum=deepClone(num);
//newNum ->  {} 空对象

//String包装对象
var str=new String("hello");
typeof str //"object"

var newStr=deepClone(str);
//newStr->  {0:'h',1:'e',2:'l',3:'l',4:'o'};

//Boolean包装对象
var bol=new Boolean(true);
typeof bol //"object"

var newBol=deepClone(bol);
// newBol ->{} 空对象

....

(2)valueof()函数

所有对象都有valueOf方法,valueOf方法对于:如果存在任意原始值,它就默认将对象转换为表示它的原始值。对象是复合值,而且大多数对象无法真正表示为一个原始值,因此默认的valueOf()方法简单地返回对象本身,而不是返回一个原始值。数组、函数和正则表达式简单地继承了这个默认方法,调用这些类型的实例的valueOf()方法只是简单返回这个对象本身。

对于原始值或者包装类:

function baseClone(base){
 return base.valueOf();
}

//Number
var num=new Number(1);
var newNum=baseClone(num);
//newNum->1

//String
var str=new String('hello');
var newStr=baseClone(str);
// newStr->"hello"

//Boolean
var bol=new Boolean(true);
var newBol=baseClone(bol);
//newBol-> true

其实对于包装类,完全可以用=号来进行克隆,其实没有深度克隆一说,

这里用valueOf实现,语法上比较符合规范。

对于Date类型:

因为valueOf方法,日期类定义的valueOf()方法会返回它的一个内部表示:1970年1月1日以来的毫秒数.因此我们可以在Date的原型上定义克隆的方法:

Date.prototype.clone=function(){
  return new Date(this.valueOf());
}

var date=new Date('2010');
var newDate=date.clone();
// newDate->  Fri Jan 01 2010 08:00:00 GMT+0800 

对于正则对象RegExp:

RegExp.prototype.clone = function() {
var pattern = this.valueOf();
var flags = '';
flags += pattern.global ? 'g' : '';
flags += pattern.ignoreCase ? 'i' : '';
flags += pattern.multiline ? 'm' : '';
return new RegExp(pattern.source, flags);
};

var reg=new RegExp('/111/');
var newReg=reg.clone();
//newReg->  /\/111\//

8.简单实现Node的Events模块

简介:观察者模式或者说订阅模式,它定义了对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知。

node中的Events模块就是通过观察者模式来实现的:

var events=require('events');
var eventEmitter=new events.EventEmitter();
eventEmitter.on('say',function(name){
    console.log('Hello',name);
})
eventEmitter.emit('say','Jony yu');

这样,eventEmitter发出say事件,通过On接收,并且输出结果,这就是一个订阅模式的实现,下面我们来简单的实现一个Events模块的EventEmitter。

(1)实现简单的Event模块的emit和on方法

function Events(){
this.on=function(eventName,callBack){
  if(!this.handles){
    this.handles={};
  }
  if(!this.handles[eventName]){
    this.handles[eventName]=[];
  }
  this.handles[eventName].push(callBack);
}
this.emit=function(eventName,obj){
   if(this.handles[eventName]){
     for(var i=0;o<this.handles[eventName].length;i++){
       this.handles[eventName][i](obj);
     }
   }
}
return this;
}

这样我们就定义了Events,现在我们可以开始来调用:

 var events=new Events();
 events.on('say',function(name){
    console.log('Hello',nama)
 });
 events.emit('say','Jony yu');
 //结果就是通过emit调用之后,输出了Jony yu

(2)每个对象是独立的

因为是通过new的方式,每次生成的对象都是不相同的,因此:

var event1=new Events();
var event2=new Events();
event1.on('say',function(){
    console.log('Jony event1');
});
event2.on('say',function(){
    console.log('Jony event2');
})
event1.emit('say');
event2.emit('say');
//event1、event2之间的事件监听互相不影响
//输出结果为'Jony event1' 'Jony event2'

9.箭头函数中this指向举例

var a=11;
function test2(){
  this.a=22;
  let b=()=>{console.log(this.a)}
  b();
}
var x=new test2();
//输出22

定义时绑定。

如何优雅的使用react hooks来进行状态管理

如何优雅的使用react hooks来进行状态管理


  在使用react和redux的过程中,一直有一个问题,哪些状态需要放在redux中,状态需要保存在组件内的local state中,此外不合理的使用redux可能会带来状态管理混乱的问题,此外对于local state局部状态而言,react hooks提供了一个比class中的setState更好的一个替代方案。本文主要从状态管理出发,讲讲如何优雅的使用hooks来进行状态管理。

  • 如何使用redux
  • react hooks管理local state
  • react hooks如何解决组件间的通信

一、如何使用redux

  首先要明确为什么要使用redux,这一点很重要,如果不知道为什么使用redux,那么在开发的过程中肯定不能合理的使用redux.首先来看redux的本质:

redux做为一款状态管理工具,主要是为了解决组件间通信的问题。

既然是组件间的通信问题,那么显然将所有页面的状态都放入redux中,是不合理的,复杂度也很高。

(1)全量使用redux

  笔者在早期也犯了这个问题,在应用中,不管什么状态,按页面级路由拆分,全部放在redux中,页面任何状态的更改,通过react-redux的mapState和mapDispatch来实现。

Lark20190919-153334

redux中的状态从状态更新到反馈到视图,是一个过程链太长,从dispatch一个action出发,然后走reducer等逻辑,一个完整的链路包含:

创建action,创建redux中间件,创建相应type的reducer函数,创建mapState和mapDispatch等。

如果将所有状态都保存在redux中,那么每一个状态必须走这几步流程,及其繁琐,毫无疑问增加了代码量

(2)减少局部状态和redux状态的不合理混用

  全量使用redux的复杂度很高,我们当然考虑将一部分状态放在redux中,一部分状态放在local state中,但是这种情况下,很容易产生一个问题,就是如果local State跟redux中的state存在状态依赖。

举例来说,在redux中的状态中有10个学生

    //redux
    
    students = [{name:"小明",score:70},{name:"小红",score:50}....]

在local state中我们保存了分数在60分以上的学生

    // local state
    
    state = [{name:"小明",score:70}]

  如果redux中的学生改变了,我们需要从redux中动态的获取students信息,然后改变局部的state.结合react-redux,我们需要在容器组件中使用componentWillReceivedProps或者getDerivedStateFromProps这个声明周期,来根据props改变局部的local state.

  componentWillReceivedProps这里不讨论,为了更高的安全性,在react中用静态的getDerivedStateFromProps代替了componentWillReceivedProps这里不讨论,而getDerivedStateFromProps这个声明周期函数在props和state变化的时候都会去执行,因此如果我们需要仅仅在props的改变而改变局部的local state,在这个声明周期中会存在着很复杂的判断逻辑。

redux中的状态和local state中的状态相关联的越多,getDerivedStateFromProps这个声明周期函数就越复杂

  给我们的启示就是尽可能的减少getDerivedStateFromProps的使用,如果实在是redux和local state有关联性,用id会比直接用对象或者数组好,比如上述的例子,我们可以将学生分组,并给一个组号,每次在redux中的学生信息发生改变的时候会改变相应的组号。
这样在getDerivedStateFromProps只需要判断组号是否改变即可:

    class Container extends React.Component{
      state = {
         group_id:number
      }
      
      static getDerivedStateFromProps(props,state){
         if(props.group_id!==state.group_id){
         
            ... 更新及格的学生
         }else{
            return null
         }
      }
    }

  这里推荐https://github.com/paularmstrong/normalizr,如果实在redux和local state关联性强,可以先将数据范式化,范式化后的数据类似于给一个复杂结构一个id,这样子会简化getDerivedStateFromProps的逻辑.

(3)本节小结

  如何使用redux,必须从redux的本质出发,redux的本质是为了解决组件间的通信问题,因此组件内部独有的状态不应该放在redux中,此外如果redux结合class类组件使用,可以将数据范式化,简化复杂的判断逻辑。

二、react hooks管理local state

  前面将了应该如何使用redux,那么如何维护local state呢,React16.8中正式增加了hooks。通过hooks管理local state,简单易用可扩展。

在hooks中的局部状态常见的有3种,分别是useState、useRef和useReducer

(1) useState

useState是hooks中最常见的局部状态,比如:

    const [hide, setHide] = React.useState(false);
    const [name, setName] = React.useState('BI');

理解useState必须明确,在react hooks中:

每一次渲染都有它自己的 Props and State

一个经典的例子就是:

    function Counter() {
      const [count, setCount] = useState(0);
    
      function handleAlertClick() {
        setTimeout(() => {
          alert('You clicked on: ' + count);
        }, 3000);
      }
    
      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>
            Click me
          </button>
          <button onClick={handleAlertClick}>
            Show alert
          </button>
        </div>
      );
    }

如果我按照下面的步骤去操作:

  • 点击增加counter到3
  • 点击一下 “Show alert”
  • 点击增加 counter到5并且在定时器回调触发前完成

猜猜看会alert出什么?
counter-46c55d5f1f749462b7a173f1e748e41e

结果是弹出了3,alert会“捕获”我点击按钮时候的状态,也就是说每一次的渲染都会有独立的props和state.

(2) useRef

  在react hooks中,我们知道了每一次的渲染都会有独立的props和state,那么如果我们需要跟类组件一样,每次都能拿到最新的渲染值时,应该怎么做呢?此时我们可以用useRef

useRef提供了一个Mutable可变的数据

我们来修改上述的例子,来是的alert为5:

    function Counter() {
        const [count, setCount] = useState(0)
        const late = useRef(0)
        function handleAlertClick() {
            setTimeout(() => {
                alert('You clicked on: ' + late.current)
            }, 3000)
        }
        useEffect(() => {
            late.current = count
        })
        return (
            <div>
                <p>You clicked {count} times</p>
                <button onClick={() => setCount(count + 1)}>Click me</button>
                <button onClick={handleAlertClick}>Show alert</button>
            </div>
        )
    }

如此修改以后就不是alert3 而是弹出5

(3) useReducer

  react hooks中也提供了useReducer来管理局部状态.

当你想更新一个状态,并且这个状态更新依赖于另一个状态的值时,你可能需要用useReducer去替换它们。

同样的用例子来说明:

    function Counter() {
      const [state, dispatch] = useReducer(reducer, initialState);
      const { count, step } = state;
    
      useEffect(() => {
        const id = setInterval(() => {
          dispatch({ type: 'tick' });
        }, 1000);
        return () => clearInterval(id);
      }, [dispatch]);
    
      return (
        <>
          <h1>{count}</h1>
          <input value={step} onChange={e => {
            dispatch({
              type: 'step',
              step: Number(e.target.value)
            });
          }} />
        </>
      );
    }
    
    const initialState = {
      count: 0,
      step: 1,
    };
    
    function reducer(state, action) {
      const { count, step } = state;
      if (action.type === 'tick') {
        return { count: count + step, step };
      } else if (action.type === 'step') {
        return { count, step: action.step };
      } else {
        throw new Error();
      }
    }

解释上面的结果主要来看useEffect部分:

    useEffect(() => {
        const id = setInterval(() => {
          dispatch({ type: 'tick' });
        }, 1000);
        return () => clearInterval(id);
      }, [dispatch]);

  在state中的count依赖与step,但是使用了useReducer后,我们不需要在useEffect的依赖变动数组中使用step,转而用dispatch来替代,这样的好处就是减少不必要的渲染行为.

  此外:局部状态不推荐使用 useReducer ,会导致函数内部状态过于复杂,难以阅读。 useReducer 建议在多组件间通信时,结合 useContext 一起使用。

三、react hooks如何解决组件间的通信

  react hooks中的局部状态管理相比于类组件而言更加简介,那么如果我们组件采用react hooks,那么如何解决组件间的通信问题。

(1) UseContext

  最基础的想法可能就是通过useContext来解决组件间的通信问题。

比如:

function useCounter() {
  let [count, setCount] = useState(0)
  let decrement = () => setCount(count - 1)
  let increment = () => setCount(count + 1)
  return { count, decrement, increment }
}

let Counter = createContext(null)

function CounterDisplay() {
  let counter = useContext(Counter)
  return (
    <div>
      <button onClick={counter.decrement}>-</button>
      <p>You clicked {counter.count} times</p>
      <button onClick={counter.increment}>+</button>
    </div>
  )
}

function App() {
  let counter = useCounter()
  return (
    <Counter.Provider value={counter}>
      <CounterDisplay />
      <CounterDisplay />
    </Counter.Provider>
  )
}

  在这个例子中通过createContext和useContext,可以在App的子组件CounterDisplay中使用context,从而实现一定意义上的组件通信。

此外,在useContext的基础上,为了其整体性,业界也有几个比较简单的封装:

https://github.com/jamiebuilds/unstated-next
https://github.com/diegohaz/constate

但是其本质都没有解决一个问题:

如果context太多,那么如何维护这些context

  也就是说在大量组件通信的场景下,用context进行组件通信代码的可读性很差。这个类组件的场景一致,context不是一个新的东西,虽然用了useContext减少了context的使用复杂度。

(2) Redux结合hooks来实现组件间的通信

  hooks组件间的通信,同样可以使用redux来实现。也就是说:

在React hooks中,redux也有其存在的意义

  在hooks中存在一个问题,因为不存在类似于react-redux中connect这个高阶组件,来传递mapState和mapDispatch, 解决的方式是通过redux-react-hook或者react-redux的7.1 hooks版本来使用。

  • redux-react-hook

  在redux-react-hook中提供了StoreContext、useDispatch和useMappedState来操作redux中的store,比如定义mapState和mapDispatch的方式为:

import {StoreContext} from 'redux-react-hook';

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

import {useDispatch, useMappedState} from 'redux-react-hook';

export function DeleteButton({index}) {
  // Declare your memoized mapState function
  const mapState = useCallback(
    state => ({
      canDelete: state.todos[index].canDelete,
      name: state.todos[index].name,
    }),
    [index],
  );

  // Get data from and subscribe to the store
  const {canDelete, name} = useMappedState(mapState);

  // Create actions
  const dispatch = useDispatch();
  const deleteTodo = useCallback(
    () =>
      dispatch({
        type: 'delete todo',
        index,
      }),
    [index],
  );

  return (
    <button disabled={!canDelete} onClick={deleteTodo}>
      Delete {name}
    </button>
  );
}
  • react-redux 7.1的hooks版

   这也是官方较为推荐的,react-redux 的hooks版本提供了useSelector()、useDispatch()、useStore()这3个主要方法,分别对应与mapState、mapDispatch以及直接拿到redux中store的实例.

简单介绍一下useSelector,在useSelector中除了能从store中拿到state以外,还支持深度比较的功能,如果相应的state前后没有改变,就不会去重新的计算.

举例来说,最基础的用法:

import React from 'react'
import { useSelector } from 'react-redux'

export const TodoListItem = props => {
  const todo = useSelector(state => state.todos[props.id])
  return <div>{todo.text}</div>
}

实现缓存功能的用法:

import React from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'

const selectNumOfDoneTodos = createSelector(
  state => state.todos,
  todos => todos.filter(todo => todo.isDone).length
)

export const DoneTodosCounter = () => {
  const NumOfDoneTodos = useSelector(selectNumOfDoneTodos)
  return <div>{NumOfDoneTodos}</div>
}

export const App = () => {
  return (
    <>
      <span>Number of done todos:</span>
      <DoneTodosCounter />
    </>
  )
}

在上述的缓存用法中,只要todos.filter(todo => todo.isDone).length不改变,就不会去重新计算.

四、总结

   react中完整的状态管理分为全局状态和局部状态,而react hooks简化了局部状态,使得管理局部状态以及控制局部渲染极其方便,但是react hooks本质上还是一个视图组件层的,并没有完美的解决组件间的通信问题,也就是说,redux等状态管理机和react hooks本质上并不矛盾。

  在我的实践中,用redux实现组件间的通信而react hooks来实现局部的状态管理,使得代码简单已读的同时,也减少了很多不必要的redux样板代码.

手把手教你实现json嵌套对象的范式化和反范式化

手把手教你实现json嵌套对象的范式化和反范式化


在json对象嵌套比较复杂的情况下,可以将复杂的嵌套对象转化成范式化的数据。比如后端返回的json对象比较复杂,前端需要从复杂的json对象中提取数据然后呈现在页面上,复杂的json嵌套,使得前端展示的逻辑比较混乱。

特别的,如果我们使用了flux或者redux等作为我们前端的状态管理机(state对象),通过控制state对象的变化,从而呈现不同的视图层的展示,如果我们在状态管理的时候,将state对象范式化,可以减小state对象操作的复杂性,从而可以清晰的展示视图更新的过程。

  • 什么是数据范式化和反范式化
  • 数据范式化的实现
  • jest编写简单的单元测试

本文的源码地址为:https://github.com/forthealllight/normalize


1.什么是数据范式化

(1)数据范式化的定义

本文不会具体介绍在数据库中关于范式的定义,广义的数据范式化,就是除了最外层属性之外,其他关联的属性用外键来引用。

数据范式化的好处有:可以减少数据的冗余

(2)数据范式化举例

比如有一个person对象如下所示:

{
  'id':1,
  'name':'xiaoliang',
  'age':20,
  'hobby':[{
    id:30,
    desp:'足球'
  },{
    id:40,
    desp:'篮球'
  },{
    id:50,
    desp:'羽毛球'
  }]
}

在上述的对象中,hobby存在嵌套,我们将perosn的无嵌套的其他属性作为主属性,而hobby属性表示的是需要外键来引用的属性,我们将id作为外键的名称,将上述的嵌套对象经过范式化处理可以得到:

{
  person:{
     '1':{
         'id':1,
         'name':'xiaoliang',
         'age':20,
         'hobby':['30','40','50']
     }
  },
  hobby:{
    '30':{
      id:'30',
      desp:'足球'
    },
    '40':{
      id:'40',
      desp:'篮球',
    },
    '50':{
      id:'50',
      desp:'羽毛球'
    }
  }
}

上述对象就是范式化之后的结果,我们发现主对象person里面的hobby属性中,此时变成了id号组成的数组,通过id作为外键来索引另一个对象hobby中的具体值。

(3)数据范式化的优点

那么这样做到底有什么好处呢?

比如我们现在新增了一个人id为2:

{
  'id':2,
  'name':'xiaoyu',
  'age':20,
  'hobby':[{
    id:30,
    desp:'足球'
  }]
}

他的兴趣还好中同样包含了足球,那么如果有复杂嵌套对象的形式,对象变成如下的形式:

  [
    {
      'id':1,
      'name':'xiaoliang',
      'age':20,
      'hobby':[{
        id:30,
        desp:'足球'
      },{
        id:40,
        desp:'篮球'
      },{
        id:50,
        desp:'羽毛球'
      }]
    },
    {
      'id':2,
      'name':'xiaoyu',
      'age':20,
      'hobby':[{
        id:30,
        desp:'足球'
      }]
    }
]

上述的这个对象嵌套层级就比较深,比如现在我们发现hobby中的足球的描述发生了变化,比如:

desp:'足球'——> desp:'英式足球'

如果在上述的嵌套对象中直接改变,我们需要改变两处位置,其一是id为1的person中的id为30的hobby的desp,另一处是id为2处的person的id为30处的hobby的desp.

这还是person只有2个实例的情况,如果person的实例更多,那么,如果仅仅一个hobby改变,就需要改变多处位置。也就显得操作比较冗余。

如果用数据范式化来处理,效果如何呢?,将上述的对象范式化得到:

{
  person:{
     '1':{
         'id':1,
         'name':'xiaoliang',
         'age':20,
         'hobby':['30','40','50']
     },
     '2':{
        'id':2,
        'name':'xiaoyu',
        'age':30,
        'hobby':[30]
     }
  },
  hobby:{
    '30':{
      id:'30',
      desp:'足球'
    },
    '40':{
      id:'40',
      desp:'篮球',
    },
    '50':{
      id:'50',
      desp:'羽毛球'
    }
  }
}

此时如果同样的发生了:

***desp:'足球'——>  desp:'英式足球'***

这样的变化,映射之后只需要改变,hobby被查询对象:

hobby:{
    '30':{
      id:'30',
      desp:'英式足球'
    },
    ......
}

这样,无论有多少实例引用了id为30的这个hobby,我们修改所引起的操作只需要一处就能到位。

(4)数据范式化的缺点

那么数据范式化有什么缺点呢?

一句话可以概括数据范式化的缺点:查询性能低下

从上述范式化后的数据可以看出:

person:{
 '1':{
     'id':1,
     'name':'xiaoliang',
     'age':20,
     'hobby':['30','40','50']
 },
 '2':{
    'id':2,
    'name':'xiaoyu',
    'age':30,
    'hobby':[30]
 }
}

在上述范式化的数据里,hobby是通过id来表示,如果要索引每个id的具体值和对象,比如要到上一层的“hobby”对象中去查询。而原始的嵌套对象可以很直观的展示出来,每一个id所对应的hobby对象是什么。

2.数据范式化的实现(此小节和之后的内容可以选读)

下面我们来尝试编写范式化(normalize)和反范式化的函数(denormalize).

函数名称 函数的具体表示
schema.Entity(name, [entityParams], [entityConfig]) --name为该schema的名称
--entityParams为可选参数, 定义该schema的外键,定义的外键可以不存在
--entityConfig为可选参数,目前仅支持一个参数 定义该entity的主键,默认值为字符串'id'
normalize(data, entity) -- data 需要范式化的数据,必须为符合schema定义的对象或由该类对象组成的数组
-- entity实例
denormalize (normalizedData, entity, entities) -- normalizedData
-- entity -同上
-- entities 同上

实现数据范式化和反范式化,主要是上面3个函数,下面我们来一一分析。

本文需要范式化的原始数据为:

const originalData = {
  "id": "123",
  "author":  {
    "uid": "1",
    "name": "Paul"
  },
  "title": "My awesome blog post",
  "comments": {
    total: 100,
    result: [{
        "id": "324",
        "commenter": {
        "uid": "2",
          "name": "Nicole"
        }
      }]
  }
}

(1)schema.Entity

范式化之前必须对嵌套对象进行处理,深层嵌套的情况下,需要用实体Entity进行解构,层级最深的实体需要首先被定义,然后一层层的解耦到最外层。

该实体的构造方法,接受3个参数,第一个参数name,表示范式化后的对象的属性的名称,第二个参数entityParams,表示实体化后,原始的嵌套对象和一定义的实体之间的一一对应关系,第三个参数表示的是
用来索引嵌套对象的主键,默认的情况下,我们用id来索引。

上述实例的实体化为:

const user = new schema.Entity('users', {}, {
  idAttribute: 'uid'
})
const comment = new schema.Entity('comments', {
  commenter: user
})
const article = new schema.Entity('articles', {
  author: user,
  comments: {
    result: [ comment ]
  }
});

实体化还是从最里层到最外层。并且第三个参数表示索引的主键。

如何实现构造方法呢?schema.Entity的实现代码为,首先定义一个类:

export default class EntitySchema {
  constructor (name, entityParams = {}, entityConfig = {}) {
    const idAttribute = entityConfig.idAttribute || 'id'
    this.name = name
    this.idAttribute = idAttribute
    this.init(entityParams)
  }
  /**
   * [获取当前schema的名字]
   * @return {[type]} [description]
   */
  getName () {
    return this.name
  }
  getId (input) {
    let key = this.idAttribute
    return input[key]
  }
  /**
   * [遍历当前schema中的entityParam,entityParam中可能存在schema]
   * @param  {[type]} entityParams [description]
   * @return {[type]}              [description]
   */
  init (entityParams) {
    if (!this.schema) {
      this.schema = {}
    }
    for (let key in entityParams) {
      if (entityParams.hasOwnProperty(key)) {
        this.schema[key] = entityParams[key]
      }
    }
  }
}

定义一个EntitySchema类,构造方法中,因为entityParams存在嵌套的情况,因此需要在init方法中遍历entityParams中的schema属性。此外为了定义了获取主键和name名的方法,getName和getId。

(2)normalize(data, entity)

上述就是范式化的函数,接受两个参数,第一个参数为原始的需要被范式化的数据,第二个参数为最外层的实体。同样在上述例子原始数据被范式化,可以通过如下方式来实现:

normalize(originData,articles)

上述的例子中,最外层的实体为articles。

那么如何实现该范式化,首先考虑到最外层的实体,可能存在嵌套,且最外层实体的对象的属性值不一定是一个schema实体,也可能是数组等结构,因此要分别处理schema实体和非schema实体的情况:

const flatten = (value, schema, addEntity) => {
  if (typeof schema.getName === 'undefined') {
    return noSchemaNormalize(schema, value, flatten, addEntity)
  }
  return schemaNormalize(schema, value, flatten, addEntity)
}

如果传入的是一个schema实体:

const schemaNormalize = (schema, data, flatten, addEntity) => {
  const processedEntity = {...data}
  const currentSchema = schema
  Object.keys(currentSchema.schema).forEach((key) => {
    const schema = currentSchema.schema[key]
    const temple = flatten(processedEntity[key], schema, addEntity)
    // console.log(key,temple);
    processedEntity[key] = temple
  })
  addEntity(currentSchema, processedEntity)
  return currentSchema.getId(data)
}

那么情况为递归该schema,直到从最外层的schema递归到最里层的schema.

如果传入的不是一个schema实体:

const noSchemaNormalize = (schema, data, flatten, addEntity) => {
  // 非schema实例要分别针对对象类型和数组类型做不同的处理
  const object = { ...data }
  const arr = []
  let tag = schema instanceof Array
  Object.keys(schema).forEach((key) => {
    if (tag) {
      const localSchema = schema[key]
      const value = flatten(data[key], localSchema, addEntity)
      arr.push(value)
    } else {
      const localSchema = schema[key]
      const value = flatten(data[key], localSchema, addEntity)
      object[key] = value
    }
  })
  // 根据判别的结果,返回不同的值,可以是对象,也可以是数组
  if (tag) {
    return arr
  } else {
    return object
  };
}

如果不是一个实体,那么分为是一个对象和是一个数组两种情况分别来处理。

最后有一个addEntity,递归到里层,再往外层,得到对应的schema的name所包含的id,和此id所指向的具体对象。

const addEntities = (entities) => (schema, processedEntity) => {
  const schemaKey = schema.getName()
  const id = schema.getId(processedEntity)
  if (!(schemaKey in entities)) {
    entities[schemaKey] = {}
  }
  const existingEntity = entities[schemaKey][id]
  if (existingEntity) {
    entities[schemaKey][id] = Object.assgin(existingEntity, processedEntity)
  } else {
    entities[schemaKey][id] = processedEntity
  }
}

最后我们的normalize方法具体为:

const normalize = (data, schema) => {
  const entities = {}
  const addEntity = addEntities(entities)

  const result = flatten(data, schema, addEntity)
  return { entities, result }
}

(3)denormalize反范式化方法

denormalize反范式化方法,接受3个参数,其中normalizedData 和entities表示范式化后的对象的属性,而entity表示最外层的实体。

调用的方式为:

const normalizedData = normalize(originalData, article);
// 还原范式化数据
const {result, entities} = normalizedData
const denormalizedData = denormalize(result, article, entities)

反范式化的具体代码与范式化相似,就不具体说明,详情请看源代码。

3. jest简单单元测试

直接给出简单的单元测试代码:

//范式化数据用例,原始数据
const originalData = {
  "id": "123",
  "author":  {
    "uid": "1",
    "name": "Paul"
  },
  "title": "My awesome blog post",
  "comments": {
    total: 100,
    result: [{
        "id": "324",
        "commenter": {
        "uid": "2",
          "name": "Nicole"
        }
      }]
  }
}
//范式化数据用例,范式化后的结果数据
const normalizedData={
  result: "123",
  entities: {
    "articles": {
      "123": {
        id: "123",
        author: "1",
        title: "My awesome blog post",
        comments: {
    	total: 100,
    	result: [ "324" ]
        }
      }
    },
    "users": {
      "1": { "uid": "1", "name": "Paul" },
      "2": { "uid": "2", "name": "Nicole" }
    },
    "comments": {
      "324": { id: "324", "commenter": "2" }
    }
 }
}
//开始测试上述用例下的,范式化结果对比
test('test originalData to normalizedData', () => {
  const user = new schema.Entity('users', {}, {
    idAttribute: 'uid'
  });
  const comment = new schema.Entity('comments', {
    commenter: user
  });
  const article = new schema.Entity('articles', {
    author: user,
    comments: {
      result: [ comment ]
    }
  });
  const data = normalize(originalData, article);
  expect(data).toEqual(normalizedData);
});
//开始测试上述例子,反范式化的结果对比
test('test normalizedData to originalData',()=>{
  const user = new schema.Entity('users', {}, {
    idAttribute: 'uid'
  });
  // Define your comments schema
  const comment = new schema.Entity('comments', {
    commenter: user
  });
  // Define your article
  const article = new schema.Entity('articles', {
    author: user,
    comments: {
      result: [ comment ]
    }
  });
  const data = normalize(originalData, article)
  //还原范式化数据
  const {result,entities}=data;
  const denormalizedData=denormalize(result,article,entities);
  expect(denormalizedData).toEqual(originalData)
})

react-tiny-virtual-list源码阅读

React-tiny-virtual-list源码阅读


  在上一章介绍了如何在React,通过虚拟列表的形式优化长列表。介绍了虚拟列表的原理,以及比较常用的优化长列表的组件库React-virtualized和React-tiny-virtual-list。本文中来读一读React-tiny-virtual-list源码。

  • 虚拟列表的原理简介
  • React-tiny-virtual-list组件的使用
  • React-tiny-virtual-list的源码分析
  • React-tiny-virtual-list的总结

一、虚拟列表原理简介

优化长列表的原理很简单,基本原理可以一句话概括:

用数组保存所有列表元素的位置,只渲染可视区内的列表元素,当可视区滚动时,根据滚动的offset大小以及所有列表元素的位置,计算在可视区应该渲染哪些元素。

具体实现步骤如下所示:

  1. 首先确定长列表所在父元素的大小,父元素的大小决定了可视区的宽和高
  2. 确定长列表每一个列表元素的宽和高,同时初始的条件下计算好长列表每一个元素相对于父元素的位置,并用一个数组来保存所有列表元素的位置信息
  3. 首次渲染时,只展示相对于父元素可视区内的子列表元素,在滚动时,根据父元素的滚动的offset重新计算应该在可视区内的子列表元素。这样保证了无论如何滚动,真实渲染出的dom节点只有可视区内的列表元素。
  4. 假设可视区内能展示5个子列表元素,及时长列表总共有1000个元素,但是每时每刻,真实渲染出来的dom节点只有5个。
  5. 补充说明,这种情况下,父元素一般使用position:relative,子元素的定位一般使用:position:absolute或sticky

  通过虚拟列表的方式,不需要同时渲染很多dom节点,只需要渲染出可视区的dom节点,这种方式可以大大减小渲染时间,提升用户体验。

二、React-tiny-virtual-list组件的使用

  React-tiny-virtual-list是一个极简的React虚拟列表优化组件库,我们来看如何使用这个组件:

import VirtualList from 'react-tiny-virtual-list';
const data = ['A', 'B', 'C', 'D', 'E',   'F','G','H','I','J','K','L'];
class TinyVirtual extends Component {
  render(){
    return <VirtualList
            width={"100%"}
            height={200}
            itemCount={data.length}
            itemSize={50}
            renderItem={({index, style}) =>
              <div key={index} style={style}>
                 The style property contains the item's absolute position Letter: {data[index]}, Row: #{index}
              </div>
            }
            />
  }
}

效果为:

911543739688_ pic_hd

  该组件中通过width和height分别定义了宽度和高度,通过itemCount定义了所有需要被渲染成dom节点的数据数组的长度。itemSize定义了渲染的每一个dom的高度,而renderItem定义了如何结合数据来渲染一个dom。

  此外react-tiny-virtual-list的List组件还有scrollDirection属性决定是垂直滚动还是水平滚动,以及recomputeSizes函数用于重新计算列表中每一个dom元素的高度等。

三、React-tiny-virtual-list的源码分析

  下面我们来根据常用的width、height、itemCount、itemSize和renderItem来分析React-tiny-virtual-list的源码。React-tiny-virtual-list的源码包含了一个VirtualList组件,以及处理关于保存了VirtualList组件所渲染的列表元素的位置,同时决定其能否在可视区显示的工具类——SizeAndPositionManager类。

下面来一一分析:

(1)、VirtualList的初始化和render方法

首先来看这个VirtualList组件初始化和render的方法中做了哪些事情,在初始化VirtualList组件时:

itemSizeGetter = (itemSize) => {
  return index => this.getSize(index, itemSize);
};

sizeAndPositionManager = new SizeAndPositionManager({
    itemCount: this.props.itemCount,
    itemSizeGetter: this.itemSizeGetter(this.props.itemSize),
    estimatedItemSize: this.getEstimatedItemSize(),
});

state = {
  offset:
      this.props.scrollOffset ||
      (this.props.scrollToIndex != null &&
        this.getOffsetForIndex(this.props.scrollToIndex)) ||
      0,
 scrollChangeReason: SCROLL_CHANGE_REASON.REQUESTED,
};

在初始化的时候,在state中定义并赋值了整个列表应该滚动到哪一个位置的offset属性,可以根据该值来决定整个列表中,哪几个列dom需要在可视区内被渲染。

并且根据虚拟列表的原理我们指导,需要在内存中保存列表中每一个dom元素的相对于父元素的位置等信息,并且在父元素滚动等操作的时候需要更新子元素列的位置信息。在这里我们通过一个SizeAndPositionManager类来实现。

首先看这个类的状态定义:

class SizeAndPositionManager {
  constructor({itemCount, itemSizeGetter, estimatedItemSize}: Options) {
    this.itemSizeGetter = itemSizeGetter;
    this.itemCount = itemCount;
    this.estimatedItemSize = estimatedItemSize;
    this.itemSizeAndPositionData = {};
    this.lastMeasuredIndex = -1;
}

这个类中有itemSizeGetter方法用于获取从props传入的每一个子元素size的值,以及列表总列数等信息。itemSizeAndPositionData对象用户保存每一列的大小和相对于父元素的定位信息。lastMeasuredIndex表示可视区内最后一个元素的索引。

此外在VirtualList组件的初始化中还有:

styleCache= {};

用于在内存中保存所有子元素的样式,通过index索引。初始化结束后,再来看render的过程:

render() {
    const {
      estimatedItemSize,
      height,
      ...props
    } = this.props;
    const {offset} = this.state;//保存在当前的父元素中,相对于顶部父亲元素滚动的举例offset
    const {start, stop} = this.sizeAndPositionManager.getVisibleRange({
      containerSize: this.props[sizeProp[scrollDirection]] || 0,
      offset,
      overscanCount,
    }); //根据父元素滚动的距离,以及父元素的高度等信息,来决定父元素可视区内应该展示哪几个子元素
    const items: React.ReactNode[] = []; //保存应该在可视区内被渲染的子列表数组
    //找出应该在可视区内被渲染的子列表元素的index之后,就需要通过this.getStyle方法来获取该元素的样式以及在父元素汇总的距离。获取渲染元素的索引值,以及样式之后,通过在属性中通过props传入的renderItem方法,构建应该渲染出的列表元素。
    if (typeof start !== 'undefined' && typeof stop !== 'undefined') {
    for (let index = start; index <= stop; index++) {
        items.push(
          renderItem({
            index,
            style: this.getStyle(index, false),
          }),
        );
    }
  
   return (
     <div ref={this.getRef} {...props} style={wrapperStyle}>
      <div style={innerStyle}>{items}</div>
    </div>
  );
}

其中需要在于this.getStyle方法:

getStyle(index: number, sticky: boolean) {

    const style = this.styleCache[index];

    if (style) {
      return style;
    }

    const {scrollDirection = DIRECTION.VERTICAL} = this.props;
    const {
      size,
      offset,
    } = this.sizeAndPositionManager.getSizeAndPositionForIndex(index);

    return (this.styleCache[index] = sticky
      ? {
          ...STYLE_STICKY_ITEM,
          [sizeProp[scrollDirection]]: size,
          [marginProp[scrollDirection]]: offset,
          [oppositeMarginProp[scrollDirection]]: -(offset + size),
          zIndex: 1,
        }
      : {
          ...STYLE_ITEM,
          [sizeProp[scrollDirection]]: size,
          [positionProp[scrollDirection]]: offset,
        });
}

首先从styleCache的内存中去查找,如果找到就直接返回,否则就根据在初始的时候定义的this.sizeAndPositionManager的getSizeAndPositionForIndex中根据索引来得到响应元素的大小,以及相对于父元素的位置,并以index为索引保存在styleCache内存中。

此外,在render的return中,我们通过ref的方式在virtual dom中获取了父元素。

getRef = (node) => {
  this.rootNode = node;
};

(2)、VirtualList组件处理滚动事件

在VirtualList组件的componentDidMount的声明周期中开始监听滚动事件:

const {scrollOffset, scrollToIndex} = this.props;
this.rootNode.addEventListener('scroll', this.handleScroll, {
  passive: true,
});

接着来看处理滚动事件的函数this.handleScroll:

handleScroll = (event) => {
    const {onScroll} = this.props;
    const offset = this.getNodeOffset();

    if (
      offset < 0 ||
      this.state.offset === offset ||
      event.target !== this.rootNode
    ) {
      return;
    }

    this.setState({
      offset,
      scrollChangeReason: SCROLL_CHANGE_REASON.OBSERVED,
    });

    if (typeof onScroll === 'function') {
      onScroll(offset, event);
    }
};

getNodeOffset() {
  const {scrollDirection = DIRECTION.VERTICAL} = this.props;
  return this.rootNode[scrollProp[scrollDirection]];
}

在这个handleScroll事件中,通过getNodeOffset方法可以获得滚动后父元素滚动的高度,然后在this.setState方法中更新这个offset的值。只要state中的offset的值发生变化,就会重新触发render,在render中根据新的offset来确定可视区应该显示哪些列表。

四、React-tiny-virtual-list总结

  • 从源码可以看出来React-tiny-virtual-list的组件是不支持动态高度的,也就是做子元素必须是指定的固定高度,否则随着父元素的滚动,可视区内应该展示哪些子元素会不准确。主要缺陷在于在滚动过程中,没有区重新计算子元素的高度。
  • 其次,在源码中我们也可以看出来,其实React-tiny-virtual-list并没有复用可视区内的dom。

从Express到Nestjs,谈谈Nestjs的设计**和使用方法

从Express到Nestjs,谈谈Nestjs的设计**和使用方法


  最近已经使用过一段时间的nestjs,让人写着有一种java spring的感觉,nestjs可以使用express的所有中间件,此外完美的支持typescript,与数据库关系映射typeorm配合使用可以快速的编写一个接口网关。本文会介绍一下作为一款企业级的node框架的特点和优点。

  • 从依赖注入(DI)谈起
  • 装饰器和注解
  • nestjs的“洋葱模型”
  • nestjs的特点总结

一、从依赖注入(DI)谈起

(1)、angular中的依赖注入

  从angular1.x开始,实现了依赖注入或者说控制反转的模式,angular1.x中就有controller(控制器)、service(服务),模块(module)。笔者在早年间写过一段时间的angular1.3,下面举例来说明:

var myapp=angular.module('myapp',['ui.router']);
myapp.controller('test1',function($scope,$timeout){}
myapp.controller('test2',function($scope,$state){}

  上面这个就是angular1.3中的一个依赖注入的例子,首先定义了模块名为“myapp”的module, 接着在myapp这个模块中定义controller控制器。将myapp模块的控制权交给了myapp.controller函数。具体的依赖注入的流程图如下所示:

default

myapp这个模块如何定义,由于它的两个控制器决定,此外在控制器中又依赖于$scope、$timeout等服务。这样就实现了依赖注入,或者说控制反转。

(2)、什么是依赖注入

  用一个例子来通俗的讲讲什么是依赖注入。

class Cat{

}
class Tiger{

}
class Zoo{
  constructor(){
     this.tiger = new Tiger();
     this.cat = new Cat();
  }
}

  上述的例子中,我们定义Zoo,在其constructor的方法中进行对于Cat和Tiger的实例化,此时如果我们要为Zoo增加一个实例变量,比如去修改Zoo类本身,比如我们现在想为Zoo类增加一个Fish类的实例变量:

class Fish{}

class Zoo{
  constructor(){
     this.tiger = new Tiger();
     this.cat = new Cat();
     this.fish = new Fish();
  }
}

  此外如果我们要修改在Zoo中实例化时,传入Tiger和Cat类的变量,也必须在Zoo类上修改。这种反反复复的修改会使得Zoo类并没有通用性,使得Zoo类的功能需要反复测试。

我们设想将实例化的过程以参数的形式传递给Zoo类:

class Zoo{
  constructor(options){
     this.options = options;
  }
}
var zoo = new Zoo({
  tiger: new Tiger(),
  cat: new Cat(),
  fish: new Fish()
})

  我们将实力化的过程放入参数中,传入给Zoo的构造函数,这样我们就不用在Zoo类中反复的去修改代码。这是一个简单的介绍依赖注入的例子,更为完全使用依赖注入的可以为Zoo类增加静态方法和静态属性:

class Zoo{
  static animals = [];
  constructor(options){
     this.options = options;
     this.init();
  }
  init(){
    let _this = this;
    animals.forEach(function(item){
      item.call(_this,options);
    })
  }
  static use(module){
     animals.push([...module])
  }
}
Zoo.use[Cat,Tiger,Fish];
var zoo = new Zoo(options);

  上述我们用Zoo的静态方法use往Zoo类中注入Cat、Tiger、Fish模块,将Zoo的具体实现移交给了Cat和Tiger和Fish模块,以及构造函数中传入的options参数。

(3)、nestjs中的依赖注入

  在nestjs中也参考了angular中的依赖注入的**,也是用module、controller和service。

@Module({
  imports:[otherModule],
  providers:[SaveService],
  controllers:[SaveController,SaveExtroController]
})
export class SaveModule {}

  上面就是nestjs中如何定一个module,在imports属性中可以注入其他模块,在prividers注入相应的在控制器中需要用到的service,在控制器中注入需要的controller。

二、装饰器和注解

  在nestjs中,完美的拥抱了typescript,特别是大量的使用装饰器和注解,对于装饰器和注解的理解可以参考我的这篇文章:Typescript中的装饰器和注解。我们来看使用了装饰器和注解后,在nestjs中编写业务代码有多么的简洁:

import { Controller, Get, Req, Res } from '@nestjs/common';

@Controller('cats')

export class CatsController {
  @Get()
  findAll(@Req() req,@Res() res) {
    return 'This action returns all cats';
  }
}

  上述定义两个一个处理url为“/cats”的控制器,对于这个路由的get方法,定义了findAll函数。当以get方法,请求/cats的时候,就会主动的触发findAll函数。

  此外在findAll函数中,通过req和res参数,在主题内也可以直接使用请求request以及对于请求的响应response。比如我们通过req上来获取请求的参数,以及通过res.send来返回请求结果。

三、nestjs的“洋葱模型”

  这里简单讲讲在nestjs中是如何分层的,也就是说请求到达服务端后如何层层处理,直到响应请求并将结果返回客户端。

在nestjs中在service的基础上,按处理的层次补充了中间件(middleware)、异常处理(Exception filters)、管道(Pipes),守卫(Guards),以及拦截器(interceptors)在请求到打真正的处理函数之间进行了层层的处理。

default

上图中的逻辑就是分层处理的过程,经过分层的处理请求才能到达服务端处理函数,下面我们来介绍nestjs中的层层模型的具体作用。

(1)、middleware中间件

  在nestjs中的middle完全跟express的中间件一摸一样。不仅如此,我们还可以直接使用express中的中间件,比如在我的应用中需要处理core跨域:

import * as cors from 'cors';
async function bootstrap() {
  onst app = await NestFactory.create(/* 创建app的业务逻辑*/)
  app.use(cors({
    origin:'http://localhost:8080',
    credentials:true
  }));
  await app.listen(3000)
}
bootstrap();

在上述的代码中我们可以直接通过app.use来使用core这个express中的中间件。从而使得server端支持core跨域等。

初此之外,跟nestjs的中间件也完全保留了express中的中间件的特点:

  • 在中间件中接受response和request作为参数,并且可以修改请求对象request和结果返回对象response
  • 可以结束对于请求的处理,直接将请求的结果返回,也就是说可以在中间件中直接res.send等。
  • 在该中间件处理完毕后,如果没有将请求结果返回,那么可以通过next方法,将中间件传递给下一个中间件处理。

在nestjs中,中间件跟express中完全一样,除了可以复用express中间件外,在nestjs中针对某一个特定的路由来使用中间件也十分的方便:

class ApplicationModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('cats');
  }
}

上面就是对于特定的路由url为/cats的时候,使用LoggerMiddleware中间件。

(2)、Exception filters异常过滤器

  Exception filters异常过滤器可以捕获在后端接受处理任何阶段所跑出的异常,捕获到异常后,然后返回处理过的异常结果给客户端(比如返回错误码,错误提示信息等等)。

  我们可以自定义一个异常过滤器,并且在这个异常过滤器中可以指定需要捕获哪些异常,并且对于这些异常应该返回什么结果等,举例一个自定义过滤器用于捕获HttpException异常的例子。

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();
    const status = exception.getStatus();

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
      });
  }
}

  我们可以看到host是实现了ArgumentsHost接口的,在host中可以获取运行环境中的信息,如果在http请求中那么可以获取request和response,如果在socket中也可以获取client和data信息。

  同样的,对于异常过滤器,我们可以指定在某一个模块中使用,或者指定其在全局使用等。

(3)Pipes管道

  Pipes一般用户验证请求中参数是否符合要求,起到一个校验参数的功能。

  比如我们对于一个请求中的某些参数,需要校验或者转化参数的类型:

@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
  transform(value: string, metadata: ArgumentMetadata): number {
    const val = parseInt(value, 10);
    if (isNaN(val)) {
      throw new BadRequestException('Validation failed');
    }
    return val;
  }
}

  上述的ParseIntPipe就可以把参数转化成十进制的整型数字。我们可以这样使用:

@Get(':id')
async findOne(@Param('id', new ParseIntPipe()) id) {
  return await this.catsService.findOne(id);
}

  对于get请求中的参数id,调用new ParseIntPipe方法来将id参数转化成十进制的整数。

(4)Guards守卫

  Guards守卫,其作用就是决定一个请求是否应该被处理函数接受并处理,当然我们也可以在middleware中间件中来做请求的接受与否的处理,与middleware相比,Guards可以获得更加详细的关于请求的执行上下文信息。

通常Guards守卫层,位于middleware之后,请求正式被处理函数处理之前。

下面是一个Guards的例子:

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    return validateRequest(request);
  }
}

这里的context实现了一个ExecutionContext接口,该接口中具有丰富的执行上下文信息。

export interface ArgumentsHost {
  getArgs<T extends Array<any> = any[]>(): T;
  getArgByIndex<T = any>(index: number): T;
  switchToRpc(): RpcArgumentsHost;
  switchToHttp(): HttpArgumentsHost;
  switchToWs(): WsArgumentsHost;
}

export interface ExecutionContext extends ArgumentsHost {
  getClass<T = any>(): Type<T>;
  getHandler(): Function;
}

除了ArgumentsHost中的信息外,ExecutionContext还包含了getClass用户获取对于某一个路由处理的,控制器。而getClass用于获取返回对于指定路由后台处理时的处理函数。

对于Guards处理函数,如果返回true,那么请求会被正常的处理,如果返回false那么请求会抛出异常。

(5)、interceptors拦截器

   拦截器可以给每一个需要执行的函数绑定,拦截器将在该函数执行前或者执行后运行。可以转换函数执行后返回的结果等。

概括来说:

interceptors拦截器在函数执行前或者执行后可以运行,如果在执行后运行,可以拦截函数执行的返回结果,修改参数等。

再来举一个超时处理的例子:

@Injectable()
export class TimeoutInterceptor implements NestInterceptor{
  intercept(
    context:ExecutionContext,
    call$:Observable<any>
  ):Observable<any>{
    return call$.pipe(timeout(5000));
  }
}

该拦截器可以定义在控制器上,可以处理超时请求。

四、nestjs的特点总结

  最后总结一下nestjs的优缺。

nestjs的优点:

  • 完美的支持typescript,因此可以使用日益繁荣的ts生态工具
  • 兼容express中间件,因为express是最早出现的轻量级的node server端框架,nestjs能够利用所有express的中间件,使其生态完善
  • 层层处理,一定程度上可以约束代码,比如何时使用中间件、何时需要使用guards守卫等。
  • 依赖注入以及模块化的**,提供了完整的mvc的链路,使得代码结构清晰,便于维护,这里的m是数据层可以通过modules的形式注入,比如通过typeorm的entity就可以在模块中注入modules。
  • 完美支持rxjs

前端监控和前端埋点方案设计

前端监控和前端埋点方案设计


在线上项目中,需要统计产品中用户行为和使用情况,从而可以从用户和产品的角度去了解用户群体,从而升级和迭代产品,使其更加贴近用户。用户行为数据可以通过前端数据监控的方式获得,除此之外,前端还需要实现性能监控和异常监控。性能监控包括首屏加载时间、白屏时间、http请求时间和http响应时间。异常监控包括前端脚本执行报错等。

实现前端监控有三个步骤:前端埋点和上报、数据处理和数据分析。本文针对整个前端监控,设计适用的方案。本文的主要内容分为:

  • 为什么需要前端监控
  • 常用前端埋点方案总结
  • 前端埋点方案选型和前端上报方案设计
  • 前端监控结果可视化展示系统的设计

一、为什么需要前端监控

前端监控的目的是:

获取用户行为以及跟踪产品在用户端的使用情况,并以监控数据为基础,指明产品优化的方向

前端监控可以分为三类:数据监控、性能监控和异常监控。下面我们来一一的了解。

(1)数据监控

数据监控,顾名思义就是监听用户的行为。常见的数据监控包括:

  • PV/UV:PV(page view),即页面浏览量或点击量。UV:指访问某个站点或点击某条新闻的不同IP地址的人数
  • 用户在每一个页面的停留时间
  • 用户通过什么入口来访问该网页
  • 用户在相应的页面中触发的行为

统计这些数据是有意义的,比如我们知道了用户来源的渠道,可以促进产品的推广,知道用户在每一个页面停留的时间,可以针对停留较长的页面,增加广告推送等等。

(2)性能监控

性能监控指的是监听前端的性能,主要包括监听网页或者说产品在用户端的体验。常见的性能监控数据包括:

  • 不同用户,不同机型和不同系统下的首屏加载时间
  • 白屏时间
  • http等请求的响应时间
  • 静态资源整体下载时间
  • 页面渲染时间
  • 页面交互动画完成时间

这些性能监控的结果,可以展示前端性能的好坏,根据性能监测的结果可以进一步的去优化前端性能,比如兼容低版本浏览器的动画效果,加快首屏加载等等。

(3)异常监控

此外,产品的前端代码在执行过程中也会发生异常,因此需要引入异常监控。及时的上报异常情况,可以避免线上故障的发上。虽然大部分异常可以通过try catch的方式捕获,但是比如内存泄漏以及其他偶现的异常难以捕获。常见的需要监控的异常包括:

  • Javascript的异常监控
  • 样式丢失的异常监控

二、常用前端埋点方案总结

在上一节中介绍了前端监控的作用,那么如何实现前端监控呢,实现前端监控的步骤为:前端埋点和上报、数据处理和数据分析。首要的步骤就是前端埋点和上报,也就是数据的收集阶段。数据收集的丰富性和准确性会影响对产品线上效果的判别结果。

目前常见的前端埋点方法分为三种:代码埋点、可视化埋点和无痕埋点。下面一一介绍每一种埋点的方法。

(1) 代码埋点

代码埋点,就是以嵌入代码的形式进行埋点,比如需要监控用户的点击事件,会选择在用户点击时,插入一段代码,保存这个监听行为或者直接将监听行为以某一种数据格式直接传递给server端。此外比如需要统计产品的PV和UV的时候,需要在网页的初始化时,发送用户的访问信息等。

代码埋点的优点:

  • 可以在任意时刻,精确的发送或保存所需要的数据信息。

缺点:

  • 工作量较大,每一个组件的埋点都需要添加相应的代码

(2)可视化埋点

通过可视化交互的手段,代替代码埋点。将业务代码和埋点代码分离,提供一个可视化交互的页面,输入为业务代码,通过这个可视化系统,可以在业务代码中自定义的增加埋点事件等等,最后输出的代码耦合了业务代码和埋点代码。

可视化埋点听起来比较高大上,实际上跟代码埋点还是区别不大。也就是用一个系统来实现手动插入代码埋点的过程。

缺点:

  • 可视化埋点可以埋点的控件有限,不能手动定制。

(3)无埋点

无埋点并不是说不需要埋点,而是全部埋点,前端的任意一个事件都被绑定一个标识,所有的事件都别记录下来。通过定期上传记录文件,配合文件解析,解析出来我们想要的数据,并生成可视化报告供专业人员分析因此实现“无埋点”统计。

从语言层面实现无埋点也很简单,比如从页面的js代码中,找出dom上被绑定的事件,然后进行全埋点。

无埋点的优点:

  • 由于采集的是全量数据,所以产品迭代过程中是不需要关注埋点逻辑的,也不会出现漏埋、误埋等现象

缺点:

  • 无埋点采集全量数据,给数据传输和服务器增加压力
  • 无法灵活的定制各个事件所需要上传的数据

三、前端埋点方案选型和前端上报方案设计

第一章中介绍了前端所需要监听的信息,在第二章中介绍了前端埋点的常见方式,本文来根据需求,来定制我们的埋点和上报方案。

(1)监控数据

首先我们需要明确一个产品或者网页,普遍需要监控和上报的数据。监控的分为三个阶段:用户进入网页首页、用户在网页内部交互和交互中报错。每一个阶段需要监控和上报的数据如下图所示:

default

(2)埋点方案

在实际项目中考虑到上报数据的灵活定制,以及减少数据传输和服务器的压力,在所需埋点处不多的情况下,常用的方式是代码埋点。

以用户进入首页为例,我们在首页渲染完成后会发送事件类型和类型相关的数据给server端,告知首页的监控信息。

default

(3)上报周期和上报数据类型

如果埋点的事件不是很多,上报可以时时进行,比如监控用户的交互事件,可以在用户触发事件后,立刻上报用户所触发的事件类型。如果埋点的事件较多,或者说网页内部交互频繁,可以通过本地存储的方式先缓存上报信息,然后定期上报。

接着来确定需要埋点上报的数据,上报的信息包括用户个人信息以及用户行为,主要数据可以分为:

  • who: appid(系统或者应用的id),userAgent(用户的系统、网络等信息)

  • when: timestamp(上报的时间戳)

  • from where: currentUrl(用户当前url),fromUrl(从哪一个页面跳转到当前页面),type(上报的事件类型),element(触发上报事件的元素)

  • what: 上报的自定义扩展数据data:{},扩展数据中可以按需求定制,比如包含uid等信息

上报数据的对象为:

{   
    ----------------上报接口本身提供--------------------
    currentUrl,  
    fromUrl,
    timestamp,
    userAgent:{
       os,
       netWord,
    }
    ----------------业务代码配置和自定义上报数据------------
    type,
    appid,
    element,
    data:{
        uid,
        uname
    }
}

(4)埋点和上报举例

我们以上报首屏加载事件为例,DOM提供了document的DOMContentLoaded事件来监听dom挂载,提供了window的load事件来监听页面所有资源加载渲染完毕。

<script type="text/javascript">
  var start=Date.now();
  document.addEventListener('DOMContentLoaded', function() {
     fetch('some api',{
         type:'dom complete',
         data:{
           domCompletedTime:Date.now()-start
         }
     })
  });
  window.addEventListener('load', function() {
     fetch('some api',{
         type:'load complete',
         data:{
           LoadCompletedTime:Date.now()-start
         }
     })
  });
</script>

(5)前端埋点系统的前后端通信加密

在上报数据的前后端通信中,需要和server端协商加密机制,利用 OpenSSL库来实现的加密,OpenSSL已经是一个广泛被采用的加密算法。前端可以采用node的crypto模块。

首先来看hash算法,crypto.createHash() 来创建一个Hash实例,可利用的hash算法如下:

  • md5

  • sha1

  • sha256

  • sha512

  • ripemd160

以sha256算法加密为例:

const str="123445";//需要加密的字段
const hash=crypto.createHash('sha256');//指定加密算法
hash.update(str); //通过算法加密相应的字段
const result=hash.digest('hex');//转化成十六进制

四、前端监控结果可视化展示系统的设计

当后端得到前端上报的信息之后,经过数据分析和处理,需要前端可视化的展示数据分析后的结果。

可以在开源中后台系统ant-design-pro的基础上进行二次开发,首先要明确展示信息。展示的信息包括单个用户和整体应用。

对于单个用户来说需要展示的监控信息为:

  • 单个用户,在交互过程中触发各个埋点事件的次数
  • 单个用户,在某个时间周期内,访问本网页的入口来源
  • 单个用户,在每一个子页面的停留时间

对于全体用户需要展示的信息为:

  • 某一个时间段内网页的PV和UV
  • 全体用户访问网页的设备和操作系统分析
  • 某一个时间段内访问本网页的入口来源分析
  • 全体用户在访问本网页时,在交互过程中触发各个埋点事件的总次数
  • 全体用户在访问本网页时,网页上报异常的集合

删选功能集合:

  • 时间筛选:提供今日(00点到当前时间)、本周、本月和全年
  • 用户删选:提供根据用户id删选出用户行为的统计信息
  • 设备删选:删选不同系统的整体展示信息

Redux源码深度解读

简要介绍:用了一段时间redux,今天看了一下redux的源码,大致整理了心得如下。

1、什么是redux,这里就不做介绍,如想了解可以移步 ReadMe.redux,整体redux的代码只有800行,src下面分为一下几个部分。

  • applyMiddleware.js

  • bindActionCreators.js

  • combineReducers.js

  • compose.js

  • createStore.js

  • index.js

首先我们来看index.js主js的内容,很简单,就是引入和模块和抛出模块,这里有一句提醒内容,如果是production生产环境并且js已经被压缩,会输出warning信息

function isCrushed() {}

if (
  process.env.NODE_ENV !== 'production' &&
  typeof isCrushed.name === 'string' &&
  isCrushed.name !== 'isCrushed'
) {
  warning(
    'You are currently using minified code outside of NODE_ENV === \'production\'. ' +
    'This means that you are running a slower development build of Redux. ' +
    'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' +
    'or DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) ' +
    'to ensure you have the correct code for your production build.'
  )
}

注: isCrushed.name 函数名.name是一个es6的属性,返回函数的名称。
除了index.js外,我们接下去从redux实现的接口,来深度分析一下Redux的源码。

2、compose.js

我们首先从compose.js入手,首先redux贯穿始终的是函数式变成的**,个人对于函数式编程的理解为:

  • 首先是纯函数(相同的输入产生相同的输出)

  • 在范畴论理,状态或者输出表示点,函数表示边,从点到点的转移可以看成运算符,函数也是一种运算符,因为运算符是纯净的,因此函数式编程中的函数也是纯净的

  • 函数式编程中的函数,与变量等价,可以作为参数传递或者成为其他函数函数体里的一部分

  • 因为是函数式编程,便于函数的组合,这里有一个curry和compose的组合过程

基础了解函数式编程之后,下面我们来看compose.js的源码:

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

这个compose其实很简单,传入的参数为函数数组,返回的为reduce从左到右合并后的新的函数。是一个类似于链式调用的过程。

来看:

funcs.reduce((a, b) => (...args) => a(b(...args)))

这句特别重要,组合函数的这部非常重要,我们发现...args参数会依次的从右到左执行,比如将b(...args)的执行结果,传入a中作为参数继续执行。

3、applyMiddleware.js

applyMiddleware.js其实是基于compose.js来实现的

export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState, enhancer) => {
    const store = createStore(reducer, preloadedState, enhancer)
    let dispatch = store.dispatch
    let chain = []

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

上述代码如果applyMiddleware(...Middleware)(createStore)这样调用,会生成一个新的createStore函数,用于创建新的createStore,新在哪里呢?就是链式的调用了所有的middleware:

let chain = []

const middlewareAPI = {
   getState: store.getState,
   dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)

来看上述的代码,chain是一个函数数组,是middleware({})执行后的返回函数的数组,compose(...chain)是链式的组合函数,这里的...args是初始时候的store.dispatch,当最右边的函数以store.dispatch为参数,执行后生成一个新的store.dispatch,又向外传递,因此middleware是从右到左执行的。

从上述的描述中,我们知道了middle的书写形式,如果以纯函数的形式,首先第一个参数应该是{getState:'',dispatch:''},第二个参数是store.dispatch,第三个参数应该是action,因此最基本形式的middleware应该是:

return ({ dispatch, getState }) => next => action => {

}

我们以redux-thunk为例,redux-thunk中间件是严格按照上述的形式,

代码只有13行:

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

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

这个中间件的功能其实很简单,也就是action如果是一个函数就选择执行这个函数,并且action函数执行的时候,会传入dispatch和getState.

4、createStore.js

createStore相对而言会较为的复杂,我们还是从接口出发。

(1) 首先看,createStore()函数的返回值store有哪些接口:

  • getState():返回当前的state树

  • dispatch(action):分发action,是改变state的唯一方法

  • subscribe(listener):添加一个监听器,当state变化的时候,执行监听器里面的函数。

  • unsubscribe(listener):subscribe的返回值,用于移除监听器

  • replaceReducer(nextReducer):替换store中当前的reducer

(2) 下面根据代码,依次来看,各个接口的实现情况。

首先明确createStore的形参,形参有3个,分别是reducer(处理函数),initState(初始化state),enhancer(一个高阶函数,可以改变store的接口)。

export default function createStore(reducer, preloadedState, enhancer) {

}
  • getState函数:
let currentState = preloadedState
function getState() {
    return currentState
  }

getState函数比较简单,类似于一个get的方法,返回currentState的值

  • dispatch函数:
function dispatch(action) {
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
        'Use custom middleware for async actions.'
      )
    }

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

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

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

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

    return action
  }

dispatch也不复杂,去掉判断类型(因为action必须是对象)的部分,其实只有2步:

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

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

这就很显然易见了,就是执行currentReducer()传入当前的currentState和action,返回新的state,并且执行监听函数数组里面的所有函数。

  • subscribe:
function subscribe(listener) {
    if (typeof listener !== 'function') {
      throw new Error('Expected listener to be a function.')
    }

    let isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

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

      isSubscribed = false

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

监听函数也挺简单,就是一个简单的移入和移出,这是一个底层 API。多数情况下,你不会直接使用它,会使用一些 React(或其它库)的绑定。比如react-redux中的容器组件中的props改变会自动的更新(也算一个监听过程)。

  • replaceReducer:
function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }

    currentReducer = nextReducer
    dispatch({ type: ActionTypes.INIT })
  }

这个函数就更加的简单了,replaceReducer是为了改变当前的reducer,因此只要将currentReducer赋值为形参即可。

5、combineReducers.js

最复杂的部分就是combineReducer.js了

  • combineReducer(reducer)接受一个reducer对象,recuder是key表

示属性名,value是一个小的reduce函数:

export default function combineReducers(reducers) {

}
  • 遍历reducer中的对象,取出其中的value值(reduce函数),生成一个新的对象:
const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]
    finalReducers[key] = reducers[key]
  }
  • 因为reduce函数有一个初始执行过程,即会自动执行一次 type: ActionTypes.INIT,因此有以下过程:
assertReducerShape(finalReducers);
function assertReducerShape(reducers) {
  Object.keys(reducers).forEach(key => {
    const reducer = reducers[key]
    const initialState = reducer(undefined, { type: ActionTypes.INIT })
  • 最后就是最终要的返回的combine函数:

其原理也很简单,state的属性名和reducer对象的key是相对的,因此也就是在所有的小的reduce函数中,传入相对的state[key],action,依次执行后得到一个新的newState,然后与state做比较,选择性返回。

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

6、bindActionCreators.js

这个一般比较少用,这里就不分析源码了,只简单的阐述功能,

bindActionCreators()把 action creators 转成拥有同名 keys 的对象,但使用 dispatch 把每个 action creator 包围起来,这样可以直接调用它们。

React内部原理,第一部分:基础渲染

React本质,第一部分:基础渲染


在接下来的五篇文章中,会用通俗的方式“重新构造”一个React,通过完成一个简易版本的React的构造,可以帮助我们理解React是如何实现的,以及组件的生命周期存在的原因和作用。

这一系列的文章包括:

  • 第一部分:基础渲染
  • 第二部分:componentWillMount and componentDidMount
  • 第三部分:基础更新
  • 第四部分:setState
  • 第三部分:transaction

声明:
这个系列的文章基于React15.3,所以最新的React的特性比如Fiber等是不支持的,本系列根据React的原理所构建的"Peact"尽可能的实现React的相关功能,但没有完全实现。


一、背景知识:元素(Element)和组件(Components)

在React中有三种不同的实体类型:原生的DOM元素、虚拟React元素(Virtual React Elements)和组件(Components)。

原生的DOM元素

这就是我们通常所说的dom元素,浏览器使用真实的dom元素来组织web网页,在某一时刻,React会通过调用document.createElement()方法去创建一个真实的dom元素,或者使用浏览器的DOM API去更新真实的dom元素, 更新的元素的API比如:element.insertBefore(), element.nodeValue等等.

虚拟的React元素

一个虚拟的react元素在内存中控制真实的DOM元素,在更新前如何渲染.这个react元素可以代表的是一个原生的dom元素或者是开发者自己定义的组件.

译者注:这里的虚拟React元素,就是React virtual dom的关键,主要流程为:

用户dom操作——>改变虚拟React元素——>在浏览器渲染

组件(Component)

"组件(Component)"在React中是一个特殊的术语,React可以在组件中实现不同的工作,不同的组件实现了不同的功能,比如ReactDOM提供了ReactDOMComponent实现了虚拟React元素渲染到真实的DOM元素的映射。

自定义的组合组件

在React中,自定义的组件可以通过React.createClass()或者es6形式的,class extend React.Component的方式创建一个类组件。在类组件中会有componentWillMount、shouldComponentUpdate等生命周期函数.组件的生命周期函数是React的一个难点。组件中的生命周期函数除了一些开发者常用的,还有形如mountComponent和receiveComponent等,开发者很少使用的生命钩子.

二、React是声明式的

定义React类组件是声明式的,在需要使用的时候再去实例化,在React的类组件中,我们是这样声明式的定义的:

class MyComponent extends React.Component{
    render(){
       return <div>hello</div>
    }
}

将jsx语言编译后可以得到:

class MyComponent extends React.Component{
    render(){
       return React.createElement('div',null,'hello')
    }
}

从上述代码中,我们可能有一个疑问,在组件实例化的时候,是不是在调用React.createElement之前就已经创建了一个真实的dom元素.其实并不是这样,组件在实例化后,会通过render方法来调用React.createElement去创建真实的dom元素.React声明式的创建组件的方式,可以控制真实dom的渲染.

三、模拟React实现的微型模型Peact

基于上述的虚拟react元素,以及自定义组件的原理,我们来仿造一个简单的类React实现上述功能,用Peact来表示.Peact应该有一个render方法:

Feact.render(<h1>hello world</h1>, document.getElementById('root'));

首先抛弃jsx语法,直接用Peact.createElement来代替(jsx语法最后也需要创建原生dom元素):

Feact.render(
    Feact.createElement('h1', null, 'hello world'),
    document.getElementById('root')
)

来看Peact.createElement方法的实现:

const Feact = {
    createElement(type, props, children) {
        const element = {
            type,
            props: props || {}
        };
        if (children) {
            element.props.children = children;
        }
    return element;
   }
}

在Feact.createElement方法中,返回的element对象可以表示我们所需要渲染到浏览器的元素信息.

如何实现render方法

接着来看render方法的实现,在Feact.render()方法中,参数为我们所需要渲染的信息,以及在哪里渲染.render方法是构建Feact app的最根本的方法,我们首先通过如下方式来定义render方法:

const Feact = {
    createElement() { /* as before */ },
    render(element, container) {
    const componentInstance = new  FeactDOMComponent(element);
    return componentInstance.mountComponent(container);
    }
};

当render被调用后,我们就可以得到一个完成的web网页。在render方法中,通过FeactDOMComponent方法将渲染信息映射成真实的dom元素,我们来看FeactDOMComponent方法的具体实现:

class  FeactDOMComponent{
    contructor(element){
        this._currentElement=element;
    }
    mountComponent(container){
        const domElement=document.createElement(this._currentElement.type);
        const text=this._currentElement.props.children;
        const textNode=document.createTextNode(text);
        domElement.appendChild(textNode);
        container.appendChild(domElement);
        this._hostNode = domElement;//会在第三章用到
        return domElement;
    }
}

增加自定义组件

在render方法中不仅可以渲染单个简单编码的元素,还应该可以渲染自定义的组件, 实现Peact.createClass自定义组件的方法如下:

const Feact = {
    createClass(spec) {
        function Constructor(props) {
            this.props = props;
        }
            Constructor.prototype.render = spec.render;
            return Constructor;
        }, 

    render(element, container) {
        // our previous implementation can't
        // handle user defined components,
        // so we need to rethink this method
    }
};

const MyTitle = Feact.createClass({
    render() {
        return Feact.createElement('h1', null, this.props.message);
    }
};
    //显示声明了一个MyTitle组件,接着是创建实例化的过程,原文缺省了实例化的过程。
let Title=new MyTitle(props);
Feact.render({
    Feact.createElement(MyTitle, { message: 'hey there Feact' }),
    document.getElementById('root')
);

改进Feact.render()方法

之前定义的方法无法渲染自定义的组件,因此我们需要修改Feact.render()方法来使其可以渲染自定义组件。

Feact = {
    render(element, container) {
        const componentInstance =
            new FeactCompositeComponentWrapper(element);

        return componentInstance.mountComponent(container);
    }
}
class FeactCompositeComponentWrapper {
    constructor(element) {
        this._currentElement = element;
    }

    mountComponent(container) {
        const Component = this._currentElement.type;
        const componentInstance = new Component(this._currentElement.props);
        const element = componentInstance.render();

        const domComponentInstance = new FeactDOMComponent(element);
        return domComponentInstance.mountComponent(container);
    }
}

给予了用户去定义组件的能力,并且Feact可以根据props传递过来的值动态的更新和渲染dom节点。

改进的自定义组件

在之前自定义组件的方法中,自定义的组件只能返回原生的dom元素,不能返回组件,也就是自定义组件目前不能嵌套式的在组件中返回组件,比如我们要实现这样的组件,有可能在组件中返回组件。

const MyMessage = Feact.createClass({
    render() {
        if (this.props.asTitle) {
            return Feact.createElement(MyTitle, {
                message: this.props.message
            });
        } else {
            return Feact.createElement('p', null, this.props.message);
        }
    }
}

上述的自定义组件MyMessage可以选择性的返回组件或者是原生的dom元素。这种类型的自定义组件,按之前定义的 FeactCompositeComponentWrapper方法是无法渲染的,因此对于这种组件我们需要重新的定义 FeactCompositeComponentWrapper方法。

class FeactCompositeComponentWrapper {
    constructor(element) {
        this._currentElement = element;
    }

    mountComponent(container) {
        const Component = this._currentElement.type;
        const componentInstance =
            new Component(this._currentElement.props);
        let element = componentInstance.render();

        while (typeof element.type === 'function') {
            element = (new element.type(element.props)).render();
        }

        const domComponentInstance = new FeactDOMComponent(element);
        domComponentInstance.mountComponent(container);
    }
}

在mountComponent方法中,如果还是组件需要一个while循环,直到循环取到最底层的原生dom元素。

再次修改Feact.render()

第一个版本的Feact.render()只能渲染原生的dom节点,而第二个版本的Feact.render()只能渲染组件,因此我们需要一个通用的方法,既可以渲染原生的dom节点,又可以渲染组件。具体修改的Feact.render()方法如下所示:

const TopLevelWrapper = function(props) {
    this.props = props;
};
TopLevelWrapper.prototype.render = function() {
    return this.props;
};

const Feact = {
    render(element, container) {
        const wrapperElement =
            this.createElement(TopLevelWrapper, element);

        const componentInstance =
            new FeactCompositeComponentWrapper(wrapperElement);
        // as before
    }
};

具体实现就是用一个上层的组件TopLevelWrapper包裹,并且其render方法返回的是props,在FeactCompositeComponentWrapper中判断是原生的dom元素还是组件,并进行渲染。

Webpack4.0初体验

简要介绍:Webpack4.0.1版本已经发布了2周了,下面用体验一下Webpack4.0

1 .安装Webpack4.0

(1)

Node.js 4 is no longer supported. Source Code was upgraded to a higher ecmascript version

明确不支持node.js4.X,在本文中使用的是:

node -v
v8.9.3

(2)

CLI has been move to webpack-cli, you need to install webpack-cli to use the CLI

将CLI移入到webpack-cli中,需要安装webpack-cli

npm i webpack webpack-cli -d

2 .Webpack 零配置体验

在官网给出的示例中,可以不用配置entry和output,默认的entry:'/src',
默认的output:'./dist',但是零配置的情况下'./src'的入口文件的文件名必须是index.js,否则会报错。

|—src
| --app.js

ERROR in Entry module not found: Error: Can't resolve './src' in 'C:\Users\yuxl\Desktop\react-webpack'

默认入口为src目录下的index.js文件,输出为dist下的main.js文件

3 . 约定了生产模式和开发模式下的配置

在之前的版本中,针对生产环境和开发环境,需要做不同的配置,
常见的都是指定标量,然后在webpack.config.js配置文件中,进行环境判别,比如:

"scripts":{
 
   "prod":"NODE_ENV=production webpack -p"
 
}

判别出环境后,再在配置文件中,根据不同的环境做不同的打包工作。
或者是生成两个配置文件,webpack.dev.js和webpack.prod.js,分别对应于两个环境。

(1) 在Webpack4.0中,通过mode指定环境

我们发现如果直接这样运行:

"scripts":{
  "build":"webpack"
}

npm run build,会有一个提示:

The 'mode' option has not been set. Set 'mode' option to 'development' or 'production' to enable defaults for this environment.

在Webpack中,提供了mode变量,用于配置运行环境,mode的值可以为development,表示的是开发模式,或者是production,表示的是生产模式。

我们以生产环境为例:

"scripts":{
  "build":"webpack --mode production"
}

打包后:

 Asset       Size  Chunks             Chunk Names
main.js  561 bytes       0  [emitted]  main
Entrypoint main = main.js
   [0] ./src/index.js 19 bytes {0} [built]

打包后的代码是经过压缩等处理的。在Webpack中约定了针对开发环境和生成环境的一些默认操作。

(2) mode两种环境中约定了哪些操作

production enables all kind of optimizations to generate optimized bundles

生产环境使用了所有的optimizations优化配置,来得到优化后的bundles结果。顾名思义也就是production中采用了配置中所有的内置optimization。

development enables comments and hint for development and enables the eval devtool

在开发环境中,使用了所有的评论和提示功能,并且在devtool中设置了sourcemap的类型为eval。

补充一下:

{
  devtool:eval
}

这样配置sourcemap,在webpack中不会生成具体的.map文件,只是以sourceURL的形式。具体可见devtool.

production doesn't support watching, development is optimized for fast incremental rebuilds

在生产环境中不支持文件的监听,在开发环境中的约定配置使得重新build的速度更快。

production also enables module concatenating (Scope Hoisting)

在生产环境中支持Scope Hoisting, Scope Hoisting 指将所有的打包后的文件放在一个函数里,所带来的好处有,其一函数声明少了,文件的体积比之前小,其二就是运行代码所创建的函数作用域也少了。

There is a hidden none mode which disables everything

mode还可以选择模式为none,无任何约定配置。

4 .移除了CommonsChunkPlugin

(1) 更新处

CommonsChunkPlugin was removed -> optimization.splitChunks, optimization.runtimeChunk

移除了CommonsChunkPlugin,并用内置的optimization.splitChunks.

我们以公共的react和redux为例,比如在button.js中:

require('react');
require('redux');

在index.js中引用了button.js:

require('./components/button.js');

那么,如果不进行optimization.splitChunks.配置,打包后的结构分析图为:

1

发现打包后的main.js中存在 redux和react。业务代码中存在第三方插件库redux和react,显然是不合理的。

(2) optimization.splitChunks

笔者没有找到optimization.splitChunks的文档,只能试着尝试一下,参数如下:

optimization: {
		splitChunks: {
			minSize: 1,
            chunks: "initial",
			name:"vendor"
		}
	}

更改配置后,我们可以得到的打包后的分析图为:

2

我们发现此时达到了CommonsChunkPlugin复用第三方代码的问题,但是也存在一个问题,我们发现vendor.js中,有包含业务代码index.js。(这个目前还没解决,到时候看optimization.splitChunks再试一下)。

5 .其他

此外,webpack4.0之前只支持.js类型,webpack4.0增加到了5种扩展名文件,还有webpack4.0也增加了tree shaking

完整代码的地址:https://github.com/forthealllight/try-webpack4

在Typescript项目中,如何优雅的使用ESLint和Prettier


  对于Typescript项目的编码规范而言,主要有两种选择ESLint和TSLint。ESLint不仅能规范js代码,通过配置解析器,也能规范TS代码。此外由于性能问题,TypeScript 官方决定全面采用ESLint,甚至把仓库作为测试平台,而 ESLint 的 TypeScript 解析器也成为独立项目,专注解决双方兼容性问题。
  最近在我的项目的编码规范中全量的用ESLint代替了TSLint,针对其中遇到的问题做一个记录。

  • 用ESLint来规范Typescript代码
  • 用ESLint来规范React代码
  • 结合Prettier和ESLint来规范代码
  • 在VSCode中使用ESLint
  • husky和lint-staged构建代码工作流
  • gitlab的CI/CD来规范代码

一、用ESLint来规范Typescript代码

首先安装依赖:

npm i -d eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

这三个依赖分别是:

  • eslint: ESLint的核心代码
  • @typescript-eslint/parser:ESLint的解析器,用于解析typescript,从而检查和规范Typescript代码
  • @typescript-eslint/eslint-plugin:这是一个ESLint插件,包含了各类定义好的检测Typescript代码的规范

安装好这3个依赖包之后,在根目录下新建.eslintrc.js文件,该文件中定义了ESLint的基础配置,一个最为简单的配置如下所示:

module.exports = {

    parser:  '@typescript-eslint/parser', //定义ESLint的解析器
    extends: ['plugin:@typescript-eslint/recommended'],//定义文件继承的子规范
    plugins: ['@typescript-eslint'],//定义了该eslint文件所依赖的插件
    env:{                          //指定代码的运行环境
        browser: true,
        node: true,
    }                               
}
  • 在ts项目中必须执行解析器为@typescript-eslint/parser,才能正确的检测和规范TS代码
  • env环境变量配置,形如console属性只有在browser环境下才会存在,如果没有设置支持browser,那么可能报console is undefined的错误。

二、用ESLint来规范React代码

如果在你的TS项目中同时使用了React,那么为了检测和规范React代码的书写必须安装插件eslint-plugin-react,然后增加配置:

module.exports = {

    parser:  '@typescript-eslint/parser',
    extends: [
    'plugin:react/recommended'  
    'plugin:@typescript-eslint/recommended'
    ],                              //使用推荐的React代码检测规范
    plugins: ['@typescript-eslint'],
    env:{                         
        browser: true,
        node: true,
    },
    settings: {             //自动发现React的版本,从而进行规范react代码
        "react": {
            "pragma": "React",
            "version": "detect"
        }
    }, 
    parserOptions: {        //指定ESLint可以解析JSX语法
        "ecmaVersion": 2019,
        "sourceType": 'module',
        "ecmaFeatures":{
            jsx:true
        }
    }
    rules: {
    
    }
}

在Rules中可以自定义你的React代码编码规范。

三、结合Prettier和ESLint来规范代码

  Prettier中文的意思是漂亮的、美丽的,是一个流行的代码格式化的工具,我们来看如何结合ESLint来使用。首先我们需要安装三个依赖:

npm i -g prettier eslint-config-prettier eslint-plugin-prettier

其中:

  • prettier:prettier插件的核心代码
  • eslint-config-prettier:解决ESLint中的样式规范和prettier中样式规范的冲突,以prettier的样式规范为准,使ESLint中的样式规范自动失效
  • eslint-plugin-prettier:将prettier作为ESLint规范来使用

然后在项目的根目录下创建.prettierrc.js文件:

module.exports =  {
    "printWidth": 120,
    "semi": false,
    "singleQuote": true,
    "trailingComma": "all",
    "bracketSpacing": false,
    "jsxBracketSameLine": true,
    "arrowParens": "avoid",
    "insertPragma": true,
    "tabWidth": 4,
    "useTabs": false  
  };

接着修改.eslintrc.js文件,引入prettier:

module.exports = {
    parser:  '@typescript-eslint/parser',
    extends:[ 
    'prettier/@typescript-eslint',
    'plugin:prettier/recommended'
    ],
    settings: {
        "react": {
            "pragma": "React",
            "version": "detect"
        }
    },
    parserOptions: {
        "ecmaVersion": 2019,
        "sourceType": 'module',
        "ecmaFeatures":{
            jsx:true
        }
    },
    env:{
        browser: true,
        node: true,
    }  

上述新增的extends的配置中:

  • prettier/@typescript-eslint:使得@typescript-eslint中的样式规范失效,遵循prettier中的样式规范
  • plugin:prettier/recommended:使用prettier中的样式规范,且如果使得ESLint会检测prettier的格式问题,同样将格式问题以error的形式抛出

四、在VSCode中集成ESLint配置

  为了开发方便我们可以在VSCode中集成ESLint的配置,使得代码在保存或者代码变动的时候自动进行ESLint的fix过程。

  首先需要安装VSCode的ESLint插件,安装插件完毕后,在settings.json文件中修改其配置文件为:

{
       "eslint.enable": true,  //是否开启vscode的eslint
       "eslint.autoFixOnSave": true, //是否在保存的时候自动fix eslint
       "eslint.options": {    //指定vscode的eslint所处理的文件的后缀
           "extensions": [
               ".js",
               ".vue",
               ".ts",
               ".tsx"
           ]
       },
       "eslint.validate": [     //确定校验准则
           "javascript",
           "javascriptreact",
           {
               "language": "html",
               "autoFix": true
           },
           {
               "language": "vue",
               "autoFix": true
           },
           {
               "language": "typescript",
               "autoFix": true
           },
           {
               "language": "typescriptreact",
               "autoFix": true
           }
       ]
}

主要注意的有两点:

  • eslint.options中可以通过configFile属性来执行eslint规范的绝对路径,默认会向上查找,在根路径中指定。
  • eslint.validate中必须通过{ language: XXX}的形式来指定typescript和typescriptreact

五、husky和lint-staged构建代码工作流

  首先来看husky,Husky 能够帮你阻挡住不好的代码提交和推送,首先我们在package.json中定义如下的script:

 "scripts": {
   "lint": "eslint src --fix --ext .ts,.tsx "
}

接着在package.json定义husky的配置:

 "husky": {
   "hooks": {
      "pre-commit": "npm run lint"
    }
},

我们在git的hook的阶段来执行相应的命令,比如上述的例子是在pre-commit这个hook也就是在提交之前进行lint的检测。

  接着来看lint-staged,上述我们通过在husky的pre-comit这个hook中执行一个npm命令来做lint校验。除了定义个npm lint命令外,我们也可以直接通过使用lint-staged,来在提交前检测代码的规范。
  使用lint-staged来规范代码的方式如下,我们修改package.json文件为:

{
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.{.ts,.tsx}": [
      "eslint",
      "git add"
    ]
  }
}

同样在git commit的时候会做lint的检测。

六、gitlab的CI/CD来规范代码

   仅仅通过git的hook来执行代码的规范检测有一个问题,我们可以在git commit的时候通过--no-verify来跳过代码的规范检测。但是某些情况下,我们对于某一个重要分支,该分支上的代码必须完整通过代码的规范检测,此时我们可以使用gitlab的CI/CD。

   同样在package.json中增加一个lint的npm 命令:

  "scripts": {
     "lint": "eslint src --fix --ext .ts,.tsx "
  }

然后在根目录增加.gitlab-ci.yml文件,该文件中的配置为:

stages:
  - lint

before_script:
  - git fetch --all
  - npm install 

lint:
  stage: lint
  script:
    - npm run lint
  only
    - 特定分支1
    - 特定分支2

  然后配置相应的gitlab runner,这里不具体详描,最后的结果就是在我们指定的分支上的提交或者merge都会进行所配置的命令检测。这样保证了特定分支不受git commit跳过操作--no-verify的影响。

彻彻底底教会你使用Redux-saga(包含样例代码)

Redux-saga使用心得总结(包含样例代码),本文的样例代码地址:样例代码地址 ,欢迎star


最近将项目中redux的中间件,从redux-thunk替换成了redux-saga,做个笔记总结一下redux-saga的使用心得,阅读本文需要了解什么是redux,redux中间件的用处是什么?如果弄懂上述两个概念,就可以继续阅读本文。

  • redux-thunk处理副作用的缺点
  • redux-saga写一个hellosaga
  • redux-saga的使用技术细节
  • redux-saga实现一个登陆和列表样例

1.redux-thunk处理副作用的缺点

(1)redux的副作用处理

redux中的数据流大致是:

UI—————>action(plain)—————>reducer——————>state——————>UI

default

redux是遵循函数式编程的规则,上述的数据流中,action是一个原始js对象(plain object)且reducer是一个纯函数,对于同步且没有副作用的操作,上述的数据流起到可以管理数据,从而控制视图层更新的目的。

但是如果存在副作用,比如ajax异步请求等等,那么应该怎么做?

如果存在副作用函数,那么我们需要首先处理副作用函数,然后生成原始的js对象。如何处理副作用操作,在redux中选择在发出action,到reducer处理函数之间使用中间件处理副作用。

redux增加中间件处理副作用后的数据流大致如下:

UI——>action(side function)—>middleware—>action(plain)—>reducer—>state—>UI

default

在有副作用的action和原始的action之间增加中间件处理,从图中我们也可以看出,中间件的作用就是:

转换异步操作,生成原始的action,这样,reducer函数就能处理相应的action,从而改变state,更新UI。

(2)redux-thunk

在redux中,thunk是redux作者给出的中间件,实现极为简单,10多行代码:

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

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

这几行代码做的事情也很简单,判别action的类型,如果action是函数,就调用这个函数,调用的步骤为:

action(dispatch, getState, extraArgument);

发现实参为dispatch和getState,因此我们在定义action为thunk函数是,一般形参为dispatch和getState。

(3)redux-thunk的缺点

hunk的缺点也是很明显的,thunk仅仅做了执行这个函数,并不在乎函数主体内是什么,也就是说thunk使
得redux可以接受函数作为action,但是函数的内部可以多种多样。比如下面是一个获取商品列表的异步操作所对应的action:

export default ()=>(dispatch)=>{
    fetch('/api/goodList',{ //fecth返回的是一个promise
      method: 'get',
      dataType: 'json',
    }).then(function(json){
      var json=JSON.parse(json);
      if(json.msg==200){
        dispatch({type:'init',data:json.data});
      }
    },function(error){
      console.log(error);
    });
};

从这个具有副作用的action中,我们可以看出,函数内部极为复杂。如果需要为每一个异步操作都如此定义一个action,显然action不易维护。

action不易维护的原因:

  • action的形式不统一
  • 就是异步操作太为分散,分散在了各个action中

2.redux-saga写一个hellosaga

跟redux-thunk不同的是,redux-saga是控制执行的generator,在redux-saga中action是原始的js对象,把所有的异步副作用操作放在了saga函数里面。这样既统一了action的形式,又使得异步操作集中可以被集中处理。

redux-saga是通过genetator实现的,如果不支持generator需要通过插件babel-polyfill转义。我们接着来实现一个输出hellosaga的例子。

(1)创建一个helloSaga.js文件

export function * helloSaga() {
  console.log('Hello Sagas!');
}

(2)在redux中使用redux-saga中间件

在main.js中:

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import { helloSaga } from './sagas'
const sagaMiddleware=createSagaMiddleware();
const store = createStore(
 reducer,
 applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(helloSaga);
//会输出Hello, Sagas!

和调用redux的其他中间件一样,如果想使用redux-saga中间件,那么只要在applyMiddleware中调用一个createSagaMiddleware的实例。唯一不同的是需要调用run方法使得generator可以开始执行。

3.redux-saga的使用技术细节

redux-saga除了上述的action统一、可以集中处理异步操作等优点外,redux-saga中使用声明式的Effect以及提供了更加细腻的控制流。

(1)声明式的Effect

redux-saga中最大的特点就是提供了声明式的Effect,声明式的Effect使得redux-saga监听原始js对象形式的action,并且可以方便单元测试,我们一一来看。

  • 首先,在redux-saga中提供了一系列的api,比如take、put、all、select等API ,在redux-saga中将这一系列的api都定义为Effect。这些Effect执行后,当函数resolve时返回一个描述对象,然后redux-saga中间件根据这个描述对象恢复执行generator中的函数。

首先来看redux-thunk的大体过程:

action1(side function)—>redux-thunk监听—>执行相应的有副作用的方法—>action2(plain object)

2

转化到action2是一个原始js对象形式的action,然后执行reducer函数就会更新store中的state。

而redux-saga的大体过程如下:

action1(plain object)——>redux-saga监听—>执行相应的Effect方法——>返回描述对象—>恢复执行异步和副作用函数—>action2(plain object)

default

对比redux-thunk我们发现,redux-saga中监听到了原始js对象action,并不会马上执行副作用操作,会先通过Effect方法将其转化成一个描述对象,然后再将描述对象,作为标识,再恢复执行副作用函数。

通过使用Effect类函数,可以方便单元测试,我们不需要测试副作用函数的返回结果。只需要比较执行Effect方法后返回的描述对象,与我们所期望的描述对象是否相同即可。

举例来说,call方法是一个Effect类方法:

import { call } from 'redux-saga/effects'

function* fetchProducts() {
  const products = yield call(Api.fetch, '/products')
  // ...
}

上述代码中,比如我们需要测试Api.fetch返回的结果是否符合预期,通过调用call方法,返回一个描述对象。这个描述对象包含了所需要调用的方法和执行方法时的实际参数,我们认为只要描述对象相同,也就是说只要调用的方法和执行该方法时的实际参数相同,就认为最后执行的结果肯定是满足预期的,这样可以方便的进行单元测试,不需要模拟Api.fetch函数的具体返回结果。

import { call } from 'redux-saga/effects'
import Api from '...'

const iterator = fetchProducts()

// expects a call instruction
assert.deepEqual(
  iterator.next().value,
  call(Api.fetch, '/products'),
  "fetchProducts should yield an Effect call(Api.fetch, './products')"
)

(2)Effect提供的具体方法

下面来介绍几个Effect中常用的几个方法,从低阶的API,比如take,call(apply),fork,put,select等,以及高阶API,比如takeEvery和takeLatest等,从而加深对redux-saga用法的认识(这节可能比较生涩,在第三章中会结合具体的实例来分析,本小节先对各种Effect有一个初步的了解)。

引入:

import {take,call,put,select,fork,takeEvery,takeLatest} from 'redux-saga/effects'
  • take

take这个方法,是用来监听action,返回的是监听到的action对象。比如:

const loginAction = {
   type:'login'
}

在UI Component中dispatch一个action:

dispatch(loginAction)

在saga中使用:

const action = yield take('login');

可以监听到UI传递到中间件的Action,上述take方法的返回,就是dipath的原始对象。一旦监听到login动作,返回的action为:

{
  type:'login'
}
  • call(apply)

call和apply方法与js中的call和apply相似,我们以call方法为例:

call(fn, ...args)

call方法调用fn,参数为args,返回一个描述对象。不过这里call方法传入的函数fn可以是普通函数,也可以是generator。call方法应用很广泛,在redux-saga中使用异步请求等常用call方法来实现。

yield call(fetch,'/userInfo',username)
  • put

在前面提到,redux-saga做为中间件,工作流是这样的:

UI——>action1————>redux-saga中间件————>action2————>reducer..

从工作流中,我们发现redux-saga执行完副作用函数后,必须发出action,然后这个action被reducer监听,从而达到更新state的目的。相应的这里的put对应与redux中的dispatch,工作流程图如下:

default

从图中可以看出redux-saga执行副作用方法转化action时,put这个Effect方法跟redux原始的dispatch相似,都是可以发出action,且发出的action都会被reducer监听到。put的使用方法:

 yield put({type:'login'})
  • select

put方法与redux中的dispatch相对应,同样的如果我们想在中间件中获取state,那么需要使用select。select方法对应的是redux中的getState,用户获取store中的state,使用方法:

const state= yield select()
  • fork

fork方法在第三章的实例中会详细的介绍,这里先提一笔,fork方法相当于web work,fork方法不会阻塞主线程,在非阻塞调用中十分有用。

  • takeEvery和takeLatest

takeEvery和takeLatest用于监听相应的动作并执行相应的方法,是构建在take和fork上面的高阶api,比如要监听login动作,好用takeEvery方法可以:

takeEvery('login',loginFunc)

takeEvery监听到login的动作,就会执行loginFunc方法,除此之外,takeEvery可以同时监听到多个相同的action。

takeLatest方法跟takeEvery是相同方式调用:

takeLatest('login',loginFunc)

与takeLatest不同的是,takeLatest是会监听执行最近的那个被触发的action。

4.redux-saga实现一个登陆和列表样例

接着我们来实现一个redux-saga样例,存在一个登陆页,登陆成功后,显示列表页,并且,在列表页,可

以点击登出,返回到登陆页。例子的最终展示效果如下:

login

样例的功能流程图为:

default

接着我们按照上述的流程来一步步的实现所对应的功能。

(1)LoginPanel(登陆页)

登陆页的功能包括

  • 输入时时保存用户名
  • 输入时时保存密码
  • 点击sign in 请求判断是否登陆成功

I)输入时时保存用户名和密码

用户名输入框和密码框onchange时触发的函数为:

 changeUsername:(e)=>{
    dispatch({type:'CHANGE_USERNAME',value:e.target.value});
 },
changePassword:(e)=>{
  dispatch({type:'CHANGE_PASSWORD',value:e.target.value});
}

在函数中最后会dispatch两个action:CHANGE_USERNAME和CHANGE_PASSWORD

在saga.js文件中监听这两个方法并执行副作用函数,最后put发出转化后的action,给reducer函数调用:

function * watchUsername(){
  while(true){
    const action= yield take('CHANGE_USERNAME');
    yield put({type:'change_username',
    value:action.value});
  }
}
function * watchPassword(){
  while(true){
    const action=yield take('CHANGE_PASSWORD');
    yield put({type:'change_password',
    value:action.value});
  }
}

最后在reducer中接收到redux-saga的put方法传递过来的action:change_username和change_password,然后更新state。

II)监听登陆事件判断登陆是否成功

在UI中发出的登陆事件为:

toLoginIn:(username,password)=>{
  dispatch({type:'TO_LOGIN_IN',username,password});
}

登陆事件的action为:TO_LOGIN_IN.对于登入事件的处理函数为:

 while(true){
    //监听登入事件
    const action1=yield take('TO_LOGIN_IN');
    const res=yield call(fetchSmart,'/login',{
      method:'POST',
      body:JSON.stringify({
        username:action1.username,
        password:action1.password
    })
    if(res){
      put({type:'to_login_in'});
    }
});

在上述的处理函数中,首先监听原始动作提取出传递来的用户名和密码,然后请求是否登陆成功,如果登陆成功有返回值,则执行put的action:to_login_in.

(2) LoginSuccess(登陆成功列表展示页)

登陆成功后的页面功能包括:

  • 获取列表信息,展示列表信息
  • 登出功能,点击可以返回登陆页面

I)获取列表信息

import {delay} from 'redux-saga';

function * getList(){
  try {
   yield delay(3000);
   const res = yield call(fetchSmart,'/list',{
     method:'POST',
     body:JSON.stringify({})
   });
   yield put({type:'update_list',list:res.data.activityList});
 } catch(error) {
   yield put({type:'update_list_error', error});
 }
}

为了演示请求过程,我们在本地mock,通过redux-saga的工具函数delay,delay的功能相当于延迟xx秒,因为真实的请求存在延迟,因此可以用delay在本地模拟真实场景下的请求延迟。

II)登出功能

const action2=yield take('TO_LOGIN_OUT');
yield put({type:'to_login_out'});

与登入相似,登出的功能从UI处接受action:TO_LOGIN_OUT,然后转发action:to_login_out

(3) 完整的实现登入登出和列表展示的代码

function * getList(){
  try {
   yield delay(3000);
   const res = yield call(fetchSmart,'/list',{
     method:'POST',
     body:JSON.stringify({})
   });
   yield put({type:'update_list',list:res.data.activityList});
 } catch(error) {
   yield put({type:'update_list_error', error});
 }
}

function * watchIsLogin(){
  while(true){
    //监听登入事件
    const action1=yield take('TO_LOGIN_IN');
    
    const res=yield call(fetchSmart,'/login',{
      method:'POST',
      body:JSON.stringify({
        username:action1.username,
        password:action1.password
      })
    });
    
    //根据返回的状态码判断登陆是否成功
    if(res.status===10000){
      yield put({type:'to_login_in'});
      //登陆成功后获取首页的活动列表
      yield call(getList);
    }
    
    //监听登出事件
    const action2=yield take('TO_LOGIN_OUT');
    yield put({type:'to_login_out'});
  }
}

通过请求状态码判断登入是否成功,在登陆成功后,可以通过:

yield call(getList)

的方式调用获取活动列表的函数getList。这样咋一看没有什么问题,但是注意call方法调用是会阻塞主线程的,具体来说:

  • 在call方法调用结束之前,call方法之后的语句是无法执行的

  • 如果call(getList)存在延迟,call(getList)之后的语句 const action2=yieldtake('TO_LOGIN_OUT')在call方法返回结果之前无法执行

  • 在延迟期间的登出操作会被忽略。

用框图可以更清楚的分析:

default

call方法调用阻塞主线程的具体效果如下动图所示:

login_1

白屏时为请求列表的等待时间,在此时,我们点击登出按钮,无法响应登出功能,直到请求列表成功,展示列表信息后,点击登出按钮才有相应的登出功能。也就是说call方法阻塞了主线程。

(4) 无阻塞调用

我们在第二章中,介绍了fork方法可以类似与web work,fork方法不会阻塞主线程。应用于上述例子,我们可以将:

yield call(getList)

修改为:

yield fork(getList)

这样展示的结果为:

login_2

通过fork方法不会阻塞主线程,在白屏时点击登出,可以立刻响应登出功能,从而返回登陆页面。

5.总结

通过上述章节,我们可以概括出redux-saga做为redux中间件的全部优点:

  • 统一action的形式,在redux-saga中,从UI中dispatch的action为原始对象

  • 集中处理异步等存在副作用的逻辑

  • 通过转化effects函数,可以方便进行单元测试

  • 完善和严谨的流程控制,可以较为清晰的控制复杂的逻辑。

前端面试总结——css篇

1.css盒模型

简介:就是用来装页面上的元素的矩形区域。CSS中的盒子模型包括IE盒子模型和标准的W3C盒子模型。

box-sizing(有3个值哦):border-box,padding-box,content-box.

  • 标准盒子模型:

2018-07-10 4 24 03

  • IE盒子模型:

2018-07-10 4 24 12

区别:从图中我们可以看出,这两种盒子模型最主要的区别就是width的包含范围,在标准的盒子模型中,width指content部分的宽度,在IE盒子模型中,width表示content+padding+border这三个部分的宽度,故这使得在计算整个盒子的宽度时存在着差异:

标准盒子模型的盒子宽度:左右border+左右padding+width
IE盒子模型的盒子宽度:width

在CSS3中引入了box-sizing属性,box-sizing:content-box;表示标准的盒子模型,box-sizing:border-box表示的是IE盒子模型

最后,前面我们还提到了,box-sizing:padding-box,这个属性值的宽度包含了左右padding+width

也很好理解性记忆,包含什么,width就从什么开始算起。

2.画一条0.5px的线

  • 采用meta viewport的方式

  • 采用 border-image的方式

  • 采用transform: scale()的方式

3.link标签和import标签的区别

  • link属于html标签,而@import是css提供的
  • 页面被加载时,link会同时被加载,而@import引用的css会等到页面加载结束后加载。
  • link是html标签,因此没有兼容性,而@import只有IE5以上才能识别。
  • link方式样式的权重高于@import的。

4.transition和animation的区别

Animation和transition大部分属性是相同的,他们都是随时间改变元素的属性值,他们的主要区别是transition需要触发一个事件才能改变属性,而animation不需要触发任何事件的情况下才会随时间改变属性值,并且transition为2帧,从from .... to,而animation可以一帧一帧的。

5.Flex布局

文章链接:
http://www.ruanyifeng.com/blog/2015/07/flex-grammar.html?utm_source=tuicool(语法篇)
http://www.ruanyifeng.com/blog/2015/07/flex-examples.html(实例篇)

Flex是Flexible Box的缩写,意为"弹性布局",用来为盒状模型提供最大的灵活性。
布局的传统解决方案,基于盒状模型,依赖 display属性 + position属性 + float属性。它对于那些特殊布局非常不方便,比如,垂直居中就不容易实现。

简单的分为容器属性和元素属性
容器的属性:

  • flex-direction:决定主轴的方向(即子item的排列方法)
    .box {
    flex-direction: row | row-reverse | column | column-reverse;
    }
  • flex-wrap:决定换行规则
    .box{
    flex-wrap: nowrap | wrap | wrap-reverse;
    }
  • flex-flow:
    .box {
    flex-flow: || ;
    }
  • justify-content:对其方式,水平主轴对齐方式
  • align-items:对齐方式,竖直轴线方向

项目的属性(元素的属性):

  • order属性:定义项目的排列顺序,顺序越小,排列越靠前,默认为0
  • flex-grow属性:定义项目的放大比例,即使存在空间,也不会放大
  • flex-shrink属性:定义了项目的缩小比例,当空间不足的情况下会等比例的缩小,如果定义个item的flow-shrink为0,则为不缩小
  • flex-basis属性:定义了在分配多余的空间,项目占据的空间。
  • flex:是flex-grow和flex-shrink、flex-basis的简写,默认值为0 1 auto。
  • align-self:允许单个项目与其他项目不一样的对齐方式,可以覆盖align-items,默认属性为auto,表示继承父元素的align-items

比如说,用flex实现圣杯布局

6.BFC(块级格式化上下文,用于清楚浮动,防止margin重叠等)

直译成:块级格式化上下文,是一个独立的渲染区域,并且有一定的布局规则。

  • BFC区域不会与float box重叠
  • BFC是页面上的一个独立容器,子元素不会影响到外面
  • 计算BFC的高度时,浮动元素也会参与计算

那些元素会生成BFC:

  • 根元素
  • float不为none的元素
  • position为fixed和absolute的元素
  • display为inline-block、table-cell、table-caption,flex,inline-flex的元素
  • overflow不为visible的元素

7.垂直居中的方法

(1)margin:auto法

css:

div{
  width: 400px;
  height: 400px;
  position: relative;
  border: 1px solid #465468;
 }
 img{
      position: absolute;
      margin: auto;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
 }

html:

<div>
 <img src="mm.jpg">
</div>

定位为上下左右为0,margin:0可以实现脱离文档流的居中.

(2)margin负值法

.container{
  width: 500px;
  height: 400px;
  border: 2px solid #379;
  position: relative;
}
.inner{
  width: 480px;
  height: 380px;
  background-color: #746;
  position: absolute;
  top: 50%;
  left: 50%;
  margin-top: -190px; /*height的一半*/
  margin-left: -240px; /*width的一半*/
 }

补充:其实这里也可以将marin-top和margin-left负值替换成,
transform:translateX(-50%)和transform:translateY(-50%)

(3)table-cell(未脱离文档流的)

设置父元素的display:table-cell,并且vertical-align:middle,这样子元素可以实现垂直居中。

css:
div{
    width: 300px;
    height: 300px;
    border: 3px solid #555;
    display: table-cell;
    vertical-align: middle;
    text-align: center;
}
img{
    vertical-align: middle;
}

(4)利用flex

将父元素设置为display:flex,并且设置align-items:center;justify-content:center;

css:
.container{
      width: 300px;
      height: 200px;
      border: 3px solid #546461;
      display: -webkit-flex;
      display: flex;
      -webkit-align-items: center;
      align-items: center;
      -webkit-justify-content: center;
      justify-content: center;
 }
 .inner{
      border: 3px solid #458761;
      padding: 20px;
 }

8.关于js动画和css3动画的差异性

渲染线程分为main thread和compositor thread,如果css动画只改变transform和opacity,这时整个CSS动画得以在compositor trhead完成(而js动画则会在main thread执行,然后出发compositor thread进行下一步操作),特别注意的是如果改变transform和opacity是不会layout或者paint的。
区别:

  • 功能涵盖面,js比css大
  • 实现/重构难度不一,CSS3比js更加简单,性能跳优方向固定
  • 对帧速表现不好的低版本浏览器,css3可以做到自然降级
  • css动画有天然事件支持
  • css3有兼容性问题

9.块元素和行元素

块元素:独占一行,并且有自动填满父元素,可以设置margin和pading以及高度和宽度
行元素:不会独占一行,width和height会失效,并且在垂直方向的padding和margin会失
效。

10.多行元素的文本省略号

 display: -webkit-box
-webkit-box-orient:vertical
-webkit-line-clamp:3
overflow:hidden

11.visibility=hidden, opacity=0,display:none

opacity=0,该元素隐藏起来了,但不会改变页面布局,并且,如果该元素已经绑定一些事件,如click事件,那么点击该区域,也能触发点击事件的visibility=hidden,该元素隐藏起来了,但不会改变页面布局,但是不会触发该元素已经绑定的事件display=none,把元素隐藏起来,并且会改变页面布局,可以理解成在页面中把该元素删除掉一样。

12.双边距重叠问题(外边距折叠)

多个相邻(兄弟或者父子关系)普通流的块元素垂直方向marigin会重叠

折叠的结果为:

两个相邻的外边距都是正数时,折叠结果是它们两者之间较大的值。
两个相邻的外边距都是负数时,折叠结果是两者绝对值的较大值。
两个外边距一正一负时,折叠结果是两者的相加的和。

响应式布局的常用解决方案对比(媒体查询、百分比、rem和vw/vh)

响应式布局的常用解决方案对比(媒体查询、百分比、rem和vw/vh)


简要介绍:前端开发中,静态网页通常需要适应不同分辨率的设备,常用的自适应解决方案包括媒体查询、百分比、rem和vw/vh等。本文从px单位出发,分析了px在移动端布局中的不足,接着介绍了几种不同的自适应解决方案。

  • px和视口
  • 媒体查询
  • 百分比
  • 自适应场景下的rem解决方案
  • 通过vw/vh来实现自适应

一、px和视口

在静态网页中,我们经常用像素(px)作为单位,来描述一个元素的宽高以及定位信息。在pc端,通常认为css中,1px所表示的真实长度是固定的。

那么,px真的是一个设备无关,跟长度单位米和分米一样是固定大小的吗?

答案是否定的,下面图1.1和图1.2分别表示pc端下和移动端下的显示结果,在网页中我们设置的font-size统一为16px。

px_text_pc

图1.1 pc端下font-size为16px时的显示结果

px_text_mobile

图1.2 移动端下font-size为16px时的显示结果

从上面两幅图的对比可以看出,字体都是16px,显然在pc端中文字正常显示,而在移动端文字很小,几乎看不到,说明在css中1px并不是固定大小,直观从我们发现在移动端1px所表示的长度较小,所以导致文字显示不清楚。

那么css中的1px的真实长度到底由什么决定呢?

为了理清楚这个概念我们首先介绍像素和视口的概念

1. 像素

像素是网页布局的基础,一个像素表示了计算机屏幕所能显示的最小区域,像素分为两种类型:css像素和物理像素。

我们在js或者css代码中使用的px单位就是指的是css像素,物理像素也称设备像素,只与设备或者说硬件有关,同样尺寸的屏幕,设备的密度越高,物理像素也就越多。下表表示css像素和物理像素的具体区别:

css像素 为web开发者提供,在css中使用的一个抽象单位
物理像素 只与设备的硬件密度有关,任何设备的物理像素都是固定的

那么css像素与物理像素的转换关系是怎么样的呢?为了明确css像素和物理像素的转换关系,必须先了解视口是什么。

2. 视口

广义的视口,是指浏览器显示内容的屏幕区域,狭义的视口包括了布局视口、视觉视口和理想视口

(1) 布局视口(layout viewport)

布局视口定义了pc网页在移动端的默认布局行为,因为通常pc的分辨率较大,布局视口默认为980px。也就是说在不设置网页的viewport的情况下,pc端的网页默认会以布局视口为基准,在移动端进行展示。因此我们可以明显看出来,默认为布局视口时,根植于pc端的网页在移动端展示很模糊。

(2) 视觉视口(visual viewport)

视觉视口表示浏览器内看到的网站的显示区域,用户可以通过缩放来查看网页的显示内容,从而改变视觉视口。视觉视口的定义,就像拿着一个放大镜分别从不同距离观察同一个物体,视觉视口仅仅类似于放大镜中显示的内容,因此视觉视口不会影响布局视口的宽度和高度。

(3) 理想视口(ideal viewport)

理想视口或者应该全称为“理想的布局视口”,在移动设备中就是指设备的分辨率。换句话说,理想视口或者说分辨率就是给定设备物理像素的情况下,最佳的“布局视口”。

上述视口中,最重要的是要明确理想视口的概念,在移动端中,理想视口或者说分辨率跟物理像素之间有什么关系呢?

为了理清分辨率和物理像素之间的联系,我们介绍一个用DPR(Device pixel ratio)设备像素比来表示,则可以写成:

1 DPR = 物理像素/分辨率

在不缩放的情况下,一个css像素就对应一个dpr,也就是说,在不缩放

1 CSS像素 = 物理像素/分辨率

此外,在移动端的布局中,我们可以通过viewport元标签来控制布局,比如一般情况下,我们可以通过下述标签使得移动端在理想视口下布局:

<meta id="viewport" name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1; user-scalable=no;">

上述meta标签的每一个属性的详细介绍如下:

属性名 取值 描述
width 正整数 定义布局视口的宽度,单位为像素
height 正整数 定义布局视口的高度,单位为像素,很少使用
initial-scale [0,10] 初始缩放比例,1表示不缩放
minimum-scale [0,10] 最小缩放比例
maximum-scale [0,10] 最大缩放比例
user-scalable yes/no 是否允许手动缩放页面,默认值为yes

其中我们来看width属性,在移动端布局时,在meta标签中我们会将width设置称为device-width,device-width一般是表示分辨率的宽,通过width=device-width的设置我们就将布局视口设置成了理想的视口。

3. px与自适应

上述我们了解到了当通过viewport元标签,设置布局视口为理想视口时,1个css像素可以表示成:

1 CSS像素 = 物理像素/分辨率

我们直到,在pc端的布局视口通常情况下为980px,移动端以iphone6为例,分辨率为375 * 667,也就是说布局视口在理想的情况下为375px。比如现在我们有一个750px * 1134px的视觉稿,那么在pc端,一个css像素可以如下计算:

PC端: 1 CSS像素 = 物理像素/分辨率 = 750 / 980 =0.76 px

而在iphone6下:

iphone6:1 CSS像素 = 物理像素 /分辨率 = 750 / 375 = 2 px

也就是说在PC端,一个CSS像素可以用0.76个物理像素来表示,而iphone6中 一个CSS像素表示了2个物理像素。此外不同的移动设备分辨率不同,也就是1个CSS像素可以表示的物理像素是不同的,因此如果在css中仅仅通过px作为长度和宽度的单位,造成的结果就是无法通过一套样式,实现各端的自适应。

二、媒体查询

在前面我们说到,不同端的设备下,在css文件中,1px所表示的物理像素的大小是不同的,因此通过一套样式,是无法实现各端的自适应。由此我们联想:

如果一套样式不行,那么能否给每一种设备各一套不同的样式来实现自适应的效果?

答案是肯定的。

使用@media媒体查询可以针对不同的媒体类型定义不同的样式,特别是响应式页面,可以针对不同屏幕的大小,编写多套样式,从而达到自适应的效果。举例来说:

@media screen and (max-width: 960px){
    body{
      background-color:#FF6699
    }
}

@media screen and (max-width: 768px){
    body{
      background-color:#00FF66;
    }
}

@media screen and (max-width: 550px){
    body{
      background-color:#6633FF;
    }
}

@media screen and (max-width: 320px){
    body{
      background-color:#FFFF00;
    }
}

上述的代码通过媒体查询定义了几套样式,通过max-width设置样式生效时的最大分辨率,上述的代码分别对分辨率在0~320px,320px~550px,550px~768px以及768px~960px的屏幕设置了不同的背景颜色。

通过媒体查询,可以通过给不同分辨率的设备编写不同的样式来实现响应式的布局,比如我们为不同分辨率的屏幕,设置不同的背景图片。比如给小屏幕手机设置@2x图,为大屏幕手机设置@3x图,通过媒体查询就能很方便的实现。

但是媒体查询的缺点也很明显,如果在浏览器大小改变时,需要改变的样式太多,那么多套样式代码会很繁琐。

三、百分比

除了用px结合媒体查询实现响应式布局外,我们也可以通过百分比单位 " % " 来实现响应式的效果。

比如当浏览器的宽度或者高度发生变化时,通过百分比单位,通过百分比单位可以使得浏览器中的组件的宽和高随着浏览器的变化而变化,从而实现响应式的效果。

为了了解百分比布局,首先要了解的问题是:

css中的子元素中的百分比(%)到底是谁的百分比?

直观的理解,我们可能会认为子元素的百分比完全相对于直接父元素,height百分比相对于height,width百分比相对于width。当然这种理解是正确的,但是根据css的盒式模型,除了height、width属性外,还具有padding、border、margin等等属性。那么这些属性设置成百分比,是根据父元素的那些属性呢?此外还有border-radius和translate等属性中的百分比,又是相对于什么呢?下面来具体分析。

1. 百分比的具体分析

(1)子元素height和width的百分比

子元素的height或width中使用百分比,是相对于子元素的直接父元素,width相对于父元素的width,height相对于父元素的height。比如:

<div class="parent">
  <div class="child"></div>
</div>

如果设置:
.father{
width:200px;
height:100px;
}
.child{
width:50%;
height:50%;
}
展示的效果为:
2018-06-22 7 00 29

(2) top和bottom 、left和right

子元素的top和bottom如果设置百分比,则相对于直接非static定位(默认定位)的父元素的高度,同样

子元素的left和right如果设置百分比,则相对于直接非static定位(默认定位的)父元素的宽度。

展示的效果为:

2018-06-22 7 42 14

(3)padding

子元素的padding如果设置百分比,不论是垂直方向或者是水平方向,都相对于直接父亲元素的width,而与父元素的height无关。

举例来说:

.parent{
  width:200px;
  height:100px;
  background:green;
}
.child{
  width:0px;
  height:0px;
  background:blue;
  color:white;
  padding-top:50%;
  padding-left:50%;
}

展示的效果为:

2018-06-22 7 55 13

子元素的初始宽高为0,通过padding可以将父元素撑大,上图的蓝色部分是一个正方形,且边长为100px,说明padding不论宽高,如果设置成百分比都相对于父元素的width。

(4)margin

跟padding一样,margin也是如此,子元素的margin如果设置成百分比,不论是垂直方向还是水平方向,都相对于直接父元素的width。这里就不具体举例。

(5)border-radius

border-radius不一样,如果设置border-radius为百分比,则是相对于自身的宽度,举例来说:

  <div class="trangle"></div>

设置border-radius为百分比:

.trangle{
  width:100px;
  height:100px;
  border-radius:50%;
  background:blue;
  margin-top:10px;
}

展示效果为:

2018-06-22 8 09 20

除了border-radius外,还有比如translate、background-size等都是相对于自身的,这里就不一一举例。

2. 百分比单位布局应用

百分比单位在布局上应用还是很广泛的,这里介绍一种应用。

比如我们要实现一个固定长宽比的长方形,比如要实现一个长宽比为4:3的长方形,我们可以根据padding属性来实现,因为padding不管是垂直方向还是水平方向,百分比单位都相对于父元素的宽度,因此我们可以设置padding-top为百分比来实现,长宽自适应的长方形:

<div class="trangle"></div>

设置样式让其自适应:

.trangle{
  height:0;
  width:100%;
  padding-top:75%;
}

通过设置padding-top:75%,相对比宽度的75%,因此这样就设置了一个长宽高恒定比例的长方形,具体效果展示如下:

jest

3. 百分比单位缺点

从上述对于百分比单位的介绍我们很容易看出如果全部使用百分比单位来实现响应式的布局,有明显的以下两个缺点:

(1)计算困难,如果我们要定义一个元素的宽度和高度,按照设计稿,必须换算成百分比单位。
(2)从小节1可以看出,各个属性中如果使用百分比,相对父元素的属性并不是唯一的。比如width和height相对于父元素的width和height,而margin、padding不管垂直还是水平方向都相对比父元素的宽度、border-radius则是相对于元素自身等等,造成我们使用百分比单位容易使布局问题变得复杂。

四、自适应场景下的rem解决方案

1. rem单位

首先来看,什么是rem单位。rem是一个灵活的、可扩展的单位,由浏览器转化像素并显示。与em单位不同,rem单位无论嵌套层级如何,都只相对于浏览器的根元素(HTML元素)的font-size。默认情况下,html元素的font-size为16px,所以:

    1 rem = 16px

为了计算方便,通常可以将html的font-size设置成:

    html{ font-size: 62.5% }

这种情况下:

    1 rem = 10px

2.通过rem来实现响应式布局

rem单位都是相对于根元素html的font-size来决定大小的,根元素的font-size相当于提供了一个基准,当页面的size发生变化时,只需要改变font-size的值,那么以rem为固定单位的元素的大小也会发生响应的变化。
因此,如果通过rem来实现响应式的布局,只需要根据视图容器的大小,动态的改变font-size即可。

function refreshRem() {
    var docEl = doc.documentElement;
    var width = docEl.getBoundingClientRect().width;
    var rem = width / 10;
    docEl.style.fontSize = rem + 'px';
    flexible.rem = win.rem = rem;
}
win.addEventListener('resize', refreshRem);

上述代码中将视图容器分为10份,font-size用十分之一的宽度来表示,最后在header标签中执行这段代码,就可以动态定义font-size的大小,从而1rem在不同的视觉容器中表示不同的大小,用rem固定单位可以实现不同容器内布局的自适应。

3. rem2px和px2rem

如果在响应式布局中使用rem单位,那么存在一个单位换算的问题,rem2px表示从rem换算成px,这个就不说了,只要rem乘以相应的font-size中的大小,就能换算成px。更多的应用是px2rem,表示的是从px转化为rem。

比如给定的视觉稿为750px(物理像素),如果我们要将所有的布局单位都用rem来表示,一种比较笨的办法就是对所有的height和width等元素,乘以相应的比例,现将视觉稿换算成rem单位,然后一个个的用rem来表示。另一种比较方便的解决方法就是,在css中我们还是用px来表示元素的大小,最后编写完css代码之后,将css文件中的所有px单位,转化成rem单位。

px2rem的原理也很简单,重点在于预处理以px为单位的css文件,处理后将所有的px变成rem单位。可以通过两种方式来实现:

1) webpack loader的形式:

npm install px2rem-loader

在webpack的配置文件中:

module.exports = {
  // ...
  module: {
    rules: [{
      test: /\.css$/,
      use: [{
        loader: 'style-loader'
      }, {
        loader: 'css-loader'
      }, {
        loader: 'px2rem-loader',
        // options here
        options: {
          remUni: 75,
          remPrecision: 8
        }
      }]
    }]
  }

}

2)webpack中使用postcss plugin

npm install postcss-loader

在webpack的plugin中:

var px2rem = require('postcss-px2rem');

module.exports = {
  module: {
    loaders: [
      {
        test: /\.css$/,
        loader: "style-loader!css-loader!postcss-loader"
      }
    ]
  },
  postcss: function() {
    return [px2rem({remUnit: 75})];
  }
}

4. rem 布局应用举例

网易新闻的移动端页面使用了rem布局,具体例子如下:

jest1

5. rem 布局的缺点

通过rem单位,可以实现响应式的布局,特别是引入相应的postcss相关插件,免去了设计稿中的px到rem的计算。rem单位在国外的一些网站也有使用,这里所说的rem来实现布局的缺点,或者说是小缺陷是:

在响应式布局中,必须通过js来动态控制根元素font-size的大小。

也就是说css样式和js代码有一定的耦合性。且必须将改变font-size的代码放在css样式之前。

五. 通过vw/vh来实现自适应

1. 什么是vw/vh ?

css3中引入了一个新的单位vw/vh,与视图窗口有关,vw表示相对于视图窗口的宽度,vh表示相对于视图窗口高度,除了vw和vh外,还有vmin和vmax两个相关的单位。各个单位具体的含义如下:

单位 含义
vw 相对于视窗的宽度,视窗宽度是100vw
vh 相对于视窗的高度,视窗高度是100vh
vmin vw和vh中的较小值
vmax vw和vh中的较大值

这里我们发现视窗宽高都是100vw/100vh,那么vw或者vh,下简称vw,很类似百分比单位。vw和%的区别为:

单位 含义
% 大部分相对于祖先元素,也有相对于自身的情况比如(border-radius、translate等)
vw/vh 相对于视窗的尺寸

从对比中我们可以发现,vw单位与百分比类似,单确有区别,前面我们介绍了百分比单位的换算困难,这里的vw更像"理想的百分比单位"。任意层级元素,在使用vw单位的情况下,1vw都等于视图宽度的百分之一。

2. vw单位换算

同样的,如果要将px换算成vw单位,很简单,只要确定视图的窗口大小(布局视口),如果我们将布局视口设置成分辨率大小,比如对于iphone6/7 375*667的分辨率,那么px可以通过如下方式换算成vw:

1px = (1/375)*100 vw

此外,也可以通过postcss的相应插件,预处理css做一个自动的转换,postcss-px-to-viewport可以自动将px转化成vw。
postcss-px-to-viewport的默认参数为:

var defaults = {
  viewportWidth: 320,
  viewportHeight: 568, 
  unitPrecision: 5,
  viewportUnit: 'vw',
  selectorBlackList: [],
  minPixelValue: 1,
  mediaQuery: false
};

通过指定视窗的宽度和高度,以及换算精度,就能将px转化成vw。

3. vw/vh单位的兼容性

可以在https://caniuse.com/ 查看各个版本的浏览器对vw单位的支持性。

2018-06-27 8 19 53

从上图我们发现,绝大多数的浏览器支持vw单位,但是ie9-11不支持vmin和vmax,考虑到vmin和vmax单位不常用,vw单位在绝大部分高版本浏览器内的支持性很好,但是opera浏览器整体不支持vw单位,如果需要兼容opera浏览器的布局,不推荐使用vw。

小结:本文介绍在布局中常用的单位,比如px、%、rem和vw等等,以及不同的单位在响应式布局中的优缺点。

优雅的学习webgl(1)—从0开始构造你的第一个webgl程序

优雅的学习webgl(1)—从0开始构造你的第一个webgl程序


    学习webgl也有小半年的时间了,有了一些心得和体会,在这里做一个记录,整个系列的代码都会给出,这篇文章是这个系列的第一篇文章,带你走进webgl的世界。

  • 什么是webgl
  • 用webgl画点
  • 用webgl实现一个彩色正方形

这个系列的源码地址为:源码的地址为: https://github.com/forthealllight/webgl-demo

一、什么是webgl

  在介绍什么是webgl之前,我们来看一个最简单的webgl程序。

<html lang="en">
  <head>
    <title>WebGL Demo</title>
    <meta charset="utf-8">
  </head>

  <body>
    <canvas id="glcanvas" width="640" height="480"></canvas>
  </body>

  <script>
    main();
    // start here
    function main() {
      const canvas = document.querySelector("#glcanvas");
      // Initialize the GL context
      const gl = canvas.getContext("webgl");
      // Only continue if WebGL is available and working
      if (!gl) {
        alert("你的浏览器不支持webgl");
        return;
      }

      // Set clear color to black, fully opaque
      gl.clearColor(0.0, 0.0, 0.0, 1.0);
      // Clear the color buffer with specified clear color
      gl.clear(gl.COLOR_BUFFER_BIT);
    }
  </script>
</html>

上述就是一个webgl的例子,做的事情很简单就是清空了webgl画布的颜色。在上述的例子中,我们没有引入什么其他的插件,就构建了一个webgl程序,因为webgl本身就是浏览器层面的。Webgl是内嵌在浏览器中的,你不必安装任何插件或者库,如果浏览器支持webgl,就可以通过getContext的方法创建一个webgl实例。

const gl = canvas.getContext("webgl");

    一提到webgl就容易跟3D渲染绘图联系在一起,实际上这两者并没有绝对关联,webgl也可以用来绘制2D平面图形,2D动画等等。那么webgl到底是什么呢?一句话概括就是:

通过绘图渲染技术OpenGL在浏览器里面进行图形渲染的技术

    了解过图形渲染技术的同学都听过OpenGL等,我们可以通过C语言,在window等平台上编写复杂和渲染出复杂的图形,通过OpenGL绘制和渲染图形有很高的平台要求以及编程语言的限制。而Webgl源自于OpenGL,从OpenGL2.0的着色器行为中诞生了适用于移动式穿戴设备的OpenGL ES标准,在这个标准下演化出了webgl,使得我们可以通过结合javascript语言和GLSL ES着色器语言,在浏览器中绘制出复杂的图形,不需要编译也不需要引入任何插件.

二、用webgl画点

    接着我们来看如何用webgl来绘制一个完整的图案,绘制图案跟上一小节最简单的清空画布的webgl程序不同。我们需要webgl中的两个重要的概念——着色器。
    webgl的本质就是通过顶点着色器和片元着色器,将图形渲染到浏览器中。之后我们会详细的介绍顶点着色器和片元着色器,这里可以简单的理解为顶点着色器决定了每个顶点的位置,片元着色器决定了图形的颜色。

1、初始化着色器程序

    绘制图案之前必须进行着色器程序初始化,根据着色球类型创建着色器对象,将着色器对象编译后绑定到程序对象,最后编译和连接程序对象从而完整了初始化的过程,接下来就可以使用程序对象来绘制图形。

    上面的两段话特别绕,总结就是:

GLSL着色器语言是以字符串的形式存在浏览器中的,为了能够将字符串编译成可以在显卡中运行的着色器程序,必须进行着色器程序初始化

在初始化着色器程序中我们必须用到两个对象着色器对象和程序对象,我们来简单介绍一下这两者。

  • 着色器对象: 我们前面提到了顶点着色器和片元着色器,着色器对象就是管理顶点着色器或者片元着色器的对象
  • 程序对象: 程序对象则是管理着色器对象的容器

我们用图来区别这两者的关系:

Lark20191204-145309

从这个图可以看出层级关系,我们要初始化着色器程序,就是按照如下的层级关系依次来初始化。

  1. 创建着色器对象(gl.createShader())
  2. 向着色器对象中填充着色器(gl.shaderSource())
  3. 编译着色球(gl.compileShader())
  4. 创建程序对象(gl.createProgram())
  5. 为程序对象分配着色器(gl.attashShader())
  6. 连接程序对象(gl.linkProgram())
  7. 使用程序对象(gl.useProgram())

结合上面图形的层次结构,以及上述的7步初始化着色器的步骤,我们可以来介绍着色器初始化函数initShaderProgram

function initShaderProgram(gl, vsSource, fsSource) {
  const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource); //创建顶点着色器对象
  const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);//创建片元着色器对象

  // Create the shader program

  const shaderProgram = gl.createProgram();
  gl.attachShader(shaderProgram, vertexShader);
  gl.attachShader(shaderProgram, fragmentShader);
  gl.linkProgram(shaderProgram);

  return shaderProgram;
}

这就是最上层的程序对象的创建过程,至于着色器对象的创建可以通过如下函数
loadShader:

function loadShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);

  return shader;
}

上述我们就完成了着色器程序的初始化,就可以在浏览器中编译GLSL ES语言渲染出图形。下面我们来看最简单的画点

2、画一个点

    我们上一小节将了如何初始化着色器,初始化着色器可以通过initShaderProgram方法,我们可以用webgl来画一个最简单的size为10的点

首先通过字符串的方式定义顶点着色器和片元着色器:

const vsSource = `
    ashouttribute vec4 aVertexPosition;

    uniform mat4 uModelViewMatrix;
    uniform mat4 uProjectionMatrix;

    void main() {
      gl_Position = vec4(0.0,0.0,0.0,1.0);
      gl_PointSize = 10.0;
    }
  `;

  // Fragment shader program

  const fsSource = `
    void main() {
      gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
    }
  `;

然后将这两个字符串传入initShaderProgram程序,就完成了着色器初始化。

const shaderProgram = initShaderProgram(gl, vsSource, fsSource)

最后清空画布,并使用这个着色器程序开始画图:

  gl.clearColor(0.0, 0.0, 0.0, 1.0);  
  gl.clearDepth(1.0);               
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
  gl.useProgram(shaderProgram);
  gl.drawArrays(gl.POINTS,0,1);

这样就在浏览器中画出了一个点。

Lark20191204-154158

源码的地址为: https://github.com/forthealllight/webgl-demo/tree/master/demo1

三、用webgl实现一个彩色正方形(选择性阅读)

    最后我们来实现一个较为复杂的例子,用webgl来画一个正方形。我们可以通过4次每次画一个点,画4个点,然后将这4个点连起来就成为了一个正方形,此外如果我们要一次性的画出4个点并连成正方形,就需要使用缓冲区。

我们可以简单理解,缓冲区保存了很多信息,我们可以读取缓冲区的信息,在一次绘制中绘出我们想要的图形。

我们来看创建一个缓冲区的步骤:

  1. 创建缓冲区对象(gl.createBuffer())
  2. 绑定缓冲区对象(gl.bindBuffer())
  3. 将数据写入缓冲区对象(gl.bufferData())
  4. 将缓冲区对象分配给一个attribute变量(gl.vertexAttribPointer())
  5. 开启attribute变量(gl.enableVertexAttribArray())

这个五步可以简记为:创建-绑定-写入-分配-开启这么几步,并且我们要使用缓冲区对象这五步是必不可免的.

我们可以定义initBuffers方法来创建缓冲区,我们可以分别创建一个顶点缓冲区(用于读取图形的顶点坐标)以及片元缓冲区(用于读取每个片元的颜色信息)

function initBuffers(gl) {
  //顶点缓冲区
  const positionBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);

  const positions = [
     1.0,  1.0,
    -1.0,  1.0,
     1.0, -1.0,
    -1.0, -1.0,
  ];

  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
  var colors = [
    1.0,  1.0,  1.0,  1.0,    // white
    1.0,  0.0,  0.0,  1.0,    // red
    0.0,  1.0,  0.0,  1.0,    // green
    0.0,  0.0,  1.0,  1.0,    // blue
  ];
 //颜色缓冲区
  const colorBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);

  return {
    position: positionBuffer,
    color: colorBuffer,
  };
}

然后,在绘图的时候,通过:

gl.bindBuffer();
gl.vertexAttribPointer();
gl.enableVertexAttribArray()

来分别使用缓冲区,除了缓冲区外,我们还需要在顶点着色器和片元着色器中定义全局变量,来接受从缓冲区的传递过来的值。这里先省略,后期在详细介绍缓冲区和着色器的时候会提到.

最后给出绘制出来的图像:

Lark20191204-162205

源码的地址为: https://github.com/forthealllight/webgl-demo/tree/master/demo2

    最后补充一下,在第二张的画一个点的例子中,我们机会没有引入任何插件,实际上虽然webgl是浏览器内置的不需要引入,但是为了方便矩阵操作等,也有一些比较实用的工具

总结一下ES6/ES7中promise、generator和async/await中的异常捕获方法

总结一下ES6/ES7中promise、generator和async/await中的异常捕获方法


简要介绍:ES6中为了处理异步,增加了promise、generator和async,它们各自都有不同的内部异常捕获方法,本文总结一下promise、generator和async的异常捕获方法。

  • Promise的异常捕获方式
  • generator的异常捕获方式
  • async/await的异常捕获方式
  • 总结

1. Promise的异常捕获方式

(1) 在Promise的构造体内进行错误处理

var promise=new Promise(function(resolve,reject){
   try {
      throw new Error('test');
   }catch(e){
      reject(e)
   }
})

Promise的构造体内进行错误处理,类似于我们在ES5中的错误处理方式。

(2) 通过Promise.prototype.catch来进行错误处理

生成Promise实例后,我们可以通过Promise原型上的catch方法来捕获Promise实例内部的错误。

var promise=new Promise(function(resolve,reject){
   reject(new Error('test'));
})
promise.catch(function(e){
 //something to deal with the error
 console.log(e)
})

上述的例子,跟(1)中是类似的,此外catch方法还可以处理链式调用中的错误,比如:

var promise=new Promise(function(resolve,reject){
   resolve();
})
promise.then(function(){
   // if some error throw
}).then(function(){
   // if some error throw 
}).catch(function(e){
  //something to deal with the error
  console.log(e)
})
 // Error : test1

上述的代码中,最后一个catch方法可以捕获前面链式调用过程中任何一步then方法里面所抛出的错误。

catch方面里面还可以再抛错误,这个错误会被后面的catch捕获

var promise=new Promise(function(resolve,reject){
  reject(new Error('test1'))
})
promise.catch(function(e){
  console.log(e);
  throw new Error('test2')
}).catch(function(e){
  console.log(e)
})
  
  // Error : test1
  // Error : test2

(3) Promise.all中的异常捕获

如果组成Promise.allpromise有自己的错误捕获方法,那么Promise.all中的catch就不能捕获该错误。

var p1=new Promise(function(resolve,reject){
  reject(new Error('test1'))
}).catch(function(e){
  console.log("由p1自身捕获",e);
})
var p2=new Promise(function(resolve,reject){
  resolve();
})
var p=Promise.all([p1,p2]);
p.then(function(){

}).catch(function(e){
  //在此处捕获不到p1中的error
  console.log(e)
})
//由p1自身捕获 Error: test1

(4) Promise.try中的异常捕获

ES2018中可以通过Promise.try来同步处理,可能是异步也可能是同步的函数。

function f(){}
Promise.try(f);
console.log(2);

上述的f方法,不管是同步还是异步,都会执行该方法,再输出2。

Promise.try的错误处理中,通过catch方法既可以捕获f是同步函数情况下的错误,也可以捕获f是异步函数情况下的错误。

function f(){}
Promise.try(f).then(function(){

}).catch(function(e){

})

(5) 在promise中无法被捕获的错误

promise实例resolve之后,错误无法被捕获。

var promise=new Promise(function(resolve,reject){
   resolve();
   throw new Error('test');//该错误无法被捕获
})
promise.then(function(){
  //
}).then(function(e){
  console.log(e)
})

该错误可以用尾调用resolve来避免。

2.generator中的异常捕获方式

(1) 构造体内部捕获

function * F(){
   try{
     yield 1
   }catch(e){
     console.log(e)
   }
}
var f=F();
f.throw(new Error('test1'))

上述这样在内部不能捕获到test1错误,为什么呢? 这个generator的原理有关,调用F()仅仅返回一个状态生成器,并没有执行generator里面的方法,因此在f上直接跑错误是无法捕获的。

那么怎么才能捕获错误呢?采用如下的方式:

function * F(){
   try{
     yield 1
   }catch(e){
     console.log(e)
   }
}
var f=F();
f.next()//增加了一句next执行,可以执行generator里面的内容
f.throw(new Error('test1'))
//捕获错误 Error test1

这样就能捕获该错误。

此外捕获错误后,会执行一次next方法

function * F(){
   try{
     yield 1
   }catch(e){
     console.log(e)
   }
   yield 2
   return 3
}
var f=F();
f.next();  //{value :1,done:false}
f.throw(new Error('test1'))  //{value:2,done:false}
f.next();  //{value:3,done:true}

到次,我们现在直到在generator中,next、throw和done都会执行一次next方法。

(2) 在generator构造体外部捕获错误

function F(){
  yield 1;
  yield 2;
  return 3;
}
var f=F();
try{
  f.throw(new Error('test1'))
}catch(e){
  console.log(e)
}
// Error test1

在构造体外部捕获,可以直接f.throw

(3) 如果Generator的错误没有被捕获,就不会继续执行

function F(){
  yield 1;
  throw new Error('test1');
  yield 2;
  return 3
}
var f=F();
f.next() // {value:1,done:false}
f.next() // {value:undefined,done:true}

上述例子中,只要错误没有被处理,就会返回done:true就停止执行generator

3. async/await中的错误处理

(1) async中的错误处理

因为async的返回值也是个promise,跟promise的错误处理差不多。

此外,async里面throw Error 相当于返回Promise.reject

async function F(){
  throw new Error('test1')
}
var f=F();
f.catch(function(e){console.log(e)});
// Error:test1

(2) await中的错误处理

async中,await的错误相当于Promise.reject

async function F(){
  await Promise.reject('Error test1').catch(function(e){
     console.log(e)
  })
}
var f=F(); // Error:test1

(3) await中的promise.reject必须要被捕获

await如果返回的是reject状态的promise,如果不被捕获,就会中断async函数的执行。

async function F(){
  await Promise.reject('Error test1');
  await 2
}
var f=F()

上述代码中,前面的Promise.reject没有被捕获,所以不会执行await 2

4.总结

  • Promise、generator错误都可以在构造体里面被捕获,而async/await返回的是promise,可以通过catch直接捕获错误。

  • generator 抛出的错误,以及await 后接的Promise.reject都必须被捕获,否则会中断执行。

Redux源码解读拾遗,createStore的第三个参数

简要介绍:再看redux文档的时候,发现了createStore是允许第三个参数的,看了一下源码明白了第三个参数的作用。

一、createStore的第三个参数的定义

(1) 官方定义:createStore(reducer, [initialState], enhancer),

第三个参数enhancer, 是一个组合 store creator 的高阶函数,返回一个

新的强化过的 store creator。这与 middleware 相似,它也允许你通过

复合函数改变 store 接口。

(2) 关于第三个参数的源码:

export default function createStore(reducer, preloadedState, enhancer) {
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    return enhancer(createStore)(reducer, preloadedState)
  }

去掉前面一些类型判断,我们来看这一句:

 return enhancer(createStore)(reducer, preloadedState)

这句的形式像什么,柯里化后传入的第一个参数为createStore,这很类

似于我们再定义中间件的时候,applyMiddleware这个函数,这个函数

返回了提升后的createStore。

二、applyMiddleware的两种写法

因此在applyMiddleware的时候,就会存在两种写法,这里我们以利用redux-thunk为例。

(1) 直接调用applyMiddleware生成新的createStore

import thunk from 'redux-thunk'
let createStoreWithMiddleware = applyMiddleware(thunk)(createStore)

(2) 在createStore中调用

import thunk from 'redux-thunk'
let createStoreWithMiddleware = createStore(reducer,preState,applyMiddleware(thunk))

React Hooks工程实践总结

React Hooks工程实践总结


    最近在项目中基本上全部使用了React Hooks,历史项目也用React Hooks重写了一遍,相比于Class组件,React Hooks的优点可以一句话来概括:就是简单,在React hooks中没有复杂的生命周期,没有类组件中复杂的this指向,没有类似于HOC,render props等复杂的组件复用模式等。本篇文章主要总结一下在React hooks工程实践中的经验。

  • React hooks中的渲染行为
  • React hooks中的性能优化
  • React hooks中的状态管理和通信

一、React hooks中的渲染行为

1.React hooks组件是如何渲染的

    理解React hooks的关键,就是要明白,hooks组件的每一次渲染都是独立,每一次的render都是一个独立的作用域,拥有自己的props和states、事件处理函数等。概括来讲:

每一次的render都是一个互不相关的函数,拥有完全独立的函数作用域,执行该渲染函数,返回相应的渲染结果

而类组件则不同,类组件中的props和states在整个生命周期中都是指向最新的那次渲染.

React hooks组件和类组件的在渲染行为中的区别,看起来很绕,我们可以用图来区别,

未命名文件 (6)的副本

    上图表示在React hooks组件的渲染过程,从图中可以看出,react hooks组件的每一次渲染都是一个独立的函数,会生成渲染区专属的props和state. 接着来看类组件中的渲染行为:

未命名文件 (6)

    类组件中在渲染开始的时候会在类组件的构造函数中生成一个props和state,所有的渲染过程都是在一个渲染函数中进行的并且,每一次的渲染中都不会去生成新的state和props,而是将值赋值给最开始被初始化的this.props和this.state。

2.工程中注意React hooks的渲染行为

    理解了React hooks的渲染行为,就指示了我们如何在工程中使用。首先因为React hooks组件在每一次渲染的过程中都会生成独立的所用域,因此,在组件内部的子函数和变量等在每次生命的时候都会重新生成,因此我们应该减少在React hooks组件内部声明函数。

写法一:

function App() {
  const [counter, setCounter] = useState(0);
  function formatCounter(counterVal) {
    return `The counter value is ${counterVal}`;
  }
  return (
    <div className="App">
      <div>{formatCounter(counter)}</div>
      <button onClick={() => setCounter(prevState => ++prevState)}>
        Increment
      </button>
    </div>
  );
}

写法二:

function formatCounter(counterVal) {
  return `The counter value is ${counterVal}`;
}
function App() {
 const [counter, setCounter] = useState(0);
 return (
   <div className="App">
     <div>{formatCounter(counter)}</div>
     <button onClick={()=>onClick(setCounter)}>
       Increment
     </button>
   </div>
 );
}

    App组件是一个hooks组件,我们知道了React hooks的渲染行为,那么写法1在每次render的时候都会去重新声明函数formatCounter,因此是不可取的。我们推荐写法二,如果函数与组件内的state和props无相关性,那么可以声明在组件的外部。如果函数与组件内的state和props强相关性,那么我们下节会介绍useCallback和useMemo的方法。

    React hooks中的state和props,在每次渲染的过程中都是重新生成和独立的,那么我们如果需要一个对象,从开始到一次次的render1 , render2, ...中都是不变的应该怎么做呢。(这里的不变是不会重新生成,是引用的地址不变的意思,其值可以改变)

我们可以使用useRef,创建一个“常量”,该常量在组件的渲染期内始终指向同一个引用地址。

通过useRef,可以实现很多功能,比如在某次渲染的时候,拿到前一次渲染中的state。

function App(){
   const [count,setCount] = useState(0)
   const prevCount = usePrevious(count);
   return (
    <div>
      <h1>Now: {count}, before: {prevCount}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
   );
}
function usePrevious(value) {
   const ref = useRef();
   useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
}

上述的例子中,我们通过useRef()创建的ref对象,在整个usePrevious组件的周期内都是同一个对象,我们可以通过更新ref.current的值,来在App组件的渲染过程中,记录App组件渲染中前一次渲染的state.

这里其实还有一个不容易理解的地方,我们来看usePrevious:

function usePrevious(value) {
   const ref = useRef();
   useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
}

这里的疑问是:为什么当value改变的时候,返回的ref.current指向的是value改变之前的值?

也就是说:

为什么useEffect在return ref.current之后才执行?

为了解释这个问题,我们来聊聊神奇的useEffect.

3.神奇的useEffect

    hooks组件的每一次渲染都可以看成一个个独立的函数 render1,render2 ... rendern,那么这些render函数之间是怎么关联的呢,还有上小节的问题,为什么在usePrevious中,useEffect在return ref.current之后才执行。带着这两个疑问我们来看看在hooks组件中,最为神奇的useEffect。

    用一句话概括就是:

每一渲染都会生成不同的render函数,并且每一次渲染通过useEffect会生成一个不同的Effects,Effects在每次渲染后声效。

每次渲染除了生成不同的作用域外,如果该hooks组件中使用了useEffect,通过useEffect还会生成一个独有的effects,该effects在渲染完成后生效。

举例来说:

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

上述的例子中,完成的逻辑是:

  • 渲染初始的内容:<p>You clicked 0 times</p>
  • 渲染完成之后调用这个effect:{ document.title = 'You clicked 0 times' }。
  • 点击Click me
  • 渲染新的内容渲染的内容: <p>You clicked 1 times</p>
  • 渲染完成之后调用这个effect:() => { document.title = 'You clicked 1 times' }。

也就是说每次渲染render中,effect位于同步执行队列的最后面,在dom更新或者函数返回后在执行。

我们在来看usePrevious的例子:

function usePrevious(value) {
   const ref = useRef();
   useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
}

因为useEffect的机制,在新的渲染过程中,先返回ref.current再执行deps依赖更新ref.current,因此usePrevios总是返回上一次的值。

现在我们知道,在一次渲染render中,有自己独立的state,props,还有独立的函数作用域,函数定义,effects等,实际上,在每次render渲染中,几乎所有都是独立的。我们最后来看两个例子:

(1)

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setTimeout(() => {
      console.log(`You clicked ${count} times`);
    }, 3000);
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

(2)

function Counter() {
  const [count, setCount] = useState(0);
  
  setTimeout(() => {
      console.log(`You clicked ${count} times`);
  }, 3000);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

这两个例子中,我们在3内点击5次Click me按钮,那么输出的结果都是一样的。

You clicked 0 times
You clicked 1 times
You clicked 2 times
You clicked 3 times
You clicked 4 times
You clicked 5 times

    总而言之,每一次渲染的render,几乎都是独立和独有的,除了useRef创建的对象外,其他对象和函数都没有相关性.

二、React hooks中的性能优化

    前面我们讲了React hooks中的渲染行为,也初步
提到了说将与state和props无关的函数,声明在hooks组件外面可以提高组件的性能,减少每次在渲染中重新声明该无关函数. 除此之外,React hooks还提供了useMemo和useCallback来优化组件的性能.

(1).useCallback

有些时候我们必须要在hooks组件内定义函数或者方法,那么推荐用useCallback缓存这个方法,当useCallback的依赖项不发生变化的时候,该函数在每次渲染的过程中不需要重新声明

useCallback接受两个参数,第一个参数是要缓存的函数,第二个参数是一个数组,表示依赖项,当依赖项改变的时候会去重新声明一个新的函数,否则就返回这个被缓存的函数.

function formatCounter(counterVal) {
  return `The counter value is ${counterVal}`;
}
function App(props) {
 const [counter, setCounter] = useState(0);
 const onClick = useCallback(()=>{
   setCounter(props.count)
 },[props.count]);
 return (
   <div className="App">
     <div>{formatCounter(counter)}</div>
     <button onClick={onClick}>
       Increment
     </button>
   </div>
 );
}

上述例子我们在第一章的例子基础上增加了onClick方法,并缓存了这个方法,只有props中的count改变的时候才需要重新生成这个方法。

(2).useMemo

    useMemo与useCallback大同小异,区别就是useMemo缓存的不是函数,缓存的是对象(可以是jsx虚拟dom对象),同样的当依赖项不变的时候就返回这个被缓存的对象,否则就重新生成一个新的对象。

为了实现组件的性能优化,我们推荐:

在react hooks组件中声明的任何方法,或者任何对象都必须要包裹在useCallback或者useMemo中。

(3)useCallback,useMemo依赖项的比较方法

    我们来看看useCallback,useMemo的依赖项,在更新前后是怎么比较的

import is from 'shared/objectIs';
function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
) {
  if (prevDeps === null) {
    return false;
  }

  
 if (nextDeps.length !== prevDeps.length) {
   return false
 }
 
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++)   {
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}

其中is方法的定义为:

function is(x: any, y: any) {
  return (
    (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) 
  );
}

export default (typeof Object.is === 'function' ? Object.is : is);

这个is方法就是es6的Object.is的兼容性写法,也就是说在useCallback和useMemo中的依赖项前后是通过Object.is来比较是否相同的,因此是浅比较。

三、React hooks中的状态管理和通信

  react hooks中的局部状态管理相比于类组件而言更加简介,那么如果我们组件采用react hooks,那么如何解决组件间的通信问题。

(1) UseContext

  最基础的想法可能就是通过useContext来解决组件间的通信问题。

比如:

function useCounter() {
  let [count, setCount] = useState(0)
  let decrement = () => setCount(count - 1)
  let increment = () => setCount(count + 1)
  return { count, decrement, increment }
}

let Counter = createContext(null)

function CounterDisplay() {
  let counter = useContext(Counter)
  return (
    <div>
      <button onClick={counter.decrement}>-</button>
      <p>You clicked {counter.count} times</p>
      <button onClick={counter.increment}>+</button>
    </div>
  )
}

function App() {
  let counter = useCounter()
  return (
    <Counter.Provider value={counter}>
      <CounterDisplay />
      <CounterDisplay />
    </Counter.Provider>
  )
}

  在这个例子中通过createContext和useContext,可以在App的子组件CounterDisplay中使用context,从而实现一定意义上的组件通信。

此外,在useContext的基础上,为了其整体性,业界也有几个比较简单的封装:

https://github.com/jamiebuilds/unstated-next
https://github.com/diegohaz/constate

但是其本质都没有解决一个问题:

如果context太多,那么如何维护这些context

  也就是说在大量组件通信的场景下,用context进行组件通信代码的可读性很差。这个类组件的场景一致,context不是一个新的东西,虽然用了useContext减少了context的使用复杂度。

(2) Redux结合hooks来实现组件间的通信

  hooks组件间的通信,同样可以使用redux来实现。也就是说:

在React hooks中,redux也有其存在的意义

  在hooks中存在一个问题,因为不存在类似于react-redux中connect这个高阶组件,来传递mapState和mapDispatch, 解决的方式是通过redux-react-hook或者react-redux的7.1 hooks版本来使用。

  • redux-react-hook

  在redux-react-hook中提供了StoreContext、useDispatch和useMappedState来操作redux中的store,比如定义mapState和mapDispatch的方式为:

import {StoreContext} from 'redux-react-hook';

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

import {useDispatch, useMappedState} from 'redux-react-hook';

export function DeleteButton({index}) {
  // Declare your memoized mapState function
  const mapState = useCallback(
    state => ({
      canDelete: state.todos[index].canDelete,
      name: state.todos[index].name,
    }),
    [index],
  );

  // Get data from and subscribe to the store
  const {canDelete, name} = useMappedState(mapState);

  // Create actions
  const dispatch = useDispatch();
  const deleteTodo = useCallback(
    () =>
      dispatch({
        type: 'delete todo',
        index,
      }),
    [index],
  );

  return (
    <button disabled={!canDelete} onClick={deleteTodo}>
      Delete {name}
    </button>
  );
}
  • react-redux 7.1的hooks版

   这也是官方较为推荐的,react-redux 的hooks版本提供了useSelector()、useDispatch()、useStore()这3个主要方法,分别对应与mapState、mapDispatch以及直接拿到redux中store的实例.

简单介绍一下useSelector,在useSelector中除了能从store中拿到state以外,还支持深度比较的功能,如果相应的state前后没有改变,就不会去重新的计算.

举例来说,最基础的用法:

import React from 'react'
import { useSelector } from 'react-redux'

export const TodoListItem = props => {
  const todo = useSelector(state => state.todos[props.id])
  return <div>{todo.text}</div>
}

实现缓存功能的用法:

import React from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'

const selectNumOfDoneTodos = createSelector(
  state => state.todos,
  todos => todos.filter(todo => todo.isDone).length
)

export const DoneTodosCounter = () => {
  const NumOfDoneTodos = useSelector(selectNumOfDoneTodos)
  return <div>{NumOfDoneTodos}</div>
}

export const App = () => {
  return (
    <>
      <span>Number of done todos:</span>
      <DoneTodosCounter />
    </>
  )
}

在上述的缓存用法中,只要todos.filter(todo => todo.isDone).length不改变,就不会去重新计算.

前端实现弹幕效果的方法总结(包含css3和canvas的实现方式)

前端实现弹幕效果的方法总结(包含css3和canvas的实现方式)


之前在一个移动端的抽奖页面中,在抽奖结果的展示窗口需要弹幕轮播显示,之前踩过一些小坑,现在总结一下前端弹幕效果的实现方式。

  • css3实现乞丐版的弹幕
  • css3弹幕性能优化
  • canvas实现弹幕
  • canva弹幕的扩展功能

1. css3实现乞丐版的弹幕

(1)如何通过css3实现弹幕

首先来看如何通过css的方法实现一个最简单的弹幕:

首先在html中定义一条弹幕的dom结构:

<div class="block">我是弹幕</div>

弹幕的移动可以通过移动这个block来实现,以从右向左移动的弹幕为例,弹幕的初始位置在容器的最左侧且贴边隐藏(弹幕的最左边与容器的最右贴合),可以通过绝对定位加transform来实现:

.block{
   position:absolute;
}

初始位置:

from{
    left:100%;
    transform:translateX(0)
}

移动到最左边的结束位置为(弹幕的最右边与容器的最左边贴合):

to{
   left:0;
   transform:translateX(-100%)
}

起始位置和结束位置的具体图示如下所示:

default

根据起始位置和结束位置可以定义完整的两帧弹幕动画:

@keyframes barrage{
   from{
     left:100%;
     transform:translateX(0);
   }
   to{
     left:0;
     transform:translateX(-100%);
   }
}

给弹幕元素引入这个动画:

.block{
  position:absolute;
  /* other decorate style */
  animation:barrage 5s linear 0s;
}

这样就可以实现一个乞丐版的弹幕效果:

123

(2)通过绝对定位和left实现弹幕的缺陷

首先明确一下css的渲染过程

  • I)根据HTML的结构生成DOM树(DOM树中包含了display:none的节点)
  • II)在DOM树的基础上,根据节点的几何属性(margin/padding/width/height/left等)生成render树
  • III)在render树的基础上继续渲染color,font等属性

其中如果I)中和II)中的属性发生变化会发生reflow(回流),如果仅仅III)中的属性发生改变,只会发生repaint(重绘)。显然从css的渲染过程我们也可以看出来:reflow(回流)必伴随着重绘。

reflow(回流):当render树中的一部分或者全部因为大小边距等问题发生改变而需要重建的过程叫做回流
repaint(重绘):当元素的一部分属性发生变化,如外观背景色不会引起布局变化而需要重新渲染的过程叫做重绘

reflow(回流)会影响浏览器css的渲染速度,因此在做网页性能优化的时候要减少回流的发生。

在第一节,我们通过left属性,实现了弹幕的效果,left会改变元素的布局,因此会发生reflow(回流),表现在移动端页面上会造成弹幕动画的卡顿。

2. css3弹幕性能优化

我们直到了第一节中的弹幕动画存在卡顿的问题,下面我们看看如何解决动画的卡顿。

(1)CSS开启硬件加速

在浏览器中用css开启硬件加速,使用GPU(Graphics Processing Unit)可以提升网页性能。鉴于此,我们可以发挥GPU的力量,从而使我们的网站或应用表现的更为流畅。

CSS animations, transforms 以及 transitions 不会自动开启GPU加速,而是由浏览器的缓慢的软件渲染引擎来执行。那我们怎样才可以切换到GPU模式呢,很多浏览器提供了某些触发的CSS规则。

比较常见的方式是,我们可以通过3d变化(translate3d属性)来开启硬件加速,鉴于此,我们修改动画为:

@keyframes barrage{
   from{
     left:100%;
     transform:translate3d(0,0,0);
   }
   to{
     left:0;
     transform:translate3d(-100%,0,0);
   }
}

这样就可以通过开启硬件加速的方式,优化网页性能。但是这种方式没有从根本上解决问题,同时使用GPU增加了内存的使用,会减少移动设备的电池寿命等等。

(2)不改变left属性

第二种方法,就是想办法在弹幕动画的前后不改变left属性的值,这样就不会发生reflow。

我们想仅仅通过translateX来确定弹幕节点的初始位置,但是translateX(-100%)是相对于弹幕节点本身的,而不是相对于父元素,因此我们耦合js和css,在js中获取弹幕节点所在的父元素的宽度,接着根据宽度来定义弹幕节点的初始位置。

以父元素为body时为例:

//css
 .block{
  position:absolute;
  left:0;
  visibility:hidden;
  /* other decorate style */
  animation:barrage 5s linear 0s;
}
//js
let style = document.createElement('style');
document.head.appendChild(style);
let width = window.innerWidth;
let from = `from { visibility: visible; -webkit-transform: translateX(${width}px); }`;
let to = `to { visibility: visible; -webkit-transform: translateX(-100%); }`;
style.sheet.insertRule(`@-webkit-keyframes barrage { ${from} ${to} }`, 0);

除了耦合js计算了父元素的宽度,从而确定弹幕节点的初始位置之外,这里在弹幕节点中我们为了防止初始位置就有显示,增加了visibility:hidden属性。防止弹幕节点在未确定初始位置时就显示在父容器内。只有弹幕开始从初始位置滚动,才会变得可见。

但是这种css的实现方式,在实现弹幕的扩展功能方面比较麻烦,比如如何控制弹幕暂停等等。

3. canvas实现弹幕

除了通过css实现弹幕的方法之外,通过canvas也可以实现弹幕。

通过canvas实现弹幕的原理就是时时的重绘文字,下面来一步步的实现。

  • 获取画布

    let canvas = document.getElementById('canvas');
    let ctx = canvas.getContext('2d');

  • 绘制文字

    ctx.font = '20px Microsoft YaHei';
    ctx.fillStyle = '#000000';
    ctx.fillText('canvas 绘制文字', x, y);

    上面的fillText就是实现弹幕效果的主要api,其中x表示横方向的坐标,y表示纵方向的坐标,只要时时的改变x,y进行重绘,就可以实现动态的弹幕效果。

  • 清除绘制内容

    ctx.clearRect(0, 0, width, height);

  • 具体实现

通过定时器,定时改变x,y,每次改变之前先进性清屏,然后根据改变后的x,y进行重绘。当存在多条弹幕的情况下,定义:

let colorArr=_this.getColor(color);  弹幕数组多对应的颜色数组
let numArrL=_this.getLeft();  弹幕数组所对应的x坐标位置数组
let numArrT=_this.getTop();  弹幕数组所对应的y坐标位置数组
let speedArr=_this.getSpeed(); 弹幕数组所对应的弹幕移动速度数组

定时的重绘弹幕函数为:

_this.timer=setInterval(function(){
    ctx.clearRect(0,0,canvas.width,canvas.height);
    ctx.save();
    for(let j=0;j<barrageList.length;j++){
      numArrL[j]-=speedArr[j];
      ctx.fillStyle = colorArr[j]
      ctx.fillText(barrageList[j],numArrL[j],numArrT[j]);
      ctx.restore();
 },16.7);

实现的效果为:

1234

4. canva弹幕的扩展功能

通过canvas实现弹幕的方式,很方便做比如暂停弹幕滚动等扩展功能,此外,也可以给弹幕增加头像,给每条弹幕增加边框等等功能,以后再补充。

最后给一个简单的react弹幕组件;https://github.com/forthealllight/react-barrage

RxJS入门——基础操作指北

RxJS入门——基础操作指北


本章所基于的RxJS的版本问5.5.9,在本章中介绍RxJS的基础知识,比如Observable、Observer、Subscription、Subject以及Operation等

  • 了解RxJS的设计**
  • 掌握RxJS的基础用法
  • 梳理RxJS的接口API

一、RxJS的设计**

简单理解RxJS的设计**,之前实现了PromiseA+规范下的promise,创建一个Promise后,有一个状态的初始值pending,两个状态的改变值,fullfilled和rejected.在Promise的订阅机制中,一旦状态发生改变,状态的改变就是不可逆的.而RxJS不同,Observable相当于一个消息生成器,给消息生成器设置处理函数,通过消息生成器传递信息给处理函数,处理函数作为订阅者执行相应的逻辑,并返回结果.

因为Observable这个消息生成器是多值的,因此订阅者执行后返回的值也是多值的。

二、RxJS的基本用法

来看RxJS的基本用法,简单的介绍Observable、Observer、Subscription、Subject以及Operation的使用

1.Observable、Observer

(1)首先来看基本的用法,

第一步创建可观察对象:

import Rx from 'rxjs/Rx';
const observable=Rx.Observable.create(
 function  subscribe(observer){
       try{
         observer.next(1);
         observer.next(2);
         observer.next(3);
         observer.complete();
       }catch(e){
         observer.error(e);
       }
})

在每一个可观察对象中会推送消息给订阅者,比如在上述函数中通过在next、complete和error方法中传递参数的形式将消息推送给订阅者。

第二步,创建订阅者

const observer={
  next:x=>console.log(x),
  error:error=>console.log(error),
  complete:()=>console.log('Observer got a complete notification')
}

订阅者有几个函数属性,用于接受可观察对象中的值并执行。

第三步,建立观察订阅关系:

observable.subscribe(observer);

执行返回的结果为:

1
2
3
Observer got a complete notification

(2)Observable异步推送

const observable=Rx.Observable.create(function              subscribe(observer){
       try{
         observer.next(1);
         observer.next(2);
         observer.next(3);
         setTimeout(()=>{
           observer.next(4)
         },1000)
       }catch(e){
         observer.error(e);
       }
});
const observer={
  next:x=>console.log(x),
  error:error=>console.log(error),
  complete:()=>console.log('Observer got a complete notification')
}
console.log('before');
observable.subscribe(observer);
console.log('after');

通过setTimeout异步推送值给next方法,输出结果为:

before
1
2
3
after
4

(3)取消Observable的订阅

var subscription = observable.subscribe(observer);
subscription.unsubscribe();

2. Subject

(1)基础用法

RxJS中的Subject是一种特殊的Observable,通过Subject定义的可观察对象可以被多个Observer订阅.

const subject=new Rx.Subject();
subject.subscribe({
  next:(v)=>console.log('observerA:'+v)
});
subject.subscribe({
  next:(v)=>console.log('observerB:'+v)
});
subject.next(1);
subject.next(2);

输出信息为:

observerA: 1
observerB: 1
observerA: 2
observerB: 2

(2)Subject特殊类型

Subject也有几种特殊的类型,BehaviorSubject、ReplaySubject 和 AsyncSubject。依次来看每一种类型是如何使用的:

  • BehaviorSubject

通过BehaviorSubject构建的观察者,会将最新值发送给订阅者,并且观察者一旦创建就是有初值的:

const subject=new Rx.BehaviorSubject(0);

如上述的代码中创建了一个观察者,默认值为0,也就是上述代码其实默认执行了:

subject.next(0)

因此,如果:

const subject=new Rx.BehaviorSubject(0);
subject.subscribe({
  next:(v)=>console.log('observerA:'+v)
});

虽然我们没有在subject中传入值,但是因为有默认值,因此在控制台输出:

observerA:0

BehaviorSubject方法定义的观察者,会始终使用最新值,也就是将最新值传递给订阅者.完整的例子为:

    const subject=new Rx.BehaviorSubject(0);
    subject.subscribe({
      next:(v)=>console.log('observerA:'+v)
    });
    subject.next(1);
    subject.next(2);
    subject.subscribe({
      next:(v)=>console.log('observerB:'+v)
    });
    subject.next(3);

输出的值为:

observerA: 0
observerA: 1
observerA: 2
observerB: 2
observerA: 3
observerB: 3

注意创建第二个订阅者B时,因为最新的next函数的参数是2,因此第二个订阅者会输出2

  • ReplaySubject

ReplaySubject可以缓存旧的观察者的值,传递给新的订阅者,在构造的函数中可以制定缓存旧值的个数.直接看例子:

    const subject = new Rx.ReplaySubject(3); // 为新的订阅者缓冲3个值
    
    subject.subscribe({
      next: (v) => console.log('observerA: ' + v)
    });
    
    subject.next(1);
    subject.next(2);
    subject.next(3);
    subject.next(4);
    
    subject.subscribe({
      next: (v) => console.log('observerB: ' + v)
    });
    
    subject.next(5);

在observerB订阅之前,我们在观察者中保留了最新的3个值,此时最新的3个值分别为2,3,4,因此在observerB订阅时,就会输出2,3,4,完成的输出为:

    observerA: 1
    observerA: 2
    observerA: 3
    observerA: 4
    observerB: 2
    observerB: 3
    observerB: 4
    observerA: 5
    observerB: 5

此外,可以设置时间,来缓存多少时间段内的观察者的值。

  • AsyncSubject

AsyncSubject只有在可观察对象complete的时候,才会将最新的值传递给订阅者,举例来说:

    const subject = new Rx.AsyncSubject();

    subject.subscribe({
      next: (v) => console.log('observerA: ' + v)
    });
    
    subject.next(1);
    subject.next(2);
    subject.next(3);
    subject.next(4);
    
    subject.subscribe({
      next: (v) => console.log('observerB: ' + v)
    });
    
    subject.next(5);
    subject.complete();

输出的结果为:

observerA: 5
observerB: 5

小提示:Observer也可以只是一个函数的形式,这种清空下这个函数等同与next属性的函数,也就是说,下面两种方法是等价的:

subject.subscribe({
   next:(v)=>console.log('observerB:'+v)
})

和省略的条件下:

subject.subscribe(v=>console.log('observerB:'+v))

3.Operators(操作符)

(1)操作符的定义

可观察对象上有很多操作符,比如.map(...)、.filter(...)等等,操作符的本身是一个纯函数,接受一个Observable,返回一个新的Observable,并且如果对订阅新生成的Observable,那么同时也会使得旧的观Observable也被订阅.

举例来说:

function multiplyByTen(input) {
  var output = Rx.Observable.create(function subscribe(observer) {
    input.subscribe({
      next: (v) => observer.next(10 * v),
      error: (err) => observer.error(err),
      complete: () => observer.complete()
    });
  });
  return output;
}
var input = Rx.Observable.from([1, 2, 3, 4]);
var output = multiplyByTen(input);
output.subscribe(x => console.log(x));

在上述的例子中,multiplyByTen就类似于一个操作符函数,该函数接受一个Observable,这里为input,同时返回一个新的Observable,这里为output,最后我们在新创建的Observable上进行订阅:

output.subscribe(x => console.log(x));

因为旧的Observable此时也同时被订阅,因此输出的结果为:

10
20
30
40

(2)实例操作符和静态操作符

实例操作符定义在Observable原型上,在该实例操作符所定义的方法中,通过this取得实例的具体值:

Rx.Observable.prototype.multiplyByTen = function multiplyByTen() {
  var input = this;
  return Rx.Observable.create(function subscribe(observer) {
    input.subscribe({
      next: (v) => observer.next(10 * v),
      error: (err) => observer.error(err),
      complete: () => observer.complete()
    });
  });
}

在实例方法中,不需要给该方法传递具体Observable作为参数输入,而是在Observable本身上直接调用.实例调用的方法为:

var observable = Rx.Observable.from([1, 2, 3, 4]).multiplyByTen();
observable.subscribe(x => console.log(x));

静态方法就是纯函数,接受Observable作为参数.比如在我们上述使用的Rx.Observable.create,就是一个常见的静态方法

4.常见操作符举例

Observable中的操作符很多我们来举几个例子。

(1) from 操作符

可以将任何值转化成一个可观察对象Observable,这里的任何值包括数组、类数组对象、promise、迭代对象以及类可观察者对象.首先,比如我们可以将一个数组转化成一个Observable:

const array=[10,20,30];
var result=Rx.Observable.from(array);
result.subscribe(x=>console.log(x));

如果传入的是数组,那么会返回一个next数组值的Observable,最后的输出为:

10
20
30

如果from接受的参数是一个迭代对象,比如generator构造的状态机,那么会有:

var iterator = generateDoubles(3);
var result = Rx.Observable.from(iterator).take(10);
result.subscribe(x => console.log(x));

输入的结果为:

3 6 12 24 48 96 192 384 768 1536

(2) fromEvent 操作符

该操作符与事件有关,将Dom事件,nodejs中通过EventEmitter所出发的事件等转化成一个可观察对象Observer,举例来看DOM事件的例子:

var clicks = Rx.Observable.fromEvent(document, 'click');
clicks.subscribe(x => console.log(x));

这样,点击docment后会在next方法中传入一个鼠标事件对象MouseEvent,因此点击,在订阅者中会输出一个鼠标事件对象.输出的结果为:

MouseEvent {isTrusted: true, screenX: 48, screenY: 123, clientX: 35, clientY: 25, …}

此外,fromEvent还可以接受第三个参数option,默认的事件是遵循在冒泡阶段执行,默认为false,如果将option设置为true,将在冒泡阶段进行.比如:

    var clicksInDocument=Rx.Observable.fromEvent(document, 'click', true); 
    var clicksInDiv = Rx.Observable.fromEvent(someDivInDocument, 'click');
    
    clicksInDocument.subscribe(() => console.log('document'));
    clicksInDiv.subscribe(() => console.log('div'));

如果不设置option,那么应该先输出div,后输出document,但是此时的情况下设置了option为true,那么会先输出document,后输出div,事件在捕获阶段进行.

(3) fromEventPattern

该操作符将添加事件的函数,转化成一个可观察的Observable,举例来说:

function addClickHandler(handler) {
  document.addEventListener('click', handler);
}

function removeClickHandler(handler) {
  document.removeEventListener('click', handler);
}

var clicks = Rx.Observable.fromEventPattern(
  addClickHandler,
  removeClickHandler
);
clicks.subscribe(x => console.log(x));

上述的过程中有两个函数,这两个函数内部是执行的事件添加的过程,通过fromEventPattern可以将函数转化成可观察对象,最后输出的值也是一个MouseEvent.

(4)fromPromise

该操作符将promise转化成一个可观察的Observable,举例来说:

var result = Rx.Observable.fromPromise(fetch('http://myserver.com/'));
result.subscribe(x => console.log(x), e => console.error(e));

上述代码中fetch返回一个promise,我们将这个promise转化成一个可观察的Observable对象.next方法对应与fullied,同时error方法对应了rejected.

(5)interval

创建一个可观察对象Observable,定时的输出(emit)连续的序列.举例来说:

var numbers = Rx.Observable.interval(1000);
numbers.subscribe(x => console.log(x));

上述的方法会以生序的方式输出序列.
输出结果为1,2,3,...每个数字间隔1000ms

(6)merge

顾名思义,merge操作符就是将几个可观察对象融合,生成一个组合形式的新的可观察对象,举例来看:

var clicks = Rx.Observable.fromEvent(document, 'click');
var timer = Rx.Observable.interval(1000);
var clicksOrTimer = Rx.Observable.merge(clicks, timer);
clicksOrTimer.subscribe(x => console.log(x));

通过merge方法将interval和click方法融合,这样之后,新的Observable会有2个可观察的属性,对于订阅者而言,输出信息为,依次输出1,2,3...,当有点击事件发生时,输出MouseEvent

(7)never

该操作符表示生成一个不会emit出任何信息的可观察Observable,举例来说:

function info() {
  console.log('Will not be called');
}
var result = Rx.Observable.never().startWith(7);
result.subscribe(x => console.log(x), info, info);

该方法不会有任何的emit过程,只在初始的时候输出了7

(8)of

该操作符表示将一组数据在一次中完全输出,同时一次性完全输出后,可观察的状态变为complete.举例来说:

var numbers = Rx.Observable.of(10, 20, 30);
var letters = Rx.Observable.of('a', 'b', 'c');
var interval = Rx.Observable.interval(1000);
var result = numbers.concat(letters).concat(interval);
result.subscribe(x => console.log(x));

输出结果为:一次性输出10,20,30,a,b,c
然后定时输出:1,2,3,4...

(9)range

该操作符表示同时输出一段范围内的值,举例来说:

var numbers = Rx.Observable.range(1, 10);
numbers.subscribe(x => console.log(x));

输出的值为1,2,3,4,5,6,7,8,9,10

(10)delay

该操作符表示延迟emit,举例来说:

var clicks = Rx.Observable.fromEvent(document, 'click');
var delayedClicks = clicks.delay(1000); // each click emitted after 1 second
delayedClicks.subscribe(x => console.log(x));

上述的方法表示点击后,延迟1000ms才进行emit.

(11)debounce

顾名思义,该操作符表示的是去抖动,也就是说规定,多次重复的事件中,只执行最近的一次

var clicks = Rx.Observable.fromEvent(document, 'click');
var result = clicks.debounce(() => Rx.Observable.interval(1000));
result.subscribe(x => console.log(x));

在上述的例子中,就实现了debounce,此时按钮事件在1秒内只能被执行一次.并且emit的是最近的一次的观察信息。

(12)throttle

节流,限制了可观察信息emit的频率,举例来说:

var clicks = Rx.Observable.fromEvent(document, 'click');
var result = clicks.throttle(ev => Rx.Observable.interval(1000));
result.subscribe(x => console.log(x));

上面的代码说明,可观察信息emit的频率最高为1hz(1/1000ms)

三、Scheduler调度器

调度器控制了何时启动订阅以及可观察对象何时emit,普通的Observable通过observeOn方法来指定调度器,

var observable = Rx.Observable.create(function (observer) {
  observer.next(1);
  observer.next(2);
  observer.next(3);
  observer.complete();
})
.observeOn(Rx.Scheduler.async);

console.log('just before subscribe');
observable.subscribe({
  next: x => console.log('got value ' + x),
  error: err => console.error('something wrong occurred: ' + err),
  complete: () => console.log('done'),
});
console.log('just after subscribe');

上述是一个异步调度的过程,输出的信息为:

just before subscribe
just after subscribe
got value 1
got value 2
got value 3
done

先输出同步信息,再输出异步调度信息.

如果指定:

observeOn(Rx.Scheduler.queue);

那就是顺序执行先入先出,输出的信息为:

just before subscribe
got value 1
got value 2
got value 3
done
just after subscribe

此外,所有的操作符默认都是有第三个参数,用于指定调度器.

前端面试总结——http、html和浏览器篇

1.http和https

https的SSL加密是在传输层实现的。

(1)http和https的基本概念

http: 超文本传输协议,是互联网上应用最为广泛的一种网络协议,是一个客户端和服务器端请求和应答的标准(TCP),用于从WWW服务器传输超文本到本地浏览器的传输协议,它可以使浏览器更加高效,使网络传输减少。

https: 是以安全为目标的HTTP通道,简单讲是HTTP的安全版,即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。

https协议的主要作用是:建立一个信息安全通道,来确保数组的传输,确保网站的真实性。

(2)http和https的区别?

http传输的数据都是未加密的,也就是明文的,网景公司设置了SSL协议来对http协议传输的数据进行加密处理,简单来说https协议是由http和ssl协议构建的可进行加密传输和身份认证的网络协议,比http协议的安全性更高。
主要的区别如下:

  • Https协议需要ca证书,费用较高。
  • http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
  • 使用不同的链接方式,端口也不同,一般而言,http协议的端口为80,https的端口为443
  • http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。

(3)https协议的工作原理

客户端在使用HTTPS方式与Web服务器通信时有以下几个步骤,如图所示。

  • 客户使用https url访问服务器,则要求web 服务器建立ssl链接。
  • web服务器接收到客户端的请求之后,会将网站的证书(证书中包含了公钥),返回或者说传输给客户端。
  • 客户端和web服务器端开始协商SSL链接的安全等级,也就是加密等级。
  • 客户端浏览器通过双方协商一致的安全等级,建立会话密钥,然后通过网站的公钥来加密会话密钥,并传送给网站。
  • web服务器通过自己的私钥解密出会话密钥。
  • web服务器通过会话密钥加密与客户端之间的通信。

(4)https协议的优点

  • 使用HTTPS协议可认证用户和服务器,确保数据发送到正确的客户机和服务器;
  • HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全,可防止数据在传输过程中不被窃取、改变,确保数据的完整性。
  • HTTPS是现行架构下最安全的解决方案,虽然不是绝对安全,但它大幅增加了中间人攻击的成本。
  • 谷歌曾在2014年8月份调整搜索引擎算法,并称“比起同等HTTP网站,采用HTTPS加密的网站在搜索结果中的排名将会更高”。

(5)https协议的缺点

  • https握手阶段比较费时,会使页面加载时间延长50%,增加10%~20%的耗电。
  • https缓存不如http高效,会增加数据开销。
  • SSL证书也需要钱,功能越强大的证书费用越高。
  • SSL证书需要绑定IP,不能再同一个ip上绑定多个域名,ipv4资源支持不了这种消耗。

2.tcp三次握手,一句话概括

客户端和服务端都需要直到各自可收发,因此需要三次握手。

简化三次握手:

2018-07-10 3 42 11

从图片可以得到三次握手可以简化为:C发起请求连接S确认,也发起连接C确认我们再看看每次握手的作用:第一次握手:S只可以确认 自己可以接受C发送的报文段第二次握手:C可以确认 S收到了自己发送的报文段,并且可以确认 自己可以接受S发送的报文段第三次握手:S可以确认 C收到了自己发送的报文段

3.TCP和UDP的区别

(1)TCP是面向连接的,udp是无连接的即发送数据前不需要先建立链接。

(2)TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付。 并且因为tcp可靠,面向连接,不会丢失数据因此适合大数据量的交换。

(3)TCP是面向字节流,UDP面向报文,并且网络出现拥塞不会使得发送速率降低(因此会出现丢包,对实时的应用比如IP电话和视频会议等)。

(4)TCP只能是1对1的,UDP支持1对1,1对多。

(5)TCP的首部较大为20字节,而UDP只有8字节。

(6)TCP是面向连接的可靠性传输,而UDP是不可靠的。

4.WebSocket的实现和应用

(1)什么是WebSocket?

WebSocket是HTML5中的协议,支持持久连续,http协议不支持持久性连接。Http1.0和HTTP1.1都不支持持久性的链接,HTTP1.1中的keep-alive,将多个http请求合并为1个

(2)WebSocket是什么样的协议,具体有什么优点?

  • HTTP的生命周期通过Request来界定,也就是Request一个Response,那么在Http1.0协议中,这次Http请求就结束了。在Http1.1中进行了改进,是的有一个connection:Keep-alive,也就是说,在一个Http连接中,可以发送多个Request,接收多个Response。但是必须记住,在Http中一个Request只能对应有一个Response,而且这个Response是被动的,不能主动发起。
  • WebSocket是基于Http协议的,或者说借用了Http协议来完成一部分握手,在握手阶段与Http是相同的。我们来看一个websocket握手协议的实现,基本是2个属性,upgrade,connection。

基本请求如下:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

多了下面2个属性:

Upgrade:webSocket
Connection:Upgrade
告诉服务器发送的是websocket
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

5.HTTP请求的方式,HEAD方式

  • head:类似于get请求,只不过返回的响应中没有具体的内容,用户获取报头
  • options:允许客户端查看服务器的性能,比如说服务器支持的请求方式等等。

6.一个图片url访问后直接下载怎样实现?

请求的返回头里面,用于浏览器解析的重要参数就是OSS的API文档里面的返回http头,决定用户下载行为的参数。

下载的情况下:

  1. x-oss-object-type:
         Normal
  2. x-oss-request-id:
         598D5ED34F29D01FE2925F41
  3. x-oss-storage-class:
         Standard

7.web Quality (无障碍)

能够被残障人士使用的网站才能称得上一个易用的(易访问的)网站。
残障人士指的是那些带有残疾或者身体不健康的用户。

使用alt属性:

<img src="person.jpg"  alt="this is a person"/>

有时候浏览器会无法显示图像。具体的原因有:

  • 用户关闭了图像显示
  • 浏览器是不支持图形显示的迷你浏览器
  • 浏览器是语音浏览器(供盲人和弱视人群使用)
    如果您使用了 alt 属性,那么浏览器至少可以显示或读出有关图像的描述。

8.几个很实用的BOM属性对象方法?

什么是Bom? Bom是浏览器对象。有哪些常用的Bom属性呢?

(1)location对象

location.href-- 返回或设置当前文档的URL
location.search -- 返回URL中的查询字符串部分。例如 http://www.dreamdu.com/dreamdu.php?id=5&name=dreamdu 返回包括(?)后面的内容?id=5&name=dreamdu
location.hash -- 返回URL#后面的内容,如果没有#,返回空
location.host -- 返回URL中的域名部分,例如www.dreamdu.com
location.hostname -- 返回URL中的主域名部分,例如dreamdu.com
location.pathname -- 返回URL的域名后的部分。例如 http://www.dreamdu.com/xhtml/ 返回/xhtml/
location.port -- 返回URL中的端口部分。例如 http://www.dreamdu.com:8080/xhtml/ 返回8080
location.protocol -- 返回URL中的协议部分。例如 http://www.dreamdu.com:8080/xhtml/ 返回(//)前面的内容http:
location.assign -- 设置当前文档的URL
location.replace() -- 设置当前文档的URL,并且在history对象的地址列表中移除这个URL location.replace(url);
location.reload() -- 重载当前页面

(2)history对象

history.go() -- 前进或后退指定的页面数 history.go(num);
history.back() -- 后退一页
history.forward() -- 前进一页

(3)Navigator对象

navigator.userAgent -- 返回用户代理头的字符串表示(就是包括浏览器版本信息等的字符串)
navigator.cookieEnabled -- 返回浏览器是否支持(启用)cookie

9.HTML5 drag api

  • dragstart:事件主体是被拖放元素,在开始拖放被拖放元素时触发,。
  • darg:事件主体是被拖放元素,在正在拖放被拖放元素时触发。
  • dragenter:事件主体是目标元素,在被拖放元素进入某元素时触发。
  • dragover:事件主体是目标元素,在被拖放在某元素内移动时触发。
  • dragleave:事件主体是目标元素,在被拖放元素移出目标元素是触发。
  • drop:事件主体是目标元素,在目标元素完全接受被拖放元素时触发。
  • dragend:事件主体是被拖放元素,在整个拖放操作结束时触发

10.http2.0

首先补充一下,http和https的区别,相比于http,https是基于ssl加密的http协议
简要概括:http2.0是基于1999年发布的http1.0之后的首次更新。

  • 提升访问速度(可以对于,请求资源所需时间更少,访问速度更快,相比http1.0)
  • 允许多路复用:多路复用允许同时通过单一的HTTP/2连接发送多重请求-响应信息。改善了:在http1.1中,浏览器客户端在同一时间,针对同一域名下的请求有一定数量限制(连接数量),超过限制会被阻塞。
  • 二进制分帧:HTTP2.0会将所有的传输信息分割为更小的信息或者帧,并对他们进行二进制编码
  • 首部压缩
  • 服务器端推送

11.补充400和401、403状态码

(1)400状态码:请求无效

产生原因:

  • 前端提交数据的字段名称和字段类型与后台的实体没有保持一致
  • 前端提交到后台的数据应该是json字符串类型,但是前端没有将对象JSON.stringify转化成字符串。

解决方法:

  • 对照字段的名称,保持一致性
  • 将obj对象通过JSON.stringify实现序列化

(2)401状态码:当前请求需要用户验证

(3)403状态码:服务器已经得到请求,但是拒绝执行

12.fetch发送2次请求的原因

fetch发送post请求的时候,总是发送2次,第一次状态码是204,第二次才成功?

原因很简单,因为你用fetch的post请求的时候,导致fetch 第一次发送了一个Options请求,询问服务器是否支持修改的请求头,如果服务器支持,则在第二次中发送真正的请求。

13.Cookie、sessionStorage、localStorage的区别

共同点:都是保存在浏览器端,并且是同源的

  • Cookie:cookie数据始终在同源的http请求中携带(即使不需要),即cookie在浏览器和服务器间来回传递。而sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存。cookie数据还有路径(path)的概念,可以限制cookie只属于某个路径下,存储的大小很小只有4K左右。 (key:可以在浏览器和服务器端来回传递,存储容量小,只有大约4K左右)

  • sessionStorage:仅在当前浏览器窗口关闭前有效,自然也就不可能持久保持,localStorage:始终有效,窗口或浏览器关闭也一直保存,因此用作持久数据;cookie只在设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭。(key:本身就是一个回话过程,关闭浏览器后消失,session为一个回话,当页面不同即使是同一页面打开两次,也被视为同一次回话)

  • localStorage:localStorage 在所有同源窗口中都是共享的;cookie也是在所有同源窗口中都是共享的。(key:同源窗口都会共享,并且不会失效,不管窗口或者浏览器关闭与否都会始终生效)

补充说明一下cookie的作用:

  • 保存用户登录状态。例如将用户id存储于一个cookie内,这样当用户下次访问该页面时就不需要重新登录了,现在很多论坛和社区都提供这样的功能。 cookie还可以设置过期时间,当超过时间期限后,cookie就会自动消失。因此,系统往往可以提示用户保持登录状态的时间:常见选项有一个月、三个 月、一年等。

  • 跟踪用户行为。例如一个天气预报网站,能够根据用户选择的地区显示当地的天气情况。如果每次都需要选择所在地是烦琐的,当利用了 cookie后就会显得很人性化了,系统能够记住上一次访问的地区,当下次再打开该页面时,它就会自动显示上次用户所在地区的天气情况。因为一切都是在后 台完成,所以这样的页面就像为某个用户所定制的一样,使用起来非常方便

  • 定制页面。如果网站提供了换肤或更换布局的功能,那么可以使用cookie来记录用户的选项,例如:背景色、分辨率等。当用户下次访问时,仍然可以保存上一次访问的界面风格。

14.web worker

在HTML页面中,如果在执行脚本时,页面的状态是不可相应的,直到脚本执行完成后,页面才变成可相应。web worker是运行在后台的js,独立于其他脚本,不会影响页面你的性能。并且通过postMessage将结果回传到主线程。这样在进行复杂操作的时候,就不会阻塞主线程了。

如何创建web worker:

  • 检测浏览器对于web worker的支持性
  • 创建web worker文件(js,回传函数等)
  • 创建web worker对象

15.对HTML语义化标签的理解

HTML5语义化标签是指正确的标签包含了正确的内容,结构良好,便于阅读,比如nav表示导航条,类似的还有article、header、footer等等标签。

16.iframe是什么?有什么缺点?

定义:iframe元素会创建包含另一个文档的内联框架
提示:可以将提示文字放在<iframe></iframe>之间,来提示某些不支持iframe的浏览器

缺点:

  • 会阻塞主页面的onload事件
  • 搜索引擎无法解读这种页面,不利于SEO
  • iframe和主页面共享连接池,而浏览器对相同区域有限制所以会影响性能。

17.Doctype作用? 严格模式与混杂模式如何区分?它们有何意义?

Doctype声明于文档最前面,告诉浏览器以何种方式来渲染页面,这里有两种模式,严格模式和混杂模式。

  • 严格模式的排版和 JS 运作模式是 以该浏览器支持的最高标准运行。
  • 混杂模式,向后兼容,模拟老式浏览器,防止浏览器无法兼容页面。

18.Cookie如何防范XSS攻击

XSS(跨站脚本攻击)是指攻击者在返回的HTML中嵌入javascript脚本,为了减轻这些攻击,需要在HTTP头部配上,set-cookie:

  • httponly-这个属性可以防止XSS,它会禁止javascript脚本来访问cookie。
  • secure - 这个属性告诉浏览器仅在请求为https的时候发送cookie。

结果应该是这样的:Set-Cookie=.....

19.Cookie和session的区别

HTTP是一个无状态协议,因此Cookie的最大的作用就是存储sessionId用来唯一标识用户

20. 一句话概括RESTFUL

就是用URL定位资源,用HTTP描述操作

21.讲讲viewport和移动端布局

可以参考我的这篇文章:

响应式布局的常用解决方案对比(媒体查询、百分比、rem和vw/vh)

22. click在ios上有300ms延迟,原因及如何解决?

(1)粗暴型,禁用缩放

 <meta name="viewport" content="width=device-width, user-scalable=no"> 

(2)利用FastClick,其原理是:

检测到touchend事件后,立刻出发模拟click事件,并且把浏览器300毫秒之后真正出发的事件给阻断掉

Typescript工程实践总结

Typescript工程实践总结


    使用typescript陆续也有1年多的时间,总结一下自己在typescript工程实践中的一些经验,主要包含了以下几个方面。

  • Typescript工程初始化
  • Typescript内置函数
  • 易混淆typescript知识点
  • 其他心得补充

一、Typescript工程初始化

    刚开始迁移到typescript的时候,不知道从何开始,我们首先想到的就是如何将typescript编译成javascript.我们先从tsconfig.json讲起。

(1)、tsconfig.json

    tsconfig.json是typescript编辑的配置项,该配置决定了如何将typescript编译成ECMAscript,这里介绍比较重要的配置项:

"compilerOptions": {
    "target": "es2018", //可选属性es3,es5,esnext等
    "module": "esnext", //可选commonjs,AMD,UMD,esnext等
}

    这两个是比较关键的配置项,决定了将typescript编译成ECMAscript,以及采用何种模块系统,我们诚然可以直接将target设置成es5,这样就不需要通过typescript先编译成es6,然后用babel再将es6编译成es5,但是一般在项目的迁移中,大部分就项目可能使用的是es6,如果我们先将typescript编译成es6,我们可以复用就项目中babel的配置,将typescript编译完成了es6,继续编译成es5.

    整个流程应该是:

typescript ————————> es2018/es6 ————————> es3/es5

    上述流程中typescript经过tsc编译成es6,然后通过babel将es6编译成es5。如果是没有历史包袱的typescript项目也可以直接通过tsc将typescript编译成es5:

typescript ————————> es3/es5

    此外,除了tsc可以编译typescript以外,还可以使用webpack插件,比如ts-loader。从babel7.0开始,babel也支持typescript的编译。

    除了target指定typescript的编译结果外,我们在tsconfig.json还有一个比较重要的属性就是module,一般而言,对于工程项目类我们使用module:esnext,对于插件或者工具类使用module:umd。在最后一节的心得补充中,会详细的讲讲不同模块化影响以及要注意的地方。

    除了这几个配置属性外,一定要强调的一个属性就是noImplicitAny,这个属性决定是否禁用any,建议初始化项目的时候把这个属性设置成true,这样避免将typescript当成anyscript使用,因为本质typescript最大的优点就是类型检测,如果使用any会跳过类型检测。

(2)、webpack配置

    在工程中,我们会选择用webpack来打包编译typescript,常使用的webpack配置包含了结合tsconfig来使用路径别名,ts-loader和typings-for-css-modules-loader.

    在webpack中,大量使用了路径别名,结合tsconfig.json文件中的path,可以使用路径别名的同时,使得对于目录中不同文件有静态提示。

//webpack.config
export default {
    resolve:{
        alias: {
              '@component': path.resolve(__dirname,'../app/cdn/component')
         }
    }
}

在tsconfig.json文件中:

{
   "paths":{
      "@component/*":["app/cdn/component/*"],
    }
}

此外,在webpack中使用ts-loader来编译typescript.

    最后如果我们常见的在项目中使用less或者sass来编写模块化的css,并使用 CSS module 的方案来防止样式冲突。我们在webpack中使用了 typings-for-css-modules-loader ,这个loader会监听 Less 文件的变化,在 Less 文件的同级目录下自动生成后缀名为 .less.d.ts 声明文件,方便在 tsx 文件中引入样式。

(3)、eslint配置

    在我之前的一篇文章中总结过在typescript中如何使用eslint,在Typescript项目中,如何优雅的使用ESLint和Prettier

    eslint解决了编码规范的问题,不仅仅使得代码清晰易读,还避免了很多运行时可能报错的问题,比如下面一个例子中:

let number = 2;
switch(number){
  case 1:
    let name = 'jony';
    break;
  case 2:
    name = 'yu';
    break;
}

    这个例子就是经典的switch的块级作用域问题,上述的代码编译的时候不会报错,但是在执行的时候会报错。这种运行时的错误,在typescript的语法中是不可避免的,我们可以通过eslint的编码规范来避免这种有可能出现运行时报错的代码。

    对于Typescript项目的编码规范而言,主要有两种选择ESLint和TSLint。ESLint不仅能规范js代码,通过配置解析器,也能规范TS代码。此外由于性能问题,TypeScript 官方决定全面采用ESLint,甚至把仓库作为测试平台,而 ESLint 的 TypeScript 解析器也成为独立项目,专注解决双方兼容性问题。

二、Typescript内置函数

    在typescript的使用中,最经常,最频繁的就是写各种声明,包括接口声明、枚举等等,通过Typescript的内置函数,可以复用一些类型声明,简洁且有效减少了代码量。

Recommend Projects

  • React photo React

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

  • Vue.js photo Vue.js

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

  • Typescript photo Typescript

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

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

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

Recommend Topics

  • javascript

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

  • web

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

  • server

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

  • Machine learning

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

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

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

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.