Fork me on GitHub
image frame

Walter

面朝大海 春暖花开

JS变量提升

浅谈JS变量提升(2020新年第一章)

首先,先丢出来一道题:

console.log(typeof a);

a();

var a = 3;

function a() {

    console.log(typeof a);

}

console.log(typeof a);

a = 6;

a();

如果我们不了解JavaScript中的变量提升和函数声明提升,那我们的答案可能就是这样的:

undefined

ReferenceError: b is not defined

...

那么正确的答案是什么呢?我们首先把这段代码还原:

function a() {

    console.log(typeof a);

}

console.log(typeof a);

a();

a = 3;



console.log(typeof a);

a = 6;

a();

好了,这下看一看执行结果是什么呢?

"function"

"function"

"number"

"a is not a function"

为什么会发生提升?

首先我们需要知道一点编译原理的知识。JavaScript的源代码在运行的时候,会经过两个阶段:编译和执行。而且,编译阶段往往就在执行阶段的前几微秒甚至更短的时间内。

编译

词法解析

这个阶段会先把字符组成的字符串解析成一个个有意义的代码块,这些代码块也被称为词法单元。如 var a = 1; 会被解析成: var 、a、=、1、; ,空格是否会被解析成词法单元,就要看空格在这个语言中有意义。

语法解析

这个阶段会把词法解析生成的词法单元流解析成有元素逐级嵌套所组成的程序语法树,AST。至于AST的结构是如何构成的,这里有几个网站,大家可以去试试: 网站1网站2

代码生成

这个阶段就是将语法解析阶段生成的AST转译成可执行的代码。

变量提升

LHS查询

在编译的过程中,先将标识符和函数声明给提升到其对应的作用域的顶端。标识符解析的时候,会进行LHS查询,在LHS查询的时候,如果标识符一直找不到声明的位置,那么最终就会在全局环境生成一个全局变量。

LHS : 指的是赋值操作的左端。
标识符解析:请查看我的另外一篇文章。

RHS查询

说到LHS查询,就不得不提对应的RHS查询,相信大家已经看出RHS的意思了把,它指的是赋值操作的源头。RHS查询的时候,如果找不到对应的标识符,就会抛出一个异常:ReferenceError。

函数提升

函数提升和变量提升还有点不一样。函数提升,只会提升函数声明,而不会提升函数表达式。

f();

fn();//fn is not a function 

//函数表达式
var fn = function(){
    console.log(1)
}
//函数声明
function f(){
    console.log(0)
}

这里我把代码还原,我们来看一下:

//函数声明
function f(){
    console.log(0)
}

var fn;

f();

fn();

fn = function(){
    console.log(1)
}

这下明白为什么执行函数fn的时候,会报错了吧。

块级作用域

块级作用域是在ES6中提出的一个概念,ES5中是没有块级作用域的,只有全局作用域和函数作用域。
我们还是先看个案例:

console.log(a);//undefined
console.log(fn);//undefined
var flag = true;

if (!flag) {
    var a = 1;
};

if (true) {
    function fn(a) {
        yideng = a;
        console.log("yideng1");
    };
}
console.log(a);//undefined
console.log(fn);//function

还是把这段代码还原一下:

var a,flag,fn;
......

或许有人会问:为什么a和函数fn会被提到最顶端?变量a和函数fn都是在块级作用域中,其中a是通过var进行声明的,函数fn是一个函数声明。而非严格模式下,块级作用域是阻挡不了var声明的变量和函数声明的提升的。但是,声明函数。
更进一步

{
    a = 5;

    function a() {};

    console.log(window.a, a); //5 5

    a = 0;

    console.log(window.a, a); //5 0

}

console.log(window.a, a); //5 5

大家看到这个答案是不是很疑惑?当我第一次看到这个答案的时候,内心也是一万个🦙奔腾而过。
我们上面说过,块级作用域中,只有var声明的变量和函数声明可以直接提到全局作用域。块级作用域中的不实用var声明的变量,也会提升到全局作用域,但是只有在块级作用域中运行之后,才会反射到全局作用域上。
而且块级作用域和函数作用域完全不同:函数作用域会在执行完毕之后,重写window。而块级作用域之后进行一次有意义的重写。所谓有意义的重写,就是在外层作用域没有声明和赋值。
在块级作用域中,函数声明会提升到当前作用域顶层,至于会不会重写外层变量,那就要看是否有实际意义。

实战

现在,大家是不是对变量和函数的提升机制已经有一定的了解了?那么让我们在深度实战一下。

//'use strict'
console.log(a);

console.log(f);

var flag = true;

if (!flag) {
    var a = 1;
};

if (flag) {
    function f(a) {
        f = a;
        console.log("yideng1");
    };
}

console.log(typeof f);

在ES6非严格模式下,代码块中,只有使用 var 声明的变量和函数声明是可以提升的,但是函数声明只能将函数的名字提升出去。
ES5非严格模式下,各个浏览器表现不一样。

"undefined"

"undefined"

"function"

在ES6严格模式下:代码块中,只有使用 var 声明的变量可以提升出去。
ES5严格模式下,直接报错。因为ES5函数不允许在块级作用域中声明函数。函数的声明只能在函数作用域或者顶级作用域中。

"undefined"

"ReferenceError:f is not defined "//报错

再来看一个题:

function f() {
    console.log(typeof f); //function
    // var f = 3;
    f = 3;
    console.log(typeof f); //number
};

f();


var s = function s() {
    console.log(typeof s); //function
    // var s = 3;
    s = 3;
    console.log(typeof s); //function 
};

s();

上述代码中,函数f是具名函数,函数s是函数表达式。具名函数中,可以在函数内部改变函数名,而函数表达式,如果有函数名,则它的函数名只能作用在其自身作用域中,且不可改变改变函数名。

web Worker(2019最后一更)

web Worker的使用

web Worker

什么是web Worker

JavaScript是单线程的。所有的任务只能在同一个线程中进行,一次只能执行一个任务,后面的只能等待。而且现在计算机大多都是多核的,单线程不能有效的利用计算机的资源。
web Worker是HTML5 提出的JavaScript多线程解决方案。我们可以将一些大计算量的操作交给web worker去运算,而不必“冻结”页面。

web Worker的作用

为JavaScript创建多线程。允许主线程创建子线程,将主线程的复杂的任务分配给子线程去执行。在主线程运行的同时,子线程也在后台执行,二者互不影响。等子线程处理完成之后,再将结果传递给主线程。这样,子线程分担了主线程的负载,让主线程的运行“顺畅”起来,不会被阻塞。

web Worker的使用限制

  • 同源限制。分配的worker线程的脚本文件,必须和主线程同源。
  • DOM限制。worker线程所在的脚本文件,无法读取主线程所在的网页的DOM对象。但是可以使用 locationnavigator 对象。
  • 主线程和子线程通讯。主线程和worker线程不在同一个上下文对象,所以必须使用消息进行通讯。
  • 脚本限制。worker线程中不允许使用 alertconfirm 。但是可以使用 XMLHttpRequest 对象。
  • 文件限制。worker线程不能读取本地文件,它所加载的脚本文件,必须来源于网络。

基本使用

主线程

主线程新建worker线程:

//new Worker(jsUrl, options);
let worker = new Worker('child.js');

Worker 构造函数的参数是一个脚本文件,该脚本文件就是要Worker线程的脚本文件。该文件必须来自网络,否则则会失败。Worker接收第二个参数,该参数是对象形式,它的一个作用就是指定Worker的名称。

主线程和子线程通讯

主线程调用 worker.postMessage() 方法,向子线程传递数据。

worker.postMessage('准备好了吗?开始通信!')//or
worker.postMessage({
  type: 'common',
  message: "准备好了吗?开始通信!"
});

主线程指定监听事件

主线程使用 worker.onmessage() 方法,监听从子线程发送的数据:

worker.onmessage = function(data){
    console.log('主线程收到子线程发的消息!!!');
}

错误处理

主线程中可以监听worker是否发生错误。如果发生错误,worker就会触发主线程的 onerror 方法。

worker.onerror = function(e){
    //e.lineno :错误代码的行数
  //e.filename :发生错误的文件
  //e.message :错误详细信息
}

终止执行

当任务完成以后,主线程可以通过 worker.terminate() 方法,终止worker的执行。

worker.terminate();

worker线程

worker监听函数

在worker中,使用监听函数,来获取主线程发送的数据:

self.addEventListener('message', function(e) {
    console.log(`子线程接收消息:${e.data.message}`);
    self.postMessage(`我收到你发的消息了! 你发的消息是:${e.data.message}`)
})

上述代码中, self 代表子线程自身,即子线程的全局对象。

worker线程加载脚本

如果我们在worker中需要引入其他脚本文件的时候,可以使用 importScripts() 方法导入。

importScripts('underscore.js');

导入之后,就可以在worker线程中使用了。

关闭worker

当worker执行完毕之后,可以将其关闭:

self.close()

基础

webpack4.0(一):基础

概念

context

基础目录,绝对路径,用户设置从配置文件中解析入口文件和加载器。
如:

module.exports = {
    context:path.resolve(__dirname,'js),
  entry:'./main.js'
}

等同于:

module.exports = {
  entry:'./js/main.js
}

context配置可以不要,默认为当前目录下查找。但是如果要使用的话,建议添加第二个参数,用于告诉webpack是从当前目录下哪个文件夹中查找。

entry

webpack中有多种定义入口的形式。

单入口写法
const config = {
    entry:'./src/main.js'
};
module.exports = config;
对象语法
const config = {
    entry:{
    main:'./src/main.js',
    venders:'./src/venders.js'
  }
};
module.exports = config;

output

配置output可以指定webpack如何向硬盘中写入,注意的是:webpack可以配置多个入口文件,但是只有一个输出配置。

在webpack中,output最低的配置要求就是:

  • filename:用于指定输出的文件名
  • path:用于指定输出文件的路径
import path from 'path';
const config = {
  output:{
    filename:'[name].js',
    path:path.resolve(__dirname,'dist')
  }
};
module.exports = config;

mode

mode模块用于告诉webpack使用相应的模式进行优化。

  • development:开发环境。会将 process.env.NODE_ENV 的值设为 development。启用 NamedChunksPluginNamedModulesPlugin
  • production:生产环境。会将 process.env.NODE_ENV 的值设为 production。启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPluginUglifyJsPlugin
module.exports = {
  mode: 'production'
};

loader

我们都知道webpack可以对资源进行打包的。但是webpack只能处理js模块,对于其他模块的处理,就需要用loader对模块的源代码进行转换。loader可以使你在import或者“加载”模块的时候,进行预编译。loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript,或将内联图像转换为 data URL。loader 甚至允许你直接在 JavaScript 模块中 import CSS文件!

module.exports = {
  module:{
    rules:[
      {
        test:/\.css$/,
        use:['style-loader','css-loader']
      }
    ]
  }
}
编写一个loader

loader工具库:loader-utils、配合schema-utils

import { getOptions } from 'loader-utils';
import validateOptions from 'schema-utils';
//loader的API请参考:https://webpack.docschina.org/api/loaders/

所谓 loader 只是一个导出为函数的 JavaScript 模块。

  • 新建一个js文件:
//loader-utils 它提供了许多有用的工具,但最常用的一种工具是获取传递给 loader 的选项
const loadersUtils = require('loader-utils');
//这是最简单的一个loader
module.exports = function(content) {
  //content是文件内容
  const options = loadersUtils.getOptions(this)
  console.log(content, '--------content');
  console.log(options, '--------options');

   //同步loader
    // return '{};' + content
    /**
     * this.callback() 参数:
     * error:Error | null,当loader出错时向外跑出一个Error
     * content:经过loader处理过的内容
     * sourceMap:为方便调试生成的编译后内容的source map
     * ast:本次编译生成的AST静态语法树,之后执行的loader可以直接使用这个AST,可以省去重复生成AST的过程
     * 
     */
    // this.callback(null, '{};' + content)
   //异步 使用promise
    function timeout(time) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(content)
            }, time)
        })
    };
       // let data = await timeout(1000);
    // return data
    const callback = this.async()
    timeout(1000).then(data => {
        callback(null, data)
    })
}
  • webpack.config.js中使用该loader:
rules: [{
  test: /\.js$/,
  exclude: /node_modules/,
  use: {
    loader: resolve('./loaders/index.js'),
    options: {
        test: 1
    }
  }
}]
  • 如果有多个loader,可以在webpack.config.js中这样配置:
resolveLoader:{
    modules:[
        'node_modules',
        path.resolve(__dirname,'loaders')//loaders代表你存放loaders的文件夹
    ]
}

plugins

webpack事件流:

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

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

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

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

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

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

具体请参考:https://juejin.im/post/5abf33f16fb9a028e46ec352

alias

resolve.alias 是通过别名来将原导入路径映射成一个新的导入路径。比如如下配置:

module.exports = {
  entry: './js/main.js',
  output: {
    filename: 'bundle.js',
    // 将输出的文件都放在dist目录下
    path: path.resolve(__dirname, 'dist')
  },
  resolve: {
    alias: {
      components: './src/components'
    }
  }
}

如上代码配置,当我通过 import xxx from ‘components/xxx’ 导入时,实际上被alias替换成了
import xxx from ‘./src/components/xxx’;

extensions

在使用 import 导入文件时,有时候没有带入文件的后缀名,webpack会自动带上后缀去访问文件是否存在,那么默认的后缀名为
[‘.js’, ‘.json’]; 即:
extensions: [‘.js’, ‘.json’]; 如果我们想自己配置.vue后缀名,防止在导入文件时候不需要加上后缀;我们可以使用webpack做如下配置:

module.exports = {
  entry: './js/main.js',
  output: {
    filename: 'bundle.js',
    // 将输出的文件都放在dist目录下
    path: path.resolve(__dirname, 'dist')
  },
  resolve: {
    alias: {
      components: './src/components'
    },
    extensions: ['.js', '.vue'];
  }
}

Externals

Externals 用来告诉webpack在构建代码时使用了不处理应用的某些依赖库,依然可以在代码中通过AMD,CMD或window全局方式访问。
什么意思呢?就是说我们在html页面中引入了jquery源文件后,比如如下代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <script src="http://code.jquery.com/jquery-1.12.0.min.js"></script>
  <link href="dist/main.css" rel="stylesheet" type="text/css" />
</head>
<body>
  <div id="app"></div>
  <script src="dist/bundle.js"></script>
</body>
</html>

但是我们想在模块化的源代码里导入和使用jquery,首先我们肯定需要安装下 jquery依赖包,npm install –save jquery, 然后我们编写如下代码进行使用:

const $ = require('jquery');
console.log($);

构建打包后我们会发现输出的文件里面包含jquery的内容,这导致了jquery库引入了两次,并且导致了bundle.js文件变得更大,浪费加载流量。因此 Externals 配置项就是来解决这个问题的。
通过externals 可以告诉webpack在javascript运行环境中已经内置了哪些全局变量,不用将这些代码打包到代码里面去。
因此我们可以做如下配置:

module.export = {
  externals: {
    jquery: 'jQuery'
  }
};

什么是AST?

请参考:https://segmentfault.com/a/1190000016231512#comment-area

转换成AST的目的就是将我们书写的字符串文件转换成计算机更容易识别的数据结构,这样更容易提取其中的关键信息,而这棵树在计算机上的表现形式,其实就是一个单纯的Object。

JavaScript强制类型转换 置顶

类型

在说JavaScript的强制类型转换之前,我们先了解下JavaScript中的数据类型有哪些。JavaScript中类型分为以下两种:

  • 原始数据类型:null、undefined、bool、string、number、boolean以及symbol(es6提出)
  • 对象数据类型:Object(function、date 等属于Object的子类型)

那么这两种数据类型有什么区别呢?

原始数据类型在内存中的存放地点是存放在栈中,所以在传递的过程中,是值传递。

而对象数据类型的存放方式是:真实数据存放在堆中,而在栈中存放的是指向实际数据存储的地方,即堆的指针。

这也就是值传递和引用传递。

字符串、数字和布尔类型之间的转换

ToString

它负责处理非字符串到字符串的转换。

基本类型值的转换规则是:null转换为”null”,undefined转换为”undefined”,布尔转换成对应的数字:true转换为1,false转换为0。数字直接转换为对应的字符串。

对应对象,除非对象内部定义了tostring方法,否则toString()(Object.prototype.toString())返回其内部属性[[class]]的值。如果内部自定义了toString方法,则调用该内部方法并使用其返回值。

对象的强制类型转换为string是通过Toprimitive抽象操作来完成的。

ToPrimitive(input,PreferredType?):其中PreferredType为number或者string。

如果input是原始值,则返回该原始值对应的字符串。

如果input不是原始值,则调用valueOf()方法,如果该方法返回原始值,则使用该方法返回值。

如果valueOf()返回的不是原始值,则调用toString()方法,如果该方法返回原始值,则使用该方法返回的原始值。

否则,报错TypeError

JSON字符串化

工具函数JSON.stringify可以将JSON对象序列化为字符串,内部也是调用了ToString。

JSON.stringify使用规则:对于基本数据类型的值,使用JSON.stringify和使用toString返回结果一样。对于所有安全的JSON值,都可以使用JSON.stringify方法,所谓的JSON安全值是指:除了undefined、symbol、function以及包含循环引用的对象。

JSON.stringify接收第二个参数:可以为字符串数组或者函数。如果是一个字符串数组,则根据数组内的元素,返回指定的对象属性和属性值。如果为函数,则为过滤函数,接收两个参数,key、value,我们可以根据自己的需要自行设定返回值。如果不需要某个属性,返回undefined即可过滤。

JSON.stringify接收第三个参数:用于指定输出字符串的输出格式。

如果对象内部有自定义的toJSON方法,则使用该方法。

var obj = {
  a:10,
  b:0,
  // toJSON:function(){
  //     return 0
  // }
};
let str = JSON.stringify(obj,function(key,value){//过滤函数
  if(key === 'a'){
    return 100
  }else{
    return value
  }
});
let o = JSON.parse(str,function(key,value){//还原函数
  if(key === 'a'){
    return 1
  }else{
    return value
  }
})
console.log(o);

ToNumber

用来将非数字处理成数字。转换方式遵循ToPrimitive,

ToBoolean

JavaScript中的值大概可以分为两类:

  • 可以被强制转换为false的值,如:undefined、null、false、+0、-0、NaN和””
  • 其他可以被转换为true的值

假值对象:封装了假值的对象。

var a = new Boolean( false );//true
var b = new Number( 0 );//true
var c = new String( "" );//true

它们的值为true,但是如果对其进行强制类型转换,则结果为false。

强制类型转换

隐式强制类型转换

字符串和数字的隐式强制转换

运算符:

var a = "42";
var b = "0";
var c = 42;
var d = 0;
a + b; // "420"
c + d; // 42

var a = [1,2];
var b = [3,4];
a + b; // "1,23,4"

根据 ES5 规范 11.6.1 节,如果某个操作数是字符串或者能够通过以下步骤转换为字符串 的话,+ 将进行拼接操作。如果其中一个操作数是对象(包括数组),则首先对其调用 ToPrimitive 抽象操作(规范 9.1 节),该抽象操作再调用 [[DefaultValue]](规范 8.12.8 节),以数字作为上下文。

宽松相等和严格相等

宽松相等(loose equals)== 和严格相等(strict equals)=== 都用来判断两个值是否“相等”,但是它们之间有一个很重要的区别,特别是在判断条件上。

  • == 在相等比较中,允许进行类型转换
  • === 不允许类型转换
抽象相等
  • 如果表达式两端都是null、undefined,则为true。
  • 如果NaN在其中一端,则返回false。
  • 如果是string、number、boolean并且类型不一致,则先使用ToNumber转换为number在比较。
  • 如果是对象,则使用ToPrimitive,获取原始值再比较。

显示类型转换

字符串和数字的显示强制转换
var a = 42;
var b = String( a );
var c = "3.14";
var d = Number( c );
b; // "42"
d; // 3.14

String(..) 遵循前面讲过的 ToString 规则,将值转换为字符串基本类型。Number(..) 遵循 前面讲过的 ToNumber 规则,将值转换为数字基本类型。

运算符

字位运算符只适用于 32 位整数,运算符会强制操作数使用 32 位 格式。这是通过抽象操作 ToInt32 来实现的

ToInt32:先试用ToNumber转换为数字,再执行ToInt32。

~~:~~ 中的第一个 ~ 执行 ToInt32 并反转字位。然后第二个 ~ 再进行一次字位反转,即将所有 字位反转回原值

parseInt:只支持字符串解析成数字。

~~和parseInt的区别:

1、解析成数字允许字符串中有非数字字符,从左到右依次解析,遇到非数字字符停止解析

2、强制转换成数字,不允许存在非数字字符,否则转换失败NaN

重排与重绘

重排和重绘

浏览器构建页面布局

  • 构建DOM树(parse):渲染引擎解析html文档,首先将标签转换成DOM树中的DOM node,然后生成内容树(Content Tree/DOM Tree)。同时,也会进行CSS解析,构建CSS Rules Tree
  • 构建渲染树(render tree):将 CSS Rules Tree依附于DOM Tree 之上,形成Render Tree
  • 构建布局渲染树(reflow/layout):从根节点递归调用,计算每一个元素的大小、位置等,给出每个节点所应该在屏幕上出现的精确坐标。
  • 绘制渲染树(paint/repaint):遍历渲染树,使用UI层来绘制每个节点。

重绘(repaint / redraw)

重绘是指一个元素外观的改变所触发的浏览器行为,浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。

触发重绘的条件:改变元素外观属性。如:color,background-color等。

重排(重构/回流/reflow)

当渲染树中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建, 这就称为回流(reflow)。每个页面至少需要一次回流,就是在页面第一次加载的时候。

重排发生的根本原理就是元素的几何属性发生了改变。如:

  • 添加或删除可见的DOM元素
  • 元素位置改变
  • 元素本身的尺寸发生改变
  • 内容改变
  • 页面渲染器初始化
  • 浏览器窗口大小发生改变
  • 读取某些元素属性:(offsetLeft/Top/Height/Width, clientTop/Left/Width/Height, scrollTop/Left/Width/Height, width/height, getComputedStyle(), currentStyle(IE) )

重排和重绘的关系

在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘。

所以:重排必定会引起重绘,但是重绘不一定引起重排。

重绘和重排的代价很高,耗时,会造成浏览器的卡顿

优化

  • 浏览器自己的优化:浏览器会维护1个队列,把所有会引起回流、重绘的操作放入这个队列,等队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会flush队列,进行一个批处理。这样就会让多次的回流、重绘变成一次回流重绘。

  • 开发者自己的优化:尽量减少重绘重排的次数,合并多次的DOM修改和样式修改,减少对style样式的请求

    • 直接改变元素的className
.active {
    padding: 5px;
    border-left: 1px;
    border-right: 2px;
}
// javascript
var el = document.querySelector('.el');
el.className = 'active';
  • display:none;先设置元素为display:none;然后进行页面布局等操作;设置完成后将元素设置为display:block;这样的话就只引发两次重绘和重排;
let ul = document.querySelector('#mylist');
ul.style.display = 'none';
appendNode(ul, data);
ul.style.display = 'block';
  • 减少重绘重排
var el = document.querySelector('.el');
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
el.style.padding = '5px';

//替换为
var el = document.querySelector('.el');
el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px';
  • 如果需要创建多个DOM节点,可以使用DocumentFragment创建完后一次性的加入document;
var fragment = document.createDocumentFragment();

var li = document.createElement('li');
li.innerHTML = 'apple';
fragment.appendChild(li);

var li = document.createElement('li');
li.innerHTML = 'watermelon';
fragment.appendChild(li);

document.getElementById('fruit').appendChild(fragment);
  • 将原始元素拷贝到一个独立的节点中,操作这个节点,然后覆盖原始元素
let old = document.querySelector('#mylist');
let clone = old.cloneNode(true);
appendNode(clone, data);
old.parentNode.replaceChild(clone, old);
  • 缓存布局信息,减少对style的多次请求
div.style.left = 1 + div.offsetLeft + 'px';
div.style.top = 1 + div.offsetTop + 'px';

//替换为
current = div.offsetLeft;
div.style.left = 1 + ++current + 'px';
div.style.top = 1 + ++current + 'px';
  • 尽量不要使用table布局。

HTML5拖拽事件

在网页中,拖放是一个很常见的一个用户界面模式。简单来说,就是鼠标点击目标元素不放,移动鼠标位置,然后松开鼠标,即可将目标元素移动到另一个区域。它的实现其实很简单。以下介绍两种常见的实现方法。

使用onmouse事件实现拖放

通过监听鼠标事件实现元素拖放:必须借用onmousedownonmousemoveonmouseup三个事件实现,且三者的顺序不能乱。

  • onmousedown:鼠标按下事件,一般用在拖放元素上。
  • onmousemove:鼠标移动事件,一般用在document或者window上。
  • onmouseup:鼠标抬起事件,同上。

具体实现如下:

//获取拖放元素
var img = document.getElementById('img');
//记录鼠标点击位置和元素边界的距离
var dirX = 0,dirY = 0; 
//给元素添加监听事件
img.onmousedown = function(e) {
  dirX = e.clientX - img.offsetLeft;
    dirY = e.clientY - img.offsetTop;
  document.onmousemove = function(e) {
    //记录鼠标移动距离
    let moveX, moveY; 
    moveX = e.clientX - dirX;
    moveY = e.clientY - dirY;
    //处理边界问题
    if (moveX < 0) {
      moveX = 0
    } else if (moveX > window.innerWidth - img.offsetWidth) {
      moveX = window.innerWidth - img.offsetWidth
    }
    if (moveY < 0) {
      moveY = 0
    } else if (moveY > window.innerHeight - img.offsetHeight) {
      moveY = window.innerHeight - img.offsetHeight
    };
    //实时的改变元素位置
    img.style.left = moveX + 'px';
    img.style.top = moveY + 'px'
  }
  //监听鼠标抬起事件
  document.onmouseup = function(e) {
    //事件取消
    document.onmouseup = document.onmousemove = null
  }
}

使用HTML5Drag实现拖放

拖拽元素时,将依次发生以下事件:

  • ondragstart
  • ondrag:拖拽期间持续触发该事件
  • ondragend

在目标元素(放置目标元素)上将会发生以下事件:

  • ondragenter:元素进入目标元素
  • ondragover:元素在目标元素之中移动
  • ondragleave/ondrop:元素离开目标元素/鼠标抬起

dataTransfer 对象:使用该对象进行数据传输

  • setData:接收两个参数:第一个参数设置传输数据的类型:text 或者 URL,第二个参数设置值
  • getData:接收值,只发生在ondrop

dropEffecteffectAllowed :通过dataTransfer对象还可以确定被拖拽元素以及放置目标的元素能够接受什么操作。其中dropEffect可以知道被拖拽元素可以执行那种放置行为。effectAllowed可以知道允许拖拽元素执行哪种dropEffect

effectAllowed有四种值:

  • “none”:不能把拖动的元素放在这里。这是除文本框之外所有元素的默认值。
  • “move”:应该把拖动的元素移动到放置目标。
  • “copy”:应该把拖动的元素复制到放置目标。
  • “link”:表示放置目标会打开拖动的元素(但拖动的元素必须是一个链接,有 URL)。

要使用 dropEffect 属性,必须在 ondragenter 事件处理程序中针对放置目标来设置它。

effectAllowed的属性值如下:

  • “uninitialized”:没有给被拖动的元素设置任何放置行为。
  • “none”:被拖动的元素不能有任何行为。
  • “copy”:只允许值为”copy”的 dropEffect。
  • “link”:只允许值为”link”的 dropEffect。
  • “move”:只允许值为”move”的 dropEffect。
  • “copyLink”:允许值为”copy”和”link”的 dropEffect。
  • “copyMove”:允许值为”copy”和”move”的 dropEffect。
  • “linkMove”:允许值为”link”和”move”的 dropEffect。
  • “all”:允许任意 dropEffect。

必须在 ondragstart 事件处理程序中设置 effectAllowed 属性

使用HTML5Drag事件来实现自由拖拽,具体实现如下:

var EventUtil = {
    /**
     * 绑定事件
     * @param { type } = { 事件类型 }  
     * @param { callback }  = { 回调函数 } 
     * @param { bool }   = { 指定事件是否在捕获或冒泡阶段执行。true - 事件句柄在捕获阶段执行。默认,事件句柄在冒泡阶段执行 }
     * */
    addHandler: function(el, type, callback, bool = false) {
        if (el.addEventListener) {
            el.addEventListener(type, callback, bool)
        } else if (el.attachEvent) {
            e.attactEvent('on' + type, callback)
        } else { //默认使用DOM0级的事件
            el['on' + type] = callback;
        }
    },
}
var img = document.getElementById('img');
//记录鼠标点击位置和边界的距离
var dirX = 0,dirY = 0; 
var dirX, dirY;
EventUtil.addHandler(img, 'dragstart', function(e) {
  console.log(e, '开始拖拽');
  dirX = e.clientX - img.offsetLeft;
  dirY = e.clientY - img.offsetTop;
  console.log(dirX, dirY);
});
EventUtil.addHandler(window, 'dragenter', function(e) {
  console.log('进入目标元素');
});
EventUtil.addHandler(window, 'dragover', function(e) {
  // console.log('在目标元素中移动');
  //计算鼠标移动距离
  let moveX = moveY = 0;
  moveX = e.clientX - dirX;
  moveY = e.clientY - dirY;
  //边界处理
  if (moveX < 0) {
    moveX = 0
  } else if (moveX > (window.innerWidth - img.offsetWidth)) {
    moveX = window.innerWidth - img.offsetWidth
  }
  if (moveY < 0) {
    moveY = 0
  } else if (moveY > (window.innerHeight - img.offsetHeight)) {
    moveY = window.innerHeight - img.offsetHeight
  }
  img.style.left = moveX + 'px';
  img.style.top = moveY + 'px';
  //阻止默认行为
  EventUtil.preventDefault(e)
});
EventUtil.addHandler(window, 'dragleave', function(e) {
  console.log('离开目标元素');
});
EventUtil.addHandler(window, 'drop', function(e) {
  //设置磁吸效果
  if (e.clientX > (window.innerWidth / 2)) {
    img.style.left = window.innerWidth - img.offsetWidth + 'px'
  } else {
    img.style.left = 0 + 'px';
  }
  // console.log(e,'drop');
  // let data = e.dataTransfer.getData('text');
  // console.log('drop');
  // EventUtil.removeHandler()
  // e.target.appendChild(document.getElementById(data))
});

拖放事件流程

浅谈JavaScript异步编程

JavaScript异步编程

Promise

Promise是异步编程的一种实现方式,它比传统的解决方案更加合适、强大。所谓Promise,就是一种容器,容器里面保存着未来才会发生的事情(一般是异步操作)的结果。

Promise是一个对象,它提供一系列的API可以将异步操作的事情以同步的形式表现出来,比起”回调地狱”,它看起来更加合适、强大。它具有以下几大特点:

  • Promise的状态不受任何外界的影响。Promise的异步操作有三种状态: pending(执行中)、fulfilled(已成功)、rejected(已失败)。只有异步操作的结果才能够决定Promise状态的走向。
  • Promise的状态一旦改变,变不可再次变动。它的状态变化只有两种情况:pending => fulfilledpengding => rejected。状态一旦发生变化,就不再改变,称之为 resolved(已定型)。

Promise一旦新建之后,就会立即执行。而then指定的回调函数只有同步操作执行完之后才会执行。

const promise = new Promise((resolve,reject) => {

});
promise.then(() => {//成功回调
  //todo
},(error) => {//失败回调

}).catch(e => {
  //todo
})

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。但是一般默认不指定rejected状态的回调函数,而是使用catch

finally不管promise最终的执行结果如何,都会执行。

all:方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

Promise.all()方法接受一个数组作为参数,p1p2p3都是 Promise 实例,如果不是,就会先调用Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例

举个🌰:

var p1 = new Promise(resolve => {
  setTimeout(() => {
    resolve({
        msg: 'p1'
    })
  }, 3000)
});
var p2 = new Promise(resolve => {
  setTimeout(() => {
    resolve({
        msg: 'p2'
    })
  }, 1000)
});
var p3 = new Promise(resolve => {
  setTimeout(() => {
    resolve({
        msg: 'p3'
    })
  }, 500)
});
let p = Promise.all([p1,p2,p3]);
p.then(([res1,res2,res3]) => {
  console.log(res1);
  console.log(res2);
  console.log(res3);
}).catch(e => {

});
//执行结果:p1 p2 p3

p的状态由p1,p2,p3决定:

  • 只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。

  • 只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

race:该方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

let p = Promise.race([p1, p2, p3]);
p.then((res) => {
    console.log(res);
});

只要只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

allSettled:该方法由ES2020引入。Promise.allSettled()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束。

anyPromise.any()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。该方法目前是一个第三阶段的提案

resolvePromise.resolve()将现有对象转为 Promise 对象。

  • 参数是一个Promise实例:那么Promise.resolve将不做任何修改、原封不动地返回这个实例。

  • 参数是一个 thenable 实例:thenable对象指的是具有then方法的对象,比如下面这个对象。:

    let thenable = {
      then: function(resolve, reject) {
        resolve(42);
      }
    };

    Promise.resolve方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then方法。

  • 参数不是具有then方法的对象,或根本就不是对象:如果参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的 Promise 对象,状态为resolved

    const p = Promise.resolve('Hello');
    
    p.then(function (s){
      console.log(s)
    });
    // Hello

rejectPromise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected

Generator/yield

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。

Generator 函数有多种理解角度。语法上,Generator 函数是一个状态机,封装了多个内部状态。

执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

形式上,Generator 函数是一个普通函数,但是有两个特征:

1、function关键字与函数名之间有一个星号;

2、函数体内部使用yield表达式,定义不同的内部状态

Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}
let hwg = helloWorldGenerator();
hwg.next();//{ value:hello,done:false }
hwg.next();//{ value:world,done:false }
hwg.next();//{ value:ending,done:true }
hwg.next();//{ value:undefined,done:true }

总结:调用Generator函数,会返回一个遍历器对象,它是函数内部的指针,以后每调用一次next函数,就会返回一个带有value和done属性的对象。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。

Generator 函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历 Generator 函数。

async/await

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。它是Generator的语法糖。async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await

async函数对 Generator 函数的改进,体现在以下几点。

  • 内置执行器。

    Generator 函数的执行必须靠执行器。而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。

  • 返回值是 Promise

    async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。

进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

async函数返回一个 Promise 对象。async函数内部return语句返回的值,会成为then方法回调函数的参数。

async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。

举个🌰:

function timeFn(s){
  //Promise 新建后就会立即执行。
  return new Promise((resolve,reject) => {
    setTimeout(() => {
      resolve({
        msg:'hello'
      })
    },s)
  })
}
async function fun(time){
  //Promise的写法
  // timeFn(time).then(res => {
  //     console.log(res);
  // })

  //async await的写法
  let str = await timeFn(time);
  console.log(str);
}
fun(3000)

程序会在3s之后打印{msg:’hello’}

await:正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。另一种情况是,await命令后面是一个thenable对象(即定义then方法的对象),那么await会将其等同于 Promise 对象。

如果await后面的异步操作出错,那么等同于async函数返回的 Promise 对象被reject

await命令只能用在async函数之中,如果用在普通函数,就会报错。

async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。

async function fn(args) {
  // ...
}

// 等同于

function fn(args) {
  return spawn(function* () {
    // ...
  });
}
//spawn 自动执行器

浏览器中的进程、线程和Event Loop

#浏览器

浏览器是多进程的。每一个Tab页就是一个独立的进程。

  • 主进程:个人理解问“管家”的角色,它控制着浏览器的各个任务,页面展示、网络请求、历史回退、前进等等
  • 第三方插件进程:管理第三方插件
  • GPU线程:用于3D绘制
  • 渲染进程:对前端来说,最重要的进程。

浏览器内核(渲染进程)

渲染进程是多线程的。它包含一下几大线程:

  • GUI线程
    • 它负责页面的渲染、布局。
    • 当页面发生重绘或者回流的时候,就会触发该线程
    • 它和JS引擎线程互斥
  • JS引擎线程
    • 负责程序的解析和执行
  • 事件触发线程
    • 它控制事件循环,管理着一个事件任务队列TaskQueue
    • 当异步任务满足条件时,会将该异步任务的回调函数放入到JS引擎线程所在的执行栈中执行
  • 定时触发器线程
    • setTimeOut 和 setInterval 所在的线程
    • 定时器任务的计时不是由JS引擎计时的,而是由该线程控制
    • 当定时任务计时完成之后,会通知事件触发线程
  • 异步http请求线程
    • 一个独立的ajax请求线程
    • 当请求完成之后,会通知事件触发线程

为什么GUI线程和JS引擎线程互斥

因为JS是可以操作DOM的,如果使用JS操作DOM的同时,GUI线程也在渲染DOM,那么渲染完成之后的元素可能就不是之前的元素了

为什么JS引擎是单线程

  • 创建JavaScript语言的时候,多进程多线程的架构并不流行,硬件支持度不高
  • 多进程多线程操作需要加锁,操作成本较高,较为复杂
  • 如果多个线程同时操作一个DOM,那么结果会是不可预料的

Event Loop

  • JS的任务分为同步任何和异步任务的
  • 同步任务都在JS引擎的执行栈上执行
  • 事件触发线程管理一个任务队列,TaskQueue,当异步任务满足条件时,事件触发线程会将其放到任务队列当中,当主线程(JS引擎线程)执行完执行栈中的任务之后,会读取任务队列中的任务,如果有,就将可执行的异步任务的回调函数推入到执行栈中,开始执行。如果没有,则再次向事件触发线程发起询问,直到有为止。

Event Loop

代码解释:

let timerCallback = function() {
  console.log('timerCallback');
};
let httpCallback = function() {
  console.log('httpCallback');
}

// 同步任务
console.log('同步任务1');
// 同步任务
// 通知定时器线程 1s 后将 timerCallback 交由事件触发线程处理
// 1s 后事件触发线程将 timerCallback 加入到事件队列中
setTimeout(timerCallback,1000);
// 同步任务
// 通知异步http请求线程发送网络请求,请求成功后将 httpCallback 交由事件触发线程处理
// 请求成功后事件触发线程将 httpCallback 加入到事件队列中
$.get('www.xxxx.com',httpCallback);
// 同步任务
console.log('同步任务2');
//...
// 所有同步任务执行完后
// 询问事件触发线程在事件事件队列中是否有需要执行的回调函数
// 如果没有,一直询问,直到有为止
// 如果有,将回调事件加入执行栈中,开始执行回调代码

总结:

  • JS引擎线程只执行执行栈中的事件
  • 执行栈中的代码执行完毕,就会读取事件队列中的事件
  • 事件队列中的回调事件,是由各自线程插入到事件队列中的
  • 如此循环

宏任务、微任务

宏任务:可以理解为浏览器级别的任务。主代码块,setTimeout,setInterval等,都属于宏任务

微任务:JS引擎级别的任务。Promise,process.nextTick等,属于微任务

执行顺序:

  • 当前代码块对应的宏任务
  • 当前宏任务结束之后,下一个宏任务开始之前,执行当前对列的微任务
  • GUI引擎渲染
  • 下一个宏任务

详解JS的for循环 置顶

for

let arr1 = [1, 2, 3];
arr1['name'] = 'l'
for (let i = 0, len = arr1.length; i < len; i++) {
  console.log(i, arr1[i]);
}

for循环可以使用breakcontinue

for in

  • for-in 只能遍历“可枚举的属性” 可以使用查看是否可枚举Object.getOwnPropertyDescriptor(object,property)
  • 它实际上遍历的是对象的属性,而不是“索引值”, 所以for in可以遍历对象
  • 遍历原型上的属性(可以配合 Object.hasOwnProperty())
  • for in遍历的顺序并不确定
  • 性能:每次迭代操作会同时搜索实例或者原型属性, for-in 循环的每次迭代都会产生更多开销,因此要比其他循环类型慢
Array.prototype.age = 20;
for (let key in arr1) {
    console.log(`arr1[${key}] = ${arr1[key]}`);
}
console.log(arr1.hasOwnProperty('age'));

forEach

  • forEach 方法为数组中含有有效值的每一项执行一次 callback 函数
  • 那些已删除(使用 delete 方法等情况)或者从未赋值的项将被跳过(不包括那些值为 undefined 或 null 的项)
  • 不能使用break 和 continue
  • forEach 的速度不如 for
arr1[4];
arr1.forEach((item,index,arr1) => {
    console.log(item,index,arr1);
})

看🌰:mac电脑、chrome浏览器的执行环境下

console.time('for');
for(let i = 0; i< 100000 ;i++){
    console.log(i);
}
console.timeEnd('for');//耗时约8.5s

arr = new Array(100000).fill(0);
console.time('foreach');
arr.forEach((item,index) => {
    console.log(index);
})
console.timeEnd('foreach');//耗时约10.34s

for of

  • for of 是用来遍历可迭代对象的,js 引擎是通过判断对象是否具有 Symbol.iterator 来判断的,

    • [][Symbol.iterator]
    • {}[Symbol.iterator]
  • 迭代器:有next()方法的对象,next()方法返回:done和value,done标示是否遍历完,value就是当前的值

  • 跟 forEach 相比,它可以正确响应 break, continue, return。

  • for-of 循环不仅支持数组,还支持大多数类数组对象,例如 DOM nodelist 对象。

  • for-of 循环也支持字符串遍历,它将字符串视为一系列 Unicode 字符来进行遍历

  • for-of循环不支持普通对象,但如果你想迭代一个对象的属性,你可以用for-in 循环

for (let [item,index] of arr1.entries()) {
  console.log(item,index,'===========for of');
}

既然知道了for of的工作原理,那么我们是否可以手动实现一个功能,让对象可以被for of遍历呢?

function* createIterator(obj){
  let propKeys = Reflect.ownKeys(obj);

  for (let propKey of propKeys) {
    yield [propKey, obj[propKey]];
  }
}

const obj = {
  name:'kaka',
  age:24
}

for(let [key,value] of createIterator(obj)){
  console.log(key,value,'=========forof');
}

tree-shaking

js:tree-shaking

  • 它是指帮助开发者消除不同模块之间的一些无效代码的feature。在webpack中,也是有tree-shaking功能的,但是它的功能十分简单粗暴:只寻找import引入进来的变量是否出现过在模块内,非常简单粗暴。因为在开发过程中,开发者经常会犯这种错误:一些模块曾经引入了进来,但是后来却没有使用到,忘记删除了,这就会导致打包的时候,webpack自带的tree-shaking功能无法将这些无效的feature去除。

  • 这是我们需要借助一些第三方插件去做这个事情:

    npm install --save-dev webpack-deep-scope-plugin
    
    //在webpack.config.js中引用
    const WebpackDeepScopeAnalysisPlugin = require('webpack-deep-scope-plugin').default;
    
    module.export = {
        ......
        plugins:[
            new WebpackDeepScopeAnalysisPlugin()
        ]
    }

    详情请参考:https://diverse.space/2018/05/better-tree-shaking-with-scope-analysis

css:tree-shaking

  • spa:purifycss-webpack该插件会把所有未被引用的css全部去除

    production:
    npm install --save-dev purifycss-webpack
    
    //在webpack.config.js中引用
    const path = require('path');
    //查找文件
    const glob = require('glob');
    const PurifyCSSPlugin = require('purifycss-webpack');
    module.export = {
        ......
        plugins:[
            new PurifyCSSPlugin({
          // Give paths to parse for rules. These should be absolute!
          paths: glob.sync(path.join(__dirname, './dist/*.html')),
        }),
        ]
    }
  • mpa:mini-css-extract-plugin将css从js中抽离出来

    注意:它和style-loader 互斥, 不能同时开启,且HMR(热更新的时候也不支持) , 只用于开发环境。

    npm install --save-dev mini-css-extract-plugin;
    
    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    
    module.export = {
      ......
      rules: [
        {
          test: /\.css$/i,
          use: [
            // 'style-loader',
            {//和style-loader 互斥 不能同时开启 HMR(热更新的时候也不支持)  只用于开发环境
              loader: MiniCssExtractPlugin.loader,
              options: {
                publicPath: '../',
              },
            },
            loader: 'css-loader'
            }
          ]
        }
      ]
    }

单元测试

单元测试

单元测试是指对软件中最小可测试单元进行检查验证,也称作模块测试。在nodeJs中通常是指对某个函数或者API进行正确验证,以保证代码的可用性。

单元测试有很多种,常见的有:行为驱动开发(BDD)和 测试驱动开发(TDD)。

  • 行为驱动开发(BDD):行为驱动开发关注的是整个系统的最终实现是否和用户期望一致。
  • 测试驱动开发(TDD):测试驱动开发的目的是取得快速反馈,使所有功能都是可用的。

Mocha

Mocha是现在最流行的一种单元测试框架。Mocha功能比较丰富,支持BDD、TDD

风格的测试,而且支持异步、同步的测试。

安装:

npm install -g mocha

or

npm install -S mocha

简单使用

创建一个demo01.js文件,文件中包含一个求和函数:

function add(...rest){
    return rest.reduce((a,b) => {
        return a + b
    })
}

module.exports = {
    add
}

再创建一个test的文件夹,进行测试:

let lib = require('../demo01.js');
let assert = require('assert');
let should = require('should');

describe('Math',() => {
    describe('#add',() => {
        it('add(0,0) should return 0',() => {
                //使用assert断言库
            // assert.strictEqual(0,lib.add(0,0));

            //使用should.js断言库进行测试
            lib.add(0,0).should.be.equal(0);
        });
        it('add(1,-1) should return 0',() => {
            // assert.strictEqual(0,lib.add(1,-1))
            lib.add(1,-1).should.be.equal(0);
        });
        it('add(1,1) should return 2',() => {
            // assert.strictEqual(2,lib.add(1,1))
            lib.add(1,1).should.be.equal(2)
        });
    })
})

describe(moduleName,testDetail):描述将要测试的模块。

it(info,function()):测试语句放在回调函数中:

  • info 是正确输出时的简单语句描述
  • 一个it对应一个实际的可能情况

关于assets的更多的API,可参考node.js官网

关于should.js的更多的API,可参考should.js官网

然后,在package.json中写一下命令:

"scripts": {
  "test": "mocha"
}

执行命令,mocha即可自动执行test文件夹下所有的测试脚本。

异步测试

使用Mocha进行异步测试,只需要在测试完成之后回调一个回调函数即可,例如:

//异步测试
function asyncFn(callback){
    setTimeout(() => {
        console.log('async');
        callback && callback()
    },1000)
};
module.exports = {
    asyncFn
}

测试:

const lib = require('../demo03.js');

describe('async.js',() => {
    describe('async',() => {
        it('should wait 1000ms',(done) => {
            lib.asyncFn(done);
        })
    })
})

路由测试

需要借用supertest库对后端的API接口进行测试.

安装:

npm install --save-dev supertest

supertest支持各种框架。本例以Express为例:

//测试路由
const express = require('express');
const app = express();

app.get('/user',(req,res) => {
    res.status(200).json({
        name:'ykx',
        age:24
    })
});

module.exports = app;

express设置了一个返回json格式的/user路由,通过get请求,返回用户名和年龄。

测试:

const request = require('supertest');
const app = require('../demo04.js');

describe('GET /user',() => {
    it('should an name with age',(done) => {
        request(app)
        .get('/user')
        .expect('Content-Type','application/json;charset=utf-8')
        .expect(200,{
            name:'ykx',
            age:24
        },done)
    })
})

测试覆盖率

测试覆盖率一般包含四个维度:

  • 行覆盖率:是否每一行都执行了
  • 函数覆盖率:是否每一个函数都执行了
  • 分支覆盖率:是否每个if代码块都执行了
  • 语句覆盖率。是否每个语句都执行了

在NodeJs中可以使用Istanbul代码覆盖率工具。

安装:

npm install -g istanbul

然后在package.json中去配置命令:

"scripts": {
  "test": "mocha",
  "cover":"istanbul cover node_modules/mocha/bin/_mocha test/"
}

执行npm run cover命令,会在根目录下生成coverage文件夹,里面包含所有的测试数据。

MySQL自然连接查询

什么是自然连接?

通过mysql自己判断完成连接过程,不需要指定连接条件,mysql会根据多个表内的相同字段作为连接条件。

自然连接的分类

自然连接分为:内自然连接(inner natural join)和 外自然连接。其中外自然连接又可分为左外自然连接(left natural join )和 右外自然连接(right natural join)。

注意:自然连接没有判断语句!!!

语法

假如存在两张表:tab1和tab2

  • 内自然连接

    select * from tab1 natural join tab2;

    内自然连接相当于using为判断语句的内连接:即:

    select * from tbl_name1 inner join tbl_name2 using;
  • 左外自然连接

    select * from tab1 natural left join tab2;

    相当于using为判断语句的左外连接:

    select * from tab1 left outer join tab2 using;
  • 右外自然连接

    select * from tab1 natural right join tab2;

    相当于using为判断语句的右外连接:

    select * from tab1 natural right join tab2;