前端技术百问

48,661 阅读46分钟

问:如何理解html标签语义化?

  • html5新出的标签,每个标签都有自己语义,什么标签做什么事。让人看的懂,也让机器可以看的懂,利于SEO

问:css权重是什么?

  • 设置节点样式的方式有很多种,不同的方式它们的权重并不相同,当它们给一个节点设置同一个样式时,谁的权重高谁就生效。
  • important:无限高
  • 行内样式:权重值为1000
  • id选择器:权重值为100
  • 类、伪类、属性选择器:权重值为10
  • 元素选择器:权重值为1

问:盒模型有几种,它们区别是什么?

  • 标准盒模型:设置的宽高只是包括内容区,内边距和边框另算。
  • 怪异盒模型:设置的宽高包含了内边距和边框。 使用box-sizing属性设置:border-box:怪异盒模型、content-box:标准盒模型。

问:什么是BFC

  • 块级格式上下文,一句话来说就是让块级元素有块级元素该有的样子,触发BFC可以清除浮动、让margin不重叠。

问:如何触发BFC

  • float的值不为none
  • overflow的值不为visible
  • display的值为table-celltable-captioninline-block之一。
  • position的值不为staticreleative中的任何一个。

问:你常用的清除浮动方式是什么?

.clear:after, .clear:before {
  content: ' ';
  display: table;
}
.clear:after {
  clear: both;
}

问:em、rem的区别?

  • em:如果父级有设置字体大小,1em就是父级的大小,没有1em等于自身默认的字体大小。
  • rem:相对于html标签的字体大小。

问:不使用border属性画一条1px的线?

<div style='height: 1px; background: #666; overflow: hidden;'></div>

<hr size='1'></hr>

问:移动端1px问题?

box-shadow: 
  0  -1px 1px -1px #e5e5e5,   //上边线
  1px  0  1px -1px #e5e5e5,   //右边线
  0  1px  1px -1px #e5e5e5,   //下边线
  -1px 0  1px -1px #e5e5e5;   //左边线
  0 0 0 1px #e5e5e5;   //四条线

问:定位的方式有哪几种,它们的区别是什么?

  • relative:相较于自身定位,设置的位置相对于自己进行位移。不脱离文档流。
  • absolute:相较于最近有定位的父节点定位,设置的位置相较于父节点。会脱离文档流,导致父节点高度塌陷。
  • fixed:相较于当前窗口进行定位,设置的位置相较于窗口。脱离文档流。

问:垂直水平居中的实现方式有哪些?

  • 父级设置text-align: centerline-height等同高度。
  • 子节点绝对定位,设置position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);
  • 子节点绝对定位,需要设置宽度和高度。设置position: absolute;top:0;left:0;right:0;bottom:0;margin:auto;
  • 父级设置display: table,子节点设置display:table-cell;text-align:center;vertical-align:middle;
  • 父级设置display: flex;justify-content:center;align-items:center;
  • 父节点设置display: grid;,子节点设置:align-self:center;justify-self: center;

问:你知道的左右宽度固定,中间自适应的三栏布局方案有哪些?

浮动:
.parent {overflow: hidden;}
.left {float: left; width: 100px;}
.right: {float: right; width: 100px;}
<div class='parent'>
  <div class='left'></div>
  <div class='right'></div>
  <div class='center'></div>
</div>

定位1.parent {postion: relative};
.left {position: absolute; left: 0; width: 100px};
.right {position: absolute; right: 0; width: 100px};
.center {postion: absolute; left: 100px; right: 100px};

定位2.parent {postion: relative};
.left {position: absolute; left: 0; width: 100px};
.right {position: absolute; right: 0; top: 0; width: 100px};
.center {margin: 0 100px 0 100px};

表格:
.parent {dispaly: table; width: 100%;}
.left {display: table-cell; width: 100px;}
.center {display: table-cell;}
.right {display: table-cell; width: 100px;}

弹性:
.parent {display: flex;}
.left {width: 100px;}
.center {flex: 1;}
.right {width: 100px;}

网格:
.parent {
  display: grid; 
  width: 100%; 
  grid-template-rows: 100px; 
  grid-template-columns: 100px auto 100px;
}

问:实现三个圆形的水平自适应布局?

难点在于高度的自适应
.parent {
  display: table;
  width: 100%;
}
.child {
  display: table-cell;
  padding-top: 33.33%;
  background: red;
  border-radius: 50%;
}

.parent {
  overflow: hidden;
}
.child {
  float: left;
  width: 33.33%;
  padding-top: 33.33%;
  border-radius: 50%;
  background: red;
}

.parent {
  display: flex;
}
.child {
  flex: 1;
  padding-top: 33.33%;
  border-radius: 50%;
  background: red;
}

问:介绍下flex布局?

主轴方向:水平排列(默认) | 水平反向排列 | 垂直排列 | 垂直反向排列
flex-direction: row | row-reverse | column | column-reverse;

换行:不换行(默认) | 换行 | 反向换行(第一行在最后面)
flex-wrap: nowrap | wrap | wrap-reverse;

flex-direction属性和flex-wrap属性的简写形式,默认值为row nowrap
flex-flow: <flex-direction> || <flex-wrap>;

主轴对齐方式:起点对齐(默认) | 终点对齐 | 居中对齐 | 两端对齐 | 分散对齐
justify-content: flex-start | flex-end | center | space-between | space-around;

交叉轴对齐方式:拉伸对齐(默认) | 起点对齐 | 终点对齐 | 居中对齐 | 第一行文字的基线对齐
align-items: stretch | flex-start | flex-end | center | baseline;

多根轴线对齐方式:拉伸对齐(默认) | 起点对齐 | 终点对齐 | 居中对齐 | 两端对齐 | 分散对齐
align-content: stretch | flex-start | flex-end | center | space-between | space-around;

问:JavaScript的变量有哪些类型?

  • 分为两种:基础类型和引用类型。基础类型目前有六种,分别是booleannullundefinednumberstringsymbol
  • 除了以上的基础类型之外,其他就是引用类型了,有ArrayObjectFunction

问:基础类型和引用的区别?

  • 它们在内存中存储的方式不同。基础类型存储的是值,而引用类型存储的是指向内存中某个空间的指针;
  • 基础类型赋值就是把值赋给另外一个变量,而引用类型的赋值是赋值的原来变量的指针,所以当引用类型发生改变时,只要是指向同一个指针的变量的都会发生改变。

问:函数参数是对象时会发生什么问题?

  • 函数参数是对象时,相当于是将对象的指针传递给了函数,如果在函数的内部改变了对象的值,外面对象的值也会发生改变,数组也是如此。

问:typeofinstanceof判断变量类型的区别?

  • typeof对于基础类型除了null以外都可以显示正确的类型,对于数组和对象都会显示object,对于函数会显示function
  • instanceof主要是用来判断引用类型,它的原理是根据原型链来查找。

问:有没有更好的判断变量类型的方法?

  • 可以使用Object.prototype.toString.call(var),可以更加准确的判断某个变量的类型。

问:类数组转为数组的方式有哪些?

[].slice.call(arguments)
Array.from(arguments)
[...arguments]

问:如何判断一个变量是否是数组?

arr instanceof Array
Array.prototype.isPrototypeOf(arr)
Array.isArray(arr)
Object.prototype.toString.call(arr) === '[object Array]'
arr.constructor === Array

问:将多维数组扁平化?

function flatten(arr) {
  return [].concat(...arr.map(v => {
    return Array.isArray(v) ? flatten(v) : v;
  }))
}

function flatten(arr) {
  return arr.reduce((pre, cur) => {
    return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
  }, [])
}

function flatten(arr) {
  return arr.flat(Infinity);
}

function flatten(arr) {  // 纯数字
  return arr.toString().split(',').map(Number);
}

function flatten(arr) {
  const ret = [];
  while (arr.length) {
    const item = arr.shift();
    if (Array.isArray(item)) {
      arr.unshift(...item);
    } else {
      ret.push(item);
    }
  }
  return ret;
}

问:数组去重?

function unique(arr) {
  return [...new Set(arr)];
}

function unique(arr) {
  return arr.filter((v, i, a) => {
    return a.indexOf(v) === i;
  })
}

function unique(arr) {
  const tmp = new Map();
  return arr.filter(v => {
    return !tmp.has(v) && tmp.set(v);
  })
}

问:字符串的testmatchsearch它们之间的区别?

`test`是检测字符串是否匹配某个正则,返回布尔值;
/[a-z]/.test(1);  // false

`match`是返回检测字符匹配正则的数组结果集合,没有返回`null`'1AbC2d'.match(/[a-z]/ig);  // ['A', 'b', 'C', 'd']

`search`是返回正则匹配到的下标,没有返回`-1`'1AbC2d'.search(/[a-z]/);  // 2

问:字符串的slicesubstringsubstr它们之间的区别?

`slice`是返回字符串开始至结束下标减去开始下标个数的新字符串,下标是负数为倒数;
'abcdefg'.slice(2,3);  // c  // 3 - 2
'abcdefg'.slice(3,2);  // ''  // 2 - 3
'abcdefg'.slice(-2,-1);  // f  // -1 - -2

`substring``slice`正常截取字符串时相同,负数为0,且下标值小的为开始下标;
'abcdefg'.substring(2,3);  //c  // 3 - 2
'abcdefg'.substring(3,2);  // c  // 3 - 2 
'abcdefg'.substring(3,-3);  // abc  // 3 - 0

`substr`返回开始下标开始加第二个参数(不能为负数)个数的新字符串。
'abcdefg'.substr(2, 3);  // cde
'abcdefg'.substr(3, 2);  // de
'abcdefg'.substr(-3, 2); // ef

问:Number('123')new Number('123')有什么区别?

  • Number('123')是一个转换函数,会尝试把参数转为整数类型;而new Number('123')则不同,这是一个构造函数,它的结果是实例化出来一个对象。
  • 同样的情况也适用用Stringnew StringBooleannew Boolean的情况。
typeof Number('123') // number
typeof new Number('123') // object

问:=====的区别?

  • ===会判断两边变量的类型和值是否全部相等,==会存在变量类型转换的问题,所以并不推荐使用,只用一种情况会被使用,var == nullvar === undefined || var === null的简写,其余情况一律使用===

问:是否===就完全靠谱?

  • 也是不一定的,例如0 === -0就为trueNaN === NaNfalse,判断两个变量是否完全相等可以使用ES6新增的APIObject.is(0, -0)Object.is(NaN, NaN)就可以准确区分。

问:在类型转换中哪些值会被转为true

  • 除了undefinednullfalseNaN''0-0以外的值都会被转为true,包括所有引用类型,即使是空的。

问:什么是基本包装类型?

  • 基本类型并不是对象,是不应该有各自方法的,为什么能调用各自的那些方法,是因为在后台对基本类型进行了包装。例如字符串、整数、布尔值,首先会使用各自的构造函数创建对应的实例,这样调用这些方法时就可以正常使用,不过再方法调用结束后,就会将实例给销毁掉,从而又是基本类型。
let s1 = 'hello'
let s2 = s1.substring(2)
↓ 后台包装
let s1 = new String('hello') // 包装
let s2 = s1.substring(2) // 可以调用方法
s1 = null // 销毁

问:toString()valueOf的区别?

  • nullundefined没有以上两个方法。
  • toString:值类型时返回自身的字符串形式;当是引用类型时,无论是一维或多维数组,将他们拍平成一个字符串,里面的nullundefined转为空字符串'',对象转为[object Object],函数的原样返回字符串形式。
  • valueOf无论是值类型还是引用类型,大部分情况下都是原样返回,当是Date类型时,返回时间戳。
  • 在进行字符串强转的时候,toString会优先于valueOf;在进行数值运算时,valueOf会优先于toString
  • 当执行toString的变量是一个整数类型时,支持传参,表示需要转为多少进制的字符串。

问:谈谈对this的理解?

  • this表示为当前的函数调用方,在运行时才能决定。如谁调用了某个方法,谁就是这个方法执行时的this

问:改变当前调用this的方式?

  • call:会立即执行调用call方法的函数,不过是以第一个参数为this的情况下调用,方法内可以传递不等的参数,作为调用call方法的参数。
  • apply:运行方式和call是一致的,只是接受的参数不同,不能是不定参数,得是一个数组。
  • bind:会改变当前的this,接受不定参数,不过不会马上执行调用bind方法的函数,而是返回一个函数作为结果,执行后才是调用函数的结果。

问:谈谈对闭包的理解?

  • 在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。

问:谈谈对原型以及原型链的理解?

  • 每一个JavaScript引用类型(数组/对象/函数)都有一个__proto__属性,这个属性是一个对象格式,也就是原型属性。在原型属性里面有一个constructor属性,这个属性是这个引用类型的构造函数,在constructor里面又有一个prototype的属性,这个属性又指回了引用类型的原型属性。
  • 原型链就是通过对象的__proto__属性层层连接起来形成的,而构造函数的prototype是一个对象属性,再构造函数实例化时就会将这个属性赋值给实例化后对象的__proto__属性,所以函数的继承也会相应的构造出对象的原型链。

问:原型继承的方式有哪些?

  • 原型链继承、借用构造函数继承、组合继承、原型式继承、寄生组合继承等等。最优化的继承方式是寄生组合继承:
function Parent(name) {
  this.name = name;
}
function Child(name, age) {
  Parent.call(this, name);
  this.age = age;
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child

问:什么是垃圾回收机制?

  • 在程序执行的过程中,解释器会为创建出来的变量分配内存来存储这些变量的实体,执行环境会负责管理代码执行过程中使用到的内存,而何时划出新的内存以及何时把占用的内存释放出来的这样一套内存自动管理机制就是垃圾回收机制。这种周期性的回收策略主要有两种。
  • 标记清除:当变量进入环境时,就将这个变量标记为'进入环境',而当这个变量离开环境时,则将其标记为'离开环境'。垃圾收集器会给内存中的每个变量都做上标记,然后它会去掉环境中的变量以及被环境中变量引用的变量的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,最后垃圾收集器完成内存清除工作。
  • 引用计数:追踪记录每个值被引用的次数,当声明了一个变量并将一个引用类型赋给该变量时,这个变量的引用次数就是1。相反如果包含这个值引用的变量又取得了另外一个值,则这个值的引用次数减1。当为0时,这说明没有办法再访问这个值了,因此垃圾收集器下次运行时,就会释放该值占用的内存。

问:如何解决引用类型变量共享的问题?

  • 可以对引用类型进行深拷贝解决,最简单暴力的深拷贝是JSON.parse(JSON.stringify(obj)),不过也会存在诸多问题,更加完善的深拷贝需要手写递归方法对不同参数分别处理,参考深拷贝的终极探索(90%的人都不知道)

问:函数防抖和节流的区别?

  • 函数防抖指一定时间内没有再次触发函数,就执行该函数,否则重新计时;节流是规定某个时间内只能执行一次函数。以wow为例:

  • debounce 函数防抖:2.5s施法的寒冰箭,再读条的过程中,你身子抖动打断了施法,再次触发技能时麻烦您重新读条。

  • throttle 函数节流:火冲为瞬发技能,不过你规定cd8s,所以即使8s内按了10次,也只能来1发,节省点体力吧。

问:请手写一下map、call、new、instanceof、Events、深拷贝、节流、Promise等?

问:varletconst的区别 ?

  • var类型会有变量提升的情况,也就是说声明会首先提升到当前作用域的顶端,在使用到时再读取定义的值。
  • 在全局作用域下定义的var变量会挂载到作用域链的顶端window下,而letconst全局定义时在作用域链中要低一级。
  • letconst没有变量提升的情况,必须要先声明再使用,否则就会出现暂时性死区的情况。
  • 而且它们的作用域存在最近的大括号之内,也就是块级作用域,并且一经定义后,同一个作用域内不能再次定义。
  • constlet的区别在于一经定义后不得再次改变const定义的值,如果是引用类型只要不改变指针,改变里面的值是没问题的。
  • const定义时必须赋值,let不必。

问:SetWeakSet的区别?

  • Set类型内存储的是不会重复的值,建议存储基础类型的值,因为引用类型的指针都不同。
  • WeakSet只能存储对象参数,否则会报错,而且是存储的引用类型的弱引用。
  • WeakSet不可被迭代,不支持forEachfor-ofkeysvalues方法,没有size属性。
const set = new Set();
const obj = {name: 'cc'};
set.add(obj);
obj = null;
[...set][0]; // {name: 'cc'} 转数组后依然可以访问到

const weakSet = new WeakSet();
const obj = {};
weakSet.add(obj);
obj = null;  // 会移除引用
weakSet.has(obj); // false

问:MapWeakMap的区别?

  • Map是解决了对象key会被自动转为字符串的一种增强key/value集合。
  • WeakMap是弱引用的Map集合,key必须是非null的对象格式,同样不可以被迭代。
const obj = Object.create(null);
obj[1] = 'cc';
obj['1']; // cc

const map = new Map();
map.set(1, 'cc');
map.has('1');  // false   1 和 '1'不会被转换

问:箭头函数和普通函数的区别?

  • 箭头函数的this是由包裹它的普通函数的this来决定;
  • 不能作为构造函数, Generator函数;
  • 参数不能使用arguments访问,需要使用Es6的不定参数访问;
  • 使用bind方法无效。

问:请实现plus(1)(2)(3)(4)等于10?

方法1function plus(n) {
  let sum = n;
  const _plus = function (n) {
    sum += n;
    return _plus;
  };
  _plus.toString = function () {
    return sum;
  };
  return _plus;
}

方法2function multi() {
  const args = [].slice.call(arguments);
  const fn = function () {
    const newArgs = args.concat([].slice.call(arguments));
    return multi.apply(this, newArgs);
  }
  fn.toString = function () {
    return args.reduce(function (a, b) {
      return a + b;
    })
  }
  return fn;
}

问:谈谈对class的理解 ?

  • JavaScript没有真正的类,一直也是通过函数加原型的形式来模拟,class也不例外,只是语法糖,本质还是函数。需要先声明再使用,内部的方法不会被遍历,且没有函数的prototype属性。不过相较ES6之前无论是定义还是继承都好理解了很多。继承主要是使用extendssuper关键字,本质类似于ES5的寄生组合继承:
class Parent {
  constructor(name) {
    this.name = name;
  }
}
class Child extends Parent {
  constructor(name, age) {
    super(name);  // 相当于Parent.call(this, name)
    this.age = age;
  }
}

问:谈谈对Promise的理解 ?

  • Promise主要解决的问题就是异步回调嵌套过深造成代码难以维护和理解。
  • Promise构造函数内的代码是同步执行的,而之后thencatch方法是异步执行的,构造函数接受两个函数参数resolvereject,它们执行时接受的参数分别会传递给thencatch表示成功的回调以及失败回调接受到的值。
  • Promise一共有三种状态pending等待状态、resolved已完成状态、rejected已拒绝状态,状态的改变只能由等待转为已完成或等待转为已拒绝状态,而且状态的改变只会发生一次。
  • 必须要实现then方法且方法里必须要返回一个Promise对象,如果是返回其他的类型会尝试包装成Promise对象;
  • then可以被链式的调用。
  • 缺点是Promise链中途无法取消;错误需要通过回调函数捕获。

问:谈谈对ES-Module的理解 ?

  • ES-ModuleES6原生支持模块化方案,通过import来引入模块,通过export defaultexport来导出模块。

问:谈谈对Proxy的理解 ?

  • Object.defineProperty有些类似,它的作用是用来自定义对象中操作。Proxy的构造函数接受两个参数,第一个参数是需要代理的对象,第二个参数是一个对象,里面会定义getset方法,当代理对象中的某个值被访问或重新赋值就会触发相应的getset方法。vue3.0就抛弃了Object.defineProperty而拥抱了Proxy,它的优点是只需要代理一次,对象内的值发生了改变就会被感知到,不再需要像以前为对象的每个值进行数据劫持;而且以前对象的新增,数组的下标设置0清空等情况都可以被感知到,在响应式里也不在需要为数组和对象收集两次依赖,相信会大大提升性能。

问:谈谈对Generator的理解?

  • JavaScript方便创建迭代器的新语法,在方法名前面添加*号,表示这个方法是一个生成器函数,在函数内部配合yield关键字指定next()方法返回值及顺序。
  • yield类似与在函数内部打上了断点,yield就是每一处的debugger,执行next()方法后进入下一个断点。
  • 不能使用箭头函数来创建生成器。

问:谈谈对asyncawait的理解 ?

  • Genneator的语法糖形式,解决的问题是以同步的形式写异步代码,让代码流程能很好的表示执行流程。在函数的前面加上async表明是一个异步函数,函数的内部需要配合await关键字使用,每一个await关键字相当于是yield,会暂停函数的执行,直到异步函数执行完毕后内部会自动执行next()方法,执行之后的代码,函数的返回结果是一个Promise对象。因为是以同步的形式书写异步代码,所以错误捕获是使用try/catch的形式。

问:谈谈对Event-Loop的理解 ?

  • JavaScript的执行机制简单来说就先执行同步代码,然后执行异步代码,而异步的代码里面又分为宏任务代码和微任务代码,先执行微任务,然后执行宏任务。首先会将所有JavaScript作为一个宏任务执行,遇到同步的代码就执行,然后开始分配任务,遇到宏任务就将它的回调分配到宏任务的队列里,遇到微任务的回调就分配到微任务的队列里,然后开始执行所有的微任务。执行微任务的过程还是遵循先同步然后分配异步任务的顺序,微任务执行完毕之后,一次Event-LoopTick就算完成了。接着挨个去执行分配好的宏任务,在每个宏任务里又先同步后分配异步任务,完成下一次Tick,循环往复直到所有的任务全部执行完成。

  • 微任务包括:process.nextTickpromiseMutationObserver

  • 宏任务包括:scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering

问:对浏览器或元素的各种距离参数你知道哪些?

  • document.documentElement.clientHeight:当前窗口内容区 + 内边距的高度
  • window.innerHeight: 当前窗口内容区 + 内边距 + 边框 + 滚动条高度
  • window.outerHeight:整个浏览器的高度(包括工具栏)
  • clientHeight: 当前元素内容区 + 内边距的高度
  • clientTop: 当前元素上边框的宽度
  • offsetHeight: 当前元素内容区 + 内边距 + 边框 + 滚动条的高度
  • offsetTop: 当前元素的边框距离父元素上外边距的距离
  • scrollHeight: 当前内部可以滚动区域的高度,如果不能滚动则为自己内容区 + 内边距的高度
  • scrollTop: 当前元素滚动离顶部的距离

问:怎么确定当前浏览器的类型?

  • 通过navigator.userAgent获取浏览器信息,根据里面的关键字来确定。

问:什么是简单请求和复杂请求?

  • 简单请求:
  1. 请求方法仅限getheadpost
  2. Content-type仅限text/plainmultipart/form-dataapplication/x-www-form-urlencoded
  • 复杂请求: 不符合以上条件者就为复杂请求,首先会发起一个option方法的预检请求,来知道服务端是否允许跨域请求。 有一个坑就是服务端设置了CORS,但当客户端发其复杂请求时会验证Authorization字段,但是客户端并没有,所以需要将option方法过滤掉。

问:从输入域名到页面显示都经历了什么?

  • 首先将域名DNS解析为对应的IP地址,然后通过Socket发送数据,经过tcp协议的三次握手,向该地址发起HTTP请求,服务器处理,浏览器收到HTTP响应的数据,关闭tcp连接,开始渲染。

问:谈谈浏览器的渲染机制?

  • 书写的JavaScriptCssHtml在网络传输中都是01的字节流,所以浏览器首先会把接受到的这些字节流转为字符串。然后首先将html字节流解析为字符串,对字符串进行标记化,确定标签名以及里面的内容,然后生成对应的node节点,根据节点的结构关系生成DOM树。然后开始解析css,和解析html类似,css一般有嵌套或继承的情况,浏览器会从里到外的递归来确定每个节点的样式是什么,从而生成一颗CSSOM树。最后是将这两颗树结合起来生成一颗渲染树,浏览器根据渲染树进行布局,调用GPU绘制生成页面显示在屏幕上。

问:什么是重绘和回流?

  • 重绘是节点的外观发生改变而不改变布局时,如改变了color这个行为;回流是指改变布局或几何属性发生改变时引起的行为,如添加移除Dom,改变尺寸。它们频繁的触发会影响页面性能。
  • 回流一定触发重绘,而重绘不一定引起回流。回流的成本比重绘高很多,而且子节点的回流,可能引起父节点一系列的回流。

问:如何减少重绘和回流?

  • 使用transform替代位移,使用translate3d开启GPU加速
  • 尽量使用visibility替代display:none
  • 不要使用tanle布局
  • 不要在循环里读取节点的属性值
  • 动画速度越快,回流次数越少

问:什么是事件流/模型?

  • 当某一个事件被触发时,分为三个阶段:
  • 1.事件通过捕获从window => document => body => 目标元素
  • 2.事件到达注册的目标上
  • 3.目标元素通过冒泡返回到window,沿途触发相同类型的事件

问:什么是事件代理?

  • 利用事件流的冒泡特性,将子节点的事件绑定在父节点上,然后在回调里面使用事件对象进行区分,优点是节省内存且不需要给子节点销毁事件。

问:什么是事件对象?

  • 这个对象里面存放着触发事件的状态,如触发事件的当前元素,键盘事件是哪个按键触发的,滚动事件的位置等等。

问:什么是跨域?

  • 也就浏览器的同源策略,出于安全的考虑,只要是协议、域名、端口有一个不同就算是跨域,ajax请求就会失败。浏览器有同源策略主要是为了防止CSRF攻击,防止利用户的登录状态发起恶意请求。

问:你知道的解决跨域的方式有几种?

  • JSONP: 利用script标签不受同源策略限制,具体可以参考40行封装一个jsonp包
  • CORS:使用自定义的HTTP头部让浏览器和服务器进行沟通,实现CORS的关键是后端,服务端设置Access-Control-Allow-Origin就可以开启,表示哪些域名可以访问资源。
  • document.domain:当二级域名相同时,例如a.test.htmlb.test.html,只需要给两个页面都设置document.domain = 'test.html',就可以实现跨域。
  • postMessage:a.html页面通过iframe嵌入了b.html页面,其中一个可以通过postMessage方法发送信息,另一页面通过监听message事件判断来源并接受消息。

问:cookiesession分别是什么?

  • cookie是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器发起请求时被携带并发送到服务器,它通常是告知服务端两个请求是否来自同一浏览器,保持用户的登录状态。
  • session代表着服务器在和客户端一次会话的过程,存储着用户会话所需的属性及配置信息,当用户在不同页面之间跳转时,使整个用户会话一直存在。

问:cookiesession有什么不同?

  • 作用范围不同:cookie存储在客户端,session保存在服务器端。
  • 存取的方式不同:cookie只能保存ASCⅡsession可以存取任意数据类型。
  • 有效期不同:cookie可设置长时间保持,session一般失效时间较短,或客户端关闭就会失效。
  • 存储大小不同:单个cookie保存的数据不能超过4ksession可存储数据远高于cookie

问:为什么需要cookiesession

  • 让服务器知道根它打交道的用户是谁以及用户的状态,浏览器发起第一次请求后服务端会返回一个sessionID存储到cookie中,当再次发起请求时服务端根据携带的cookie里的sessionID来查找对应的session信息,没有找到就说明没登录或登录失效,找到说明已经登录,可以进行之后的操作。

问:如果浏览器禁止了cookie怎么办?

  • 每次请求都携带一个SessionID的参数;或者使用token令牌,登录后服务端生成一个Token返回给客户端,以后客户端携带token进行数据请求。

问:使用cookie有哪些注意点?

  • 不建议作为存储方式使用。首先会随请求携带,影响请求的性能,其次存储空间也太小,最后一些属性的使用也需要注意。value:如果作用于登录状态,需要加密。http-only:不能通过JavaScript访问到cookie,防止XSS攻击。same-site:不能在跨域请求中携带cookie,防止CSRF攻击。

问:前后端实现登录的方式有哪些?

  • cookie + session:前端登录后,后端会种一个httpOnlycookie在前端,里面就有这个用户对应的sessionId,以后每一次前端发起请求会携带上这个cookie,后端从里面解析到sessionId后找到对应的session信息,就知道是谁再操作了。缺点是后端需要空间存储session,用户多了,服务器多了都不方便,这种方式基本属于淘汰边缘。
  • jwt + token:前端登录后,后端会返回一个包括用户信息加密的token字符串(可能还有过期时间,手机端有设备唯一码等信息),客户端自己保存了,将这个token设置到header里的Authorization,之后每次请求都带上,服务器解码这个token之后就知道是谁在访问了。优点是不占存储空间,后端解码即可。

问:浏览器实现本地存储的方式有哪几种?

  • cookie:存储大小4kb,会随请求发送到服务端,可设置过期时间。
  • localStorage:存储大小为5M,不参与请求,除非被清理,否则一直存在。
  • sessionStorage:存储大小为5M,不参与请求,页面关闭清除。
  • indexDB:存储大小没限制,不参与请求,除非被清理,否则一直存在,运行在浏览器上的非关系型数据库。

问:了解Service Worker嘛?

  • 是运行在浏览器背后的独立线程,可用于实现缓存功能,传输协议必须是HTTPS。使用Service-Worker实现缓存功能一般分为三个步骤:首先注册,然后监听install事件缓存需要的文件,最后拦截请求事件,如果缓存中已经有请求的数据就直接使用。

问:谈谈对浏览器缓存的理解?

  • 浏览器缓存是性能优化中最简单高效的一种,可以显著的减少网络传输所带来损耗,降低服务端压力。对于一个请求来说,可以分为发起网络请求、后端处理、浏览器响应三个步骤。浏览器缓存就可以做到直接使用缓存而不发起请求,或者发起了请求但后端存储的数据和缓存是一致的,就没必要传回来。

问:从哪些地方可以读取到浏览器缓存?

  • 浏览器缓存会从四个位置去读取,并且它们是有优先级的,会依次去查找,最后都没有找到才会去发起请求。
  • Service Worker:和浏览器其他内建缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存且缓存是持续性的。
  • Memory Cache:从内存中读取,速度快,不过缓存的持续性并不高,关闭页面后内存中的缓存会被释放,什么东西能进内存确定不了。
  • Disk Cache:速度没有内存快,不过存储的容量和持续性会高很多,在浏览器缓存中硬盘的覆盖面是最大的。可以根据HTTP Header中的字段判断哪些资源需要缓存,哪些可以不请求直接使用,哪些已过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。
  • Push Cache:HTTP/2中的内容,存储时间很短暂,只在会话中,一旦会话结束就被释放。

问:什么是浏览器缓存策略?

  • 缓存策略分为两种:强缓存和协商缓存,都是通过设置HTTP-Header来实现的,在强缓存没有命中之后才会尝试协商缓存。

  • 强缓存:通过设置HTTP-HeaderExpiresCache-Control实现。强缓存表示在缓存期间不需要发起请求,状态码为200

  • Expires:HTTP/1的产物,值为缓存的过期时间,如果资源过期了就需要再次发起请求,如果修改本地时间,可能会造成缓存失效。

  • Cache-control:出现于HTTP/1.1,如果和Expires同时设置,优先级会更高。可以在请求头或者响应头中设置,可以组合多种指令使用。 cache.png

  • 协商缓存:通过设置HTTP-HeaderLast-ModifiedETag实现。如果缓存过期了,就需要发起请求验证资源是否有更新。如果发起请求验证资源没有改变,返回状态304,并且更新浏览器缓存的有效期。

  • Last-Modified和If-Modified-SinceLast-Modified表示本地文件最后修改日期,If-Modified-Since会将Last-Modified的值发送给服务器,询问该日期之后的资源是否有更新,有就将新资源发送来,没有返回304状态码。

  • Last-Modified的弊端:1. 如果本地硬盘打开了缓存文件,即使没修改也会被误认为已经修改了,从而导致服务器不能准确命中缓存,导致发送相同的内容;2. 只能以秒计时,如果在不可感知的时间内修改完成文件,服务端会认为资源还是命中了,不会返回正确的资源。

  • ETag和If-None-Match:为了解决Last-Modified的弊端,ETag出现于HTTP/1.1,他的优先级比Last-Modified高。ETag类似于文件的指纹,If-None-Match会将ETag发送给服务器,询问该ETag是否变动,有变动的话就将新的资源发送回来。

问:浏览器缓存适用的应用场景有哪些?

  • 频繁变动的资源:首先设置Cache-Control:no-cache,使浏览器每次都请求服务器,然后配合ETagLast-Modified来验证资源是否有效,虽然请求数量没少,不过能显著减少响应的数据大小。
  • 不频繁变动的资源:一般使用打包工具会为生成的jscss等文件最后加上哈希值,只有当代码修改后才会生成新的文件名。所以我们可以给这些文件设置Cache-Control:max-age=31536000为一年的有效期,文件名变更了就下载新的文件,否则一直使用缓存。

问:什么是XSS

  • 就是攻击者通过页面注入可执行的代码,例如评论的留言里。XSS的类型分为两种:持久型和非持久型。
  • 持久性:将攻击的代码通过服务端写入到数据库中,例如通过评论提交的评论里面是一段脚本,如果没做好防范就会提交到数据库里,可能导致其他用户会执行这段代码,会到攻击。
  • 非持久性:一般是通过修改URL参数的方式加入攻击代码,从而诱导用户访问链接从而进行攻击。

问:如何防范XSS

  • 转义字符:将用户输入的内容,如引号、尖括号、斜杠进行转义;采用白名单过滤标签和标签属性,例如过滤script标签;使用CSP告诉浏览器限制外部资源可以加载和执行,开启CSP有两种方式:1. 设置HTTP-Header中的Content-sesurity-Policy。 2. 设置<meta>标签的方式<meta http-equiv="Content-Security-Policy">

问:什么是CSRF

  • 中文的意思是跨站请求伪造。原理是攻击者构造出一个后端请求地址,诱导用户去点击发起请求。如果是登陆状态,服务端就会以为是用户在操作,从而进行相应的逻辑。

问:如何防范CSRF

  • 不让get请求对数据进行修改;不让第三方网站访问到用户的cookie,设置cookieSameSite属性,不让cookie随跨域请求携带;组织第三方网站请求接口,验证RefererToken验证,登陆后服务器下发一个随机token,之后的请求带上。

问:什么是点击劫持?

  • 是一种视觉欺骗的攻击手段,攻击者将需要攻击的网站通过iframe嵌入的方式嵌入到自己的网页里,将iframe设置为透明,在页面中透出一个按钮诱导用户点击。

问:如何防范点击劫持?

  • 现代浏览器设置HTTP响应头X-FRAME-OPTIONS,这个响应头就是为了防御点击劫持的;远古浏览器使用js防御,当通过iframe的方式加载页面时,让攻击者网站不显示内容。

问:什么是中间人攻击?

  • 就是在攻击者在服务端和客户端建立了连接,并让对方认为连接是安全的,攻击者不仅能获得双方的通信信息,还能修改通信信息。

问:如果防范中间人攻击?

  • 不要使用公共wifi;只使用https协议,并关闭http的访问。

问:你知道的性能优化方式有哪些?

    1. 文件压缩,减小资源大小
    1. 异步组件,按需加载
    1. 小图片转base64,减少请求
    1. 雪碧图,减少请求
    1. 选择合适的图片格式和尺寸
    1. 懒加载,按需加载
    1. css放最上面,js放在body最下面,渲染优化
    1. 事件节流,减少操作
    1. 减少Dom操作和避免回流,渲染优化
    1. 浏览器缓存,减少请求次数或响应数据
    1. 减少cookie的使用,减少请求携带大小

问:babel是如何将ES6代码转为ES5的?

  • 首先解析ES6的代码字符串,生成 ES6AST语法树;
  • 然后将ES6AST转为ES5 AST语法树;
  • 最后将ES5AST转换成字符串代码。

问:有哪些方式可以提升webpack的打包速度?

  • loader:使用includeexclude指定搜索文件的范围。
  • babel-loader:配置后面加上loader:'babel-loader?cacheDirectory=true'将编译过的文件缓存起来,下次只需要编译更改过的代码文件即可。
  • HappyPack:使用这个插件开启loader多线程打包。
  • DllPlugin:将特定的类库提前打包然后引入,减少打包类库的次数,只有当类库更新版本才进行重新打包。
  • resolve.alias:配置别名,更快找到路径。
  • module.noParse:确定这个文件没有其他依赖时,让webpack打包时不扫描该文件。

问:有哪些方式可以减小webpack的打包后的体积?

  • Scope Hoisting:分析模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数里。
  • Tree Shaking:删除项目中未被引用的代码。

问:对HTTP协议的理解?

  • TCP/IP协议家族的子集,属于通信传输流中链路层、网络层、传输层、应用层中的应用层,主要职责是生成针对目标web服务器的HTTP请求报文和请求内容的处理。
  • HTTP是无状态的协议,不对请求和响应之间的通信状态进行保存,响应完成后就会断开连接。

问:什么是持久连接以及管线化?

  • 持久连接:在HTTP/1.1之前的时代,每一次HTTP请求就需要先TCP建立三次握手,传输完毕后就断开连接,会增加很多的通信开销。HTTP/1.1增加了持久连接,也就是说在一次TCP连接里面可以发送多次HTTP请求,只要任意一端没有明确提出断开连接,则保持TCP的连接状态,也就是响应头里面的Connection:keep-alive
  • 管线化:在持久连接里处理HTTP的方式是,发送响应完成后才能发起下一个请求,而管线化解决的问题是可以一次发起多个HTTP请求,且可以同时返回多次响应结果。

问:为什么发起HTTP请求前需要TCP三次握手?

  • 为了让客户端和服务端都能确定彼此发起和响应的能力是否靠谱。
  • 客户端首先发送证明客户端有发送的能力,服务端接受后返回证明服务端有响应和发送的能力,但这个时候还不能知道客户端的响应能力,所以客户端再响应之后,表明彼此连接的通道是顺畅的,然后HTTP请求就可以传输数据了。

问:为什么关闭HTTP请求前需要TCP四次挥手?

  • 关闭连接是双向的,客户端和服务器都可以提出,四次挥手是为了不让关闭太仓促,保证可靠性。
  • 如客户端首先会告知服务器申请关闭连接,服务器收到后告诉客户端收到了,不过我还没有准备好,让客户端等等。服务端数据发送数据完毕后,再次告诉客户端,我准备关闭连接了。客户端收到后怕网络不好,服务器不知道要关闭,所以第四次发送信息确认,服务器收到后断开连接,客户端也断开连接。

问:HTTP请求报文和响应报文里分别有什么?

  • 报文的结构大致是两部分,报文首部,一个空行,和报文主体,报文主体不一定非要有。
  • 请求报文:包括了请求行,里面包括请求的方法,协议版本;各种首部的字段,例如服务器域名、客户端信息、缓存信息、压缩传输的方式等。
  • 响应报文:包括了状态行,协议版本,响应的状态码;各种首部的字段,如ETag、日期、内容类型等,以及响应的报文主体。

问:http1.0http1.1的区别?

  1. http1.1加如了持久连接。
  2. 加入了更多的请求头、响应头(缓存方面的)。
  3. 新增了一些错误状态码、如409(请求的资源和资源当前状态发生冲突)、410(服务器上的某个资源被永久性的删除)。

问:httphttps的区别?

url开头不一致是最明显的区分;其次http没有https安全,http没有经过SSL/TLS加密、身份验证;还有默认的端口不一样,http80https443https需要证书,https是防止中间人攻击方式的一种。

问:为什么https更安全?

  • 一般使用公钥加密或私钥加密:
  1. 公钥加密双方都需要事先知道一个都知道加密方式的密钥,优点是加密速度快,缺点是过程中可能会被窃取,安全性没有非对称加密高。
  2. 非对称加密会加入公钥和私钥,客户端使用第三方证书去服务端获取公钥,然后用获取到的公钥把共享密钥进行加密发送给服务端,服务端使用私钥解密出共享密钥,服务端用获取到的共享密钥进行消息的加密,客户端用用共享密钥进行解密。

问:常见的响应状态码有哪些?

  • 大致可以分为2开头表示成功、3开头表示重定向、4开头客户端错误、5开头服务器错误。
  • 204:如当浏览器发出请求处理后,返回204响应,表示客户端显示的页面不发生更新。
  • 206:客户端只要想要响应内容中的一部分,服务端成功执行了这次响应。响应报文中的Content-Range则指定了哪部分的内容。
  • 301:永久重定向,表示请求的资源已被分配到了新的URI,以后使用新的吧
  • 302:临时重定向,表示请求的资源已被分配到了新的URI,现在使用新的吧
  • 303:临时重定向,表示请求的资源已被分配到了新的URI,请使用get方法获取资源。
  • 304:服务端找不到根据客户端发送附带条件的请求。(附带条件指get请求报文中包含If-MatchIf-Modified-SinceIf-None-MatchIf-RangeIf-Unmodified-Since中的一个)
  • 400:请求报文存在语法错误。
  • 403:请求资源被服务器拒绝。
  • 503:表明服务器暂时处于超负载或正在停机维护,无法处理请求。

问:get和post的区别?

  • get回退不会重新发起请求,post会;
  • get默认会被浏览器主动缓存,post不会;
  • get只能进行url编码,post支持多种编码方式;
  • get的请求参数会被拼接到url后面,post放在request-body中;
  • get产生一个tcp数据包,post会产生两个tcp包;
  • get主要是应用为获取资源,post主要是应用于传输或修改资源。

问:什么是UDP协议?

  • 属于通信传输流中的传输层,UDP是面向无连接的,传输双方没有确认机制,也就是说你要传就传吧,没有HTTP那样需要事先三从握手。
  • 缺点是不能保证数据传输的可靠性;优点是报文头信息少开销小,支持一对多、多对多、多对一的传输方式,传输实时性强。常用于直播以及游戏。

问:谈谈vue初始化从数据到视图的过程,能详细些吗?

  • 首先在vue的内部会执行_init()方法,进行一系列的初始化,首先会进行配置的合并,将用户传入的对象和自身的方法属性进行合并。
  • 然后会确定组件的父子关系,将父组件的自定义事件添加到子组件的事件中心中,挂载之后会用到的将render函数内的数据转为VNode的方法(手写render函数里的h方法),接着执行第一个生命周期钩子beforeCreate函数。
  • 接下来会初始化一些状态,比如injectdatacomputedwatchprovide等,挂载到当前实例this下,并完成数据的响应式,紧接着执行created钩子函数。
  • 开始组件的挂载阶段,如果是不带编译器版本且需要编译时,这个时候就开始将模板编译render函数,完毕之后执行beforeMount钩子。
  • 接下来执行render函数,得到VNode,之后执行patch由内而外的将VNode转为真实Dom,完成之后执行mounted钩子。
  • 如果在patch的过程中遇到了子组件的VNode,就会转为去执行子组件的初始化到真实Dom的状态过程,完毕之后才执行父组件的mounted钩子。

问:vue生命周期钩子有哪些,每个钩子阶段都做了什么?

  • beforeCreate:建立了组件的父子关系,完成了事件的初始化,还不能访问到定义的状态。
  • created:完成了provide/inject和状态的初始化,可以访问到dataprops等状态。
  • beforeMount:内部执行了$mount函数,主要作用是作为挂载到完成阶段性能检测的起始时间,以及将模板编译为render函数。
  • mounted:开始将render函数转为VNode,并创建为真实的DOM,挂载完成之后执行mounted
  • beforeUpdate:对于已经挂载完成的组件更新了组件模板的数据时,在异步更新之前触发。
  • updated:更新完成之后执行。更新相关的钩子连官网都不推荐使用,推荐使用watch监听数据变化。
  • activated:keep-alive包裹的组件,因为不会销毁,所以也不会重建,它的createdmounted钩子不会触发。被缓存的组件激活时执行activated钩子。
  • deactivated:离开当前激活的组件时触发。
  • beforeDestroy:组件销毁之前执行,这个时候还没有销毁任何东西,所有状态都可以访问到。
  • destroyed:当前组件的子组件都销毁完,当前组件也销毁完之后执行。

问:组件之间通信方式有哪些?

  • 子向父:父组件在自己作用域下定义传递自定义事件给子组件,子组件使用$emit触发,传值给父组件的回调使用。
  • 父向子:父组件通过props给子组件;父组件使用ref引用子组件实例,访问子组件的数据和方法。
  • 兄弟组件:通过当前实例的父组件的$children属性,通过name找到对应的兄弟组件实例。
  • 跨级组件通信:使用provide/inject,父组件可以向所有子组件传值。
  • 任意组件:使用vuex或者Event Bus;当前组件找到需要传值组件的实例,使用$on$emit传值。

问:父子组件如何完成数据双向绑定?

v-model形式: 只能绑定一个数据
父组件:
<template>
  <Child v-model='msg'/>  /* 可以使用value和@input的形式 */
</template>
export default {
  data() {
    return {
      msg: ''
    }
  }
}
子组件:
<template>
  <input v-model='currentMsg'/>
</template>
export default {
  props: ['value'],
  data() {
    return {
      currentMsg: this.value
    }
  },
  watch: {
    value(newVal) {
      this.currentMsg = newVal
    },
    currentMsg(newVal) {
      this.$emit('input', newVal)
    }
  }
}

sync形式: 绑定数据条数没限制
父组件:
<template>
  <Child :name.sync='name' :sex.sync='sex'/>
</template>
export default {
  data() {
    return {
      name: '',
      sex: ''
    }
  }
}
子组件:
<template>
  <div>
    <input v-model='currentName'/>
    <input v-model='currentSex'/>
  </div>
</template>
export default {
  props: ['name', 'sex'],
  data() {
    return {
      currentName: this.name,
      currentSex: this.sex
    }
  },
  watch: {
    name(newName) {
      this.currentName= newName
    },
    sex(newSex) {
      this.currentSex = newSex
    },
    currentName(newName) {
      this.$emit('update:name', newName)
    },
    currentSex(newSex) {
      this.$emit('update:sex', newSex)
    }
  }
}

问:什么是插槽、具名插槽、作用域插槽?

  • 如果没有定义插槽,在父组件内写在子组件的标签之间的内容会被忽略。插槽的作用是在父组件的作用域下往子组件内插入定义的模板内容。具名插槽是在子组件里显示的指明什么插槽需要什么内容,父组件按照名称插入模板内容。作用域插槽是把子组件内的数据传递到父组件作用域下使用,也可以配合具名插槽生成对应的模板内容。
  • 父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
默认插槽:
<button>
  <slot>插槽默认属性</slot>
</button>

具名插槽:
<div>
  <slot name="head">Head</slot>
  <slot name="center">Center</slot>
  <slot name="footer">Footer</slot>
  <slot>Default</slot>
</div>

<TestComp>
  <template v-slot:head>
    <div>添加到head</div>
  </template>
  <template #center> // 简写
    <div>添加到center</div>
  </template>
  <template #footer>
    <div>添加到footer</div>
  </template>
  <template>添加到默认</template>
</TestComp>

作用域插槽:
<div>
  <slot :user="user">{{user.lastName}}</slot>
</div>

<TestComp v-slot="{user}">
  <div>{{user.firstName}}</div>
</TestComp>

具名插槽加作用域插槽
<div>
  <slot :user="user" name="info">{{user.lastName}}</slot>
</div>

<TestComp #info="{ user }">
  <div>{{user.firstName}}</div>
</TestComp>

问:v-showv-if 区别?

  • v-if:条件渲染,当条件为false时,渲染时根本就不会渲染出Dom
  • v-show:条件展示,无论条件是什么,都会进行渲染出Dom,显隐由display:none切换。
  • 对于频繁切换显隐的状态的使用v-show,如果不频繁就使用v-if

问:vue里数组绑定class的用法?

  • 一般这样的一个数组是用计算计算属性返回,如果是确定有的,直接是数组的某一项即可,如果需用通过其他值来决定是否有没有,可以在数组里面使用对象key/value的形式,value是一个布尔值。
computed: {
  classes() {
    return [
      `${prefixCls}`, 
      {
        [`${prefixCls}-${this.type}`]: this.type !== ''
      }
    ]
  }
}

问:vue里的动画?

单个节点的过渡使用内置的transition组件,多个时使用transition-group组件。transition是一个抽象组件,不会渲染真实的Dom出来,而transition-group则不是。

动画分为enterleave状态,他们使用v-ifv-show进行状态的切换,在enter状态内会管理v-enterv-enter-activev-enter-to这三个class的添加以及删除,以及before-enterenterafter-enter的执行顺序;在leave状态会管理v-leavev-leave-activev-leave-to这三个class的添加以及删除,以及before-leaveleaveafter-leave,这两个组件只是参与动画流程的管理。

更多细节可以查阅该组件的源码分析文章 ==> Vue原理解析(十二):不让过渡/动画成为短板之transition组件实现原理

问:如何实现一个自定义过滤器?

  • 自定义过滤器解决的问题是按照一定的格式统一处理文本。一般使用Vue.filter方法进行自定义过滤器的全局注册,第一个参数是过滤器的名称,第二个参数是一个回调,回调内的第一个参数就是需要处理的文本值。同一个文本可以经过多个过滤器,使用 | 进行分割即可,过滤器也可以像使用方法般接受参数。

问:如何实现一个自定义指令?

  • 自定义指令解决的问题无可避免需要操作Dom时,会更加的方便,一般是使用Vue.directive方法进行自定义指令的全局注册。注册自定义时接受两个参数,第一个是指令的名称(不加v-),第二个是一个对象,里面主要是一些指令的钩子函数,如bind(绑定到节点时执行)、inserted(渲染到父节点时执行)、update(指令所在组件更新时)、componentUpdated(所在组件及其子组件更新时)、unbind(解除绑定时),在这些钩子的内部第一个参数就是指令绑定对应的真实Dom,第二个参数binding,指令对应的一些信息或传的参数。在钩子函数里完成对Dom的操作就是自定义指令做的事情。

问:谈谈对vuex的理解?

  • 是一个全局集中响应式状态的管理工具,状态会保存再state内,可以被所有组件所引用,一经修改后引用state内状态的组件都会响应式的更新。state不能被直接修改,必须commit内提交mutation才行,且mutation必须是同步函数。提交action可以在内部进行异步操作,可同时提交多个mutation
  • store内的state可以通过this.$store.state.xxx访问。
  • store内的getter可以通过this.$store.getters.xxx访问。
  • store内的mutation可以通过this.$store.commit('xxx')提交。
  • store内的action可以通过this.$store.dispatch('yyy')提交。

问:vuex语法糖方法有哪些以及如何使用?

computed: {
  ...mapState(['xxx', 'yyy'])
}

computed: {
  ...mapGetters(['xxx', 'yyy'])
}

methods: {
  ...mapMutations({
    zzz: 'XXX_YYY'
  })
}

methods: {
  ...mapActions(['yyy'])
}

问:vue-router如何传递参数?

方式1:
{
  path:"/home/:id",
  component:Home
}
this.$router.push({
  path:`/home/${id}`,
})
在Home组件中获取参数
this.$route.params.id

方式2:需要命名路由
{
  path:'/home',
  name: 'home'
  component:Home
}
this.$router.push({
  name:'home',
  params:{id:9527}
})
在Home组件中获取参数
this.$route.params.id

方式3:
{
  path:"/home",
  component:Home
}
this.$router.push({
  path:'/home',
  query:{id:9527}
})

在Home组件中获取参数
this.$route.query.id

问:vue-router有哪些导航守卫钩子?以及它们的执行顺序?

  • 全局守卫: beforeEach:只要当某个导航被触发时,就会执行这个钩子。 beforeResolve:在路由的beforeEnter和组件的beforeRouteEnter执行之后执行。 afterEach:在所有的导航守卫执行完毕之后执行,没有next方法。
  • 路由守卫: beforeEnter:在路由内定义,在全局beforeEach之后执行。
  • 组件守卫: beforeRouteEnter:在渲染组件对应路由被确认之前调用,不能访问this,在路由beforeEnter钩子之后执行。 beforeRouteUpdate:在当前路由改变但组件被复用时调用,例如在动态子路由之前调转时。 beforeRouteLeave:导航离开该路由时调用。
  1. beforeRouteLeave
  2. beforeEach
  3. beforeRouteUpdate
  4. beforeEnter
  5. beforeRouteEnter
  6. beforeResolve
  7. afterEach

问:如何实现异步组件?

方式1const Home= () => import('components/home')
export default new Router({
  routes: [
    {
      path: '/home',
      component: Home,
    },
  ]
})

方式2:可以指定多个路由为相同`chunk`名,会打包在一起
export default new Router({
  [{
    path: '/home',
    component: r => require.ensure([], () => r(require('../components/home')), 'home' /* chunk名 */)
  }]
})

方式3export default new Router({
  [{
    path: '/promisedemo',
    component: resolve =>  require(['../components/home'], resolve)
  }
]})

方式4:高级异步组件,带`loading``error`组件
const Home= () => lazyLoadView(import('components/home'))

export default new Router({
  routes: [
    {
      path: '/home',
      component: Home,
    },
  ]
})

function lazyLoadView(AsyncView) {
  const AsyncHandler = () => ({
    component: AsyncView,
    loading: require('@/components/loading').default,
    error: require('@/components/error').default,
    delay: 200,
    timeout: 10000
  });
  return Promise.resolve({
    functional: true,
    render(h, { data, children }) {
      return h(AsyncHandler, data, children);
    }
  });
}

问:请实现一个最小化vue响应式示例?

class Watcher {
  update() {
    console.log('更新~');
  }
}
class Dep {
  constructor() {
    this._watchers = new Set();
  }
  add(watcher) {
    if(!this._watchers.has(watcher)) {
      this._watchers.add(watcher);
    }
  }
  notify() {
    this._watchers.forEach(watch => {
      watch.update();
    })
  }
}

Dep.target = new Watcher();

function observer(target) {
  if (typeof target === 'object' && target !== null) {
    Object.keys(target).forEach(key => {
      defineReactive(target, key, target[key]);
    })
  }
}
function defineReactive(target, key, val) {
  const dep = new Dep();
  if (typeof val === 'object' && val !== null) {
    observer(val);
  }
  Object.defineProperty(target, key, {
    get() {
      dep.add(Dep.target);
      return val;
    },
    set(newVal) {
      dep.notify();
      val = newVal;
    }
  })
}

问:工厂模式?

  • 主要作用就是把创建对象的过程进行更进一层的封装,相同的部分进行提取,不同的地方传递参数即可。
function Coder(name, age) {
  this.name = name
  this.age = age
  this.career = 'coder'
  this.work = ['写代码', '写系分', '修Bug']
}
function ProductManager(name, age) {
  this.name = name
  this.age = age
  this.career = 'product manager'
  this.work = ['订会议室', '写PRD', '催更']
}
...
简单封装,不同再去一个个的new具体的角色
function Factory(name, age, career) {  
  if (career === 'coder') {
    return new Coder(name, age);
  } else if (career === 'product manager') {
    return new ProductManager(name, age);
  }
  ...
}

将角色抽象成User类,使用工厂进一步封装
function User(name, age, career, work) {
  this.name = name;
  this.age = age;
  this.career = career;
  this.work = work;
}
function Factory(name, age, career) {
  let work;
  if (career === 'coder') {
    word = ['写代码', '写细分', '修Bug'];
  } else if (career === 'product manager') {
    word = ['订会议室', '写PRD', '催更'];
  }
  ...
  return new User(name, age, career, word);
}

问:单例模式?

  • 只创建一次类的实例,其余情况都返回创建好的实例结果。例如vue里的插件,安装一次之后不会再次安装,直接返回之前已经实例化的结果。

问:实现Storage类,使得该对象为单例,基于localStorage进行封装。实现方法 setItem(key,value) 和 getItem(key)?

静态方法版:
class Storage {
  static create() {
    if (!Storage.instance) {
      Storage.instance = new Storage();
    }
    return Storage.instance;
  }
  setItem(key, value) {
    return localStorage.setItem(key, value);
  }
  getItem(key, value) {
    return localStorage.getItem(key, value);
  }
}

闭包版:
function Storage() { }
Storage.prototype.setItem = function (key, value) {
  return localStorage.setItem(key, value);
}
Storage.prototype.getItem = function (key) {
  return localStorage.getItem(key, value);
}
const createStorage = (function () {
  let instance;
  return function () {
    if (!instance) {
      instance = new Storage();
    }
    return instance
  }
})()

问:实现一个全局唯一的模态框?

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>modal</title>
  <style>
    #modal {
      position: fixed; height: 200px; width: 200px;
      line-height: 200px; top: 50%; left: 50%;
      transform: translate(-50%, -50%);
      border: 1px solid #000; text-align: center;
    }
  </style>
</head>

<body>
  <button id='open'>打开模态框</button>
  <button id='close'>关闭模态框</button>
  <script>
    const Model = (function () {
      let box;
      return function () {
        if (!box) {
          box = document.createElement('div');
          box.innerHTML = '唯一';
          box.id = 'modal';
          box.style.display = 'none';
          document.body.appendChild(box);
        }
        return box;
      }
    })()
    document.getElementById('open').addEventListener('click', () => {
      const model = Model();
      model.style.display = 'block';
    })
    document.getElementById('close').addEventListener('click', () => {
      const model = Model();
      model.style.display = 'none';
    })
  </script>
</body>
</html>

问:观察者模式和发布订阅模式的区别?

  • 如果发布者直接触及到订阅者,就可以说明是观察者模式;
  • 如果发布者不直接触及到订阅者,而是由第三方来完成实际的通信操作,就叫做发布-订阅模式。
  • 简单来说,它们就是解耦的程度不同,vue内的自定义事件的Event Emitter,发布者完全不用感知到订阅者,事件的注册和触发都发生在事件总线上,实现了完全的解耦。
  • DepWatcher就是观察者模式,Dep直接add以及notify触发watcher的更新。
观察者模式:
class Subject {
  constructor() {
    this.observers = [];
  }
  add(observer) {
    this.observers.push(observer);
  }
  remove(observer) {
    const index = this.observers.indexOf(observer);
    if (index > -1) {
      this.observers.splice(index, 1);
    }
  }
  notify() {
    const obs = this.observers;
    for (let i = 0; i < obs.length; i++) {
      obs[i].update();
    }
  }
}
class Observer {
  constructor(name) {
    this.name = name;
  }
  update() {
    console.log('my name is' + this.name);
  }
}

发布订阅模式:
class Events {
  constructor() {
    this._evnets = Object.create(null);
  }
  
  on(event, fn) {  // 往事件中心添加事件
    if (Array.isArray(event)) {
      for (let i = 0; i < event.length; i++) {
        this.on(evnet[i], fn);
      }
    } else {
      (this._evnets[event] || (this._evnets[event] = [])).push(fn);
    }
  }
  
  emit(event, ...args) {  // 触发事件中心对应事件
    const cbs = this._evnets[event];
    if (cbs) {
      for (let i = 0; i < cbs.length; i++) {
        cbs[i].apply(this, args);
      }
    }
  }
  
  off(event, fn) {  // 移除事件
    if (!arguments) {
      this._evnets = Object.create(null);
      return this;
    }
    if (Array.isArray(event)) {
      for (let i = 0; i < event.length; i++) {
        this.off(event[i], fn);
      }
      return this;
    }
    if (!fn) {
      this._evnets[event] = null;
      return this;
    }
    const cbs = this._evnets[event];
    let i = cbs.length;
    while (i--) {
      const cb = cbs[i];
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1);
        break;
      }
    }
    return this;
  }
  
  once(evnet, fn) {  // 只执行一次
    function on() {
      this.off(evnet, on);
      fn.apply(this, arguments);
    }
    on.fn = fn;
    this.on(evnet, on);
    return this;
  }
}

分享一个笔者写的组件库,说不定会用的上了 ~ ↓

你可能会用的上的一个vue功能组件库,持续完善中...