HTML 和 CSS
BFC
全称「块格式化上下文」(Block Formatting Context), 是一块独立的 css 作用域,外部如何变化都影响不到他。
如何触发
- 根元素 body
- float: right | left
- position: absolute | fixed
- display:inline-block | tabel-cells | flex
- overflow: hidden | auto | scroll
效果
- 清除浮动,常用 overflow hidde
- 垂直方向 margin 会重叠
重排和重绘
网页加载过程大致分为 5 步:
- HTML 代码转化成 DOM
- CSS 代码转化成 CSSOM
- 结合 DOM 和 CSSOM,生成一棵渲染树(包含每个节点的视觉信息)
- 生成布局(layout),即将所有渲染树的所有节点进行平面合成
- 将布局绘制(paint)在屏幕上
其中第 4 步为排列布局,重排指的就是第 4 步重做;第 5 步是绘制,那重绘指的就是第 5 步重做。按照网页加载顺序,第 4 步之后执行第 5 步,所以重排必将触发重绘,而重绘不一定会触发重排。
重绘:修改元素字体颜色,背景颜色,阴影效果等
重排:位置发生改变,如margin
padding
width
。 删除增加dom
调用offsetHeight
等读取实时量值时
由于重排性能消耗大,那么减少重排次数也就成了一个优化页面的方法,下面总结几个常用方法:
- 不要一条一条修改 css,尽量通过 class 修改,或者 csstext
- 先 display: none; 再修改样式,然后再 display: block;
- 一个元素大量修改 css,可以设置为 position: fixed | absolute, 这样重排消耗少,因为不用考虑其他元素位置
- react vue 虚拟 dom
link 和@import 区别
@import
属于 css 语法,只能加载 css,link
是 html 语法,不仅可以加载 css,还可以加载网站 icon 等
为什么要使用 css 预处理语言
类似 scss(sass3+),less 等预处理 css 语言,扩展了 css,具备变量
、mixins
、函数
, 嵌套
,带有作用域
,支持块注释 //
src 和 href 的区别
src 用于替代这个元素,而 href 用于建立这个标签与外部资源之间的关系。src 加载资源会阻塞页面,href 是加载并行。
src:
<img src="/img.jpg" />
<script src="/jquery.js"></script>
href:
<link rel="stylesheet" href="/main.css" /> <a href="/"></a>
行内元素特点
- 可以设置所有方向
margin
和padding
,但是垂直方向无法更改布局 - 设置
width
和height
属性无效 - 不会另起一行
实现换肤功能
- 设置多种颜色类名,覆盖实现
- http 请求动态加载 .css 文件
- 预编译 css 变量
js 部分
箭头函数和普通函数区别
- 没有自己 this,继承父级的 this
- 没有自己 arguments // Uncaught ReferenceError: arguments is not defined
- 没有原型 // undefined
script 的 defer 和 async 属性区别
相比于基本的 script 标签、都可以实现异步加载脚本
,只是执行脚本的时机不同,
- async:加载完成后,
立即执行
,可以在 dom 没完全加载执行,如果多个 script async,那么哪个先加载完就
执行哪个,适合加载无依
赖的 js - defer:加载完成后,等到
dom加载完成后
执行,如果多个 script defer,会严格按照他们的书写顺序依次执行
,适合加载互相依赖
的 js
深拷贝
// 冷门深拷贝利用MessageChannel
function deepClone(obj) {
return new Promise(function (resolve, reject) {
var { port1, port2 } = new MessageChannel();
port1.onmessage = function (e) {
resolve(e.data);
};
port2.postMessage(obj);
});
}
// 利用weakmap解决了循环引用报错问题,完全版
function deepClone1(_target, map = new WeakMap()) {
if (!_target || typeof _target !== "object") {
return _target;
}
if (map.get(_target)) {
console.log("进入map判断");
console.log(map.get(_target));
return map.get(_target);
}
let res = Array.isArray(_target) ? [] : {};
console.log("map set");
console.log(_target, res);
map.set(_target, res);
for (let i in _target) {
console.log("循环");
if (typeof _target[i] === "object") {
console.log("是对象");
res[i] = deepClone1(_target[i], map);
} else {
console.log("不是对象");
res[i] = _target[i];
}
}
return res;
}
函数防抖和节流
- 防抖:
function debounce(event, delay) {
let timer = null;
return function (...args) {
// ...args用来接受fun原有的参数,比如事件就是event对象
clearTimeout(timer); // 利用clearTimeout的特性,处理delay内,再此触发函数不做处理
timer = setTimeout(() => {
event.apply(this, args);
}, delay);
};
}
- 节流:
function throttle(event, delay) {
let prev = Date.now();
return function (...args) {
// ...args用来接受fun原有的参数,比如事件就是event对象
let now = Date.now();
if (now - prev >= delay) {
event.apply(this, args);
prev = Date.now();
}
};
}
继承
原型链继承: Cat 继承了 Animal,缺点:所有 Cat 实例都会共享原型链,不可以向 Animal 传参。
function Cat() {} Cat.prototype = new Animal();
call 继承/构造继承: 解决上面原型链建成俩问题,但是只是 Cat 实例,不是 Animal 的实例
function Cat(name) { Animal.call(this); }
组合继承:也就是前面两个方法合并,解决上面两个继承的缺点,唯一的缺点调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
function Cat(name) { Animal.call(this); } Cat.prototype = new Animal(); Cat.prototype.constructor = Cat; //修复构造函数指向 // 优点: // 既是cat实例,又是Animal实例 // 没有共享原型方法问题,并可以传参数
跨域
jsonp: 利用 script 标签跨域,本质是加载一个外部 js
cors:全程跨域资源共享,服务端操作,把请求头的
origin
的值取出,响应头会增加Access-Control-Allow-Origin
key 并把值设置为origin
取出的值也可以是通配符,浏览器就不会拦截响应了,从而解决跨域问题postMassage:利用可访问服务资源的
iframe
,和当前页面互相通信,传递消息,来实现跨域,postMassage
本质是两个页面的消息传递方法。// 页面1(可以获取后端数据的)传递数据,第二个参数是url匹配规则,可以是通配符 window.frames[0].postMessage(data, "*"); // 页面2(不能接收后端数据的),接收页面1的数据 window.addEventListener("message", function (e) { console.log(e.data); });
服务器中转: 由于服务端没跨域,服务端代替客户端请求目标接口,并把响应数据返回给客户端
数组方法
改变原数组
- splice: array.splice(index,howmany,item1,…..,itemX) 删除/添加
- sort: 排序
- unshift: 首加 shift: 首删; pop: 尾删; push: 尾加
- reverse: 倒序排列
- fill: 数组填充
array
.fill(value, startIndex | 0, endIndex | array.length)
[(1, 2, 3, 4, 5)].fill("a", 1, 3);
// [1, "a", "a", 4, 5]
不改变原数组,生成新数组
- slice: 浅拷贝 array.slice(startIdx, endIdx)
- join: 转字符串并分割
- toLocaleString: 转换成字符串数组
- concat: 连接数组 可用…替代
- indexOf: 检查下标
- includes: 是否包含。可替代 indexOf,indexOf === -1 不够语义化
循环方法
- forEach、map、filter
- every、some、reduce、reduceRight(和 reduce 区别是相反方向)
字符串方法
var str = "hello world";
str.charAt(1); //e 返回指定下标的字符
str.charcodeAt(1); //101 返回指定下标字符的字符编码
str.concat(123); // hello world123 连接字符串,生成新的
str.slice(0, 2); // he 截取字符串,参数为下标
str.substring(0, 2); // he 截取字符串,参数为下标
str.substr(0, 2); // he 截取字符串,第一个参数是起始下标,第二个参数为截取数量
str.indexOf("h", 0); // 0 查子字符下标,第一个参数是字符,第二个参数是起始下标
str.lastIndexOf("h", 0); // 0 查子字符下标,第一个参数是字符,第二个参数是结尾下标(和indexof区别)
str.toLowerCase(); // hello world 转小写
str.toUpperCase(); // HELLO WORLD 转大写
str.trim(); // 删除前后空格
str.split("e"); // ["h", "llo world"] 转数组,参数为分割符
str.replace("o", "a"); // hella world 替换字符,第一个参数可以是正则
str.match(/hello/g); // ["hello"] 匹配字符串返回一个数组,参数常是正则
js 垃圾回收机制
当内存中的数据不需要时候,js 会自动使用垃圾回收算法释放掉
- 引用计算 : 此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。这是最初级的垃圾收集算法。 该算法有个限制:无法处理引用循环,两个对象互相引用的情况下,引用次数至少是 1,所以不会被回收掉
- 标记清除 : 此算法把“对象是否不再需要”简化定义为“对象是否可以获得”。这个算法从根(root)全局对象开始先下查找,找到不可以被获得的对象,然后清除掉
类型检测
- typeof: 除了基本数据类型,检测不到具体 object 类型(数组,对象,时间对象,正则,set,map)
- instanceof: 例 a instanceof Fun 原理是检测 a 是不是 Fun 的实例,缺点就是被检测的值必须是引用,如下所示
new Number(1) instanceof Number;
// true
1 instanceof Number;
// false
- constructor: 实例.constructor === 构造器,缺点是 constructor 属性容易被修改
- Object.prototype.toString.call: 目前最好用的一个方法,缺点是使用 object 构造函数的 toString 方法,此方法可能被修改
Object.prototype.toString.call(1)
// "[object Number]"
Object.prototype.toString.call('1')
// "[object String]"
Object.prototype.toString.call([1])
// "[object Array]"
// 如果修改原型
Object.prototype.toString = Array.prototype.toString
Object.prototype.toString.call([1,2,3])
// "1,2,3"
...
valueOf 和 toString
- valueOf: 返回对象的原始值表示
- toString: 返回对象的字符串表示
对象转字符串或者数字的过程:如果转换成字符串,会优先调用 toString 方法,转数字,会优先调用 valueOf 方法
相关面试题:
// 下面代码a在什么值情况下会输出1
var a = ?;
if (a == 1 && a == 2 && a == 3) {
console.log(1);
}
// 解
var a = {
n : 0,
valueOf(){
return ++this.n;
}
};
proxy 和 reflect
proxy
对目标读取、函数调用等操作进行拦截,然后操作处理。它不直接操作对象,而是像代理模式。
let target = {
name: "Tom",
age: 24,
};
let handler = {
get: function (target, key) {
console.log("getting " + key);
return target[key]; // 不是target.key
},
set: function (target, key, value) {
console.log("setting " + key);
target[key] = value;
},
};
// target为目标对象,handler为代理处理
let proxy = new Proxy(target, handler);
reflect
优化了 object 内部操作,修改一些函数的返回值,如 Object.defineProperty,Reflect 与 Proxy 是一一对应的对象,是 Proxy 的方法也是 Reflect 的方法,Proxy 修改的过的方法可以在 Reflect 获取默认值。
// 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。
Object.defineProperty(target, property, attributes) // 报错
Reflect.defineProperty(target, property, attributes // false
// 让Object操作都变成函数行为。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。
'assign' in Object // true
Reflect.has(Object, 'assign') // true
requestAnimationFrame
针对实现动画 setInterval 的优化方法,执行速率和设备屏幕帧率有关系,不需要调用者指定帧速率,交给浏览器自行决定最佳帧效率,每一帧只执行一次,多次改变触发一次重拍,这样不会引起丢帧现象,也不会导致动画卡顿
事件循环
同步任务在执行栈,异步任务执行后回调区分微任务和宏任务放入对应的微宏队列中,等同步执行栈执行完毕,会先检查微任务队列,执行完毕后,检查宏任务队列,执行。其中 Promise.resolve 调用静态方法,会优先于正常的 promise 的 resolve 方法
异步微任务有哪些:
- new Promise()
异步宏任务有哪些:
- setInterval()
- setTimeout()
执行顺序: 同步放入执行栈 => 异步放入任务队列 => 异步区分微宏任务 => 微任务 => 宏任务
async/await,await 后的语句是同步的,在函数体内,await 下的语句会被放入 promise 回调函数体中,async 函数默认返回一个 promise 对象
经典面试题:
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(function () {
console.log("setTimeout");
}, 0);
async1();
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});
console.log("script end");
解析:按照从上往下顺序先执行同步代码,前两个函数async1
和async2
是函数声明,忽略。然后第 1 个输出的是script start
,然后看到setTimeout
, 由于它是异步且属于宏任务,放到最后执行。然后看到 async1 函数被执行了,进入async1
函数体,第 2 个输出的是async1 start
, 看到 await 规则是:await 右的语句被同步执行,所以第 3 个输出的是async2
,await 下的语句(函数体内)被当作 async 函数返回一个 promsise 的回调函数体的语句去处理,且被放入了异步任务队列中,等待中。然后跳出 async1 函数体,继续执行同步代码,遇到 new Promise,第 4 次输出promise1
,看到 resolve,就把 then 中回调,放入任务队列中,然后继续向下执行同步代码,第 5 次输出的最后的script end
。至此同步执行栈空了,开始把任务队列中的上下文拿到执行栈执行,由于队列先进先出,所以第一个被入栈的是 async1 的回调,也就是第 6 次输出的是async1 end
,然后把第二个队列中的上下文放入执行栈,也就是 promise 的 resolve,所以第 7 次输出promise2
,至此微任务执行完成,现在执行宏任务,也就是 timeout,所以第 8 次输出为setTimeout
。
结果:
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
迭代器和生成器
先来瞅一段代码:
function createIterator(items) {
var i = 0;
return {
// 返回一个迭代器对象
next: function () {
// 迭代器对象一定有个next()方法
let done = i >= items.length;
let value = !done ? items[i++] : undefined;
return {
value: value,
done: done,
};
},
};
}
var iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // "{ value: 1, done: false}"
console.log(iterator.next()); // "{ value: 2, done: false}"
console.log(iterator.next()); // "{ value: 3, done: false}"
console.log(iterator.next()); // "{ value: undefiend, done: true}"
这就是迭代器,迭代器是一个特殊对象,可以通过每调用一次 next,获取下一项结果。可以用迭代器替代 for 循环,因为 for 通过下标获取,多层嵌套比较复杂。
// 用迭代器模拟for
var colors = ["red", "green", "blue"];
var iterator = createIterator(colors);
while (!iterator.next().done) {
console.log(iterator.next().value);
}
迭代器模拟 for 的方法看起来太麻烦了,不如 for 来的方便,这个时候可以用生成器
(* 和 yield)来快速生成迭代器。
function* createIterator(items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
}
let iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // "{ value: 1, done: false}"
console.log(iterator.next()); // "{ value: 2, done: false}"
console.log(iterator.next()); // "{ value: 3, done: false}"
console.log(iterator.next()); // "{ value: undefiend, done: true}"
通过*
和yield
关键字创建,*
来表示这个函数内部是可以用 yield 返回每次迭代的值,和 async、await 一样。需要组合使用,yield
后面接每次迭代的值。这样就可以外部调用.next()获取下一个值。
在 es6 中,数组、Set、Map、字符串集合,都属于可迭代对象,内部都有默认迭代器,可通过object[Symbol.iterator]()
访问默认迭代器。
es6 中也提供了一个通过方法来快速使用迭代器
for(item of 集合迭代器) {...}
常见的迭代器和得到 item 的值:
- object.keys(): key(对象)/下标(数组)
- object.values(): 每项 value
- object.entries(): [下标/key, value]
事件委托与冒泡和捕获
什么是事件委托:事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。
什么是冒泡和捕获:其实都是事件传播路径。冒泡:从外到事件源进行事件传播;捕获:从内向事件源进行事件传播;
DOM 标准事件流的触发的先后顺序为:先捕获再冒泡。即当触发 dom 事件时,会先进行事件捕获,捕获到事件源之后通过事件传播进行事件冒泡。
addEventListener
事件监听方法的第三个参数默认值是false
,表示冒泡阶段调用,true
为捕获阶段调用
<body>
<button>append</button>
<ul>
<li>0</li>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
</body>
window.onload = function () {
document.querySelector("button").onclick = function (e) {
let li = document.createElement("li");
li.innerHTML = "5";
document.querySelector("ul").appendChild(li);
};
document.querySelector("ul").onclick = function (e) {
// li 新增的li也是可以被点击的
console.log(e.target.nodeName.toLowerCase());
};
};
addeventListener 和 onclick 不同点:
- addeventListener 可以给一个事件注册多个 listener
- addEventListener 可以控制时间传播行为(捕获/冒泡,使用 removeEventListener 来解决)
实现 instanceof
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
let objProto = obj.__proto__;
while(objProto) {
if (objProto === ctor.prototype) {
return true;
}
objProto = objProto.__proto__;
}
return false;
}
var Food = function() {};
var Meat = function() {};
Meat.prototype = new Food();
var meat = new Meat();
newInstanceof(meat, Meat) // true
newInstanceof(meat, Food) // true
meat instanceof Meat // true
meat instanceof Food // true
总的来说就是meat.__proto__.constructor === Meat
,由于原型链是一条链,所以循环所有__proto__
;
直到null
为止,符合Meat.prototype === meat.__proto__
即返回true
,否则返回false
。
constructor 和 super 关键字
class
是ES6
新增的关键字,为了看起来更像java
等面向对象语言,实际上是一个语法糖,等同于ES5
的function
来创建构造函数。
- constructor :
constructors
是 class 中默认的一个关键字,会自动调用该方法,class
中必须要有constructor
,如果没有,会被默认添加。当 new 时,constructor
会被执行返回一个 this,就是实例。 - super : 当
class
有继承时,在constructor
内部必须执行一遍 super()函数,B 继承 A,super
相当于A.prototype.constructor.call(this, props)
,另外 super()也指向父亲 A 的原型对象, 详细解释。如果当前类和父类都有同名方法,如何区分地获取呢,获取父类:super().fun,获取当前类:fun
vue 部分
vue 组件通信
- props 和 $emit
- provide inject:在组件 a 声明,在 a 下面所有子组件通过 inject 调用,通过参数传递
- event bus: new 出一个 vue,通过 emit 和 on 来派发和监控
- vuex 全局状态管理
- this.$children 和 this.$parent 层级较深不推荐,provid 可以替代此方法
vue 生命周期父子组件执行顺序
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount->子 mounted->父 mounted
子组件更新:父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
父组件更新:父 beforeUpdate -> 父 updated
销毁:父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
总结带 beofre 的生命周期都是从父到子调用,带 ed 的生命周期都是从子到父
前端路由
hash
14 年前,前端路由都是用 hash 的方式实现的,地址长这个样子url#hash, 修改hash
部分不会重载这个页面,每次 hash 改变,可以用 window 的hashchange
方法来监听,通过这个方法就可以实现前端路由了
history
在 14 年,HTML5 标准发布,window 上新增了两个 api:pushState
和replaceState
,也 hash 一样,不刷新来实现前端路由,而且少了#更美观了,由于没#,所以刷新时候,默认会从服务器下载对应 url 资源,这个时候我们需要改下服务器配置,都加载前端 index.html 文件,来实现前端路由
// 第一个参数是state,用于描述一些特性,会被放入历史栈中,开发者使用
// 第二个参数是title,表示新页面的标题,浏览器都会忽略这个参数
// 第二个参数是url,表示新页面的相对地址
// 跳转
window.history.pushState(null, null, "/profile");
window.history.replaceState(null, null, "/profile");
和pushState的区别是不会把记录放入历史栈中;
共同点
- 都不会重载页面
- 都会把历史记录放入栈中
不同点
- hash 只可以#后部分,必须是同源 url,history 没有限制
- history 需要修改服务器配置,来解决刷新空白问题,hash 本身就是属于前端的,不需要设置
slot 插槽
- 匿名插插: 子组件包裹的内容会被显示到
slot
标签中
<child>父给子的内容</child>
// child组件中, 父给子的内容会显示slot标签里
<slot>父给子的内容</slot>
- 具名插插:感觉名字多次显示,可复用
<div slot="down"></div>的内容
会被显示到<slot name="down"></slot>里
- 数据插槽:
// 父亲中
<div v-slot:data="{data}"></div>
// 组件中就可以用data了
<myslot v-slot="data">
{{ slotProps.user.firstName }}
</myslot>
keep-alive
keep-live 是 vue 提供的内置缓存组件,可以保存组件状态,避免重新渲染,keep-alive 有三个参数:
- include 匹配的组件会被缓存,参数:字符串、数组、正则
- exclude 匹配的组件不会被缓存,参数:字符串、数组、正则,和 include 同时存在一个组件的的话,exclude 优先级更高
- max 可以限制最大可以缓存组件的数量,字符串或者数字
<!-- 组件name为a或者b的组件缓存 -->
<keep-alive include="a,b">
<component></component>
</keep-alive>
<!-- 组件name为c的组件不缓存 -->
<keep-alive exclude="c">
<component></component>
</keep-alive>
缓存组件会多出两个生命周期,activated
和deactivated
,渲染和销毁
keep-alive 可配合 router-view 来实现组件缓存
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>
export default new Router({
routes: [
{
path: '/',
name: 'a',
component: a,
meta: {
keepAlive: false // 不缓存
}
},
{
path: '/b',
name: 'b',
component: b,
meta: {
keepAlive: true // 缓存
}
}
]
})
nextTick
nextTick 可以获取下次最新 dom 的方法,因为 vue 的 dom 更新策略是异步的,只有一个事件循环结束后才会被应用来更新 dom,这导致数据的修改不会立即表现在 dom 上,如果要获取更新后的 dom 状态,这时候就需要用到 nextTick
一些场景:
- created 中想要获取 DOM
- 更改数据获取最新的 dom
mixin
vue mixin 是针对组件间功能共享来做的。可以对组件的任意部分进行 mixin,也就是说不同的组件,想使用同一个数组,或者方法,那么 mixin 就是来做这个事的,把公共部分提取出来作为 mixin
声明:
// 声明
const toggle = {
data() {
isshowing: false;
},
methods: {
toggleShow() {
this.isshowing = !this.isshowing;
},
},
};
// 使用,toggle的方法会自动合并到当前组件中
mixins: [toggle];
vuex
核心概念:
- state:数据
- getter:类似计算属性,用来获取 state
- mutation: 同步方法,用于修改 state
- action: 异步方法,最后调用 mutation 修改 state
// 声明
import Vuex from 'vuex'
const store = new Vuex.Store({
state: {
count: 0
},
getter: {
doneTodos: (state, getters) => {
return state.todos.filter(todo => todo.done)
},
count: (state) => {
return state.count
}
},
mutations: {
increment (state, payload) {
state.count++
}
},
actions: {
addCount(context) {
// 可以包含异步操作
// context 是一个与 store 实例具有相同方法和属性的 context 对象
}
}
})
// 使用
// 调用mutation
this.$store.commit('increment', 10)
// 调用actions
this.$store.dispatch('increment', 10)
// 利用mapGetters mapGetters mapMutations mapActions便捷使用
computed: {
...mapGetters(['audio']),
...mapState({
this.count 等同于 state.count + this.localCount
count(state) {
return state.count + this.localCount
}
},
methods: {
...mapMutations({
add: 'increment' // 将 `this.add()` 映射为`this.$store.commit('increment')`
}),
...mapActions({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
})
}
react 部分
react 生命周期
- componentWillMount:在渲染前调用,客户端服务端都会调用
- componentDidMount:第一次渲染后调用,只在客户端
- componentWillReceiveProps: 接收新的 props 被调用,初始化时不会被调用
- shouldComponentUpdate: 组件更新(props,state)时调用;返回布尔值,T 为允许更新组件,F 为不更新组件
- componentWillUpdate: 组件更新(props,state)前渲染前被调用,初始化时不会调用
- componentDidUpdate:组件完成更新后调用。初始化时不会被调用
- componentWillUnmount: 组件要销毁前调用
react 数据响应式
react 的响应式是使用 diff 算法实现的。react 在 state 改变时,会调用 render() 方法,生成一个虚拟 DOM 树,React 会将这棵树与上一次生成的树进行比较,找出其中的差异,并更新差异的部分。这个过程是递归的,react 会以当前组件为根,递归比较所以子节点。为了优化性能,React 提供了 shouldComponentUpdate 生命周期方法,这个方法有两个参数,nextProps和nextState,表示上次 props 和上次 stage,返回值是布尔值,如果这个方法返回 false,react 就跳过这个组件,不做 VDOM 比较,也不更新组件。
在 react16+版本,对 diff 过程进行了优化,从原来的 diff 树结构+递归的方式优化成 fiber 双向链表+循环执行。整个的 diff 过程分为 render 阶段和 commit 阶段,render 就是对比过程,找到需要改变 dom 的那部分,commit 就是执行改变 dom 的那部分,render 阶段是可以分段的(中断,然后继续完成),类似原生的requestIdleCallback方法,在每个 16ms 帧内,会先执行用户输入,事件回调等优先级高的 js 任务。之后剩下的时间执行分段的 render 部分来进行数据更新,保证视觉上的页面流程度。
http 部分
http 缓存
分为 强缓存 和 协商缓存,如果同时存在,强缓存 优先 协商缓存
强缓存:
- 响应头 Expires:Fri, 31 Dec 2024 23:59:59 GMT,在这个时间之前,直接读缓存,否则请求新内容,因为值是一个服务端返回的时间,这是 http 1.0 的产物,如果客户端和服务端时间不一致,就会和预期不符,所以在 http 1.1 中新增 Cache Control 来控制缓存
- Cache Control:max-age=x 秒,在 x 秒内直接读缓存,避免了 expires 时间不一致问题
Cache Control 其他的 value:
- Private: 私有缓存,资源可以仅可以被客户端缓存(默认值)
- Public: 资源可以被任何缓存(包括中间代理服务器)缓存
- No-Cache: 协商缓存
- No-Store: 绝对不缓存
- Max-Age:强缓存
协商缓存 (Last-Modified 优先 ETag)
当 Cache Control 设置为 No-Cache 时使用
- Last-Modified / If-Modified-Since:服务器会返回上次修改时间 Last-Modified,浏览器下次请求时请求头会带上 If-Modified-Since ,然后由服务端验证,如果 If-Modified-Since 在 Last-Modified 时间以内 ,就返回 304,浏览器读缓存,否则返回新内容
- ETag / If-None-Match:服务器会返回内容 hash ETag 响应头,浏览器下次请求时请求头会带上 If-None-Match ,然后由服务端验证,如果一致,就返回 304,浏览器读缓存,否则返回新内容
附上一张说明图:
https 握手
https 是 http 的安全扩展,相当于 HTTP + SSL(TLS) = HTTPS。SSL 即安全套接字层,它在 OSI 七层网络模型中处于第五层即网络层,TLS 是 SSL 的新版本,最后一个 SSL 3.0 版本在 2015 年废弃,之后就是 TLS,现在常见的 TLS 版本是 1.2 和 1.3 版本。那么 TLS 是如何实现的呢?
- 浏览器 => 服务器:请求发起,建立 SSL 连接,发送 TLS 协议版本号 + 支持的加密方法列表 + 客户端随机数
- 服务器 => 浏览器:判断加密协议版本是否一致,并确认加密方法,都没问题则返回服务器证书 + 服务端随机数
- 浏览器 => 服务器:收到服务器证书,检查颁发机构、过期时间、域名,没有问题,则生成一个字符串(通过上面 客户端随机数 + 服务端随机数 生成),并用公钥(从服务器证书里取出)加密(非对称加密),发给服务器
- 服务器 => 浏览器:服务器用自己的私钥解密,得到这个字符串,后续使用这个字符串进行对称加密进行通信
http1.1长连接和http2多路复用区别
http1.1 长连接
同一个域名访问同一个文件的多个请求都可以复用一个 tcp 连接(不用像 1.0 一样 每次请求都需要重新建立连接,但不是并行,只能按顺序执行。
http2 多路复用
同一个域名访问多个文件的多个请求也可以复用一个 tcp 连接,且多个请求可以被并行处理。
其他
npmRun
会自动到 package.json 找到对应的命令执行。如果没有全局命令,会自动到node_modules/.bin/
下找到对应可执行文件执行,这时npm run xxx
等同于直接调用/images/node_modules/.bin/xxx
cookie 的几种属性和作用
- name: 名字
- value: 值
- domain: 可以访问此 cookie 的域名
- path: 可以访问此 cookie 的路径
- sameSite: 跨站点携带 cookie 设置
- None:不限制;
- Lax:默认值,介于 None 和 Strict 之间,会合理判断携带 cookie 时机,如链接 a 会携带 cookie;
- Strict:完全禁止跨站点传送 cookie;
- httponly: 值为 true 则仅在 http 请求中会传递 cookie,不可用 document.cookie 等操作获取
- secure: 是否只能使用 https 传递此 cookie
- expires: 过期时间;值是时间意思是到这个时间,cookie 失效,否则是 session,即退出浏览器后自动失效
输入 url 到页面展示经历了什么
dns 解析:按照这个步骤查询,找到 ip,立即中断返回给客户端(浏览器缓存 => 操作系统缓存(host) => 路由器缓存 => 本地服务器缓存 => isp dnf 服务器查询 => 根 dns 服务器)
tcp 三次握手
发送 http 请求报文:报文包括:请求行,请求头,请求体
服务端接收到请求: 进入 web 服务器,如 apache、nginx,然后进入后台程序
服务器响应报文:报文包括:响应行,响应头,响应体
浏览器渲染页面
- 根据 html 得到 dom 树
- 根据 css 得到 css 树
- 根据 dom 树和 css 树,生成渲染树
- 根据渲染树计算每一个节点的信息
- 根据计算好的信息绘制页面
数据传送完成,四次挥手关闭:
懒加载
页面中一些大资源加载较慢,比如 iframe,img。针对这个问题可以通过懒加载解决。
拿img
举例,先设置img
的src
属性为一个默认的资源,然后把真实url
放在标签的某个属性中。并设置好图片的宽高,避免完成加载时出现页面闪动问题。
然后实时监听页面滚动,如果当前图片是可见状态,那么把图片的真实 url 属性值取出替换到默认src
上来实现加载图片资源,从而提升页面打开速度和合理使用客户端流量。
// 获取浏览设备的网页可视高度
let clientHeight = document.documentElement.clientHeight;
// 获取已被滚出的页面高度
let scrollTop = document.documentElement.scrollTop;
// 图片顶部到网页顶部的高度
let imgOffsetTop = document.querySelector("img").offsetTop;
// 判断img在可视区域内:
imgOffsetTop < clientHeight + scrollTop;
commonjs 模块和 es6 模块有什么区别
commonjs:
- 对于基本数据类型,是值的拷贝
- 对于复杂数据类型,是浅拷贝,也就是修改一个模块的值,引用他的值也会发生改变
- 第一次加载模块会缓存,往后再加载会读取缓存的数据
es6:
- 所有数据都是只读引用,当遇到 import 语句时,会生成一个只读引用,等到文件中真的使用到模块中的值时,根据只读引用,到加载中的模块取值
- 不允许修改模块中的数据,并且是动态的,比如模块中的值发生改变,引入得到的值也会改变。
前端优化
- 静态资源放在 cdn
- 首屏加载服务端渲染
- 可以升级到 http2,可以使用 http2 多路复用 多个请求并行,可以打乱顺序,最后重组
- 使用字体图标替代图片
- gzip 压缩
- uglify plugin 优化压缩代码
- 图片懒加载
- 图片压缩,优先使用 webp
- 减少重排,多使用类名或者 csstext 处理样式
- 多使用 flex,性能优,兼容性好
常见算法介绍
二分查找:也称折半查找(Binary Search),它是一种效率较高的查找方法,前提是数据结构必须先排好序,可以在数据规模的对数时间复杂度内完成查找。但是,二分查找要求线性表具有有随机访问的特点(例如数组),也要求线性表能够根据中间元素的特点推测它两侧元素的性质,以达到缩减问题规模的效果。
贪心算法:又名贪婪法,是寻找最优解问题的常用方法,这种方法模式一般将求解过程分成若干个步骤,但每个步骤都应用贪心原则,选取当前状态下最好/最优的选择(局部最有利的选择),贪图眼前局部的利益最大化。
回溯算法:实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就 “回溯” 返回,尝试别的路径。回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。
分治算法:分治法是构建基于多项分支递归的一种很重要的算法范式。字面上的解释是「分而治之」,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
动态规划:动态规划是通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。动态规划往往用于优化递归问题,例如斐波那契数列,如果运用递归的方式来求解会重复计算很多相同的子问题,利用动态规划的思想可以减少计算量。
babel 工作原理
在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。简单来说就是一种描述源代码的结构,用于被计算机识别到,并成语言引擎执行
工作原理大致可以理解成,字符串输入
=>转换
=>输出
- 根据 babylon 解析器,把 js 代码按照规范解析成 AST 语法树
- 按照规则(stage-0,1,2,3),修改 AST 语法树,常见把高级语法,转换成低级语法
- 最后通过 babel-generator 将修改后的 AST 语法树转换成 js 代码