面试题汇总
一、JavaScript基础
1. JavaScript规定了几种语言类型?
Undefined、Null、Bollean、String、Number、Symbol、Bigint、Object
2. JavaScript对象的底层数据结构是什么?
底层是一个FixedArray,FixedArray是V8实现的一个类似于数组的类,它表示一段连续的内存。
3. Symbol类型在实际开发中的应用、可手动实现一个简单的Symbol
数据类型 “symbol” 是一种原始数据类型,该类型的性质在于这个类型的值可以用来创建匿名的对象属性。 以下示例使用"var"创建一个变量来保存 symbol。
var myPrivateMethod = Symbol();
this[myPrivateMethod] = function() {...};
当一个 symbol 类型的值在属性赋值语句中被用作标识符,该属性(像这个 symbol 一样)是匿名的;并且是不可枚举的。因为这个属性是不可枚举的,它不会在循环结构 “for( ... in ...)” 中作为成员出现,也因为这个属性是匿名的,它同样不会出现在 “Object.getOwnPropertyNames()” 的返回数组里。
这个属性可以通过创建时的原始 symbol 值访问到,或者通过遍历 “Object.getOwnPropertySymbols()” 返回的数组。在之前的代码示例中,通过保存在变量 myPrivateMethod的值可以访问到对象属性。
4. JavaScript中的变量在内存中的具体存储形式
JavaScript中的变量分为基本类型和引用类型
基本类型是保存在栈内存中的简单数据段,它们的值都有固定的大小,保存在栈空间,通过按值访问
引用类型是保存在堆内存中的对象,值大小不固定,栈内存中存放的该对象的访问地址指向堆内存中的对象,JavaScript不允许直接访问堆内存中的位置,因此操作对象时,实际操作对象的引用
5. 基本类型对应的内置对象,以及他们之间的装箱拆箱操作
一. 装箱,就是把基本类型转变为对应的对象。装箱分为隐式和显示。
隐式装箱 每当读取一个基本类型的值时,后台会创建一个该基本类型所对应的对象。在这个基本类型上调用方法,其实是在这个基本类型对象上调用方法。这个基本类型的对象是临时的,它只存在于方法调用那一行代码执行的瞬间,执行方法后立刻被销毁。具体到代码如下:
num.toFixed(2); // '123.00'
//上方代码在后台的真正步骤为
var c = new Number(123);
c.toFixed(2);
c = null;
显式装箱 通过内置对象 Boolean、Object、String 等可以对基本类型进行显示装箱。
var obj = new String('123');
拆箱
拆箱与装箱相反,把对象转变为基本类型的值。拆箱过程内部调用了抽象操作 ToPrimitive 。该操作接受两个参数,第一个参数是要转变的对象,第二个参数 PreferredType 是对象被期待转成的类型。第二个参数不是必须的,默认该参数为 number,即对象被期待转为数字类型。有些操作如 String(obj) 会传入 PreferredType 参数。有些操作如 obj + " " 不会传入 PreferredType。
具体转换过程是这样的。默认情况下,ToPrimitive 先检查对象是否有 valueOf 方法,如果有则再检查 valueOf 方法是否有基本类型的返回值。如果没有 valueOf 方法或 valueOf 方法没有返回值,则调用 toString 方法。如果 toString 方法也没有返回值,产生 TypeError 错误。 PreferredType 影响 valueOf 与 toString 的调用顺序。如果 PreferrenType 的值为 string。则先调用 toString ,再调用 valueOf。
var obj = {
valueOf : () => {console.log("valueOf"); return []},
toString : () => {console.log("toString"); return []}
}
String(obj)
// toString
// valueOf
// Uncaught TypeError: Cannot convert object to primitive value
obj+' '
//valueOf
//toString
// Uncaught TypeError: Cannot convert object to primitive value
Number(obj)
//valueOf
//toString
// Uncaught TypeError: Cannot convert object to primitive value
6. 值类型和引用类型
值类型: Undefined、Null、Bollean、String、Number、Symbol、Bigint (基本类型)
基本类型的数据保存在当前执行上线文的栈中
引用类型: object、function、array
保存在堆中,当前执行执行上下文的地址指向堆中的地址
7. null和undefined的区别
undefined表示变量声明但未初始化时的值
Null 代表空值, 代表一个空指针
8. 至少可以说出三种判断JavaScript数据类型的方式,以及他们的优缺点,如何准确的判断数组类型
一. typeof
缺点:
const arr = [];
const obj = {};
const date = new Date();
const regexp = /a/;
console.log(typeof arr); // object
console.log(typeof obj); // object
console.log(typeof date); // object
console.log(typeof regexp); // object
typeof null === 'Object'
二. instanceof
const arr = [];
const obj = {};
console.log(arr instanceof Array); // true
console.log(arr instanceof Object); // true
console.log(obj instanceof Array); // false
console.log(obj instanceof Object); // true
缺点: instanceof是能匹配类型的父类的,所以arr instanceof Array和arr instanceof Object都是true,因为Object是Array的父类。
三. constructor
const arr = [];
console.log(arr.constructor === Array); // true
console.log(arr.constructor === Object); // false
当然和instanceof的问题一样,遇到多realm的环境,constructor判断要确保类型是和判断的对象在同一个realm下。
四. Array.isArray
这个方法能够判断一个对象是否是一个Array类型或者其派生类型。
五. Object.prototype.toString
var ostring = Object.prototype.toString;
function isArray(it) {
return ostring.call(it) === '[object Array]';
}
// 不过注意不要使用stringTag判断Number、Boolean等primitive类型,因为它没法区分装箱的类型:
const ostring = Object.prototype.toString;
console.log(ostring.call(1.0)); // [object Number]
console.log(ostring.call(new Number(1.0))); // [object Number]
9.出现小数精度丢失的原因,JavaScript可以存储的最大数字、最大安全数字,JavaScript处理大数字的方法、避免精度丢失的方法
在ECMAScript数据类型中的Number类型是使用IEEE754格式来表示的整数和浮点数值,所谓浮点数值就是该数值必须包含一个小数点,并且小数点后面必须至少有一位数字。而在使用基于IEEE754数值的浮点运算时出现参数舍入的误差问题,即出现小数精度丢失,无法测试特定的浮点数值。
由于 JavaScript 采用 IEEE 754 标准,数值存储为64位双精度格式,数值精度最多可以达到 53 个二进制位(1 个隐藏位与 52 个有效位)。如果数值的精度超过这个限度,第54位及后面的位就会被丢弃,所以在相加的时候会因为小数位的限制而将二进制数字截断。
bigint可以避免精度丢失
Number.EPSILON
最大数字是Number.MAX_VALUE、最大安全数字是Number.MAX_SAFE_INTEGER。Number.MAX_VALUE大于Number.MAX_SAFE_INTEGER,我的理解是js可以精确表示最大安全数字以内的数,超过了最大安全数字但没超过最大数字可以表示,但不精确,如果超过了最大数字,则这个数值会自动转换成特殊的Infinity值。
由于内存的限制,ECMAScript并不能保存世界上所有的数值,ECMAScript能够表示的最小数值是Number.MIN_VALUE,能够表示的最大数值是Number.MAX_VALUE。超过数值是正值,则被转成Infinity(正无穷),如果是负值则被转成-Infinity(负无穷)。如果在某次返回了正或负的Infinity值,那么该值将无法继续参与下一次的计算,所以我们需要确定一个数值是不是有穷的,即是不是位于最小和最大的数值之间,可以使用isFinite()函数,如果该函数参数在最小和最大数值之间时会返回true。注意,如果参数类型不是数值,Number.isFinite一律返回false。
JavaScript 能够准确表示的整数范围在-2^53到2^53之间(不含两个端点),超过这个范围,无法精确表示这个值。ES6 引入了Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内。
二. 原型和原型链
1. 原型设计模式以及JavaScript中的原型规则
原型模式是设计模式中实现继承的一种实现方式
通过prototype扩展原型上的方法
原型模式,就是创建一个共享的原型,通过拷贝这个原型来创建新的类,用于创建重复的对象,带来性能上的提升。
原型规则
- 所有的引用类型(数组、对象、函数),都具有对象特征,即可自由扩展属性;
- 所有的引用类型,都有一个_proto_ 属性(隐式原型),属性值是一个普通对象;
- 所有函数,都具有一个prototype(显示原型),属性值也是一个普通原型;
- 所有的引用类型(数组、对象、函数),其隐式原型指向其构造函数的显式原型;(obj.proto === Object.prototype);
- 当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的_proto_(即它的构造函数的prototype)中去寻找;
设计模式
-
工厂模式 在函数内创建一个对象,给对象赋予属性及方法再将对象返回
function Person() { var People = new Object(); People.name = 'CrazyLee'; People.age = '25'; People.sex = function(){ return 'boy'; }; return People; } var a = Person(); console.log(a.name);//CrazyLee -
构造函数模式 无需在函数内部重新创建对象,而是用this指代
function Person() { this.name = 'CrazyLee'; this.age = '25'; this.sex = function(){ return 'boy' }; } var a = new Person(); console.log(a.name);//CrazyLee console.log(a.sex());//boy -
原型模式 函数中不对属性进行定义,利用prototype属性对属性进行定义,可以让所有对象实例共享它所包含的属性及方法。
function Parent() { Parent.prototype.name = 'carzy'; Parent.prototype.age = '24'; Parent.prototype.sex = function() { var s="女"; console.log(s); } } var x =new Parent(); console.log(x.name); //crazy console.log(x.sex()); //女 -
混合模式 原型模式+构造函数模式。这种模式中,构造函数模式用于定义实例属性,而原型模式用于定义方法和共享属性
function Parent(){ this.name="CrazyLee"; this.age=24; }; Parent.prototype.sayname=function(){ return this.name; }; var x =new Parent(); console.log(x.sayname()); //Crazy
2. instanceof的底层实现原理,手动实现一个instanceof
es5 foo instanceof Foo在语言内部,实际调用的是Foo[Symbol.hasInstance](foo)。
底层原理是 通过prototype 查找原型链是否属于继承关系
function instance_of(L, R) {//L 表示左表达式,R 表示右表达式
var O = R.prototype;
L = L.__proto__;
while (true) {
if (L === null)
return false;
if (O === L) // 这里重点:当 O 严格等于 L 时,返回 true
return true;
L = L.__proto__;
}
}
3. 实现继承的几种方式以及他们的优缺点
一. 原型继承
function Parent1() {
this.name = ['super1']
this.reName = function () {
this.name.push('super111')
}
}
function Child1() {
}
Child1.prototype = new Parent1()
var child11 = new Child1()
var child12 = new Child1()
var parent1 = new Parent1()
child11.reName()
console.log(child11.name, child12.name) // [ 'super1', 'super111' ] [ 'super1', 'super111' ], 可以看到子类的实例属性皆来自于父类的一个实例,即子类共享了同一个实例
console.log(child11.reName === child12.reName) // true, 共享了父类的方法
缺点: 子类实例共享属性,造成实例间的属性会相互影响
二. 构造函数继承
function Child2() {
Parent1.call(this)
}
var child21 = new Child2()
var child22 = new Child2()
child21.reName()
console.log(child21.name, child22.name) // [ 'super1', 'super111' ] [ 'super1' ], 子实例的属性都是相互独立的
console.log(child21.reName === child22.reName) // false, 实例方法也是独立的,没有共享同一个方法
缺点: 父类的方法没有被共享,造成内存浪费
4. 可以描述new一个对象的详细过程,手动实现一个new操作符
new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。new 关键字会进行如下的操作:
- 创建一个空的简单JavaScript对象(即**{}**);
- 链接该对象(即设置该对象的构造函数)到另一个对象 ;
- 将步骤1新创建的对象作为this的上下文 ;
- 如果该函数没有返回对象,则返回this。
let obj = new Object()
obj.__proto__ = fn.prototype
let result = fn.call(obj)
if(typeof(result) === "Object") {
fnObj = result
}else {
fnObj = obj
}
return fnObj
5. 理解es6 class构造以及继承的底层实现原理
通过ES6创建的类,是不允许你直接调用的。在ES5中,构造函数是可以直接运行的
class extends babel转码
//class part {}
//class a extends part {}
"use strict";
function _typeof(obj) {
if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
_typeof = function _typeof(obj) {
return typeof obj;
};
} else {
_typeof = function _typeof(obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
}
return _typeof(obj);
}
function _possibleConstructorReturn(self, call) {
if (call && (_typeof(call) === "object" || typeof call === "function")) {
return call;
}
return _assertThisInitialized(self);
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return self;
}
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
} //通过Object.create 实现 superclass 作为原型对象
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
writable: true,
configurable: true
}
});
if (superClass) _setPrototypeOf(subClass, superClass);
}
function _setPrototypeOf(o, p) { // 设置 p 作为 o 的原型
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p); }
function _instanceof(left, right) { // 判断 left 是否 是right的子类
if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {
return !!right[Symbol.hasInstance](left);
} else {
return left instanceof right;
}
}
function _classCallCheck(instance, Constructor) {
if (!_instanceof(instance, Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var part = function part() {
_classCallCheck(this, part);
};
var a =
/*#__PURE__*/
function (_part) {// 立即执行函数带入part
_inherits(a, _part); // 将 a 的__proto__ 指向 part实现继承
function a() {
_classCallCheck(this, a);//
return _possibleConstructorReturn(this, _getPrototypeOf(a).apply(this, arguments));
}
return a;
}(part);
Object.create() 用第二个参数来创建非空对象的属性描述符默认是为false的,而构造函数或字面量方法创建的对象属性的描述符默认为true。
三. 作用域和闭包
1. 词法作用域和动态作用域
词法作用域也就是在词法阶段定义的作用域,也就是说词法作用域在代码书写时就已经确定了。
词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。
js中其实只有词法作用域,并没有动态作用域,this的执行机制让作用域表现的像动态作用域,this的绑定是在代码执行的时候确定的。
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar(); //1 
// 函数在哪里调用没有关系,变量的位置在编译的词法分析阶段就确定了。
2. 理解JavaScript的作用域和作用域链
作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。
词法作用域是分层的,内层作用域可以访问外层作用域的变量
全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。
函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。
Es6 新增块级作用域:(解决变量提升)
创建函数执行上下文时会在词法环境中创建会计作用域的变量
3. 理解JavaScript的执行上下文栈,可以应用堆栈信息快速定位问题
执行上下文栈是内容的一块地址,一个js执行,会创建全局执行上下文推入栈中,函数执行时也会创建对应的上下文,一个函数作用域 包含 变量环境,(包含clourse闭包,函数声明,变量, )、词法环境, this, outer等
4. this的原理以及几种不同使用场景的取值
javascript 标准 定义了 [[thisMode]]私有属性
- lexical : 表示从上下文中找this, 这对应了箭头函数
- Global: 表示当this为undefined时,取全局对象,对应了普通函数。
- Strict: 当严格模式时使用,this严格按照调用时传入的值,可能为null或者undefined。
函数创建新的执行上下文中的词法环境记录时,会根据 [[thisMode]] 来标记新纪录 [[ThisBindingStatus]] 私有属性。
代码执行遇到 this 时,会逐层检查当前词法环境记录中的 [[ThisBindingStatus]],当找到有 this 的环境记录时获取 this 的值。
这样的规则的实际效果是,嵌套的箭头函数中的代码都指向外层 this。
5. 闭包的实现原理和作用,可以列举几个开发中闭包的实际应用
在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。
6. 理解堆栈溢出和内存泄漏的原理,如何防止
堆栈溢出(溢出指不够): 调用即进栈操作过多,返回即出栈不够,这时候就会导致栈满了,再进栈的就会溢出来
递归、无限循环
内存泄漏(泄漏指内存不能被使用和回收)
如:全局变量、定时器未销毁、DOM以外的节点引用、闭包
7. 如何处理循环的异步操作
使用promise 、aysnc await、generator (协程)
8. 理解模块化解决的实际问题,可列举几个模块化方案并理解其中原理
模块化主要体现的是一种分而治之的思想
模块化的工程意义首先在于分治的思想,对功能进行分治,有利于我们的维护;其次是复用,有利于我们的开发。
模块化方案
-
命名空间
var namespace = { methodOne: function () { /* ...*/ }, methodTwo: function () { /* ...*/ } } namespace.methodOne() // 本质上是对象 不安全 -
Module 模式
var module = (function() { var _private = 'something', var getData = function () { console.log(_private) } return { getData: getData } })() // 通过传参注入全局对象 // 难以维护,依赖模糊,请求过多。 // YUI -
commonJs
运行时加载 同步模块化的应用场景:对于服务器而言,所有的模块都是存在本地硬盘中的,读取速度快,所以可以采用同步的方式读取模块。
通过require,module.exports,exports来进行导入和导出
Nodejs
-
AMD
异步模块化的产生主要是因为同步加载方式无法应用到浏览器等受网速等限制加载速度较慢且不稳定的场景,所以通过异步加载的方式防止代码执行受阻,页面停止渲染等问题。
采用异步方式加载模块,通过define来定义一个模块,通过require来引入模块,模块的加载不影响后面语句的执行,所有依赖于这些模块的语句都写在一个回调函数中,加载完毕后,这个回调函数才运行。如:
// 定义一个模块,name为定义的模块名称,foo为该模块依赖的其他模块 define( 'name', [ 'foo' ], function(foo) { function outPutFoo () { console.log(foo.data) } return { outPutFoo: outPutFoo } }) // 导入模块 require(['name'], function (name) { name.outPutFoo(); })核心实现
function require (url, callback) { // url可以换成List,然后遍历; var $script = document.createElement('script'); $script.src = url; // 利用onload回调,实现依赖加载 $script.onload = function (e) { // 省略callback 检测 callback(); } document.body.appendChild($script); } -
CMD
CMD规范是国内SeaJS的推广过程中产生的,CMD规范中一个模块是一个文件。
CMD提倡就近依赖(按需加载),在用到某个模块的时候再去require进来。需要使用把模块变为字符串解析一遍才知道依赖了那些模块
// 定义一个模块,可通过return, exports, mudule.exports决定要导出的内容 define(function (require, exports, module) { var one = require('./one') one.do() // 就近依赖,按需加载 var two = require('./two') two.do() }) // 核心实现 // 实现 function require(url) { Ajax(url, function(res) { // 此时 res 的对应JS的 String形式 // 解析 省略 // 执行 eval(res); }); } -
UMD
兼容AMD,CommonJS 模块化语法。
(function (root, factory) { // 判断是否支持AMD(define是否存在) if (typeof define === 'function' && define.amd) { define(['b'], factory); // 判断是否支持NodeJS模块格式(exports是否存在) } else if (typeof module === 'object' && module.exports) { module.exports = factory(require('b')); // 前两个都不存在,则将模块公开到全局(window或global) } else { root.returnExports = factory(root.b); } } (this, function (b) { // ... })); -
ES6 Module
加载引用
- 编译时加载(静态执行)。
- 加载的是引用
- 不能使用表达式和变量 等运行时加载的语法
- 不能处于代码块中
-
webpack
import会被编译成 require/exports (CommonJS规范)
webpack是一个现代JavaScript应用程序的静态模块打包器。它递归的构建一个依赖关系图,其中包含应用程序的每个模块,然后将这些模块打包成一个或多个bundle.js。 webpack 支持 CommonJS,AMD,ES6等规范,所以我们在代码中可以使用多种模块加载规范,而且通过loader,它不仅可以处理JavaScript,还可以处理像css,图片等等的静态资源。
-
import xxx.js或者import xxx.css会像添加和标签一样注入到全局中去 -
webpack会将
require('abc.js')打包进引用它的文件中。以对象的形式获取。 -
webpack(require.ensure) 在commonjs中有一个Modules/Async/A规范,里面定义了
require.ensure语法。webpack实现了它,作用是可以在打包的时候进行代码分片,并异步加载分片后的代码。
-
执行机制
1. 为何try里面放return,finally还会执行,理解其内部机制
语句 Completion Record (语句执行完成状态)
Completion Record 表示一个语句执行完之后的结果,它有三个字段:
- [[type]] 表示完成的类型,有 break continue return throw 和 normal 几种类型;
- [[value]] 表示语句的返回值,如果语句没有,则是 empty;
- [[target]] 表示语句的目标,通常是一个 JavaScript 标签
finally 中的内容必须保证执行,所以 try/catch 执行完毕,即使得到的结果是非 normal 型的完成记录,也必须要执行 finally。
2. JavaScript如何实现异步编程,可以详细描述EventLoop机制
Promise
EventLoop: 事件循环
可以理解为一个for 循环 不断的冲消息队列中取出任务执行,
当执行一个消息队列中的任务中的宏任务后他会检测当前任务中的所有微任务去执行,
当遇到async 函数时,则会生产协程,
3. 宏任务和微任务分别有哪些
宏任务指消息队列中的任务 有延迟队列中的setTimeout setinterval,raf
微任务:需要异步执行的函数
promise: 当调用 Promise.resolve() 或者 Promise.reject() 的时候,也会产生微任务。
mutionObserver :监控某个 DOM 节点,然后再通过 JavaScript 来修改这个节点,或者为这个节点添加、删除部分子节点,当 DOM 节点发生变化时,就会产生 DOM 变化记录的微任务。
4. 可以快速分析一个复杂的异步嵌套逻辑,并掌握分析方法
注意async函数产生协程的时候的处理
5. 使用Promise实现串行
一: 在.then中调用其他promise
二: 使用async 函数体
三: 使用reduce
四: function *()
6. Node与浏览器EventLoop的差异
在node中,事件循环表现出的状态与浏览器中大致相同。不同的是node中有一套自己的模型。node中事件循环的实现是依靠的libuv引擎。我们知道node选择chrome v8引擎作为js解释器,v8引擎将js代码分析后去调用对应的node api,而这些api最后则由libuv引擎驱动,执行对应的任务,并把不同的事件放在不同的队列中等待主线程执行。 因此实际上node中的事件循环存在于libuv引擎中。
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<──connections─── │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
外部输入数据-->轮询阶段(poll)-->检查阶段(check)-->关闭事件回调阶段(close callback)-->定时器检测阶段(timer)-->I/O事件回调阶段(I/O callbacks)-->闲置阶段(idle, prepare)-->轮询阶段...
- timers: 这个阶段执行定时器队列中的回调如 setTimeout() 和 setInterval()。
- I/O callbacks: 这个阶段执行几乎所有的回调。但是不包括close事件,定时器和setImmediate()的回调。
- idle, prepare: 这个阶段仅在内部使用,可以不必理会。
- poll: 等待新的I/O事件,node在一些特殊情况下会阻塞在这里。
- check: setImmediate()的回调会在这个阶段执行。
- close callbacks: 例如socket.on('close', ...)这种close事件的回调。
poll 阶段
当个v8引擎将js代码解析后传入libuv引擎后,循环首先进入poll阶段。poll阶段的执行逻辑如下: 先查看poll queue中是否有事件,有任务就按先进先出的顺序依次执行回调。 当queue为空时,会检查是否有setImmediate()的callback,如果有就进入check阶段执行这些callback。但同时也会检查是否有到期的timer,如果有,就把这些到期的timer的callback按照调用顺序放到timer queue中,之后循环会进入timer阶段执行queue中的 callback。 这两者的顺序是不固定的,收到代码运行的环境的影响。如果两者的queue都是空的,那么loop会在poll阶段停留,直到有一个i/o事件返回,循环会进入i/o callback阶段并立即执行这个事件的callback。
值得注意的是,poll阶段在执行poll queue中的回调时实际上不会无限的执行下去。有两种情况poll阶段会终止执行poll queue中的下一个回调:1.所有回调执行完毕。2.执行数超过了node的限制。
process.nextTick()
node中存在着一个特殊的队列,即nextTick queue。这个队列中的回调执行虽然没有被表示为一个阶段,当时这些事件却会在每一个阶段执行完毕准备进入下一个阶段时优先执行。当事件循环准备进入下一个阶段之前,会先检查nextTick queue中是否有任务,如果有,那么会先清空这个队列。与执行poll queue中的任务不同的是,这个操作在队列清空前是不会停止的。这也就意味着,错误的使用process.nextTick()方法会导致node进入一个死循环。。直到内存泄漏。
浏览器和Node 环境下,microtask 任务队列的执行时机不同
- Node端,microtask 在事件循环的各个阶段之间执行
- 浏览器端,microtask 在事件循环的 macrotask 执行完之后执行
7. 如何在保证页面运行流畅的情况下处理海量数据
使用微任务
比如放入 setTimeout 延迟队列中来避免渲染主线程阻塞
1. 手写深拷贝
function deepCopy(ori) {
const type = getType(ori)
let copy
switch(type) {
case 'array':
return copyArray(ori, type, copy)
case 'object':
return copyObject(ori, type, copy)
case 'function':
return copyFunction(ori, type, copy)
default:
return ori
}
}
function copyArray(ori, type, copy=[]) {
for(const [index,value] of ori.entries()) {
copy[index] = deepCopy(value)
}
return copy
}
function copyObject(ori, type, copy = {}) {
for(const [key,value] of Object.entries(ori)) {
copy[key] = deepCopy(value)
}
return copy
}
function copyFunction(ori, type, copy= ()=>{}) {
const fun = eval(ori.toString())
fun.prototype = ori.prototype
return fun
}
function getType(ori) {
let types = Object.prototype.toString.call(ori)
if (types === '[object Array]') return 'array'
if (types === '[object Function]') return 'function'
if (types === '[object Object]') return 'object'
}
2. 手写reduce
Array.prototype.reduce = function reduce(callbackfn) {
const o = this,
len = o.length;
let k = 0,
accumulator = undefined,// 累加器
kPresent = false,// k下标
initialValue = arguments.length > 1? arguments[1]: undefined;// 初始值
if(typeof callbackfn !== 'function') {
throw new TypeError(callbackfn + 'is not a function')
}
if (len === 0 && arguments.length < 2){
throw new TypeError('Reduce of empty array with no initital value')
}
if(arguments.length > 1) {// 如果有初始值
accumulator = initiValue
} else {
accumulator = o[k];
++k;
}
white(k < len) {
kPresent = o.hasOwnProperty(k); // 判断k是否为o的自身属性
if(kPresent) {
const kValue = o[k]
accumulator = callbackfn.apply(undefined,[accumulator, kValue,k , 0])
}
++k
}
return accumulator
}
3. Array.isArray实现原理
Array.isArray = function(o) {
return Object.prototype.toString.call(o) === '[Object Array]';
}
4. debounce函数
function debounce(func, wait) {
let timeout;
return function() {
let context = this;
let args = arguments
if(timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(context, args)
}, wait);
}
}
debounce(fun, 1000)
5. 手写Promise
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
functiom Promise(fn) {
const _this = this;
_this.value = null;
_this.error = null;
_this.status = PENDINGl
_this.onFulfilledCallbacks = [];
_this.onRjectedCallbacks = [];
function resolve(value) {
if(value instanceof Promise) {
return value.then(resolve, reject);
}
if(_this.status === PENDING) {
setTimeout(()=>{
_this.status = FULFILLED
_this.value = value;
_this.onFulfilledCallbacks.forEach((callback) => callback(_this.value))
})
}
}
function reject(error) {
if(_this.status === PENDING) {
setTimeout(function(){
_this.status = REJECTED
_this.error = error
_this.onRejectedCallbacks.forEach((callback) => callback(_this.error))
},0)
}
}
try {
fn(resolve,reject)
}catch(e) {
reject(e)
}
}
function resolvePromise(bridgepromise, x, resolve, reject) {
if (bridgepromise === x) {
return reject(new TypeError('Circular reference'));
}
let called = false;
if (x instanceof Promise) {
if (x.status === PENDING) {
x.then(y => {
resolvePromise(bridgepromise, y, resolve, reject);
}, error => {
reject(error);
});
} else {
x.then(resolve, reject);
}
} else if (x != null && ((typeof x === 'object') || (typeof x === 'function'))) {
try {
let then = x.then;
if (typeof then === 'function') {
then.call(x, y => {
if (called) return;
called = true;
resolvePromise(bridgepromise, y, resolve, reject);
}, error => {
if (called) return;
called = true;
reject(error);
})
} else {
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
} else {
resolve(x);
}
}
Promise.prototype.then = function(onFulfilled, onRejected) {
const self = this;
let bridgePromise;
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value;
onRejected = typeof onRejected === "function" ? onRejected : error => { throw error };
if (self.status === FULFILLED) {
return bridgePromise = new MyPromise((resolve, reject) => {
setTimeout(() => {
try {
let x = onFulfilled(self.value);
resolvePromise(bridgePromise, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
})
}
if (self.status === REJECTED) {
return bridgePromise = new MyPromise((resolve, reject) => {
setTimeout(() => {
try {
let x = onRejected(self.error);
resolvePromise(bridgePromise, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
}
if (self.status === PENDING) {
return bridgePromise = new MyPromise((resolve, reject) => {
self.onFulfilledCallbacks.push((value) => {
try {
let x = onFulfilled(value);
resolvePromise(bridgePromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
self.onRejectedCallbacks.push((error) => {
try {
let x = onRejected(error);
resolvePromise(bridgePromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
});
}
}
MyPromise.prototype.catch = function(onRejected) {
return this.then(null, onRejected);
}
MyPromise.deferred = function() {
let defer = {};
defer.promise = new MyPromise((resolve, reject) => {
defer.resolve = resolve;
defer.reject = reject;
});
return defer;
}
try {
module.exports = MyPromise
} catch (e) {}
6. Promisify原理
promisify = function(fn) {
return function() {
var args = Array.from(arguments);
return new Promise(function(resolve, reject) {
fn.apply(
null,
args.concat(function(err) {
err ? reject(err) : resolve(arguments[1]);
})
);
});
};
};
#####7. 输出顺序
setTimeout(() => {
console.log(1);
});
//----若干代码逻辑
new Promise((resolve, reject) => {
resolve();
}).then(() => {
console.log(2);
});
8. Node.js为什么处理异步IO快?
Node 底层采用线程池的原理管理异步 IO,所以我们通常所的 单线程是指 Node 中 JavaScript 的执行是单线程的,但 Node 本身是多线程的。Node.js 中异步 IO 是通过事件循环的方式实现的,异步 IO 事件主要来源于网络请求和文件 IO。但是正因为如此,Node.js 处理很多计算密集型的任务。
9. Node.js 有 cluster、fork 两种模式多进程,那么这两种情况下,主进程负责 TCP 通信,怎样才可以让子进程共享用户的 Socket 对象?
cluster 模式,多实例、自动共享端口链接、自动实现负载均衡。fork 模式实现的多进程,单实例、多进程,可以通过手动分发 socket 对象给不同子进程进行定制化处理、实现负载均衡
10. Node.js 多进程维护,以及通信方式
原生的 cluster 和 fork 模式都有 API 封装好的进行通信。如果是 execfile 这样形式调起第三方插件形式,想要与第三方插件进行通信,可以自己封装一个类似 promisyfy 形式进行通信,维护这块,子进程可以监听到异常,一旦发现异常,立刻通知主进程,杀死这个异常的子进程,然后重新开启一个子进程
11. KOA 洋葱圈
洋葱圈的实现,有点类似 Promise 中的 then 实现,每次通过 use 方法定义中间件函数时候,就会把这个函数存入一个队列中,全局维护一个 ctx 对象,每次调用 next(),就会调用队列的下一个任务函数
use (fn) {
// this.fn = fn 改成:
this.middlewares.push(fn) // 每次use,把当前回调函数存进数组
}
compose(middlewares, ctx){ // 简化版的compose,接收中间件数组、ctx对象作为参数
function dispatch(index){ // 利用递归函数将各中间件串联起来依次调用
if(index === middlewares.length) return // 最后一次next不能执行,不然会报错
let middleware = middlewares[index] // 取当前应该被调用的函数
middleware(ctx, () => dispatch(index + 1)) // 调用并传入ctx和下一个将被调用的函数,用户next()时执行该函数
}
dispatch(0)
}
12. TCP快速握手
- 客户端发送syn包,头部包含fastopen 选项cookie长度为0
- 服务端根据ip生成cookie ,返回客户端
- 客户端缓存cookie
- 客户端再次访问服务器,携带上次的cookie
- 服务端检验cookie直接返回,不进行三次握手
13. TCP 链接和 UDP 的区别,什么时候选择使用 UDP 链接?
Udp: 发送数据前不需要握手,不分包,不重传,不重复,按序到达
适用视频直播
14. vue-router 有哪几种导航钩子?
- 全局导航钩子
- router.beforeEach(to, from, next),
- router.beforeResolve(to, from, next),
- router.afterEach(to, from ,next)
- 组件内钩子
- beforeRouteEnter,
- beforeRouteUpdate,
- beforeRouteLeave
- 单独路由独享组件
- beforeEnter
15. vue 的双向绑定的原理是什么
vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty()来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
具体步骤:
-
需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter 这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化
-
compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
-
Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情是:
在自身实例化时往属性订阅器(dep)里面添加自己
自身必须有一个 update()方法
待属性变动 dep.notice()通知时,能调用自身的 update() 方法,并触发 Compile 中绑定的回调,则功成身退。
-
MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据 model 变更的双向绑定效果。
16. vuex 的 store 特性是什么
- vuex 就是一个仓库,仓库里放了很多对象。其中 state 就是数据源存放地,对应于一般 vue 对象里面的 data
- state 里面存放的数据是响应式的,vue 组件从 store 读取数据,若是 store 中的数据发生改变,依赖这相数据的组件也会发生更新
- 它通过 mapState 把全局的 state 和 getters 映射到当前组件的 computed 计算属性
17. vuex 的 getter 特性是什么
- getter 可以对 state 进行计算操作,它就是 store 的计算属性
- 虽然在组件内也可以做计算属性,但是 getters 可以在多给件之间复用
- 如果一个状态只在一个组件内使用,是可以不用 getters
18. vuex 的 mutation 特性是什么
- action 类似于 muation, 不同在于:action 提交的是 mutation,而不是直接变更状态
- action 可以包含任意异步操作
19. vuex 原理
vuex 仅仅是作为 vue 的一个插件而存在,不像 Redux,MobX 等库可以应用于所有框架,vuex 只能使用在 vue 上,很大的程度是因为其高度依赖于 vue 的 computed 依赖检测系统以及其插件系统,
vuex 整体思想诞生于 flux,可其的实现方式完完全全的使用了 vue 自身的响应式设计,依赖监听、依赖收集都属于 vue 对对象 Property set get 方法的代理劫持。最后一句话结束 vuex 工作原理,vuex 中的 store 本质就是没有 template 的隐藏着的 vue 组件;
20. state 内部支持模块配置和模块嵌套
在 store 构造方法中有 makeLocalContext 方法,所有 module 都会有一个 local context,根据配置时的 path 进行匹配。所以执行如 dispatch('submitOrder', payload)这类 action 时,默认的拿到都是 module 的 local state,如果要访问最外层或者是其他 module 的 state,只能从 rootState 按照 path 路径逐步进行访问。
21. 在执行 dispatch 触发 action(commit 同理)的时候,只需传入(type, payload),action 执行函数中第一个参数 store 从哪里获取的
store 初始化时,所有配置的 action 和 mutation 以及 getters 均被封装过。在执行如 dispatch('submitOrder', payload)的时候,actions 中 type 为 submitOrder 的所有处理方法都是被封装后的,其第一个参数为当前的 store 对象,所以能够获取到 { dispatch, commit, state, rootState } 等数据。
22. Vuex 如何区分 state 是外部直接修改,还是通过 mutation 方法修改的?
Vuex 中修改 state 的唯一渠道就是执行 commit('xx', payload) 方法,其底层通过执行 this._withCommit(fn) 设置_committing 标志变量为 true,然后才能修改 state,修改完毕还需要还原_committing 变量。外部修改虽然能够直接修改 state,但是并没有修改_committing 标志位,所以只要 watch 一下 state,state change 时判断是否_committing 值为 true,即可判断修改的合法性。
23. 调试时的"时空穿梭"功能是如何实现的
devtoolPlugin 中提供了此功能。因为 dev 模式下所有的 state change 都会被记录下来,'时空穿梭' 功能其实就是将当前的 state 替换为记录中某个时刻的 state 状态,利用 store.replaceState(targetState) 方法将执行 this._vm.state = state 实现。
24. v-show 与 v-if 区别
- v-show是css切换,v-if是完整的销毁和重新创建。
- 使用 频繁切换时用v-show,运行时较少改变时用v-if
- v-if=‘false’ v-if是条件渲染,当false的时候不会渲染
25. 动态绑定class的方法
- 对象方法 v-bind:class="{'orange': isRipe, 'green': isNotRipe}"
- 数组方法 v-bind:class="[class1, class2]"
- 行内 v-bind:style="{color: color, fontSize: fontSize+'px' }"
26. 计算属性和 watch 的区别
计算属性可以缓存
计算属性是自动监听依赖值的变化,从而动态返回内容,监听是一个过程,在监听的值变化时,可以触发一个回调,并做一些事情。 所以区别来源于用法,只是需要动态值,那就用计算属性;需要知道值的改变后执行业务逻辑,才用 watch
27. 怎样理解单向数据流
这个概念出现在组件通信。父组件是通过 prop 把数据传递到子组件的,但是这个 prop 只能由父组件修改,子组件不能修改,否则会报错。子组件想修改时,只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改。 一般来说,对于子组件想要更改父组件状态的场景,可以有两种方案: 在子组件的 data 中拷贝一份 prop,data 是可以修改的,但 prop 不能
28. Watch deep and immediate
watch: {
a: function (val, oldVal) {
console.log('new a: %s, old: %s', val, oldVal)
},
// 方法名
b: 'someMethod',
// 深度 watcher
c: {
handler: function (val, oldVal) {
console.log('new c: %s, old: %s', val, oldVal)
},
deep: true//代表是否深度监听
},
// 该回调将会在侦听开始之后被立即调用
d: {
handler: function (val, oldVal) {
console.log('new d: %s, old: %s', val, oldVal)
},
immediate: true//代表如果在 wacth 里声明了之后,就会立即先去执行里面的handler方法,如果为 false就跟我们以前的效果一样,不会在绑定的时候就执行。
}
29. nextTick()
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后,立即使用这个回调函数,获取更新后的 DOM。
30.Object.entries()
Object.entries()方法返回一个给定对象自身可枚举属性的键值对数组,其排列与使用 for...in 循环遍历该对象时返回的顺序一致(区别在于 for-in 循环还会枚举原型链中的属性)。
可以让对象也可以使用 for of 来遍历
31. Map类型
map是一组键值对
// 1.Map基本使用
let m = new Map();
m.set('c', 'content')
m.get('c')//content
m.size//1
m.has('c') // true
m.delete('c')
m.has('c')
m.clear()
// Map结构和数组结构之间的转换
let map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
[...map.keys()]// [1, 2, 3]
[...map.values()]// ['one', 'two', 'three']
[...map.entries()]// [[1,'one'], [2, 'two'], [3, 'three']]
[...map]// [[1,'one'], [2, 'two'], [3, 'three']]
WeakMap
- Map对象的键可以是任何类型,但WeakMap对象中的键只能是对象引用
- WeakMap不能包含无引用的对象,否则会被自动清除出集合(垃圾回收机制)。
- WeakSet对象是不可枚举的,无法获取大小。
32. Array.from()
Array.from()方法从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。
Array.from()方法就是将一个类数组对象或者可遍历对象转换成一个真正的数组。
let str = 'hello world!';
console.log(Array.from(str)) // ["h", "e", "l", "l", "o", " ", "w", "o", "r", "l", "d", "!"]
// 将一个set对象转为数组,并在原来的基础上乘以2倍
Array.from(new Set([1,2,3,4]), x => x*2) //[2,4,6,8]
用作于 Set结构和map结构
const map = new Map([[1, 2], [2, 4], [4, 8]]);
Array.from(map);
// [[1, 2], [2, 4], [4, 8]]
const mapper = new Map([['1', 'a'], ['2', 'b']]);
Array.from(mapper.values());
// ['a', 'b'];
Array.from(mapper.keys());
// ['1', '2'];
const set = new Set(['foo', 'bar', 'baz', 'foo']);
Array.from(set);
// 数组去重合并
function combine(){
let arr = [].concat.apply([], arguments); //没有去重复的新数组
return Array.from(new Set(arr));
}
var m = [1, 2, 2], n = [2,3,3];
console.log(combine(m,n));
33. Set()
Set对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用。
语法
new Set([iterable]);
iterable
如果传递一个可迭代对象,它的所有元素将不重复地被添加到新的 Set中。如果不指定此参数或其值为null,则新的 Set为空。
使用.add可以添加对象
var a = new Set()
a.add({b:1,b:3,c:4}) // b:3 c:4
34. 组件中 data 为什么是函数 ?
组件内的数据应该互相隔离,如果使用对象引用类型,就可能导致组件的数据被修改,主要为了防止组件内的数据被修改
35. vue中的provide/inject
provider/inject:简单的来说就是在父组件中通过provider来提供变量,然后在子组件中通过inject来注入变量。
需要注意的是这里不论子组件有多深,只要调用了inject那么就可以注入provider中的数据。而不是局限于只能从当前父组件的prop属性来获取数据。
<!--父组件-->
<template>
<div>
<childOne></childOne>
</div>
</template>
<script>
import childOne from '../components/test/ChildOne'
export default {
name: "Parent",
provide {
for: "demo"
},
components:{
childOne
}
}
</script>
<!--子组件-->
<template>
<div>
{{demo}}
<childtwo></childtwo>
</div>
</template>
<script>
import childtwo from './ChildTwo'
export default {
name: "childOne",
inject: ['for'],
data() {
return {
demo: this.for
}
},
components: {
childtwo
}
}
</script>
<!--另一个组件-->
<template>
<div>
{{demo}}
</div>
</template>
<script>
export default {
name: "",
inject: ['for'],
data() {
return {
demo: this.for
}
}
}
</script>
// 在2个子组件中我们使用jnject注入了provide提供的变量for,并将它提供给了data属性。
36. Vue-filters
Vue fileter常见用于文本格式化 v-bind 或者插值
<!-- 在双花括号中 -->
{{ message | capitalize }}
<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>
filters: {
capitalize: function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
}
}
可以再组件内使用也可以定义全局过滤器使用 vue.filters(name', function(value) { callback })
37. Vue minix
混入不推荐再应用代码中使用
// 定义一个混入对象
var myMixin = {
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log('hello from mixin!')
}
}
}
// 定义一个使用混入对象的组件
var Component = Vue.extend({
mixins: [myMixin]
})
var component = new Component() // => "hello from mixin!"
// 当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。
38. Vue directive
自定义指令
注册或获取全局指令。
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus()
}
})
// 组件内指令
directives: {
focus: {
// 指令的定义
inserted: function (el) {
el.focus()
}
}
}
<input v-focus>
39. Vue observable(object)
全局的观察某个值的变化和vuex类似
40. vue响应式原理
vue会遍历data中的属性,使用 Object.defineProperty 来这只这些属性的 getter/setter
Object.defineProperty 是es5中的特性,无法shim,所以vue不支持ie8以及更低版本浏览器
每一个组件都对应一个watcher实例
他会在渲染组件时吧所有的数据属性记录作为依赖
当依赖的setter触发,watcher会接到通知,然后通知关联组件重新渲染
41. 前端,架构的认识
软件架构,是一种为了解决复杂问题的通用模式。软件架构,是关于软件系统的一系列有层次的技术决策的集合。换句话来说,当我们讨论架构的时候,不能只讨论某某架构,而是要包含其实施,以及后期的维护。
前端架构的拆解:四层设计
系统级 -> 前后端分离架构,微服务加购
应用级-> 模式库、组件库、设计系统
模块级-> 组件化、模块化
代码级-> 规范、质量、review
系统架构
首先考虑的是应用在整个系统中的位置
前端端分离、微服务、客户端展现形式、部署架构
应用级架构
指的是单个应用与外部应用的关系
脚手架、模式库、设计系统
模块级架构
便是深入单个应用内部,更细致的设计应用内部的架构。
模块化、组件化
代码级架构
开发流程、代码质量review、规范
42. 单元测试
43. mvvm的理解
Model 代表数据模型
View 代表ui视图
viewModel 负责监听model中的数据,以及控制视图的更新,处理用户的交互
Model 和view 通过 viewmodel 进行联系,model和viewmodel中双向绑定,model中的数据又要去触发view的更新,同事view的更新也会去同步model中的数据
开发者只需要专注维护对数据的操作即可
44. 如何优化SPA应用的首屏加载速度慢的问题?
- 将公用的JS库通过script标签外部引入,减小app.bundel的大小,让浏览器并行下载资源文件,提高下载速度;
- 在配置 路由时,页面和组件使用懒加载的方式引入,进一步缩小 app.bundel 的体积,在调用某个组件时再加载对应的js文件;
- 加一个首屏 loading 图,提升用户体验
45. 前端如何优化网站性能?
减少 DOM 操作
减少重排
利用浏览器缓存
控制资源文件加载优先级
减少 HTTP 请求数量
46. keep-alive原理
其实就是将需要缓存的VNode节点保存在this.cache中/在render时,如果VNode的name符合在缓存条件(可以用include以及exclude控制),则会从this.cache中取出之前缓存的VNode实例进行渲染。
47. redis为什么这么快
Redis 将数据存储在内存中,key-value 形式存储,所以获取也快。支持的 key 格式相对于 memorycache 更多,而且支持 RDB 快照形式、AOF。
RDB 持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是 fork 一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。RDB 是 Redis 默认的持久化方式,会在对应的目录下生产一个 dump.rdb 文件,重启会通过加载 dump.rdb 文件恢复数据。
48. Socket 底层原理
WebSocket是一种双向通信协议。在建立连接后,WebSocket服务器端和客户端都能主动向对方发送或接收数据,就像Socket一样;
WebSocket需要像TCP一样,先建立连接,连接成功后才能相互通信。
不需要客户端和服务端重新发起连接请求。在海量并发及客户端与服务器交互负载流量大的情况下,极大的节省了网络带宽资源的消耗,有明显的性能优势,且客户端发送和接受消息是在同一个持久连接上发起,实时性优势明显。
缺点,兼容性差,容易进入假死状态
vue中怎么实现动画
组件间通信
v-bind
父子组件 props emit
Event bus(事件总线)平级组件传值 发布订阅模式
依赖注入(provide/injuect)
Vuex
有没有看过源码,vue响应式的原理
简单来说就是使用Object.defineProperty 这个API为数据设置get 和 set。 当读取到某个属性时, 触发get 将读取它的组件对应的render watcher 收集起来; 当重置赋值时, 触发set通知组件重新渲染页面。 如果数据的类型是数组的话, 还做了单独的处理, 对可以改变数组自身的方法进行重写, 因为这些方法不是通过重新赋值改变的数组, 不会触发set, 所以要单独处理。 响应式系统也有自身的不足, 所以官方给出了 delete 来弥补。
数组响应式的原理
数组响应式总结: 数组的依赖收集还是在get 方法里, 不过依赖的存放位置会有不同, 不是在defineReactive 方法的dep, 而是在Observer 类中的dep里, 依赖的更新是在拦截器里的数组异变方法最后手动更新。
Proxy
webpack配置了解多少
使用DllPlugin优化
- 接入需要完成的事:
- 将依赖的第三方模块抽离,打包到一个个单独的动态链接库中
- 当需要导入的模块存在动态链接库中时,让其直接从链接库中获取
- 项目依赖的所有动态链接库都需要被加载
- 接入工具(webpack已内置)
- **DllPlugin插件:**用于打包出一个个单独的动态链接库文件;
- **DllReferencePlugin:**用于在主要的配置文件中引入
DllPlugin插件打包好的动态链接库文件
- 配置webpack_dll.config.js构建动态链接库
vue大量数据渲染缺陷如何解决
可以通过 循环子组件
立即执行函数有哪些
使用()会被浏览器理解为表达式而不是函数声明
立即执行函数会变成函数表达式-》加上()可直接被调用
(function a() {})()
(function a() {}())
~function() {}()
definedproperty属性有哪些,给了一个例子让判断结果
Object.getOwnPropertyDescriptors(a)
Object.getOwnPropertyDescriptor(a,'b')
import require区别
Import 是es6 规范 指在编译时加载 webpack会将import转化为require
require是CommonJS规范 在运行时加载