Git Product home page Git Product logo

front-end-knowledge-share's Introduction

Front-End-Interview & Personal blog share

  • 分享一些前端知识和面试题,包括:JavaScriptCSSVue前端性能优化与webpack算法工具分享相关的内容。
  • 这个库会不定期地更新和分享。
  • 收藏请点Star,订阅请点Watch。👋👋👋
  • 如果大家在阅读的过程中发现有出错的地方,欢迎留言指正。

1. JavaScript面试知识点总结

目录

2. CSS

目录

3. Vue相关

目录

4. 前端性能优化与webpack

目录

5. 算法

几乎在所有大厂的面试中,算法是面试者(无论前端还是后端)永远无法逃避的一个重点内容。

目录

6. 工具分享

前端知识确实庞大且繁杂,但实际工作中除了对基础知识的要求之外,项目整体自动化构建及自动化测试也成了不可缺少的技术栈。

目录

front-end-knowledge-share's People

Contributors

jchappytime avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar  avatar

front-end-knowledge-share's Issues

如何最大程度的优化webpack打包速度?

参考文章

1. lodash在webpack上的各种优化尝试

前言

想象一个场景,两个前端项目组 A 和 B 同时上线2个项目,比如 A 上线的项目叫做”牛云聊“,B 上线的项目叫做”牛外卖“。结果上线当天,A 只用了1个小时实现打包上线,而 B 却用了一天时间打包上线。这时候我们可以想象一下B项目经理脸上的表情。

实际上,在上线前我们是需要对webpack打包进行一定优化的,那么我们该从哪些方向来进行优化呢?请继续往下看。

如果你们项目组正在使用 webpack4.x 的版本,那你应该或多或少了解到 4.x 版本的webpack在生产环境下会对代码做自动的tree shaking。但是,可能当你读完这篇文章你的Tree-Shaking并没什么卵用之后,你会有不一样的看法,你的Tree-Shaking不一定真的有用!

Tree-Shaking的原理

总结一下tree-shaking的原理就是:

  • ES6的模块引入是静态分析的,所以可以在编译时正确判断到底加载了哪些代码;
  • 分析程序流,可以判断哪些变量未被引用、使用,从而可以删除这些未被使用的代码。
    更多的原理请参考:Tree-Shaking性能优化实践-百度外卖大前端
    那为什么说tree shaking不一定真的有用呢?这都是函数副作用引起的。

函数副作用

我们都知道,函数式编程的副作用就是,一个函数可能会对函数外部的变量产生影响,而这个影响就是函数的副作用。举个例子吧:

function goBack() {
  window.location.href = '/home';
}

可以看到,goBack()修改了全局变量,结果时为了让浏览器进行跳转,而修改全局变量的这个行为就可能会引发一些副作用。
所以,针对这个问题,国内外各路大神提出了很多解决办法,比如说Webpack 中的 sideEffects 到底该怎么用?"sideEffects": false没有使打包后的bundle减少

1. 优化代码重复打包

比如说,一个项目中有个lib目录下放着自己编写的函数,分析之后发现它被重复打包到了业务代码的js文件中。这种情况该如何优化呢?

  1. 将node_module目录下的依赖统一打包成一个vendor依赖;
  2. 将lib和其他表示公用的文件夹(比如common)下编写的函数库单独打包成一个common;
  3. 将依赖的第三方组件库按需打包,如果使用了组件库中体积比较大的组件,比如:moment。如果只使用一次就打包进入自己引用页面的js文件中,如果被多个页面都引用就打包进入 common 中。
    那拆包该如何在webpack中配置呢?
splitChunks: {
    chunks: 'all',
    automaticNameDelimiter: '.',
    name: undefined,
    cacheGroups: {
        default: false,
        vendors: false,
        common: {
            test: function (module, chunks) {
                // 这里通过配置规则只将 common lib  moment公共依赖打包进入common中
                if (/src\/common\//.test(module.context) ||
                    /src\/lib/.test(module.context) ||
                    /cube-ui/.test(module.context) ||
                    /better-scroll/.test(module.context)) {
                    return true;
                }
            },
            chunks: 'all',
            name: 'common',
            // 这里的minchunks 非常重要,控制moment使用的组件被超过几个chunk引用之后才打包进入common中,否则不打包进去
            minChunks: 2,
            priority: 20
        },
        vendor: {
            chunks: 'all',
            test: (module, chunks) => {
                // 将node_modules 目录下的依赖统一打包进入vendor中
                if (/node_modules/.test(module.context)) {
                    return true;
                }
            },
            name: 'vendor',
            minChunks: 2,
            // 配置chunk的打包优先级,这里的数值决定了node_modules下的 moment不会被打包到 vendor 中
            priority: 10,
            enforce: true
        }
    }
}

为了避免影响打包速度,在项目代码中就是不要将使用频率低,体积大的组件引入到这个文件中。类似于datepicker这种大型的组件,就在对应需要使用的页面中引入即可。然后再webpack配置中,通过设置minChunk来指定当这些较大的组件引用超过多少次之后才能打包到common中,否则就单独打包到对应页面的js中。

因此,优化对于第三方依赖组件的加载方式,以减少不必要的加载和执行时间的损耗。

splitChunks详解

webpack4.x会根据以下条件自动分割代码块:

  • 新代码块可以被共享引用/这些代码块都是来自node_modules文件夹里面;
  • 新代码块大于30kb (min+gziped之前的体积);
  • 按需加载的代码块,最大数量应该小于或者等于5;
  • 初始加载的代码块,最大数量应该小于或者等于3.
splitChunks: {
  // 默认:用于异步chunk,值为all
  // initial模式下会分开优化打包异步和非异步模块。all会把异步和非异步模块同时进行优化,也就是意味着module1在index1中
  // 异步引入,index2中同步引入,initial下module1会出现在两个打包块中,而all只会出现一个。
  // all所有chunk代码(同步加载和异步加载模块都可以使用)的公共部分分离出来成为一个单独的文件
  // async将异步模块代码公共部分抽离出来成为一个单独的文件
  chunks: async,
  minSize: 30000,   // 默认值是30kb,当文件体积>=minSize时将会被拆分为2个文件,否则不生成新的chunk
  minChunks: 1,   // 共享该module的最小chunk数(当>=minChunks时才会被拆分为新的chunk)
  maxAsyncRequests: 5,   // 最多有5个异步加载请求该module
  maxInitialRequests: 3,   // 初始会话时最多有3个请求该module
  automaticNameDelimiter: '~',   // 名字中间的间隔符
  name: true,   // 打包后的名称,如果设置为true默认时chunk的名字通过分隔符(默认时~)分隔开,如vendor~也可以自己手动 
                       // 指定。
  cacheGroups: {   // 设置缓存组用来抽取满足不同规则的chunk,切割成的每一个新的chunk就是一个cache group
    common: {
      name: 'common',    // 抽取的chunk名字
      chunks: 'all',    // 同外层的参数配置,覆盖外层的chunks,以chunk为维度进行抽取
      // 1. 可以为字符串,正则表达式,函数,以module为维度进行抽取;
      // 2. 只要是满足条件的module都会被抽取到该common的chunk下,为函数的第一个参数;
      // 3. 遍历到的每一个模块,第二个参数是每一个引用到该模块的chunks数组
      test(module, chunks) {
        // module.context: 当前文件模块所属的目录,该目录下包含多个文件
        // module.resource: 当前模块文件的绝对路径
      if (/datepicker/.test(module.context)) {
        let chunkName = ''; // 引用该chunk的模块名字
        chunks.forEach(item => {
          chunkName += item.name + ',';
         });
        console.log(`module-datePicker`, module.context, chunkName, chunks.length);
        }
      },
    // 优先级,一个chunk很可能满足多个缓存组,会被抽取到优先级高的缓存组中,数值高的优先被选择
    priority: 10,
    minChunks: 2,    // 最少被几个chunk引用
    reuseExistingChunk: true,    // 如果该chunk中引用了已经被抽取的chunk,直接引用该chunk,不会重复打包代码(当module未 
                                                // 发生变化时是否使用之前的mudule)
    enforce: true,    // 如果cacheGroup中没有设置minSize,据此判断是否使用上层的minSize,当为true时则使用0;为false时则使 
                             // 用上层的minSize
    },
  },
},

2. 尽可能的去掉非必要的import

这种情况一般会因为检查遗漏导致引入。举个例子:最开始写代码的时候,由于业务上的需要,导致要引入datepicker,但是设计突然更改了原型图,不需要选择日期了,这时候你注释掉了js里面对日期的各种操作,但是却遗漏了在import部分的引入。在webpack打包的时候,仍然会将datepicker打进去。

好的一点是,现在大部分的编辑器都会将这种不需要的引入进行提示,一般是颜色变灰。

3. lodash优化

lodash这个js库是我们日常开发中非常依赖的一个前端库,非常好用,但是好用的同时也存在一定的缺陷,就是全量引入后打包的体积较大。那我们能不能按需引入lodash呢?

答案是肯定的。我们可以在npm库上搜索lodash-es这个模块,通过阅读它的文档,我们可以将lodash导出为es6 modules,然后就可以通过import的方式单独导入某个函数来使用。

到底有没有必要优化lodash,其实这个存在一定的争议,可以参考lodash在webpack中的各项优化的尝试。其实优化就是根据自身的业务需求做出各种权衡后的妥协。

斐波那契数列:1、1、2、3、5、8、13、21。输入n,输出数列中第n位数的值。

解法一:

function fn(n) {
  let num1 = 1,
    num2 = 1,
    num3 = 0;
  for (let i = 0; i < n - 2; i++) {
    num3 = num1 + num2;
    num1 = num2;
    num2 = num3;
  }
  return num3;
}

// fn(7);   // 13

解法二:
递归:这种计算方式简洁并且直观,但是由于存在大量的重复计算,实际运行效率很低,并且会占用较多的内存。

function fn(n) {
  if (n <= 2) {
    return 1;
  }
  return fn(n - 1) + fn(n - 2);
}

// fn(7);   // 13

解法三:
memoization方案在《JavaScript模式》和《JavaScript设计模式》中都有提到过。memoization是一种将函数执行结果用变量缓存起来的方法。当函数进行计算之前,先看缓存对象是否有次计算结果,如果有,就直接从缓存对象中获取结果;如果没有,就进行计算,并将结果保存到缓存对象中。(闭包方式实现)

let fibonacci = (function() {
  let memory = []
  return function(n) {
      if(memory[n] !== undefined) {
        return memory[n]
    }
    return memory[n] = (n === 0 || n === 1) ? n : fibonacci(n-1) + fibonacci(n-2)
  }
})()

// fn(7);   // 13

这个时候有一个问题就是,我们使用的数组来存储,如果把数组换成对象呢?答案是速度会快很多。是因为比如说我们调用fibonacci(100)的时候,fibonacci函数在第一次计算的时候会设置memory[100]=xxx,此时数组的长度为101,而前面100项都会初始化为undefined。所以memory类型为数组时比类型为对象时要慢。

为什么Vue中不要用index作为key?实际在问diff算法

参考文章

1. 为什么Vue中不要用index作为key?
2.

前言

可以试想一下:在Vue开发中,我们的key是拿来做什么的?为什么不推荐用index作为key呢?接下来就将为你揭秘key的作用。

示例

有这样一段HTML代码:

<ul>
  <li>1</li>
  <li>2</li>
</ul>

我们知道在Vue中,都是先将节点转换为虚拟DOM,等到真正渲染的时候通过diff算法进行比对之后,才会成功渲染成我们需要的页面,而这个虚拟DOM是什么呢?其实就是JavaScript的一个对象。

那么上面示例的vdom节点大概就是下面这种形式:

{
  tag: 'ul',
  children: [
    { tag: 'li', children: [ { vnode: { text: '1' } } ] },
    { tag: 'li', children: [ { vnode: { text: '2' } } ] },
],
}

讲一讲JavaScript设计模式中的单例模式

参考文章

1. 从ES6重新认识JavaScript设计模式(一): 单例模式

单例模式

1. 什么是单例模式,特点是什么

单例模式是一种非常常用但却相对而言比较简单的设计模式。它是指在一个类中只能有一个实例,即使多次实例化该类,也只能返回第一次实例化后的对象。单例模式不仅能减少不必要的内存开销,并且在减少全局的函数和变量冲突也具有重要的意义。

单例模式能保证一个类只有一个实例,并且提供一个访问它的全局访问点。也就是说,在整个生命周期中,该对象的生产都始终是一个,不曾变化。

它的特点是:

  • 公有方法获取实例;
  • 私有的构造函数;
  • 私有的成员变量。

2. 作用

  1. 在要求线程安全的情况下,保证了类实例的唯一性,线程安全;
  2. 在不需要多实例存在时,保证了类实例的单一性,不浪费内存。

3. 最简单的单例模式

即使目前我们对单例模式还比较模糊,但在我们日常开发中也早就已经使用过单例模式了。看下面的例子:

let timeTool = {
  name: '时间处理工具',
  getISODate: function() {},
  getUTCDate: function() {},
};

以对象字面量创建对象的方式在js开发中很常见。上面的例子就是以对象字面量的方式来封装了一些方法处理时间格式。全局只暴露了一个timeTool对象,在需要使用时,只需要采用timeTool.getISODate()调用即可。timeTool对象就是单例模式的体现。在JavaScript创建对象的方式十分灵活,可以直接通过对象字面量的方式实例化一个对象,而其他面向对象的语言必须使用类进行实例化。所以这里的 timeTool已经是一个实例,且ES6中 letconst不允许重复声明的特性,确保了timeTool不能被重新覆盖。

4. 惰性单例

采用对象字面量创建单例只能适用于简单的应用场景,一旦该对象十分复杂,那么创建对象本身就需要一定的耗时,且该对象可能需要一些私有变量和私有方法。此时使用对象字面量创建单例就不再行得通了,我们还是需要采用构造函数的方式实例化对象。下面就是使用立即执行函数和构造函数的方式改造上面的timeTool工具。

let timeTool = (function() {
  let _instance = null;
  
  function init() {
    let now = new Date();    // 私有变量
    // 公用属性和方法
    this.name = '时间处理工具';
    this.getISODate = function() {
      return now.toISOString();
    }
    this.getUTCDate = function() {
      return now.toUTCString();
    }
  }

  return function() {
    if (!_instance) {
      _instance = new init();
    }
    return _instance;
  }
})()

这时的timeTool实际上是一个函数,_instance作为实例对象最开始赋值为null,init函数是其构造函数,用于实例化对象,立即执行函数返回的是匿名函数,用于判断实例是否创建,只有当调用timeTool()时进行实例化,这就是惰性单例的应用,不在js加载时就进行实例化创建,而是在需要的时候再进行单例的创建。如果再次调用,那么返回的永远是第一次实例化后的实例对象。

let ins1 = timeTool();
let ins2 = timeTool();

console.log(ins1 === ins2);     // true

单例模式的应用场景

1. 命名空间

一个项目往往都不止一个程序员来开发和维护,一个程序员很难去弄清楚另外一个程序员暴露在项目中的全局变量和方法。如果将变量和方法都暴露在全局中,变量冲突是难以避免的。就像下面的事故一样:

// 开发者A写了一大段js代码
function addNum() {}

// 另一个开发者B开始写js代码
var addNum = '';

// A重新维护该js代码
addNum();    // 这个时候抛出错误:Uncaught TypeError: addNum is not a function

命名空间就是用来解决全局变量冲突的问题,我们完全可以只暴露一个对象名,将变量作为该对象的属性,将方法作为该对象的方法,这样就能大大减少全局变量的个数。

// 开发者A写了一大段js代码
let devA = {
  addNum() {};
};

// 另一个开发者B开始写js代码
let devB = {
  addNum: '';
}

// A重新维护该js代码
devA.addNum();

上面的代码中,devAdevB就是两个命名空间,采用命名空间可以有效减少全局变量的数量,以此解决变量冲突的发生。

2. 管理模块

上面讲到的timeTool对象时一个只用来处理时间的工具库,但是实际开发过程中的库可能会有多种多样的功能,例如处理ajax请求,操作dom或者处理事件。这个时候单例模式还可以用来管理代码库中的各个模块,如下所示:

var devA = (function() {
  // ajax模块
  var ajax = {
    get: function(api, obj) { console.log('ajax get调用'); },
    post: function(api, obj) {}
  };

  // dom模块
  var dom = {
    get: function() {},
    create: function() {},
  };

  // event模块
  var event = {
    add: function() {},
    remove: function() {},
  };

  return {
    ajax: ajax,
    dom: dom,
    event: event,
  };
})()

上面的代码库中有ajax, domevent三个模块,用同一个命名空间devA来管理。在进行相应的操作的时候,只需要devA.ajax.get()进行调用即可。这样可以让库的功能更加清晰。

ES6中的单例模式

1. ES6创建对象

ES6创建对象时引入了classconstructor来创建。下面我们来看看如何使用ES6语法实例化苹果公司。

class Apple {
  constructor(name, creator, products) {
    this.name = name;
    this.creator = creator;
    this.products = products;
  }
}

let appleCompany = new Apple('苹果公司', ‘乔布斯’, ['iPhone', 'iMac', 'iPad', 'iPod']);
let copyApple = new Apple('苹果公司', ‘李四’, ['iPhone', 'iMac', 'iPad', 'iPod']);

2. ES6中创建的单例模式

显然,在全世界范围内,苹果公司有且只有一个。所以appleCompany应该是一个单例,现在我们使用ES6语法将constructor改写为单例模式的构造器。

class SingletonApple {
  constructor(name, creator, products) {
    if(!SingletonApple .instance) {
      this.name = name;
      this.creator = creator;
      this.products = products;
      // 将this挂载到SingletonApple这个类的instance属性上
      SingletonApple.instance = this;
    }
    return SingletonApple.instance;
  }
}

let appleCompany = new SingletonApple('苹果公司', ‘乔布斯’, ['iPhone', 'iMac', 'iPad', 'iPod']);
let copyApple = new SingletonApple('苹果公司', ‘李四’, ['iPhone', 'iMac', 'iPad', 'iPod']);

console.log(appleCompany === copyApple);    // true

3. ES6的静态方法优化代码

ES6为class提供了static关键字定义的静态方法,我们可以将constructor中判断是否实例化的逻辑放入一个静态方法getInstance中,调用该静态方法获取实例,constructor中只包含需要实例化的代码,这样可以增强代码的可读性、结构更加优化。

class SingletonApple {
  constructor(name, creator, products) {
      this.name = name;
      this.creator = creator;
      this.products = products;
  }

  // 静态方法
  static getInstance(name, creator, products) {
      if(!this.instance) {
        this.instance = new SingletonApple(name, creator, products);
      }
      return this.instance;
    }
}

let appleCompany = new SingletonApple('苹果公司', ‘乔布斯’, ['iPhone', 'iMac', 'iPad', 'iPod']);
let copyApple = new SingletonApple('苹果公司', ‘李四’, ['iPhone', 'iMac', 'iPad', 'iPod']);

console.log(appleCompany === copyApple);    // true

传统OO语言的单例模式

  1. 使用闭包创建,且符合惰性单例的特征。
var Singleton = function(name) {
  this.name = name;
}

Singleton.prototype.getName = function() {
  alert(this.name);
}
// 利用闭包创建符合惰性的单例
Singleton.getInstance = (function(name)) {
  var instance;
  return function(name) {
    if (!instance) {
      instance = new Singleton(name);
    }
  }
})();

var a = Singleton.getInstance('test1');
var b = Singleton.getInstance('test2');

console.log(a === b);    // true
  1. 透明的单例模式
// 反面的单例模式例子
var CreateDiv = (function() {
  var instance;
  var CreateDiv = function(html) {
    if (instance) {
      return instance;
    }
    this.html = html;
    this.init();
    return instance = this;
  };

  CreateDiv.prototype.init = function() {
    var div = document.createElement('div');
    div.innerHTML = this.html;
    document.body.appendChild(div);
  }

  return CreateDiv;
})();

var a = new CreateDiv('test1');
var b = new CreateDiv('test2');

这种形式的单例模式特点:
为了把instance封装起来,我们使用了自执行的匿名函数和闭包,并且让这个匿名函数返回真正的Singleton构造方法,当然这增加了程序的复杂性。

CreateDiv构造函数做了两件事。

  • 创建对象和执行初始化init方法;
  • 保证只有一个对象。
    但是,这样创建出来的实例,不符合设计模式中的单一职责的概念。

项目实战应用

1.实现登陆弹框

登陆弹框在项目中是一个比较经典的单例模式,因为对于大部分网站不需要用户必须登陆才能浏览,所以登陆操作的弹框可以在用户点击登陆按钮后再进行创建。而且登陆框永远只有一个,不会出现多个登陆框的情况,也就意味着再次点击登录按钮后返回的永远是一个登录框的实例。

我们梳理一下登录框的流程,然后再实现。

  1. 给顶部导航模块的登录按钮注册点击事件;
  2. 登录按钮点击后JS动态创建遮罩层和登录框;
  3. 遮罩层和登录框插入到页面中;
  4. 给登录框中的关闭按钮注册事件,用于关闭遮罩层和弹框;
  5. 给登录框中的输入框添加校验;
  6. 给登录框中的确定按钮添加事件,用于ajax请求;
  7. 给登录框中的清空按钮添加事件,用于清空输入框。

因为5和6是登录框的实际项目逻辑,与单例模式关系不大,所以这里主要实现1-4步。

1. 给页面添加顶部导航栏的HTML代码

<nav class="top-bar">
  <div class="top-bar_left">
    TEST
  </div>
  <div class="top-bar_right">
    <div class="login-btn">登录</div>
    <div class="signin-btn">注册</div>
  </div>
</nav>

2. 使用ES6语法创建Login类

class Login {

  //构造器
  constructor() {
    this.init();
  }

  //初始化方法
  init() {
    //新建div
    let mask = document.createElement('div');
    //添加样式
    mask.classList.add('mask-layer');
    //添加模板字符串
    mask.innerHTML = 
    `
    <div class="login-wrapper">
      <div class="login-title">
        <div class="title-text">登录框</div>
        <div class="close-btn">×</div>
      </div>
      <div class="username-input user-input">
        <span class="login-text">用户名:</span>
        <input type="text">
      </div>
      <div class="pwd-input user-input">
        <span class="login-text">密码:</span>
        <input type="password">
      </div>
      <div class="btn-wrapper">
        <button class="confrim-btn">确定</button>
        <button class="clear-btn">清空</button>
      </div>
    </div>
    `;
    //插入元素
    document.body.insertBefore(mask, document.body.childNodes[0]);

    //添加关闭登录框事件
    Login.addCloseLoginEvent();
    
    //静态方法: 获取元素
    static getLoginDom(cls) {
      return  document.querySelector(cls);
    }

    //静态方法: 注册关闭登录框事件
    static addCloseLoginEvent() {
      this.getLoginDom('.close-btn').addEventListener('click', () => {
        //给遮罩层添加style, 用于隐藏遮罩层
        this.getLoginDom('.mask-layer').style = "display: none";
      })
    }

  //静态方法: 获取实例(单例)
  static getInstance() {
    if(!this.instance) {
      this.instance = new Login();
    } else {
      //移除遮罩层style, 用于显示遮罩层
      this.getLoginDom('.mask-layer').removeAttribute('style');
    }
    return this.instance;
    }
  }

3. 给登录框添加注册点击事件

// 注册点击事件
Login.getLoginDom('.login-btn').addEventListener('click', () => {
  Login.getInstance();
})

分析:
在上面的登录框中,实现了只创建一个Login类,但是却实现了一个并不简单的登录功能。在第一次点击登录按钮的时候,我们调用Login.getInstance()实例化了一个登录框,且在之后的点击中并没有重新创建新的登录框,只是移除掉了display:none这个样式来显示登录框,节省了内存开销。

总结

单例模式虽然简单,但是在项目中的应用场景却是相当多的,单例模式的核心是确保只有一个实例,并提供全局访问。就像我们只需要一个浏览器的window对象,jQuery的$对象也不再需要第二个。由于JavaScript代码书写方式十分灵活,这也导致了如果没有严格的规范的情况下,大型的项目中JavaScript不利于多人协同开发,使用单例模式进行命名空间来管理模块是一个很好的开发习惯,能够有效的解决协同开发变量冲突的问题,灵活使用单例模式,也能够减少不必要的内存开销,提高使用体验。

你应该知道的几个webpack优化方法

参考文章

  1. webpack-bundle-analyzer插件快速入门
  2. 使用 happypack 提升 Webpack 项目构建速度
  3. 更多优化方法(如动态dll等等)请参考Cosen95大神的文章,小弟也是一枚搬运工

前言

本文主要介绍几个在前端工程构建过程中非常显著的一些插件,帮助提升webpack的构建速度以及深入分析影响速度的原因。但在继续下面的内容之前,也需要充分的了解一下webpack的实现原理,这篇文章已经叙述得非常仔细,我也先去膜拜一下再来继续了。

1. 速度分析

webpack有时候打包很慢,因为我们在项目中可能使用了非常多的plugin和loader,想知道具体是哪个环节慢,下面这个插件就可以计算plugin和loader的耗时。

yarn add -D speed-measure-webpack-plugin

配置也很简单,将webpack配置对象包裹起来即可:

const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");

const smp = new SpeedMeasurePlugin();

const webpackConfig = smp.wrap({
  plugins: [
    new plugin1(),
    new plugin2()
  ]
});

在项目中华引入speed-measure-webpack-plugin后,它完成的主要工作是:

  • 计算整体打包总耗时;
  • 分析每个插件和loader的耗时情况;
    知道了具体plugin和loader的耗时情况,就可以对症下药了。

2. 体积分析

打包后的体积优化是一个可以着重优化的方向,比如引入的一些第三方组件库体积过大,这个时候就要考虑是否寻求替代品了。
这里采用的是webpack-bundle-analyzer,它可以用交互式可缩放树形图来显示webpack输出文件的大小,用起来非常方便。
安装插件:

yarn add -D webpack-bundle-analyzer

安装完成之后再在webpack.config.js中简单的配置一下:

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    // 可以是server, static或者disabled
    // 在server模式下:分析器会启动HTTP服务器来显示软件报告;
    // 在static模式下:会生成带有报告的单个HTML文件;
    // 在disabled模式下:可以使用这个插件将generateStatsFile设置为true来生成Webpack Stats JSON文件;
    analyaerMode: 'server',
    //  将在“服务器”模式下使用的主机启动HTTP服务器。
    analyzerHost: "127.0.0.1",
    //  将在“服务器”模式下使用的端口启动HTTP服务器。
    analyzerPort: 8866,
    //  路径捆绑,将在`static`模式下生成的报告文件。
    //  相对于捆绑输出目录。
    reportFilename: "report.html",
    //  模块大小默认显示在报告中。
    //  应该是`stat`,`parsed`或者`gzip`中的一个。
    //  有关更多信息,请参见“定义”一节。
    defaultSizes: "parsed",
    //  在默认浏览器中自动打开报告
    openAnalyzer: true,
    //  如果为true,则Webpack Stats JSON文件将在bundle输出目录中生成
    generateStatsFile: false,
    //  如果`generateStatsFile`为`true`,将会生成Webpack Stats JSON文件的名字。
    //  相对于捆绑输出目录。
    statsFilename: "stats.json",
    //  stats.toJson()方法的选项。
    //  例如,您可以使用`source:false`选项排除统计文件中模块的来源。
    statsOptions: null,
    logLevel: "info"
 ],
};

如果想要查看更多选项,请参照官方文档
然后,我们在控制台中输入: npm run dev / yarn dev,它就会默认起一个端口号为8888的本地服务器,图中的每一块都清晰的展示了组件、第三方库的代码体积。有了它之后,我们就能针对体积偏大的模块进行相关的优化了。

3. HappyPack

安装:

yarn add -D hapyppack

HappyPack可以让webpack同一时间处理多个任务,发挥多核CPU的能力,将任务分解给多个子进程去并发的执行,子进程处理完后,再把结果发送给主进程。通过多进程模型,来达到加速构建代码的目的。
示例:

// webpack.config.js
const HappyPack = require('happypack');

exports.module = {
  rules: [
    {
      test: /.js$/,
      // 1) replace your original list of loaders with "happypack/loader":
      // loaders: [ 'babel-loader?presets[]=es2015' ],
      use: 'happypack/loader',
      include: [ /* ... */ ],
      exclude: [ /* ... */ ]
    }
  ]
};

exports.plugins = [
  // 2) create the plugin:
  new HappyPack({
    // 3) re-add the loaders you replaced above in #1:
    loaders: [ 'babel-loader?presets[]=es2015' ]
  })
];

遗憾的是,HappyPack作者表示不再维护此项目了,同时他也推荐使用webpack官方提供的thread-loader,但thread-loader 和 happypack 对于小型项目来说打包速度几乎没有影响,甚至可能会增加开销,所以建议尽量在大项目中采用。

4. 多进程并行压缩代码

一般而言,在我们的开发环境中,代码构建时间比较快,而构建用于发布到线上的代码时会添加压缩这个流程,则会导致计算量大、耗时多。
webpack提供了UglifyJS插件来压缩JS代码,但是它使用的是单线程压缩代码,也就是说多个js文件需要被压缩,它需要一个个文件进行压缩。所以说在正式环境打包压缩代码的速度非常慢(因为压缩JS代码需要先把代码解析成用Object抽象表示的AST语法树,在应用各种规则分析和处理AST,导致这个过程耗时非常大。)
所以要对压缩代码这一块做优化,常用的方法就是多进程并行压缩。目前有三种压缩方案:

  • parallel-uglify-plugin
  • uglifyjs-webpack-plugin
  • terser-webpack-plugin

4.1 parallel-uglify-plugin

上面介绍的HappyPack的**是使用多个子进程去解析和编译JS,CSS等,这样就可以并行处理多个子任务,多个子任务完成后,再将结果发到主进程中,有了这个**后,ParallelUglifyPlugin 插件就产生了。

当webpack有多个JS文件需要输出和压缩时,原来会使用UglifyJS去一个个压缩并且输出,而ParallelUglifyPlugin插件则会开启多个子进程,把对多个文件压缩的工作分给多个子进程去完成,但是每个子进程还是通过UglifyJS去压缩代码。并行压缩可以显著的提升效率。
安装:

yarn add -D webpack-parallel-uglify-plugin

使用示例:

import ParallelUglifyPlugin from 'webpack-parallel-uglify-plugin';

module.exports = {
  plugins: [
    new ParallelUglifyPlugin({
      // Optional regex, or array of regex to match file against. Only matching files get minified.
      // Defaults to /.js$/, any file ending in .js.
      test,
      include, // Optional regex, or array of regex to include in minification. Only matching files get minified.
      exclude, // Optional regex, or array of regex to exclude from minification. Matching files are not minified.
      cacheDir, // Optional absolute path to use as a cache. If not provided, caching will not be used.
      workerCount, // Optional int. Number of workers to run uglify. Defaults to num of cpus - 1 or asset count (whichever is smaller)
      sourceMap, // Optional Boolean. This slows down the compilation. Defaults to false.
      uglifyJS: {
        // These pass straight through to uglify-js@3.
        // Cannot be used with uglifyES.
        // Defaults to {} if not neither uglifyJS or uglifyES are provided.
        // You should use this option if you need to ensure es5 support. uglify-js will produce an error message
        // if it comes across any es6 code that it can't parse.
      },
      uglifyES: {
        // These pass straight through to uglify-es.
        // Cannot be used with uglifyJS.
        // uglify-es is a version of uglify that understands newer es6 syntax. You should use this option if the
        // files that you're minifying do not need to run in older browsers/versions of node.
      }
    }),
  ],
};

注意: webpack-parallel-uglify-plugin已不再维护,不推荐再继续使用。

4.2 uglifyjs-webpack-plugin

安装:

yarn add -D uglifyjs-webpack-plugin

示例:

const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

module.exports = {
  plugins: [
    new UglifyJsPlugin({
      uglifyOptions: {
        warnings: false,
        parse: {},
        compress: {},
        ie8: false
      },
      parallel: true
    })
  ]
};

其实它和上面的parallel-uglify-plugin类似,也可通过设置parallel: true开启多进程压缩。

4.3 terser-webpack-plugin

其实webpack4已经默认支持es6语法的压缩,这离不开terser-webpack-plugin。
安装:

yarn add -D terser-webpack-plugin

示例:

const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: 4,
      }),
    ],
  },
};

垂直居中的几种实现方案

参考文章

  1. CSS实现垂直居中的方法
  2. CSS垂直居中常用的几种方法

方法1

对父容器使用display: table-cell + vertical-align: middle,使其内部的子元素实现垂直居中。
原理:父元素设置为表格的单元格元素,而在表格单元格中的元素设置:vertical-align: middle会使其以中间对齐的方式来显示。

.parent {
  width: 200px;
  height: 200px;
  border: 1px solid;
  display: table-cell;
  vertical-align: middle;
}
.child{
  width: 50px;
  height: 50px;
  background: blue;
}

方法2

利用给父元素设置相对定位,子元素设置绝对定位,margin: 0 auto 和 top: 0; bottom: 0;从而实现垂直居中。
原理:因为auto默认分配剩余空间,宽度相对window是固定的,所以margin: 0 auto;可以有水平居中的效果,而高度相对window并不是固定的,所以margin: auto 0;不能垂直居中,所以让子元素上下margin值不相对于window进行计算,改为相对父元素进行计算即可。如下:

.parent {
  width: 200px;
  height: 200px;
  border: 1px solid;
  position: relative;
}
child{
  width: 200px;
  height: 200px;
  margin: auto 0;
  position: absolute;
  top: 0;
  bottom: 0;
}

方法3

flex布局可以很方便的实现垂直与水平居中,好处很多,在移动端使用比较广泛,不好的地方就是浏览器兼容性不好。代码如下:

//html
<div class="main">
  <div class="middle"></div>
</div>

//css
.main {
  width: 60px;
  height: 10%;
  background: #dddddd;
  display: flex;
  justify-content: center;
  align-items: center;
}
.middle{
  width: 30%;
  height: 50%;
  background: red;
}

方法4

绝对定位/相对定位,在不知道自己高度和父容器高度的情况下,利用绝对定位.

.parent {
  position: relative;
}

.children {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
}

讲一讲你知道的前端性能优化方案

参考文章

  1. 前端性能优化的24条建议

前言

要想对前端性能进行实质上的优化,就需要清楚的知道:从输入URL到页面展示出来这个过程发生了什么?从这个过程中的某些环节来具体的了解一下性能优化。

从输入URL到页面展示出来这个过程发生了什么?

这是一道经典的前端面试题目,重要性不必多说,它的大致流程如下:

  • URI解析
  • DNS服务器进行DNS解析
  • TCP三次握手:建立客服端与服务端的连接通道
  • 发送HTTP请求
  • 服务器处理和响应
  • TCP四次挥手:关闭客户端与服务端的连接
  • 浏览器解析和渲染
  • 页面加载完成
    接下来我们就来看一看浏览器的渲染过程到底是怎样的。

浏览器渲染过程

构建DOM树

构建DOM树的大致流程如下:

  • 首先浏览器从磁盘或网络中读取HTML原始字节,并根据文件的指定编码将他们转成字符;
  • 然后通过分词器将字节流转换为Token,在Token(令牌)生成的同时,另一个流程会同时消耗这些令牌并转换成HTML head这些节点对象,起始和结束令牌表明了节点之间的关系。
  • 当所有令牌消耗完以后就转换成了DOM(文档对象模型);
  • 最终构建出DOM结构。
    DOM树构建完成之后,接下来就是CSSOM树的构建了。

构建CSSOM树

与HTML的转换类似,浏览器会去识别CSS正确的令牌,然后将这些令牌转化为CSS节点;
PS:子节点会继承父节点的样式规则,这里对应的就是层叠规则和层叠样式表。
有了DOM和CSSOM,接下来就可以合成布局树(Render Tree)了。

构建渲染树

等 DOM 和 CSSOM 都构建好之后,渲染引擎就会构造布局树。布局树的结构基本上就是复制 DOM 树的结构,不同之处在于 DOM 树中那些不需要显示的元素会被过滤掉,如 display:none 属性的元素、head 标签、script 标签等。
复制好基本的布局树结构之后,渲染引擎会为对应的 DOM 元素选择对应的样式信息,这个过程就是样式计算。

样式计算

样式计算的目的是为了计算出 DOM 节点中每个元素的具体样式,这个阶段大体可分为三步来完成。

  • 把 CSS 转换为浏览器能够理解的结构
  • 转换样式表中的属性值,使其标准化
  • 计算出 DOM 树中每个节点的具体样式
    样式计算完成之后,渲染引擎还需要计算布局树中每个元素对应的几何位置,这个过程就是计算布局。

计算布局

现在,我们有 DOM 树和 DOM 树中元素的样式,但这还不足以显示页面,因为我们还不知道 DOM 元素的几何位置信息。那么接下来就需要计算出 DOM 树中可见元素的几何位置,我们把这个计算过程叫做布局。

绘制

通过样式计算和计算布局就完成了最终布局树的构建。再之后,就该进行后续的绘制操作了。
到这里,浏览器的渲染过程就基本结束了。

从浏览器的渲染过程中可以做的优化点

通常一个页面有三个阶段:加载阶段、交互阶段和关闭阶段。

  • 加载阶段,是指从发出请求到渲染出完整页面的过程,影响到这个阶段的主要因素有网络和 JavaScript 脚本。
  • 交互阶段,主要是从页面加载完成到用户交互的整合过程,影响到这个阶段的主要因素是 JavaScript 脚本。
  • 关闭阶段,主要是用户发出关闭指令后页面所做的一些清理操作。
    这里我们需要重点关注加载阶段和交互阶段,因为影响到我们体验的因素主要都在这两个阶段,下面我们就来逐个详细分析下。

加载阶段

原型,原型链和继承的关系,如何实现继承?

参考文章

  1. JavaScript中的继承(MDN)
  2. 说说原型,原型链和原型继承
  3. 深入理解JS原型,原型链,继承及new的实现原理
  4. 一篇文章理解JS继承——原型链/构造函数/组合/原型式/寄生式/寄生组合/Class extends

原型 prototype 和 proto

  • 每个对象都有一个__proto__属性,并且指向它的prototype对象;
  • 每个构造函数都有一个 prototype 原型对象;
  • prototype 原型对象里的constructor指向构造函数本身。
    三者的关系图如下:
    捕获
    可能大家跟我一样,对于prototype以及__proto__会有疑问,他们到底有什么作用呢?
    实例对象的__proto__指向构造函数的prototype,从而实现继承。prototype对象相当于特定类型所有实例对象都可以访问的公共容器。
function Person(nick, age) { // 构造函数Person
  this.nick = nick;
  this.age = age;
}

Person.prototype.sayName = function() {  // 在构造函数的原型上定义一个sayName()的方法
  console.log('我的名字是:' + this.nick);
}

var p1 = new Person('Jessica', 18);
var p2 = new Person('Jay', 20);

p1.sayName(); // 我的名字是:Jessica
p1.sayName(); // 我的名字是:Jay

p1.__proto__ === Person.prototype; // true
p2.__proto__ === Person.prototype; // true

Person.prototype.constructor === Person  //true

需要注意的是:

  • 当object.prototype.__proto__已被大多数浏览器厂商所支持的今天,其存在和确切行为仅在ECMScript 2015规范中被标准化为传统功能,以确保Web浏览器的兼容性。为了更好的支持,建议一般使用Objecy.getPrototypeOf()。
  • Object.create(null) 新建的对象是没有 proto 属性的。

原型链

请看下面的代码:

var arr = [1, 2, 3];
arr.valueOf();    // [1, 2, 3]

1614158197
按照之前的理论,如果自身没有该方法,我们应该无Array.prototype对象里面去找,但是你会发现 arr.__proto__上根本就没有 valueOf 方法,那它是从哪里来的呢?
我们来看一下Array.prototype中__proto__的构成:
1614158484
这里却有一个valueOf方法,为什么呢?

查找 valueOf 方法的过程

当试图访问一个对象属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,一次层层向上搜索,直到找到一个名字匹配的属性或者达到原型链的末尾。
查找 valueOf 的大致过程如下:

  1. 当前实例对象obj,查找obj的属性或方法,找到后返回;
  2. 没有找到,通过obj.proto,找到obj构造函数的prototype并且查找上面的属性和方法,找到后返回;
  3. 没有找到,把Array.prototype当做obj,重复上面的步骤;
    当然不会无限循环的找下去,原型链是有终点的,最后当找到Object.prototype时,Object.prototype.proto===null,就意味着查找结束了。一般的数组查找时对应的关系为(arr为一个实例对象,Array是此处arr的构造函数):
    arr.proto === Array.prototype; // true
    Array.prototype.proto === Object.prototype; // true
    arr.proto.proto == Object.prototype; // true

原型链的终点:
Object.prototype.proto === null; // true
所以对于这种情况,整个原型链的查找过程是:
arr ----> Array.prototype ----> Object.prototype ----> null
这就是我们常说的原型链,层层向上查找,最后还没有找到就返回undefined。

JavaScript的继承

什么是继承?

继承指的是一个对象直接使用另外一个对象的属性和方法。

由此可见,只要实现属性和方法的继承,就能达到继承的效果:

  • 得到一个对象的属性;
  • 得到一个对象的方法。

属性如何继承?

我们先创建一个Person类

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

// 方法定义在构造函数的原型上
Person.prototype.getName = function() { console.log(this.name); }

这个时候,我想创建一个Student类,我们希望它可以继承Person的所有属性,并且能够额外添加自己的特定属性;

  • 新的属性,grade-这个属性包含了学生的学习成绩。
    定义Student的构造函数:
function Student(name, age, grade) {
  Person.call(this, name, age);
  this.grade = grade;
}

属性的继承是通过在一个类内执行另外一个类的构造函数,通过call指定this为当前执行环境,这样就可以得到另外一个类的所有属性。
我们实例化这个类来看一下:

var student1 = new Student('Jessica', '29', '100');

student1.name;   // Jessica
student1.age;   // 29
student1.grade;   // 100

显然,student1成功的继承了Person的属性(name和age)。

方法如何继承呢?

我们需要让Student从Person的原型对象里继承方法。我们要怎么做呢?
我们都知道类的方法都定义在prototype里,那其实我们只需要把Person.prototype的备份赋值给Student.prototype即可。

Student.prototype = Object.create(Person.prototype)
  • Object.create简单说就是新建一个对象,使用现有的对象赋值给新建对象的__proto__。那为什么是备份呢?
    因为如果直接赋值,那会是引用关系,意味着修改Student. prototype,也会同时修改Person.prototype,如果子类继承后导致原来的父类变得可以修改了,这是极其不合理的。
  • 另外就是:在给 Student 类添加方法时,应该在修改 prototype 之后,否则会被覆盖掉,原因是赋值前后的属性值是不同的对象。
  • 还有一个问题:我们都知道 prototype 里面有一个属性 constructor 指向构造函数本身,但是因为我们是复制其他类的 prototype ,所以这个指向是不对的,也需要更正一下。如果不修改,会导致我们类型判断错误。
Student.prototype.constructor = Student;

我们整理一下,主要是prototype和constructor的问题。

Student.prototype = Object.create(Person.prototype);

Student.prototype.constructor
f Person(name, age) {
  this.name = name;
  this.age = age;
}
===================================
Student.prototype.constructor = Student;

f Student(name, age, grade) {
  Person.call(this, name, age) {
    this.grade = grade;
  }
}

所以,继承的最终方案是:

Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;

hasOwnProperty

在原型链上查询属性比较耗时,对性能也存在一定的影响,试图访问不存在的属性时会遍历整个原型链。遍历对象时,每个可枚举的属性都会被枚举出来。要检查是否具有自己定义的属性,而不是原型链上的属性,就必须使用 hasOwnProperty 方法。该方法时JavaScript中唯一处理属性并且不会遍历原型链的方法。

总结

  1. 每个对象都有一个__proto__属性,并且指向它的 prototype 原型对象;
  2. 每个构造函数都有一个 prototype 原型对象;
  3. prototype 原型对象里的 constructor 指向构造函数本身;

原型链

每个对象都有一个 proto,它指向它的 prototype 原型对象,而 prototype 原型对象又具有一个自己的 prototype 原型对象,这样层层往上直到一个对象的原型 prototype 为 null,这个查询的路径就是原型链。

JavaScript 中的继承

  1. 属性继承
function Person (name, age) {
    this.name = name
    this.age = age
}

// 方法定义在构造函数的原型上
Person.prototype.getName = function () { console.log(this.name)}

function Student(name, age, grade) {
    Person.call(this, name, age)
    this.grade= grade
}
  1. 方法继承
Student.prototype = Object.create(Person.prototype)
Student.prototype.constructor = Student

Continuous project building & Automatic Deployment-Jenkins+Docker

前言

随着项目体量的增加,如果前端的构建+部署全部由人工去处理,这无疑会增加人力成本,同时随着人员增多,各种各样的问题也会暴露出来。所以对于实现持续集成,自动化部署的要求会越来越高。

环境搭建

1. windows下利用Docker+Jenkins+GitHub搭建可持续化构建环境
2. 实战笔记:Jenkins打造强大的前端自动化工作流
3. Github配合Jenkins,实现vue等前端项目的自动构建与发布

你了解内部属性[[class]]吗?内部属性[[Class]]是什么?

参考文章

  1. 内部属性[[class]]

内部属性[[class]]

所有typeof返回值为Object的对象都包含一个内部属性。这个属性无法直接访问,一般通过Object.prototype.toString(..)
来查看。如:

Object.prototype.toString.call(['a', 'b', 'c'])
=>"[object Array]"

Object.prototype.toString.call(/regex-literal/i/);
=>"[object RegExp]"

如下图所示:
内部属性

但是我们自己创建的类就不会有这样的待遇,因为toString()找不到toStringTag属性,只能返回默认的Object标签。
(PS:有时候也可以用来判断数据的类型)
默认情况下,类的[[Class]]返回[object Object],如:
class Person {}
Object.prototype.toString.call(new Person());
=>"[object Object]"
**这个时候需要定制我们自己的[[Class]]
class Person1 {
get Symbol.toStringTag {
return 'Person1';
}
}
Object.prototype.toString.call(new Person1());
=> "[object Person1]"

说一说JavaScript的事件循环机制(Event Loop)

参考文章

1. 【前端体系】从一道面试题谈谈对EventLoop的理解
2. JavaScript 运行机制详解:再谈Event Loop
3. 详解JavaScript中的Event Loop(事件循环)机制
4. 再话js的事件循环机制
5. JavaScript中的Event Loop(事件循环)机制

前言:为什么 JavaScript 是单线程语言?(单线程+非阻塞)

JavaScript的单线程与它的用途紧密相关。作为浏览器的脚本语言,JavaScript 的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。如果说JavaScript同时又2个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这个时候浏览器该如何处理呢?
所以,为了避免复杂性,从一开始,JavaScript就是单线程,这已经成为了这门语言的核心特点,未来也不会改变。
为了利用多核CPU的计算能力,HTML5提出了WEB Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,并且不允许操作DOM。所以这个标准并没有改变JavaScript单线程的本质。

任务队列

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。但是很多时候CPU是处于空闲状态的,因为IO设备很慢,比如说Ajax获取数据,这个时候就不得不等到结果出来之后,再往下继续执行。
这个时候JavaScript的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到设备返回了结果,再回过头,把挂起的任务继续执行下去。
因此,所有的任务分成了2种,一种是同步任务(synchronous),一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务是指,不进入主线程、而进入“任务队列”(task queue)的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
具体的运行机制如下:

  • 1.所有同步任务都在主线程上执行,形成一个执行栈;
  • 2.主线程外,还存在一个任务队列,只要异步任务有了运行结果,就在“任务队列”之中放置一个事件;
  • 3.一旦“执行栈”中的所有同步任务执行完成,系统就会读取“任务队列”,看看里面有哪些事件,哪些对应的异步任务,于是结束等待状态,进入执行栈,开始执行;
  • 4.主线程不断重复上面三个步骤。
    只要主线程空了,就会去读取“任务队列”,这就是JavaScript的运行机制,整个过程都会不断重复。

事件与回调函数

  • “任务队列”是一个事件的队列/消息的队列,IO设备完成一项任务,就在任务队列中添加一个事件,表示相关的异步任务可以进入执行栈了。主线程读取任务队列,就是读取里面有哪些事件。
  • 任务队列中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(如:鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入任务队列,等待主线程读取。所谓的回调函数就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
  • 任务队列是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,任务队列上第一位的事件就自动进入主线程。但是,由于存在“定时器”功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。

Event Loop

主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。如下图所示:
事件循环
上图,在主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码会调用各种外部的API,它们在“任务队列”中加入各种事件(click, done, load)。一旦栈中的代码执行完成,主线程就会去读取“任务队列”,一次执行其中的事件对应的回调函数。执行栈中的代码(同步任务),总是在读取“任务队列”(异步任务)之前执行。

  • Heap(堆):是线性数据结构,相当于一维数组,是一种动态存储的结构。在js中,Heap是动态分配的内存大小不定,也不会自动释放,保存指向对象的指针。其作用是为了存储引用类型的值的数据。
  • Stack(栈):它会自动分配内存和自动释放占据固定大小的空间,其作用主要是:存放基本类型和简单的数据段,如:string, number, boolean等等;提供代码的执行环境。

总结起来:Event Loop会循环不断的去拿宏任务队列中最老的一个任务,推入队列中执行,并在当次循环里一次执行并清空microtask队列里的任务,执行完microtask队列里的任务之后,更新渲染。也就是:

  • 检查 Macrotask 队列是否为空,若不为空,则进行下一步,若为空,则跳到3
  • 从 Macrotask 队列中取队首(在队列时间最长)的任务进去执行栈中执行(仅仅一个),执行完后进入下一步
  • 检查 Microtask 队列是否为空,若不为空,则进入下一步,否则,跳到1(开始新的事件循环)
  • 从 Microtask 队列中取队首(在队列时间最长)的任务进去事件队列执行,执行完后,跳到3
    其中,在执行代码过程中新增的microtask任务会在当前事件循环周期内执行,而新增的macrotask任务只能等到下一个事件循环才能执行了。

宏任务与微任务

这两种任务都是在任务队列的异步事件中注册的回调函数。那为什么要引入宏任务与微任务,只有一种类型的任务可不可以呢?

页面渲染事件,各种IO的完成事件等随时被添加到任务队列中,一直会保持先进先出的原则执行,我们不能准确地控制这些事件被添加到任务队列中的位置。但是这个时候突然有高优先级的任务需要尽快执行,那么一种类型的任务就不合适了,所以引入了微任务队列。不同的异步任务被分为:宏任务和微任务。

宏任务:

  • script(整体代码)
  • setTimeout()
  • setInterval()
  • postMessage
  • I/O
  • UI交互事件

微任务:

  • new Promise().then(回调)
  • MutationObserver(html5新特性)

运行机制

异步任务的返回结果会被放到一个任务队列中,根据异步事件的类型,这个事件实际上会被放到对应的宏任务与微任务队列中去。在当前执行栈为空时,主线程会查看微任务队列是否有事件存在:

  • 存在。依次执行队列中的事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的事件,把当前的回调加到当前执行栈。
  • 不存在。再去宏任务队列中取出一个事件,并把对应的回调加到当前执行栈。
    当前执行栈执行完毕后,会立刻处理所有的微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。
    在事件循环中,每进行一次循环操作称为tick,每一次tick的任务处理模型都是比较复杂的,但关键的步骤主要是:
  1. 执行一个宏任务(栈中没有就从事件队列中获取)
  2. 执行过程如果遇到微任务,就将它添加到微任务的任务队列中;
  3. 宏任务执行完毕之后,立即执行当前微任务队列中的所有微任务(依次执行);
  4. 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染;
  5. 渲染完成后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)。

宏任务和微任务优先问题

为了方便理解,我们可以认为在任务队列里面有宏任务和微任务;宏任务有多个,微任务只有一个。执行顺序应该是:

  1. 从开始执行调用一个全局执行栈,script标签作为宏任务执行;
  2. 执行过程中同步代码立即执行,异步代码放到任务队列中,任务队列存放2种来兴的异步任务,宏任务队列,微任务队列。
  3. 同步代码执行完毕也就意味着第一个宏任务执行完毕(script)
    3.1 先查看任务队列中的微任务队列是否存在宏任务执行过程中所产生的的微任务:①如果有,就将微任务队列中的所有微任务清空;②微任务执行过程中所产生的微任务放到微任务队列中去,在此次执行中一并清空。
    3.2 如果没有再看看宏任务队列中有没有宏任务,有的话执行,没有的话事件轮训第一波结束。执行过程中所产生的微任务放到微任务队列中,完成宏任务之后执行清空微任务队列的代码。

所以总体是:宏任务优先,在宏任务执行完毕之后才会来一次性清空任务队列中的所有微任务。

再来看一道经典的题目

// 代码一执行就开始执行了一个宏任务-宏0
console.log('script start');

setTimeout(() => {   // 宏1
  console.log('setTimeout');
}, 1* 2000)

Promise.resolve().then(function() {
  console.log(promise1);   // 微1-1
})
.then(function() {
  console.log(promise2);   // 微1-4,这个then中的会等待上一个then执行完毕之后得到其他状态才会Queue注册状态对应的回调,假设上一个then中主动抛出错误且没有被捕获,那就注册的是这个then中的第二个回调了。
})

async function foo() {
  await bar();    // await是promise的语法糖,会异步等待获取其返回值
                       //  后面的代码可以理解为放到异步队列微任务中。
  console.log('async1 end');
}

foo();

function bar() {
  console.log('async2 end');
}

async function errorFunc() {
  try {
    await Promise.reject('error!');
  }.catch(e) {
    // 从这后面开始,所有的代码可以理解为放到微任务队列中去
    console.log(e);   // 微1-3
  }
  console.log('async1');
  return Promise.resolve('async1 success');
}
errorFunc().then(res => console.log(res));   // 微1-5

console.log('script end');

PS:注意一点就是Promise.then().then(),在注册异步任务的时候,第二个then中的回调是依赖第一个then中回调的结果的,如果执行没有异常才会在该异步任务执行完成之后注册状态对应的回调。

第一次执行

全局一个宏任务执行,输出同步代码。挂载宏1,微1-1, 微1-2,微1-3, 微1-4。1-表示属于第一次轮询。输出的结果是:

script start->async2 end->script end

第二次执行

同步代码执行完毕,开始执行异步任务中的微任务队列中的代码。
微任务队列:只有一个队列且会在当前轮询一次性清空。

执行微1-1:promise1
执行微1-2:async1 end
执行微1-3:error!, async1。当前异步回调执行完毕才Promise.resolve('async1 success'),然后注册.then()中的成功的回调:微1-5
执行微1-4:promise2
执行刚注册的微1-5:async1 success

到这里,第一波轮询结束。

第三次执行

开启第二波轮询:执行宏1

run: setTimeout

到此为止,整个轮询结束。其实相对难以理解的也就是微任务,对于微任务也就是上面说的只有一个队列会在此次轮询中一次清空(包括此次执行过程中所产生的微任务)。
整个打印的顺序为:script start -> async2 end ->script end -> promise1 -> async1 end -> error! -> async1 -> promise2 -> async1 success -> setTimeout

深入

通过上面的分析,再来看一些类似的题目。

console.log(1)
setTimeout(function() {
  //settimeout1
  console.log(2)
}, 0);
const intervalId = setInterval(function() {
  //setinterval1
  console.log(3)
}, 0)
setTimeout(function() {
  //settimeout2
  console.log(10)
  new Promise(function(resolve) {
    //promise1
    console.log(11)
    resolve()
  })
  .then(function() {
    console.log(12)
  })
  .then(function() {
    console.log(13)
    clearInterval(intervalId)
  })
}, 0);

//promise2
Promise.resolve()
  .then(function() {
    console.log(7)
  })
  .then(function() {
    console.log(8)
  })
console.log(9)

打印顺序是:1 -> 9 -> 7 -> 8 -> 2 -> 3 ->10 -> 11 -> 12 -> 13
解析:

  1. 第一次事件循环
  • console.log(1)执行,输出1;
  • setTimeout1执行,加入macrotask队列;
  • setInterval1执行,加入macrotask队列;
  • setTimeout2执行,加入macrotask队列;
  • promise2执行,它的两个then函数加入microtask队列;
  • console.log(9)执行,输出9;
  • 根据事件循环的定义,接下来会执行新增的microtask任务,按照进入队列的顺序,执行console.log(7)和console.log(8),输出7和8;microtask队列为空回到第一步,进入下一个事件循环,此时macrotask队列为:setTimeout1,setInterval1,setTimeout2。
  1. 第二次事件循环
  • 从macrotask队列里取出位于队首的任务(setTimeout1)并执行,输出2;
  • microtask队列为空,回到第一步,进入下一个事件循环,此时macrotask队列为:setInterval1,setTimeout2。
  1. 第三次事件循环
  • 从macrotask队列里取出位于队首的任务setInterval1并执行,输出3;
  • 然后又将新生成的setInterval1加入macrotask队列,microtask队列为空,回到第一步,进入下一个事件循环,此时macrotask队列为:setTimeout2,setInterval1。
  1. 第四次事件循环
  • 从macrotask队列里取出位于队首的setTimeout2并执行,输出10,并且执行new Promise内的函数(new promise内的函数是同步操作,并不是异步操作),输出11,并将它的2个then函数加入microtask队列;
  • 在microtask队列中,取队首的任务执行,知道为空为止。因此2个新增的microtask任务按顺序执行,输出12和13,并且将setInterval1清空。此时,microtask队列和macrotask队列都为空,浏览器会一直检查队列是否为空,等待新的任务加入队列。

注意:由于在执行microtask任务的时候,只有当microtask队列为空的时候,它才会进入下一个事件循环,因此,如果它源源不断地产生新的microtask任务,就会导致主线程一直在执行microtask任务,而没有办法执行macrotask任务,这样我们就无法进行UI渲染/IO操作/ajax请求了,因此,我们应该避免这种情况发生。在nodejs里的process.nexttick里,就可以设置最大的调用次数,以此来防止阻塞主线程。

问题思考

  1. 为什么在第一次Event Loop时,不先执行macrotask,因为按照流程的话,不应该是先检查macrotask队列是否为空,再检查microtask队列吗?
    因为一开始Js主线程中跑的任务就是macrotask任务,而根据事件循环的流程,一次事件循环只会执行一个macrotask任务,所以执行完主线程的代码之后,它就去从microtask队列里面取队首的任务来执行。

  2. 定时器是否是真实可靠的呢?比如我执行一个命令:setTimeout(task, 1000),他是否就能准确的在1000毫秒后执行呢?
    实根据以上的讨论,我们就可以得知,这是不可能的。
    因为你执行setTimeout(task,100)后,其实只是确保这个任务,会在100毫秒后进入macrotask队列,但并不意味着他能立刻运行,可能当前主线程正在进行一个耗时的操作,也可能目前microtask队列有很多个任务,所以这也可能是大家一直诟病setTimeout的原因吧。

讲讲深拷贝与浅拷贝,如何实现,有哪些方式?

参考文章

  1. js的深拷贝和浅拷贝
  2. js深拷贝 VS 浅拷贝

前言

深浅拷贝实际上针对的是复杂数据类型而言的,浅拷贝只复制对象的一层属性,深拷贝则复制了所有层级的属性。因为Js存储对象是存地址的,所以浅拷贝会导致obj1和obj2指向同一块内存地址,而深拷贝则是为对象开辟一块新的内存,复制对象进来,因此对于深拷贝而言,obj1和obj2的操作互不影响。

浅拷贝实现

指的是将一个对象的属性值复制到另外一个对象,如果有的属性的值为引用类型的话,那么会将这个引用的地址复制给对象,因此两个对象会有同一个引用类型的引用。浅拷贝可以使用Object.assign()和展开运算符来实现(如:const {a, b, c} = {1, 2, {3, 4}}; 所以c = {3, 4})。
如何实现一个对象或者数组的浅拷贝。
想一想,好像很简单,遍历对象,然后把属性和属性值都放在一个新的对象就好了~

function shallowCopy(object) {
  // 只拷贝对象
  if (!object || typeof object !== "object") {
    return;
  }
  // 根据object的类型判断是新建一个数组还是对象
  let newObject = Array.isArray(object) ? [] : {};
  
  // 遍历object,并且判断是object的属性才拷贝
  for (let key in object) {
    if (object.hasOwnProperty(key)) {
      newObject[key] = object[key];
    }
  }
  return newObject;
}

深拷贝实现

相对于浅拷贝而言,如果遇到属性值为引用类型的时候,它新建一个引用类型并将对应的值复制给它,因此对象获得的一个新的引用类型而不是一个原有类型的引用。深拷贝对于一些对象可以使用JSON的两个函数来实现,但是由于JSON的对象格式化比js的对象格式化更加严格,所以如果属性值里面出现函数或者Symbol类型的值时,会转换失败。
那如何实现一个深拷贝呢?说起来也简单,我们在拷贝的时候判断一下属性值的类型,如果是对象,我们递归调用深拷贝函数不就好了~

function deepCopy(object) {
  if (!object || typeof object !== "object")  return;
    let newObject = Array.isArray(object) ? [] : {};
    
    for (let key in object) {
      if (object.hasOwnProperty(key)) {
        newObject[key] = typeof object[key] === "object" ? deepCopy(object[key]) : object[key];
    }
  }
  
  return newObject;
}

思考

  • 【性能问题】尽管使用深拷贝会完全克隆一个新对象,不会产生副作用,但是深拷贝使用了递归,性能会不如浅拷贝,在开发中,还是要根据实际情况选择。
  • 【JSON对象的parse和stringify】JSON对象是ES5中引入的新的类型(支持的浏览器为IE8+),JSON对象parse方法可以将JSON字符串反序列化成JS对象,stringify方法可以将JS对象序列化成JSON字符串,
    借助这两个方法,也可以实现对象的深拷贝。

示例

  //例1
  var source = { name:"source", child:{ name:"child" } } 
  var target = JSON.parse(JSON.stringify(source));
  target.name = "target";  //改变target的name属性
  console.log(source.name); //source 
  console.log(target.name); //target
  target.child.name = "target child"; //改变target的child 
  console.log(source.child.name); //child 
  console.log(target.child.name); //target child
  //例2
  var source = { name:function(){console.log(1);}, child:{ name:"child" } } 
  var target = JSON.parse(JSON.stringify(source));
  console.log(target.name); //undefined
  //例3
  var source = { name:function(){console.log(1);}, child:new RegExp("e") }
  var target = JSON.parse(JSON.stringify(source));
  console.log(target.name); //undefined
  console.log(target.child); //Object {}

这种方法使用较为简单,可以满足基本的深拷贝需求,而且能够处理JSON格式能表示的所有数据类型,但是对于正则表达式类型、函数类型等无法进行深拷贝(而且会直接丢失相应的值)。
还有一点不好的地方是它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object。同时如果对象中存在循环引用的情况也无法正确处理。

讲一讲 JavaScript 的垃圾回收机制

参考文章

1. JavaScript 垃圾回收机制
2. 深入理解 V8 的垃圾回收原理
3. V8 之旅: 垃圾回收器

JavaScript 自动垃圾回收机制

垃圾回收又称为 GC 。在 JavaScript 编码过程中,开发者不需要手动跟踪内存的使用情况,只需要按照要求的标准写 JavaScript 代码,程序运行所需内存的分配以及无用内存的回收完全是自动管理的。其原理是:

  • 找出那些不再使用的变量,然后释放掉其占用的内存;
  • 垃圾收集器会按照固定的时间间隔(或者预定的收集时间)周期性地执行此操作。

局部变量正常的生命周期

局部变量只在函数执行的过程中存在。
在函数执行的过程中,会为局部变量在栈内存(或者堆内存)上分配相应的空间来存储它们的值。在函数中使用这些变量,直到函数执行结束,此时可以释放局部变量的内存供将来需要时使用。

以上情况可以比较容易地判断变量是否有存在的必要,更复杂的情况需要更精细的变量追踪策略。

JavaScript 中的垃圾回收器必须跟踪每个变量是否有用,需要为不再用的变量打上编辑,用于将来回收其占用的内存。标识无用变量的策略通常有2个:标记清除和引用计数。

标记清除

mark-and-sweep即是标记清除,也是JS中最为常用的垃圾回收方式,其执行机制如下:

  • 当变量进入环境时,就将其标记为“进入环境”
  • 当变量离开环境时,就将其标记为“离开环境”
    逻辑上,永远不能释放进入环境的变量所占用的内存,因为执行流进入相应的环境时,可能会用到它们。
    标记变量的方式有很多种,可以使用标记位的形式记录变量进入环境,也可以单独为“进入环境”和“离开环境”添加变量列表来记录变化。
    标记清除采用的收集策略是:
  • JavaScript中的垃圾收集器运行时会给存储在内存中的所有变量都加上标记;
  • 然后去掉环境中的变量以及被环境中的变量引用的变量的标记;
  • 此后,再被加上标记的变量被视为准备删除的变量;
  • 最后,垃圾收集器完成内存清除,销毁那些带标记的值并回收其占用的内存空间。
    2008年之前,IE、Firefox、Opera、Chrome 和 Safari 的 JavaScript实现使用的均为 标记清除的垃圾回收策略,区别可能在垃圾收集的时间间隔。

引用计数

reference counting 即是引用计数,它是另外一种垃圾回收策略。引用计数的本质是跟踪记录每个值被引用的次数,其执行机制如下:

  • 当声明一个变量,并将一个引用类型的值赋值给这个变量时,这个值的引用次数为1;
  • 若是同一个值又被赋值给另外一个变量,则该值的引用次数再加1;
  • 但是如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1;
  • 当这个值的引用次数为0时,则无法再访问这个值,就可以回收其占用的内存空间。
    垃圾收集器下次运行时,会释放那些引用次数为0的值所占用的内存。引用计数存在一个致命的问题:循环引用,指的是对象A包含一个指向对象B的指针,而对象B中也包含一个指向对象A的引用。下面的示例就是典型的循环引用的例子:
function cycleReference() {
  let objA = new Object();
  let objB =  new Object();

  objA .someOtherObject = objB;
  objB .someOtherObject = objA;
}

上述例子中objA和objB通过各自属性相互引用。按照引用计数的策略,两个对象的引用次数均为2.若采用标记清除策略,函数执行完毕,对象离开作用域就不存在相互引用。但采用引用计数后,函数执行完,两个对象的引用次数永不为0,会一直存在内存中,如果多次调用,导致大量内存得不到释放。
IE8浏览器 之前中有一部分对象并不是原生的 JavaScript 对象,可能是使用 C++ 以 COM 对象的形式实现的(BOM, DOM)。而 COM 对象的垃圾收集机制采用的是 引用计数策略。使 IE 的 JavaScript 引擎是使用标记清除策略实现的,但 JavaScript 访问 COM 对象仍然是基于 引用计数策略的。在这种情况下,只要在 IE 中涉及 COM 对象,就可能存在循环引用的问题。

那么该如何避免这种情况呢?
为了避免循环引用,最好在不使用这些对象时,手动断开原生的JavaScript对象与DOM元素之间的连接。IE中的循环引用与手动断开的操作如下所示:

// 存在循环引用的情况
let element = document.getElementById('some_element');
let myObj = new Object();
myObj.element = element;
element.someObj = myObj;

// 手动断开循环引用
myObj.element = null;
element.someObj = null;

PS:将变量设为null即可切断变量与它之前引用的值之间的连接。下次垃圾收集器运行时,会删除这些值并回收它们占用的内存。为了解决上述问题,IE9及以上版本把BOM和DOM对象都转换成了真正的 JavaScript 对象,避免了两种垃圾回收算法并存引起的问题。

垃圾回收的性能问题

垃圾收集器是周期运行的,确定垃圾收集的时间间隔是个非常重要的问题。IE7之前的垃圾收集器是根据内存分配量运行的,即256个变量、4096个对象(数组)字面量或64KB的字符串。达到这些临界值的任何一个,垃圾收集器就会运行。所以就导致如果一个脚本含有很多变量,在整个生命周期中一直保有前面临界值大小的变量,就会频繁触发垃圾回收,会存在严重的性能问题。

IE7重写了垃圾收集历程。新的工作方式为:触发垃圾收集的变量分配、字面量和数组元素的临界值被调整为动态修正。初始值与之前版本相同,如果垃圾收集例程回收的内存低于15%,则临界值加倍。若回收内存分配量超过85%,则临界值重置为默认值。

JavaScript V8引擎的垃圾回收机制

在JavaScript脚本中,绝大多数对象的生存期很短,只有部分对象的生存期较长,所以V8中的垃圾回收主要使用的是 分代回收(Generational collection)机制。

分代回收机制

V8引擎将保存对象的堆(heap)进行了分代:

  • 对象最初会被分在新生区(New Space)(1-8M),新生区的内存分配只需要保有一个指向内存区的指针,不断根据内存大小进行递增,当指针达到新生区的末尾,会有一次垃圾回收清理(小周期),清理掉新生区中不再活跃的死对象。
  • 对于超过2个小周期的对象,则需要将其移动至老生区(Old Space),老生区在标记-清除/标记-紧缩的过程(大周期)中进行回收。
    大周期进行的并不频繁,一次大周期通常是在移动足够多的对象至老生区后才会发生。

Scavenge 算法

由于垃圾清理发生的比较频繁,清理的过程必须很快。V8中的清理过程使用的是 Scavenge 算法,按照比较经典的 Cheney算法(https://m.tqwba.com/x_d/jishu/428898.html)来实现的。Scavenge 算法的主要过程是:

  • 新生区被分为2个大小相等的子区(semi-spaces):to-space和from-space;
  • 大多数的内存分配都是在to-space发生(某些特定对象是在老生区);
  • 当to-space耗尽时,交换to-space和from-space,此时所有的对象都在from-space;
  • 然后将from-space中活跃的对象复制到to-space或者老生区中;
  • 这些对象被直接压到to-space,提升了Cache的内存局部性,可使内存分配简洁快速。

不可忽视的写屏障 Write barriers

如果新生区有某个对象,只有一个指向它的指针,恰好该指针在老生区的对象中,在垃圾回收之前我们如何得知新生区的该对象是否活跃呢?
为解决此问题,V8在写缓冲区有一个列表,其中记录了所有老生区对象指向新生区的情况。新生区对象诞生时不会有指向它的指针,当老生区的对象出现指向新生区对象的指针时,便记录跨区指向,记录行为总是发生在写操作中。

标记-清除算法 与 标记-紧缩算法

因为新生区的内存一般都不大,所以使用 Scavenge 算法进行垃圾回收效果比较好。老生区一般占用内存较大,因此采用的是 标记-清除(Mark-Sweep)算法 与 标记-紧缩(Mark-Compact)算法。两种算法都包括两个阶段:标记阶段,清除或紧缩阶段。

标记阶段

在标记阶段,堆上所有的活跃对象都会被发现并且标记。

  • 每一页都包含用来标记的位图;
  • 位图都要占用空间(3.1% on 32-bit,1.6% on 64-bit systems)
  • 使用两位二进制标记对象的状态
  • 状态为白(white),它尚未被垃圾回收器发现;
  • 状态为灰(grey),它已被垃圾回收器发现,但它的邻接对象仍未全部处理完毕;
  • 状态为黑(black),它不仅被垃圾回收器发现,而且其所有邻接对象也都处理完毕。
    标记算法的核心是深度优先搜索,具体过程为:

display: none和visibility: hidden的区别

前言

在使用CSS隐藏一些元素的时候,我们经常用到display: none和visibility: hidden。那么2者之间有什么不一样呢。

占据空间与否

  • display: none 不占据任何空间,文档渲染时,这个元素就像不存在一样。
  • visibility: hidden 该元素空间仍然存在。

是否渲染

  • display: none 会触发reflow回流,进行渲染。
  • visibility: hidden 只会触发repaint重绘,因为没有发现位置改变,不进行渲染。

是否有继承属性

  • display: none,display不是继承属性,元素及其子元素都会消失。
  • visibility: hidden,visibility是继承属性,如果子元素使用了visibility: visible,则不继承,这个子元素又会显示出来。

CSS选择器的优先级顺序是怎样的呢?

前言

在我们日常编写CSS的时候,都会有一个疑惑就是:CSS选择器的优先级问题。我们先来看一小段代码:

.text-size {
  font-size: 15px !important;
}

这个时候,!important是具有最高优先级的,而且相比较内联的样式而言,它的优先级会更高。那么除了!important之外的其他选择器呢?

CSS选择器

我们把选择器的名称以及权重列出来看一下。

选择器名称 权重
内联样式 1000
ID选择器 100
类选择器(包括属性选择器和伪类) 10
标签和伪元素选择器 1
组合符和通配符 0

根据表中给出来的权重值,对于我们在编写CSS的时候就会有一个比较明确的顺序了:

.text-box > span {
  height: 100px;    // 权重值=10+1=11(类选择器+标签)
}

.text-box span:last-child {
  font-size: 18px;   // 权重值=10+10+1=21(类选择器+伪类+伪元素选择器)
}

.text-box div>span:first-child {
  color: red;   // 权重值=10+10+1+1=22(类选择器+标签+标签+伪元素选择器)
}

JS数据类型有哪些?如何进行类型判断?不同类型的内存图大致是怎样的?

前言

这是几乎每场面试时面试官都会问到的问题,所以重要性可以毫无疑问的排第一。同时也是需要牢记在心的一个问题,它是我们在前端开发过程中几乎每天都会用到的基础点。

参考文章

  1. Javascript的基本数据类型分析和判断

  2. 判断JS数据类型的四种方法(非常经典,需要牢记)

    js可以分为两种类型的值,一种是基本数据类型,一种复杂数据类型。
    两种类型的主要区别是它们的存储位置不同,基本数据类型的值直接保存在栈中,复杂数据类型的值保存在堆中,
    通过使用在栈中保存对应的指针来获取堆中的值。

  • 栈:基本数据类型(Undefined, Null, Boolean, Number, String, Symbol(ES6新增), BigInt(ES10新增))
    (1)Symbol 代表创建后独一无二且不可变的数据类型,它的出现我认为主要是为了解决可能出现的全局变量冲突的问题。
    (2)BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这
    个数已经超出了 Number 能够表示的安全整数范围。
  • 堆:引用数据类型(Object, Array,Function)
    以上两种类型的区别是:存储位置不同。
  1. 基本数据类型直接存储在栈(Stack)中的简单数据段,占据空间小,大小固定且是频繁使用的数据。
  2. 引用数据类型存储在堆(Heap)中的对象,占据空间大,大小不固定。但是如果存储在栈中,将会影响程序运行的性能;
    引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器找到引用值时,首先检索其在栈中的地址,
    取得内存地址后从堆中获得实体。

undefined和null又有什么区别呢?

  • 相同点
    首先 Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。
  • 不同点
  1. undefined 代表的含义是未定义,null 代表的含义是空对象。一般变量声明了但还没有定义的时候会返回 undefined,
    null主要用于赋值给一些可能会返回对象的变量,作为初始化。
  2. undefined 在 js 中不是一个保留字,这意味着我们可以使用 undefined 来作为一个变量名,但这样的做法是非常危险
    的,它会影响我们对 undefined 值的判断。但是我们可以通过一些方法获得安全的 undefined 值,比如说 void 0。
  3. 当我们对两种类型使用 typeof 进行判断的时候,Null 类型化会返回 "object",这是一个历史遗留的问题。当我们使用
    双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。

数据类型的判断

主要有以下四种方法:

1. typeof

typeof 是一个操作符,其右侧跟一个一元表达式,并返回这个表达式的数据类型。返回的结果用该类型的字符串(全小写字母)形式表示,包括以下 7 种:number、boolean、symbol、string、object、undefined、function 等。

typeof '';// string 有效
typeof 1;// number 有效
typeof Symbol();// symbol 有效
typeof true;//boolean 有效
typeof undefined;//undefined 有效
typeof null;//object 无效
typeof [] ;//object 无效

有的时候,typeof会返回一些令人不解但技术上却正确的结果:

  • 对于基本类型,除了null以外,均可以返回正确的结果;
  • 对于引用类型。除了function,均返回object类型;
  • 对于null,返回object类型;
  • 对于function,返回function类型。
    其中,null有属于自己的类型Null,引用类型中的数组、日期、正则也都有属于自己的具体类型,而typeof对于这些类型的处理,只返回了处于其原型链最顶端的Object类型。技术上没有错,但不是我们想要的结果。

2. instanceof

instanceof 是用来判断 A 是否为 B 的实例,表达式为:A instanceof B,如果 A 是 B 的实例,则返回 true,否则返回 false。 在这里需要特别注意的是:instanceof 检测的是原型,我们用一段伪代码来模拟其内部执行过程:

instanceof (A, B) = {
    var L = A.__proto__;
    var R = B.prototype;
    if(L === R) {
        // A的内部属性 __proto__ 指向 B 的原型对象
        return true;
    }
    return false;
}

从上述过程可以看出,当 A 的 proto 指向 B 的 prototype 时,就认为 A 就是 B 的实例,我们再来看几个例子:

[ ] instanceof Array;       // true
{1: 'test', 2: 'test'} instanceof Object;     // true
new Date() instanceof Date;      // true
 
function Person(){};
new Person() instanceof Person;      // true
 
[ ] instanceof Object;// true
new Date() instanceof Object;          // true
new Person instanceof Object;        // true

可以看出,instanceof可以判断出[ ]是Array的实例,但它也认为[ ]是Object的实例,这是为什呢? [ ]、Array、Object 三者之间存在怎样的关系呢?

  • 从 instanceof 能够判断出 [ ].proto 指向 Array.prototype,而 Array.prototype.proto 又指向了Object.prototype,最终 Object.prototype.proto 指向了null,标志着原型链的结束。因此,[]、Array、Object 就在内部形成了一条原型链:
    捕获
  • 从原型链可以看出,[] 的 proto 直接指向Array.prototype,间接指向 Object.prototype,所以按照 instanceof 的判断规则,[] 就是Object的实例。依次类推,类似的 new Date()、new Person() 也会形成一条对应的原型链 。因此,instanceof 只能用来判断两个对象是否属于实例关系, 而不能判断一个对象实例具体属于哪种类型。
  • instanceof 操作符的问题在于,它假定只有一个全局执行环境。如果网页中包含多个框架,那实际上就存在两个以上不同的全局执行环境,从而存在两个以上不同版本的构造函数。如果你从一个框架向另一个框架传入一个数组,那么传入的数组与在第二个框架中原生创建的数组分别具有各自不同的构造函数。
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
xArray = window.frames[0].Array;
var arr =new xArray(1,2,3);   // [1,2,3]
arr instanceof Array;   // false

针对数组的这个问题,ES5 提供了 Array.isArray() 方法 。该方法用以确认某个对象本身是否为 Array 类型,而不区分该对象在哪个环境中创建。
Array.isArray() 本质上检测的是对象的 [[Class]] 值,[[Class]] 是对象的一个内部属性,里面包含了对象的类型信息,其格式为 [object Xxx] ,Xxx 就是对应的具体类型 。对于数组而言,[[Class]] 的值就是 [object Array] 。

3. constructor

当一个函数 F被定义时,JS引擎会为F添加 prototype 原型,然后再在 prototype上添加一个 constructor 属性,并让其指向 F 的引用。当执行 var f = new F() 时,F 被当成了构造函数,f 是F的实例对象,此时 F 原型上的 constructor 传递到了 f 上,因此 f.constructor == F
可以看出,F 利用原型对象上的 constructor 引用了自身,当 F 作为构造函数来创建对象时,原型上的 constructor 就被遗传到了新创建的对象上, 从原型链角度讲,构造函数 F 就是新对象的类型。这样做的意义是,让新对象在诞生以后,就具有可追溯的数据类型。

''.constructor  == String; // true
new Number(1).constructor  == Number; // true
new Error().constructor == Error; // true

细节点:

  • null和undefined是无效对象,因此是不会有constructor存在的,这两种类型的数据需要通过其他方式来判断;
  • 函数的constructor 是不稳定的,这个主要体现在自定义对象上,当开发者重写prototype后,原来的constructor 引用就会丢失,constructor会默认为Object。因此,为了规范开发,在重写对象原型时一般都需要重新给 constructor 赋值,以保证对象实例的类型不被篡改。

4. toString()

toString() 是 Object 的原型方法,调用该方法,默认返回当前对象的 [[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型。
对于 Object 对象,直接调用 toString() 就能返回 [object Object] 。而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息。

Object.prototype.toString.call('') ;      // [object String]
Object.prototype.toString.call(1) ;      // [object Number]
Object.prototype.toString.call(true) ;      // [object Boolean]
Object.prototype.toString.call(Symbol());      //[object Symbol]
Object.prototype.toString.call(undefined) ;      // [object Undefined]
Object.prototype.toString.call(null) ;      // [object Null]
Object.prototype.toString.call(new Function()) ;      // [object Function]

Sharing of Automatic Testing Tools-Cypress

官网

Cypress.io,使用Cypress的好处是:所有的测试用例都采用Javascript编写,类似于Jquery的方式,对前端开发人员来说是非常友好的了。同时Cypress运行非常快,几乎只要当我们的页面渲染出来时就已经加载出来了,因为Cypress都是基于真实的DOM进行的测试;可视化的debug。

安装

注意:自己测试的时候最好不要以“Cypress/cypress”来命名,否则install的时候会报错。

  • 方式1:因为我们在墙内,所以下载Cypress非常慢,经常会因为网络超时而报错。
npm install cypress --save-dev
yarn add cypress --save

然后再通过离线安装的方式 npm install cypress-6.6.0.tgz,注意:此时的cypress-6.6.0.tgz必须在相应的文件夹中。

  • 方式3:先下载Windows对应的ZIP包cypress.zip这个位置下载的包都是最新版本的,下载完成之后存放在本地,使用下面的命令进行安装:
CYPRESS_INSTALL_BINARY=D:\\Install-Packages\\Cypress\\cypress.zip npm install cypress

安装完成之后,我们就可以顺利使用Cypress了,但在使用之前,为了启动更方便需要在packages.json中配置

"scripts": {
  "cy:open": "cypress open"
}

然后再运行:yarn run cy:open或者npm run cy:open,成功之后展示如下:
Cypress

如果我们的项目中配置了.babelrc,并且在运行之后报错Webpack Compilation Error则需要在启动Cypress之后的文件夹下创建一个空的.babelrc{},随后再次运行将不会在报错。该问题可以参考Webpack Compilation Error: Cannot find module

编写测试用例

接下来就可以编写自己的测试用例了。可以先参考这里测试用例。这里举一个例子:

describe('First Cypress Test', function () {
  it('Does not do much!', function () {
    cy.visit('https://www.naver.com/');   // 访问Naver搜索引擎
    cy.get('#query').type('Cypress Test');   // 获取是否存在id="query"的元素,存在则输入“Cypress Test”
    cy.wait(120);
    cy.get('#search_btn').click();   // 获取是否存在id="search_btn"的元素,存在则点击
    cy.contains('JavaScript End to End Testing').click();   // 内容包含JavaScript End to End Testing的,则点击跳转
  });
});

什么是describe,itexpect?

  • describe和it: 可以查看Mocha;
  • expect:可以查看Chai.

如何编写真实的测试用例

  1. 请参考:Write a real test
  2. 如果VScode中describe被加上了波浪线提示,请安装mocha避免yarn add @types/mocha;
  3. 如果继续有"cy",加红的波浪线提示,请在文件的第一行加上/// <reference types="cypress" />;

讲讲es6的新特性主要有哪些?

参考文章

1. ES6中常用的10个新特性讲解
2. ES6新增特性

1. 变量声明:const和let

es6推荐使用let作为局部变量的声明,相比之前的var(因为有变量提升,所以无论声明在何处,都会被视为声明在函数的最顶部),let和var声明的区别:

var x = '全局变量';
{
  let x = '局部变量';
  console.log(x);   // 局部变量
}
console.log(x);   // 全局变量

let表示声明变量,const表示声明常量(意味着它的值在设置完成之后就不能再修改了),两者都是块级作用域;如果const是一个对象,对象所包含的值是可以被修改的。也就是说,对象所指向的地址没有改变。

const person = { name: 'Jessica' };

person.name = 'Nick';   // 不会报错
person = { name: 'Nick' };    // 会报错,地址修改了

因此,需要我们注意:

  • let关键字声明的变量不具备像变量提升(hosting)的特性。(为什么只是说像,因为实际上let声明的变量也具有百年来提升的特性,但是let声明之后,变量是存放在一个暂时性死区(dead zone)中,当用户在声明之前访问这个变量时,就会出现Reference Error这样的错误,看起来就像没有提升一样);
  • let和const只在最靠近的一个块中(花括号内)生效;
  • 当使用常量const声明时,请使用大写变量,如:INIT_PAGE_SIZE;
  • const在声明时必须被赋值。

2. 模板字符串

在es6之前我们是怎样处理模板字符串的呢?通过''和'+'来构建。

'I am a string, you are a number, so we are different, right? \
He is a boolean, he is different with us. His value is\
' + value_boolean + ', my value is ' + value_string + '.'; 

对于es6而言:

  • 基本的字符串格式化,将表达式嵌入字符串中进行拼接,使用${}来界定;
  • ES6反问号搞定。
`'I am a string, you are a number, so we are different, right?He is a boolean, he is different with us. His value is${value_boolean }, my value is ${value_string} .`

3. 箭头函数

箭头函数实际上就是函数的一种简写形式,使用括号包裹参数,再使用箭头(=>),再接上函数体。它最直观的三个特点是:

// es5
var func = function(a, b) {
  return a + b;
} 
[1, 2, 3].map((function(item) {
  return item++;
}).bind(this));

// 箭头函数
var func = (a, b) => a + b;
[1, 2, 3].map(item => item++);

注意:

  • 如果箭头函数没有参数,直接写一个空括号即可;
  • 如果箭头函数的参数只有一个,可以省区包裹参数的括号;
  • 如果箭头函数有多个参数,将参数一次用逗号分隔,包裹再括号中即可。
    如下:
// 没有参数
let func = () => {
  console.log(123);
}

// 只有一个参数
let func = name => {
  console.log(name);
}

// 多个参数
let func = (name, age, height) => {
  return [name, age, height];
}

箭头函数与普通函数的区别

  1. 语法简洁,清晰
  2. 箭头函数不会创建自己的this(这个很重要!!!)
    MDN的官方解释是:箭头函数没有自己的this,arguments,super或new.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。因此它只会从自己作用域链的上一层继承this。
    所以,箭头函数会捕获它在定义时(注意不是调用时)所处的外层执行环境的this,并且继承这个this,定义时确定了之后就永远不会改变了。请看下面的例子:
var id = 'outter';

function funA() {
  setTimeout(function() {
    console.log(this.id);
  }, 1000);
}

function funB() {
  setTimeout(() => {
    console.log(this.id);
  }, 1000);
}

funA.call({ id: 'inner' });   // outter
funB.call({ id: 'inner' });   // inner

分析:

  • funA中的setTimeout中使用普通函数,当1秒后函数执行时,这个时候实在全局作用域中执行的,所以this指向Window对象,所以this.id就是全局变量id,输出“outter”;
  • funB中的setTimeout中使用箭头函数,这个箭头函数的this在定义的时候就确定了,他继承的是外层funB的执行环境中的this,而funB调用时this被call方法改变到了对象{ id: 'inner' }中,所以输出"inner"。

再来看另外一个例子:

var id = 'outter_obj';
var obj = {
  id: 'inner_obj',
  funA: function() {
    console.log(this.id);
 },
  funB: () => {
    console.log(this.id);
  }
};

obj.funA();    // inner_obj
obj.funB();    // outter_obj

分析:

  • obj的方法funA中使用普通函数定义,普通函数作为对象的方法调用时,this指向它所属的对象。所以this.id就是obj.id,所以输出‘inner_obj’;
  • 但是funB是使用箭头函数定义的,箭头函数的this实际上是继承的它定义时所处的全局执行环境中的this,所以指向Window对象,输出outter_obj。
  • 需要注意的是:定义对象的花括号{}是无法形成一个单独的执行环境的,它依旧处于全局的执行环境中。
  • 这个例子也说明了:箭头函数继承来的this指向永远不会改变(重要!!!)

call() apply()和bind()无法改变箭头函数的this指向

call() apply()和bind()方法可以用来动态修改函数执行时this的指向,但由于箭头函数的this定义时就已经确定且永远不会改变。所以这些方法永远改变不了箭头函数的this指向,虽然即使这样做了代码也不会报错。

var id = 'outter';
// 箭头函数定义在全局作用域
let fun = () => {
    console.log(this.id)
};

fun();     // 'outter'
// this的指向不会改变,永远指向Window对象
fun.call({id: 'Obj'});     // 'outter'
fun.apply({id: 'Obj'});    // 'outter'
fun.bind({id: 'Obj'})();   // 'outter'

箭头函数不能作为构造函数使用

首先,先看看构造函数的new都做了什么?主要有如下几个步骤:

  • JS内部首先会生成一个对象;
  • 再把函数中的this指向该对象;
  • 然后执行构造函数中的语句;
  • 最终返回该对象实例。
    但是,因为箭头函数没有自己的this,它的this其实继承了外层执行环境中的this,且this指向永远不会随在哪里调用、被谁调用而改变,所以箭头函数不能作为构造函数使用,否则当new调用时就会报错。
let fun = (name, age) => {
  this.name = name;
  this.age = age;
};

// 下面的代码会报错
let p = new fun('Jessica', 18);

箭头函数没有自己的arguments和prototype

  1. 箭头函数没有自己的arguments对象,因为在箭头函数中访问arguments实际上获得的是外层局部执行环境中的值。
// 例1
let fun = (val) => {
  console.log(val);    // test
  // 但下面会报错:Uncaught ReferenceError: arguments is not defined,因为外层全局环境没有arguments对象
  console.log(arguments);
};

fun('test');

// 例2
function fun(name, age) {
  let args = arguments;
  console.log(args);
  let funIn = () => {
    let argsIn = arguments;
    console.log(argsIn);
    console.log(args === argsIn);
  };
  funIn();
}
fun('Jessica', 18);

arguments

从上图可以看出,fun函数内部的箭头函数funIn中的arguments对象,其实是沿作用域链向上访问的外层fun函数的arguments对象。但是,我们仍然可以使用rest参数代替arguments对象,来访问箭头函数的参数列表。

  1. 箭头函数没有原型prototype
let sayName =() => {
  console.log('Hello Jessica');
};
console.log(sayName.prototype);    // undefined

4. 对象/数组的解构

ES5的写法:

ES5写法

ES6写法:

ES6写法

5. for...of和for...in

  • for...of用于遍历一个迭代器,比如说数组:

(1)遍历数组
1614668278

(2)遍历对象
这个时候会抛出异常。
1614668399

  • for...in用来遍历对象中的属性
    for...in如果应用于数组循环,返回的是数组下标、数组的属性和原型上的方法和属性,而for...in应用于对象循环返回的是对象的属性名和原型中的方法和属性。
    (1)遍历对象
    1614666337

(2)遍历数组
1614668006

总结:for...in更适合遍历对象,for...of更适合遍历数组。

7. ES6新增了class

ES6中支持class语法,不过,ES6的class不是新的对象继承模型,它只是原型链的语法糖表现形式。函数中使用static关键词定义构造函数的方法和属性。

class Student {
  constructor() {
    console.log("I'm a student.");
  }
 
  study() {
    console.log('study!');
  }
 
  static read() {
    console.log("Reading Now.");
  }
}
 
console.log(typeof Student); // function
let stu = new Student(); // "I'm a student."
stu.study(); // "study!"
stu.read(); // "Reading Now."

类中的继承和超集:

class Phone {
  constructor() {
    console.log("I'm a phone.");
  }
}
 
class MI extends Phone {
  constructor() {
    super();
    console.log("I'm a phone designed by xiaomi");
  }
}
 
let mi8 = new MI();

分析:

  • extends允许一个子类继承父类,需要注意的是子类的constructor函数中需要执行super()函数。当然,也可以在子类方法中调用父类的方法,如:super.parentMethodName();
  • 类的声明不会提升,如果你想使用某个class,那你必须在使用之前就定义它,否则会抛出一个ReferenceError的错误;
  • 在类中定义函数不需要使用function关键字。

8. 默认参数

ES6可以在定义函数的时候设置参数的默认值。

function sayName(name='Jessica') {
  // 如果调用函数的时候没有传参,这个时候才会使用默认值。
  console.log(`My name is ${name}`);
}

sayName();    // =>My name is Jessica
sayName('Nick');    // =>My name is Nick

闭包是什么,原理是什么,怎么用?哪些场景下会用到?

参考文章

  1. 如何更好的理解Javascript闭包
  2. 我从来不理解 JavaScript 闭包,直到有人这样向我解释它...
  3. 解密JavaScript闭包
  4. Underscore.js的函数部分基本都用到了闭包,可以看一看。目前也正在分析lodash的源码,见Lodash-Source-Code-Analyse

解释

  • 闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见方式就是在一个函数内创建另一个函数,
    创建的函数可以访问到当前函数的局部变量;这意味着,当前作用域总是能够访问外部作用域中的变量。简单来说,
    闭包就是一个访问父函数局部变量的函数。
  • 闭包有两个常用的用途:
    (1)使我们在函数外部能够访问到函数内部的变量。通过使用闭包,我们可以在外部调用闭包函数,从而在外部访问到函数
    内部的变量,可以使用这种方法来创建私有变量。
    (2)使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量
    对象不会被回收。

举例

//最简单的一种形式:一个父函数 
      function superFunc(){
        // 局部变量
        var _super_a = 1;

        var subFunc = function(){
          // _super_a++;
          alert('_super_a: ' + _super_a);
        }

        return subFunc;
      }
      // superFunc() 得到的是subFunc,superFunc()()等于subFunc()
      superFunc()();

首先,闭包是一个函数(上述示例代码中的subFunc),该函数必须有父函数(上述示例代码中的superFunc),然后调用了父函数的变量。

应用场景

  • 使用闭包可以在JavaScript中模拟块级作用域。(在ES6之前,js本身没有块级作用域的概念)
function outputNumbers(count){
  (function(){
    for(var i = 0; i < count; i++){
      alert(i);
    }
  })();
  alert(i); //导致一个错误!就是永远打印出的都是count的值
}
  • 闭包可以用于在对象中创建私有变量。
function MyObject(){
            // 私有变量和私有函数
            var privateVariable = 10;
            function privateFunction(){
              return false;
            }
            // 特权方法,调用私有方法、函数
            this.publicMethod = function(){
              privateVariable++;
              return privateFunction();
            }
          }

统计一个字符串出现最多的字母:给出一段英文连续的英文字符窜,找出重复出现次数最多的字母

思路

  • 先统计每个字母出现的次数,将其存在一个对象中;
  • 得到的这个对象就是一个以字符名为key,出现次数为value的对象,找到value最大的key并输出就可以了。

实现

var func = function(str) {
  // 统计每个字母出现的次数
  var obj = {};
  for (let i = 0; i < str.length; i++) {
    if (!obj[str.charAt(i)]) {
      obj[str.charAt(i)] = 1
    } else {
      obj[str.charAt(i)] ++;
    }
     // obj 对象的key值是字符名,value是出现次数
    // 找出value最大的key并输出就可以了
    var max = 0, thisChar;
     for (keyValue in obj) {
            if (obj[keyValue] > max) {
                max = obj[keyValue]
                thisChar= keyValue
            }
        }
  return thisChar;
  }
}

hash,contenthash,chunkhash的区别

hash

hash是跟整个项目构建相关,只要项目里面有文件修改,整个项目构建的hash值都会更改,并且全部文件都共用相同的hash值。

contenthash

使用webpack编译代码时,我们可以在js文件里面引用css文件。所以这2个文件应该共用相同的chunkhash值,但是有个问题:如果js更改了代码,css文件就算内容没有任何改变,由于该模块发生了改变,这导致CSS会重复构建,这个时候,我们可以使用 extra-text-webpack-plugin 里面的contenthash值,保证即使CSS文件所处的模块里其他文件内容改变,只要CSS文件内容不变,那么就不会重复构建。

chunkhash

采用hash计算的话,每一次构建后生成的哈希值都不一样,即使文件内容压根没有改变。这样子时没办法实现缓存效果,我们需要换另一种hash值计算方法,也就是chunkhash。它会根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值,那么只要我们不改动公共库代码,就可以保证其哈希值不会受影响。

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.