HTML/CSS
1. 盒模型
- 标准盒模型
- width 和 height 是内容区域即 content 的 width 和 height。
- 盒子总宽度= width + margin(左右) + padding(左右) + border(左右)
- IE 盒模型或怪异盒模型
- width 和 height 除了 content 区域外,还包含 padding 和 border
- 盒子宽度 = width + margin(左右)
- 通过
box-sizing
切换border-box
或content-box
2. 隐藏一个元素的方式
- display: none
- visibility: hidden
- opacity: 0
- 高度为 0
- 定位
position: absolute; left: 100%;| top: -100%
- text-indent 设置一个足够大的负值
3. display: none 和 visibility: hidden 的区别
- display: none 不占位
- visibility: hidden 占位 (内部盒子也不会显示)
4. BFC 模式
5. flex 1 代表什么
6. align-self 属性
align-self 属性允许单个项目有与其他项目不一样的对齐方式,可覆盖 align-items 属性。默认值为 auto,表示继承父元素的 align-items 属性,如果没有父元素,则等同于 stretch。
JavaScript
1. 数据类型
Number String Boolean Undefined Null Symbol Object BigInt
2. var let const 区别
- var 变量声明提升
- let const 区别
- let 变量
- const 常量
- let 可以直接修改值(或者引用)
- const 只可以改变属性值
- let const 暂时性死区
3. 箭头函数
4. 原型、原型链
5. 作用域、作用域链
6. 为什么 0.1+0.2 不等于 0.3
01. + 0.2 = 0.3 // false
7. 小数相加、整数相加
8. 实现 new 操作符
function Person(name, age) {
this.name = name
this.age = age
}
方式一:
function _new(func, ...args) {
if (typeof func !== 'function') throw new Error('func is not a function')
const context = Object.create(func.prototype)
const rest = func.apply(context, args)
return typeof rest === 'object' && rest !== null ? rest : context
}
const p = _new(Person, 'mike', 12)
方式二:借助 Reflect.construct
Reflect.construct 相当于 new target()
const p = Reflect.construct(Person, ['jack', 11])
9. 实现 instanceof
function instanceofX(obj, constructor) {
if (obj.__proto__ === null) return false
if (Object.getPrototypeOf(obj) === constructor.prototype) return true
return instanceofX(obj.__proto__, constructor)
}
10. splice 的使用及返回值
const arr = [1, 2, 3, 4, 5]
- 删除:返回所删除的元素组成的数组
arr.splice(1, 2) // 返回 [2, 3]
- 增加:
arr.splice(1, 0, 9, 9) // 返回空数组 []
- 替换:返回所有替换后的元素组成的数组
arr.splice(1, 2, 7, 8) // [2, 3]
11. 类型转换
123 instanceof Number // false
new Number(123) instanceof Number // true
Number(123) instanceof Number // false
Number === 123
// Number 就是强转换
12. arguments 参数
function sidEffecting(ary) {
ary[0] = ary[2]
}
function bar(a, b, c) {
c = 10
sidEffecting(arguments)
return a + b + c
}
bar(1, 1, 1) // 21
说明:考察 arguments
参数。arguments 内部属性及其函数形参创建 getter 和 setter 方法,因此改变形参的值会影响到函数 arguments 的值,反过来也一样。
13. 深拷贝(只考虑数组和对象)
- 方式一
function deepClone(value) {
if (Array.isArray(value)) {
return value.map(deepClone)
} else if (value && typeof value === 'object' && value !== null) {
const res = {}
for (const key in value) {
res[key] = deepClone(value[key])
}
return res
}
return value
}
- JSON.stringify()
JSON.parse(JSON.stringify(value))
缺点:
会忽略 value 为undefine、函数,key为Symbol类型的选项,而且日期对象的值会通过toJSON()转成字符串
- 可以实现值为undefined,key为symbol类型的情况
function clone(obj) {
if (typeof obj === 'object' && obj !== null) {
let map = new WeakMap()
function deep(data) {
const result = {}
const keys = [...Object.getOwnPropertyNames(data), ...Object.getOwnPropertySymbols(data)]
if (!keys.length) return result
const exist = map.get(data)
if (exist) return exist
map.set(data, result)
keys.forEach(key => {
let item = data[key]
if (typeof item === 'object' && item) {
result[key] = deep(item)
} else {
result[key] = item
}
})
return result
}
return deep(obj)
}
return obj
}
14. 克隆数组
1. 一维数组
- 扩展运算 (Shallow copy)
;[...arr]
- for 循环 (Shallow copy)
const newArr = []
for (let i = 0; i < arr.length; i++) {
newArr[i] = arr[i]
}
- while 循环 (Shallow copy)
let i = -1
const newArr = []
while (++i < arr.length) {
newArr[i] = arr[i]
}
- Array.map (Shallow copy)
const newArr = arr.map(item => item)
- Array.filter (Shallow copy)
const newArr = arr.filter(Boolean)
- Array.reduce (Shallow copy)
arr.reduce((newArr, item) => newArr.concat(item), [])
- Array.slice (Shallow copy)
const newArr = arr.slice()
- Array.concat (Shallow copy)
const newArr = arr.concat()
// Or
const newArr = arr.concat([])
- Array.from (Shallow copy)
const newArr = Array.from(arr)
- JSON.stringify & JSON.parse (Deep copy)
const newArr = JSON.parse(JSON.stringify(arr))
2. 多维数组
- 借助 Array.from()
const deepCloneArr = value =>
Array.isArray(value) ? Array.from(value, deepCloneArr) : value
- 借助 Array.prototype.map()
const deepCloneArr = value =>
Array.isArray(value) ? value.map(deepCloneArr) : value
15. 平铺数组
ES6 方法
参数默认是 1,多维用 Infinity
参数
Array.prototype.flat()
js 实现
- 二维数组
const arr = [1, 2, [3, 4], [5, 6]]
const result = [].concat(...arr)
- 多维数组
function spread(arr) {
const result = [].concat(...arr)
return result.some(item => Array.isArray(item)) ? spread(result) : result
}
16. 实现 Promise.all
Promise.allX = function (promises) {
promises = [...promises]
return new Promise((resolve, reject) => {
let time = 0
const result = []
promises.forEach((p, index) => {
Promise.resolve(p)
.then(res => {
result[index] = res
time++
if (time === promises.length) resolve(result)
})
.catch(err => {
reject(err)
})
})
})
}
17. 实现 Promise.allSettled
Promise.allSettled
接收一组 Promise
实例作为参数,返回新的 Promise
实例,只有等到所有 Promise
实例的状态都改变之后才 resolve
。
所以 Promise.allSettled
的状态总是 fulfilled
。其返回值为每个 Promise
实例的 status
和 value/reason
。
Promise.allSettled = function (promises) {
promises = [...promises]
return new Promise(resolve => {
let time = 0
const result = []
promises.forEach((p, index) => {
Promise.resolve(p)
.then(value => {
result[index] = {
value,
status: 'fulfilled' // fulfilled
}
time++
if (time === promises.length) resolve(result)
})
.catch(reason => {
result[index] = {
reason,
status: 'rejected'
}
time++
if (time === promises.length) resolve(result)
})
})
})
}
18. 实现 Promise.any
接收一组 Promise
实例作为参数,返回 Promise
实例,其中只要有一个 Promise
的状态变成 fulfilled
,则返回的 Promise
的状态就为 fulfilled
,只有所有的 Promise
都为 rejected
状态则为 rejected
Promise.any = function (promises) {
promises = [...promises]
return new Promise((resolve, reject) => {
let time = 0
const result = []
promises.forEach((p, index) => {
Promise.resolve(p)
.then(res => {
resolve(res)
})
.catch(err => {
result[index] = err
time++
if (time === promises.length) {
reject(result)
}
})
})
})
}
19. 防抖、节流实现方式
20. 多种方式实现 call/apply/bind
21. js 借助 async/await 实现休眠效果
function sleep(interval) {
return new Promise(resolve => {
setTimeout(resolve, interval)
})
}
// 用法
async function one2FiveInAsync() {
for (let i = 1; i <= 5; i++) {
console.log(i)
await sleep(1000)
}
}
one2FiveInAsync()
22. 实现多次重复尝试,比如多次请求一个接口,最多请求 3 次
const COUNT = 3
async function request(url) {
for (let i = 0; i < COUNT; ++i) {
try {
await fetch(url)
break // 跳出循环
} catch {}
}
}
23. 实现 pipe
pipe 执行顺序从左向右
- 单个参数
const pipe = (...fns) => val => fns.reduce((pre, cur) => cur(pre), val)
- 多个参数
const pipes = (...fns) =>
fns.reduce((pre, cur) => (...args) => cur(pre(...args)))
24. 实现 compose
执行顺序从右向左 第一个执行函数可以接收多个参数,后面的执行函数参数都是单个的。所有函数的执行都是同步的。
- 单个参数
const compose = (...fns) => val => fns.reduceRight((pre, cur) => cur(pre), val)
- 多个参数
const composes = (...fns) =>
fns.reduceRight((pre, cur) => (...args) => cur(pre(...args)))
// 或
const composesX = (...fns) =>
fns.reduce((pre, cur) => (...args) => pre(cur(...args)))
25. 实现 trim()
function trim(str) {
return str.replace(/^\s*|\s*$/, '')
// 或
return str.replace(/^\s*/, '').replace(/\s*$/, '')
}
26. requestAnimationFrame
27. 本地存储
- localStorage
- 持久型
- 存储容量大 大约 5M
- sessionStorage 会话
- 会话型
- 存储容量大
- cookie
- 同域下往返于客户端和服务端
- 存储容量小 大概 4k
28. 交换两个值为 number 类型的变量
let a = 3
let b = 5
- 解构
;[a, b] = [b, a]
- 加减法
a = a + b
b = a - b
a = a - b
- 交换变量
let c = a
a = b
b = c
- 对象
a = { a, b }
b = a.a
a = a.b
- 托梦做出来的吧?
a = [b, (b = a)][0]
- 位运算
a = a ^ b
b = b ^ a
a = a ^ b
29. isNaN 和 Number.isNaN 的区别?
- isNaN 在调用时会将传入的参数转换为数字类型,所以非数字传入也有可能返回 true
- Number.isNaN 首先会判断传入的参数是否为数字,如果为非数字,直接返回 false
30. js 垃圾回收机制
31. 实现图片懒加载
<div class="img-area">
<img class="my-pic" alt="loading" data-src="./img/img1.png" />
</div>
//图片什么时候出现在可视区域内
function isInSight(el) {
const rect = el.getBoundingClientRect()
//这里我们只考虑向下滚动
const clientHeight = window.innerHeight
// 这里加50为了让其提前加载
return rect.top <= clientHeight + 50
}
//data-src的属性值代替src属性值
function loadImg(el) {
if (!el.src) {
const source = el.dataset.src
el.src = source
}
}
let index = 0
function checkImgs() {
const imgs = document.querySelectorAll('.my-pic')
for (let i = index; i < imgs.length; i++) {
if (isInSight(imgs[i])) {
loadImg(imgs[i])
index = i
}
}
}
Element.getBoundingClientRect
Tips:Element.getBoundingClientRect 方法返回一个 rect 对象,提供当前元素节点的大小、位置等信息,基本上就是 CSS 盒状模型的所有信息,如下:
- x:元素左上角相对于视口的横坐标
- y:元素左上角相对于视口的纵坐标
- height:元素高度
- width:元素宽度
- left:元素左上角相对于视口的横坐标,与 x 属性相等
- right:元素右边界相对于视口的横坐标(等于 x + width)
- top:元素顶部相对于视口的纵坐标,与 y 属性相等
- bottom:元素底部相对于视口的纵坐标(等于 y + height)
32. 实现 thunk
将数组(array)拆分成多个 size 长度的区块,并将这些区块组成一个新数组。 如果 array 无法被分割成全部等长的区块,那么最后剩余的元素将组成一个区块。
例如:
chunk(['a', 'b', 'c', 'd'], 2)
// => [['a', 'b'], ['c', 'd']]
chunk(['a', 'b', 'c', 'd'], 3)
// => [['a', 'b', 'c'], ['d']]
function chunk(array, size = 1) {
const length = array == null ? 0 : array.length
let start = 0
if (!length || size < 1) return []
const result = []
while (start < length) {
result.push(array.slice(start, (start += size)))
}
return result
}
33. 实现render函数 将VNode转成真是VNode,并插入文档
VNode
const VNode = {
tagName: 'ul',
props: {
class: 'list'
},
children: [
{
tagName: 'li',
props: {
class: 'item'
},
children: ['item1']
},
{
tagName: 'li',
props: {
style: 'color: red'
},
children: ['item2']
},
{
tagName: 'li',
children: ['item3'],
props: {
style: 'cursor: pointer'
},
on: {
click: () => {
console.log('click item3')
}
}
}
]
}
实现如下:
<div id="app"></div>
// 元素节点
function createEle(tag, props = {}, on = {}) {
const ele = document.createElement(tag)
const keys = Object.keys(props)
const events = Object.keys(on)
keys.length && keys.forEach(key => {
ele.setAttribute(key, props[key])
})
events.length && events.forEach(event => {
ele.addEventListener(event, on[event])
})
return ele
}
// 文本节点
function createTextNode(text) {
return document.createTextNode(text)
}
function render(VNode) {
const { tagName, props, children, on } = VNode
const ele = createEle(tagName, props, on)
if (children.length) {
const fragment = document.createDocumentFragment()
children.forEach(child => {
if (typeof child === 'object' && child !== null) {
fragment.appendChild(render(child))
} else {
fragment.appendChild(createTextNode(child))
}
})
ele.appendChild(fragment)
}
return ele
}
document.querySelector('#app').appendChild(render(VNode))
34. (链式调用)要求设计 LazyMan 类,实现以下功能
new LazyMan('Tony')
// Hi I am Tony
new LazyMan('Tony').sleep(10).eat('lunch')
// Hi I am Tony
// 等待了10秒...
// I am eating lunch
new LazyMan('Tony').eat('lunch').sleep(10).eat('dinner')
// Hi I am Tony
// I am eating lunch
// 等待了10秒...
// I am eating diner
new LazyMan('Tony').eat('lunch').eat('dinner').sleepFirst(5).sleep(10).eat('junk food')
// Hi I am Tony
// 等待了5秒...
// I am eating lunch
// I am eating dinner
// 等待了10秒...
// I am eating junk food
35. 实现 Object.is()
比较两个值是否相等。
在这里NaN等于NaN,+0不等于-0。
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
实现:
Object.defineProperty(Object, 'is', {
value: function(x, y) {
if (x === y) {
// 针对+0 不等于 -0的情况
return x !== 0 || 1 / x === 1 / y
}
// 针对NaN的情况
return x !== x && y !== y
},
configurable: true,
enumerable: false,
writable: true
})
Vue
1. vue
- 响应式原理
- nextTick 原理
- keep-alive 原理
- key 的作用
- VNode
- 组件通信
- 生命周期
- diff 算法
- vue 中优化策略
2. vuex
1. actions 和 mutations 区别
- 必须通过提交 mutation 的方式修改 state
- mutation 中只可以做同步操作
- action 中可同步可异步
2. vuex 原理
3. vuex 如何实现数据的响应式
new Vue({
data: state
})
4. mapState 实现方式
3. vue-router
1. 导航守卫
-
全局守卫
- beforeEach 全局前置守卫
- beforeResolve 全局解析守卫
- afterEach 全局后置
-
路由独享守卫
- beforeEnter
-
组件守卫
- beforeRouteEnter
- beforeRouteUpdate
- beforeRouteLeave
2. 模式
- hash
- history
3. 原理
4. vue3
- vue2 和 vue3 响应式原理对比,及具体实现思路
- vue3 做了哪些优化
- proxy
- 静态节点标记
- VNode 重构
- composition API 有哪些特性
React
1. React
- React 如何区分 class 和 functional?
- 为什么要写 super(props)?
- 为什么在编写组件时没有用到 React 还要引入 React?
- JSX 原理
- setState() 是同步还是异步的?
- 高阶组件 HOC
- 解决了什么问题
- render props
- HOOKS
- useEffect 如何实现 class 组件的 componentDidMounted 和 componentDidUnMounted
- useCallback() 和 useMemo() 的区别
2. Redux
- 实现简易版 redux
function createStore(reducer) {
let state = reducer(undefined, {})
let callback = []
return {
getState() {
return state
},
dispatch(action) {
state = reducer(state, action)
callback.forEach(fn => fn())
},
subscribe(handler) {
callback.push(handler)
},
unsubscribe(l) {
callback = callback.filter(fn => fn !== l)
}
}
}
- 实现 bindActionCreators
/**
* @param {Object | Function} actionCreators (Function: action creator,Object:value为action creator的对象)
* @param {Function} dispatch 由store提供的dispatch函数
* @returns {Object | Function}
*/
const bindActionCreators = (actionCreators, dispatch) => {
if (typeof actionCreators === 'function') {
return (...args) => {
dispatch(actionHandle(...args))
}
}
return Object.keys(actionCreators).reduce((key, creators) => {
creators[key] = (...args) => dispatch(creators[key](...args))
return handle
}, {})
}
- 实现 combineReducers
const combineReducers = reducers => {
return (state = {}, action) => {
return Object.keys(reducers).reduce((nextState, key) => {
nextState[key] = reducers[key](state[key], action)
return nextState
}, {})
}
}
- redux 中间件实现原理
中间件本质上就是一个函数。 因为 reducer 为纯函数,且派发出 dispatch 之后,redux 内部会立即调用 reducer,所以只能在发出 action 和执行 reducer 之间做操作。
// 保存store原本的dispatch方法
const dispatch = store.dispatch
// 重写store.dispatch
store.dispatch = action => {
setTimeout(() => {
console.log('异步执行完成,派发action')
dispatch(action)
}, 2000)
}
store.dispatch({ type: 'ADD' })
Node
1. Node
- http
- fs
- path.join() 和 path.resolve()区别?
- 启动一个 http 服务
2. Express
3. Koa2
浏览器
1. 输入 URL 到页面展示做了什么
-
用户输入关键词,地址栏判断是搜索内容还是 url 地址。
如果是搜索内容,会使用浏览器默认搜索引擎加上搜索内容合成 url;
如果是域名会加上协议(如 https)合成完整的 url。 -
然后按下回车。浏览器进程通过 IPC(进程间通信)把 url 传给网络进程(网络进程接收到 url 才发起真正的网络请求)。
-
网络进程接收到 url 后,先查找有没有缓存。
有缓存并且缓存还在生存期内,直接返回缓存的资源。
缓存过期或没有缓存。(进入真正的网络请求)。如果缓存过期了,浏览器则会继续发起网络请求,并且在 HTTP 请求头中带上If-None-Match: "XXX"
首先获取域名的 IP,系统会首先自动从 hosts 文件(DNS 缓存)中寻找域名对应的 IP 地址,一旦找到,和服务器建立 TCP 连接;如果没有找到,则系统会将网址提交 DNS 域名解析服务器进行 IP 地址的解析。
-
利用 IP 地址和服务器建立 TCP 连接(3 次握手)。
-
建立连接后,浏览器网络进程构建数据包(包含请求行,请求头,请求正文,并把该域名相关 Cookie 等数据附加到请求头),然后向服务器发送请求消息。
-
服务器接收到消息后根据请求信息构建响应数据(包括响应行,响应头,响应正文),然后发送回网络进程。
-
网络进程接收到响应数据后进行解析。
如果发现响应行的返回的状态码为 301,302,说明服务器需要浏览器重定向到其他 URL,这时网络进程会找响应头中的 Location 字段的 URL,重新发起 HTTP 或者 HTTPS 请求。
如果返回的状态码为 200,说明服务器返回了数据。
如果状态码是 304,则说明缓存内容在服务端没有更新,服务器不会再返回内容,让浏览器从缓存中读取。
-
网络进程将响应头和相应行传给浏览器进程,浏览器进程根据响应头中的
Content-Type
告诉浏览器服务器返回的数据类型。如果返回的类型是 text/html,则告诉浏览器返回的是 HTML,如果是 application/octet-stream 则为下载类型,那么请求会交给浏览器的下载管理器,同时 URL 的导航到此结束,如果是 HTML,那么浏览器会继续进行导航流程。(导航:用户发出 URL 请求到页面开始解析的这个过程,就叫做导航) -
渲染进程准备:
如果是新打开的页面,会单独使用一个渲染进程;
如果是“同一站点”,则会复用父页面的渲染进程;
如果不是“同一站点”,则单独创建新的渲染进程 -
浏览器进程接收到网络进程的响应头数据后,向渲染进程发出“提交导航”(CommitNavigation)消息(带着响应头等消息)
-
渲染进程接收到消息后便和网络进程建立数据传输的“管道”。
-
等文档数据传输完成后,渲染进程会返回“确认提交”的消息给浏览器进程。意思是告诉浏览器进程我已经准备好接收网络进程的数据和解析页面数据了。
-
浏览器进程接收到“确认提交”消息后移除旧的文档,然后更新浏览器界面,包括 web 页面(空白页)、导航按钮、URL 地址栏、网络安全状态。浏览器的加载按钮还是加载中状态。至此,导航流程就完成了。接下来就是解析渲染阶段了。
- 构建 DOM
- 输入 html --> 解析器 ---> DOM Tree
- 样式计算
- css 来源:行内、style、外部(link 引入)
- css ---> styleSheets
- css 转成 styleSheets
- 属性标准化
- 计算 DOM 树中每个节点的具体样式 (继承、层叠)
- 布局 layout DOM 树&styleSheets ---> 布局树
- 创建布局树(只包含可见元素)
- 布局计算
- 分层 特定节点生成专用图层,并生成图层树
- 布局树 ---> 图层树
- 拥有层叠上下文属性的元素
- 需要剪裁的元素
- 绘制图层(生成绘制列表)图层树 ---> 绘制列表
- 栅格化(合成图层)
- 绘制列表 ---> 合成线程 ---> 图块 ---> 栅格化线程池 ---> GPU 进程 ---> 光栅化 ---> 位图 ---> 合成线程 ---> 图层
- 主线程将绘制列表提交给合成线程,合成线程将图层分块(图块是栅格化的最小单
- 合成
- 合成线程提交命令 ---> 浏览器进程 ---> viz 组件接收命令 ---> 合成图片 ---> 显示
- 合成的图层会被提交给浏览器进程,浏览器进程里会执行显示合成(Display Compositor),也就是将所有的图层合成为可以显示的页面图片。 最终显示器显示的就是浏览器进程中合成的页面图片。
- 渲染进程开始页面解析和加载子资源(边下载边解析),一旦资源加载、渲染完毕,渲染进程会发送一个消息给浏览器进程,浏览器接收到这个消息后会停止标签图标的加载动画。
- 构建 DOM
至此,一个完整的页面形成了。
2. 重绘和重排
为什么 DOM 操作耗费性能和时间?
- 渲染引擎和 js 引擎切换,即两个线程之间的切换(俗称上下文切换)耗费性能。
- 重绘/重排(元素及样式变化引起的再次渲染),而且重排渲染耗时明显高于重绘
- 重排一定引起重绘,而重绘不一定引起重排
引起重绘的操作:
- 修改元素样式,比如颜色、bgc、border-color
引起重排的操作:
- 内容改变
- 增、删 DOM
- DOM 几何属性变化,如:宽高、位置、边框、padding、margin
- DOM 树结构发生改变
- 浏览器窗口尺寸改变
- 一面一开始渲染(不可避免的)
- 获取某些布局属性时。当获取一些属性值时,浏览器为了保证获取到正确的值也会引起重排。如:
- offsetTop,offsetLeft,offsetHeight,offsetWidth
- scrollTop,scrollWidth,scrollLeft,scrollHeight
- clientWidth,clientHeight,clientLeft,clientTop
- getComputedStyle()
- getBoundingClientRect()
- 更多参考这里
如何减少重排和重绘?
- 批量操作 DOM
- 在循环外操作元素
- 拼接字符串 --> innerHTML
- DocumentFragment 文档片段
- 缓存元素集合
- 复杂动画,使用绝对定位让其脱离文档流
- CSS3 硬件加速(GPU 加速)
- transform
- opacity
- filters
- will-change
硬件加速
- 使用硬件加速可以让 transform、opacity、filters 不会引起重回和回流,但是对于 bgc 这些属性还是会引起重绘
- 硬件加速不可滥用,因为会占用内存较大,会影响性能
CSS 加载阻塞情况
- CSS 加载不会阻塞 DOM 树解析
- CSS 加载会阻塞 DOM 树的渲染 主要是浏览器出于性能考虑,避免渲染完成后又有样式变动,造成回流和重绘
- CSS 加载会阻塞后面 js 的执行
缩短白屏时间,尽可能加快 CSS 加载速度
- 使用 CDN
- 压缩 CSS
- 合理使用缓存
- 减少 HTTP 请求数(合并 CSS)
原理:
- DOM 解析和 CSS 解析是并行的过程,所以 CSS 加载不会阻塞你 DOM 解析
- render Tree 的形成依赖于 DOM Tree 和 CSSOM Tree,所以必须等到 CSS 加载完成才渲染
- 因为 js 可能会操作 DOM 节点和 CSS 样式,因此样式表会在加载完毕后执行后面的 js
跨域
原因:
浏览器同源策略的限制。
解决方案:
- jsonp
- cors
- Iframe
- postMessage
- node middleware
- nginx
- webSocket
网络
应用层
1. HTTP
1.1 状态码
- 1xx 收到请求,需要请求继续执行操作
- 2xx 成功
- 200 ok
- 204 没有资源可返回
- 206 返回部分资源,(请求范围资源)比如:音视频文件
- 3xx 重定向、浏览器需要执行某些特殊处理以完成正常请求
- 301 永久重定向 表示旧地址资源被永久地移除了(这个资源不可访问了),搜索引擎在抓取新内容的同时也将旧的网址交换为重定向之后的网址
- 302 临时重定向 表示旧地址 A 的资源还在(仍然可以访问),这个重定向只是临时地从旧地址 A 跳转到地址 B,搜索引擎会抓取新的内容而保存旧的网址
- 304 协商缓存
- 4xx 客户度错误
- 400 请求报文中存在语法错误
- 401 表示发送的请求需要有通过 HTTP 认证的认证信息
- 403 Forbidden(禁止) 请求被服务器拒绝了
- 404 Not Found
- 405 Method Not Allowed(不允许使用的方法)
- 406 请求的 content-Type 和相应的 content-type 不一致。说白了就是后台返回的资源前台无法解析
- 416 所请求的范围无法满足(读取文件时设置的 Range 有误造成的)
- 5xx 服务端错误
- 500 表示服务器在执行请求时发生了错误
- 503 表示服务器暂时处于超负载或正在进行停机维护状态,现在无法处理请求
1.2 request
- 请求行 method、URI、HTTP 版本
- 请求头
- 请求体
1.3 response
- 状态行 状态码、原因短语、服务器HTTP版本
- 响应头
- 响应体
1.4 http 头
connection: keep-alive 长连接
google chrome 默认同时最多可以建立6个TCP连接,TCP上http传输是串行的。(HTTP1.1)
HTTP2 只建立一个TCP,HTTP并行,理论上没有数量限制。
http 缓存
http缓存分为强缓存和协商缓存。详细内容查看 HTTP 缓存
2. FTP
文件传输协议
3. DNS
域名解析系统。域名和 IP 的映射,域名查 IP,IP 反查域名。
2. 传输层
1. TCP
面向连接的、可靠的、字节流服务。
- 面向连接 ---> TCP 连接
- 可靠的
- 具有重发机制,不会丢包
- TCP 三次握手
- 数据包上标记序号,到达接收方可以重组
- 字节流服务 --> 大数据包切割成报文段的小数据包
缺陷:
传输速度慢,不如 UDP
2. UDP
- 无连接的
- 易丢包
- 无序无法重组
- 传输速度快
- 场景:直播、视频会议等
3. 网络层
IP 协议 负责传输
4. 链路层
操作系统、显卡等物理器件
HTTPS
HTTP 缺点:
1. 明文传输,不加密,内容容易被窃听
解决:
- 内容(报文)加密,仍不可靠,容易被篡改
- SSL 通信加密 建立安全的通信线路,http 在该线路上传输
2. 无法验证身份,容易被伪装(通信双方无法确认对方身份)
解决:
SSL 证书认证
3. 无法保证内容的完整性,容易被篡改(明文传输,无法验证身份,导致内容容易被篡改)
解决:
SSL
HTTPS
HTTP + 通信加密 + 认证 + 内容完整性保护 = HTTPS
SSL 不仅提供了通信加密,还提供了证书认证,及内容完整性保护。
缺点:
使用 SSL 处理速度变慢
- 通信慢: SSL 通信部分消耗网络资源
- 通信双方进行加解密处理,消耗大量的 CPU 和内存等资源
HTTP1.1
HTTP/1.1 为网络效率做了大量的优化,最核心的有如下三种方式:
- 增加了持久连接
- 浏览器为每个域名最多同时维护 6 个 TCP 持久连接
- 使用 CDN 的实现域名分片机制
HTTP/1.1 的主要问题
对带宽的利用率不理想
原因:
- TCP 的慢启动
- 同时开启了多条 TCP 连接,那么这些连接会竞争固定的带宽
- HTTP/1.1 队头阻塞的问题
慢启动和 TCP 连接之间相互竞争带宽是由于 TCP 本身的机制导致的,而队头阻塞是由于 HTTP/1.1 的机制导致的。
HTTP2 特性
- 多路复用机制 一个域名只使用一个 TCP 长连接和消除队头阻塞问题
- 可以设置请求的优先级 在发送请求时,标上该请求的优先级
- 服务器推送 提前讲静态资源推送到浏览器
- 头部压缩
算法
1. 排序
1. 冒泡排序
思路:
双层遍历,第二层遍历,当前值和下一个值比较,根据大小交换位置 修改的是原数组
时间复杂度 O(n^2) 空间复杂度 O(1)
function sort(array) {
for (let i = 0; i < array.length; i++) {
for (let j = 0; j < array.length - i; j++) {
const temp = array[j + 1]
if (array[j] > array[j + 1]) {
array[j + 1] = array[j]
array[j] = temp
}
}
}
return array
}
2. 快速排序
思路:
选取数组第一项作为基准值,从数组第二项开始比较,小的放在左边数组,大的放在右边数组。
最终结束时机是当数组长度为 1 时。最终就是 arr.length 个长度为 1 的数组的合并。
返回的是新数组。
时间复杂度: O(nlogn)
function quickSort(arr) {
if (arr.length <= 1) return arr
const base = arr[0]
const left = []
const right = []
for (let i = 1; i < arr.length; i++) {
const temp = arr[i]
if (base >= temp) {
left.push(temp)
} else {
right.push(temp)
}
}
return [...quickSort(left), base, ...quickSort(right)]
}
2. 反转数组
直接修改原数组
1. 遍历
function reverse(arr) {
const len = arr.length / 2
for (let i = 0; i < len; i++) {
const temp = arr[i]
const index = arr.length - i - 1
arr[i] = arr[index]
arr[index] = temp
}
return arr
}
2. 借助 reduce
function reverseReduce(arr) {
return arr.reduce((pre, cur) => [cur, ...pre], [])
}
3. 借助 reduceRight
function reverseReduceRight(arr) {
return arr.reduceRight((pre, cur) => [...pre, cur], [])
}
3. 二分查找
查找有序数组中的某一项,假设数组是递增的
- 非递归方式
function binarySearch(arr, target) {
let start = 0
let end = arr.length - 1
while (start <= end) {
let mid = Math.floor((start + end) / 2)
const midVal = arr[mid]
if (target === midVal) return midVal
if (target > midVal) {
start = mid + 1
} else {
end = mid - 1
}
}
return false
}
}
- 递归方式
function binarySearch(arr, target, start = 0, end = arr.length) {
if (start > end) return false
let mid = Math.floor((start + end) / 2)
const midVal = arr[mid]
if (midVal === target) return midVal
if (midVal > target) {
return binarySearchX(arr, target, start, end - 1)
} else {
return binarySearchX(arr, target, start + 1, end)
}
}
4. 斐波那契数列
写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项。斐波那契数列的定义如下:
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
思路:
斐波那契数是由前两数相加而得,需对初始值 0 和 1 做单独判断。
之后只需做一次 for 循环遍历,需要注意得是,为避免时间复杂度的消耗,需要缓存前两数的结果,最后按题要求,所得的数需要经过取模运算。
var fib = function (n) {
if (n <= 1) return n
// n-1
let prev1 = 1
// n-2
let prev2 = 0
let sum = 0
for (let i = 2; i <= n; i++) {
sum = (prev1 + prev2) % 1000000007
prev2 = prev1
prev1 = sum
}
return sum
}
5. 青蛙跳台阶
一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
示例 1:
输入:n = 2
输出:2
示例 2:
输入:n = 7
输出:21
示例 3:
输入:n = 0
输出:1
思路:
和斐波那契一样的思路,区别是斐波那契的 0 项为 0,而爬楼梯的 0 项是 1
空间复杂度: O(1) 时间复杂度: O(n)
function numWays(n) {
if (n <= 1) return 1
let a = 1
let b = 2
let sum
for (let i = 3; i <= n; i++) {
sum = (a + b) % 1000000007
a = b
b = sum
}
return b
}
递归实现方式
function bar(n) {
if (n <= 1) return 1
if (n < 3) return n
return f(n - 1) + f(n - 2)
}
6. 有序数组合并 排序
假设是升序的
function concatSort(a, b) {
let i = 0,
j = 0,
k = 0
const result = []
while (i < a.length && j < b.length) {
if (a[i] < b[j]) {
result[k++] = a[i++]
} else {
result[k++] = b[j++]
}
}
while (i < a.length) {
result[k++] = a[i++]
}
while (j < b.length) {
result[k++] = b[j++]
}
return result
}
7. 删除数组中的重复项
给定一个排序数组,需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
方法一:双指针法(慢快指针)
思路:
数组完成排序后,我们可以放置两个指针 i 和 j,其中 i 是慢指针,而 j 是快指针。慢指针初始值为 0,快指针初始值为 1.
如果 arr[i] === arr[j],跳过,快指针加 1。反之,慢指针加 1,快指针的值赋值给慢指针。
function removeDuplicates(arr) {
let i = 0
for (let j = 1; j < arr.length; j++) {
if (arr[i] !== arr[j]) {
arr[++i] = arr[j]
}
}
return i + 1
}
方法二: 计数排序思想
思路:
因为数组是有序排列的,定义个变量 count = 0; 从位置 1 开始遍历,判断当前元素是否和上一个元素相等,如果相等 count+1,反之跳过。
最后 count 就是所有重复元素出现的个数。那么不重复元素组成的数组就是 数组长度 - count
function removeItem(arr) {
let count = 0
for (let i = 1; i < arr.length; i++) {
if (arr[i] === arr[i - 1]) ++count
}
return arr.length - count
}
8. 组中的第 K 个最大元素
function findKthLargest(nums, k) {
// 快速排序
function sort(nums) {
if (nums.length <= 1) return nums
let base = nums[0]
let left = []
let right = []
for (let i = 1; i < nums.length; i++) {
if (nums[i] > base) {
right.push(nums[i])
} else {
left.push(nums[i])
}
}
return [...sort(left), base, ...sort(right)]
}
nums = sort(nums)
const len = nums.length
return nums[len - k]
}
9. 两数之和
找出数组中 两个元素加起来等于 target 的元素的索引
1. 双层循环
function twoSum(nums, target) {
for (let i = 0; i < nums.length; i++) {
for (let j = 0; j < nums.length; j++) {
if (i !== j && nums[i] + nums[j] === target) {
return [i, j]
}
}
}
return []
}
2. 双指针对撞
时间复杂度 O(n)
空间复杂度 O(1)
假设有序递增数组
function search(arr, target) {
let left = 0
let right = arr.length - 1
while (left < right) {
const i = arr[left]
const j = arr[right]
if (i + j === target) return [i, j]
if (i + j > target) {
right--
} else {
left++
}
}
return []
}
10. 二维数组的查找
在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
示例:
现有矩阵 matrix 如下:
;[
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
]
给定 target = 5,返回 true。
给定 target = 20,返回 false。
时间复杂度 O(n + m)
空间复杂度 O(1)
function findNumberIn2DArray(matrix, target) {
if (matrix.length < 1 || matrix[0].length < 1) return false
let row = 0
let col = matrix[0].length - 1
while (row < matrix.length && col >= 0) {
const current = matrix[row][col]
if (current === target) return true
if (current > target) {
col--
} else {
row++
}
}
return false
}
11. 第一个只出现一次的字符
在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。
示例:
s = "abaccdeff"
返回 "b"
s = ""
返回 " "
1. Set + 正则
function firstUniqChar(s) {
for (let char of new Set(s)) {
// 正则匹配变量
if (s.match(new RegExp(char, 'g')).length === 1) {
return char
}
}
return ' '
}
2. Map 的 keys 可以保证顺序
function search(s) {
const map = s.split('').reduce((pre, cur) => {
const temp = pre.get(cur)
pre.set(cur, temp + 1 || 1)
return pre
}, new Map())
for (let key of map.keys()) {
if (map.get(key) === 1) return key
}
return ' '
}
12. 打印出从 1 到最大的 n 位数
输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。
示例 1:
输入: n = 1
输出: [1,2,3,4,5,6,7,8,9]
function printNumbers(n) {
const length = Math.pow(10, n) - 1
return Array.from({ length }, (_, index) => index + 1)
}
13. 数组中重复的数字
找出数组中重复的数字。
在一个长度为 n 的数组 nums 里的所有数字都在 0 ~ n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
示例 1:
输入:
[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3
function findRepeatNumber(arr) {
const set = new Set()
for (let item of arr) {
if (set.has(item)) return item
set.add(item)
}
}
14. 查找出数组中出现次数最多的元素
const arr = [1, 2, 3, 2, 2, 1, 3, 3, 3, 3, 4, 4332, 21, 12, 2, 2, 2, 2, 2]
function maxTime(arr) {
const obj = arr.reduce((pre, cur) => {
pre[cur] = pre[cur] + 1 || 1
return pre
}, {})
const maxVal = Math.max.apply(Math, Object.values(obj))
return Object.keys(arr).find(key => obj[key] === maxVal)
}
maxTime(arr) // 2
15. 输出以奇数结尾的字符串拼接
题:[任意字符][一位数字]_,拼接的字符串,输出以奇数结尾的字符串拼接。
输入:str1_key2_val3_d4_e5
输出:str1val3e5
let str = 'str1_key2_val3_d4_e5'
- charAt()
let arr = str.split('_')
const result = arr.reduce((pre, cur) => {
if (cur.charAt(cur.length - 1) % 2 !== 0) {
return pre.concat(cur)
} else {
return pre
}
}, '')
- slice()
const result = str.split('_').reduce((pre, cur) => {
if (cur.slice(-1) % 2) {
return (pre += cur)
}
return pre
}, '')
16. 二维数组的排列组合
设计模式
1. 单例模式
class Person {
static getInstance() {
if (!Person.instance) {
console.log('只创建一次')
Person.instance = new Person()
}
return Person.instance
}
constructor(name, age) {}
// 原型方法
getSkill() {
console.log('吃饭')
}
// 静态方法
static myWork() {
console.log('睡觉')
}
}
// 单例模式。实例只创建一次
const p1 = Person.getInstance()
const p2 = Person.getInstance()
2. 发布订阅模式
性能优化
1. webpack 构建优化
首先造成 webpack 构建速度慢的因素就是重复的编译、代码压缩
方案
- 缓存
- 大部分 loader 提供了缓存选项
- 或者使用
cache-loader
;需要定义在所有 loader 的最前面
- js 压缩
- 开启缓存
- 开启 parallel(并行编译)
-
happypack
多核编译 (多线程)-
MiniCssExtractPlugin
无法与happypack
共存 -
MiniCssExtractPlugin
必须置于cache-loader
执行之后,否则无法生效
-
- 通过
DllPlugin
抽离静态依赖包,避免反复编译,比如 lodash 等,或者通过externals
CDN 引入。 Tree shaking
-
Scope hoisting
构建后的代码会存在大量闭包,造成体积增大,运行代码时创建的函数作用域变多,内存开销变大。Scope hoisting 将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突
提升构建体验
-
progress-bar-webpack-plugin
构建进度提示 -
webpack-build-notifier
构建完成后提示,还有提示音 -
webpack-dashboard
构建界面
2. 资源优化
图片
- 减小 favicon.ico 的体积 设置强缓存,过期时间设置几个月
- 压缩图片
- webpack
- 在线压缩工具
- 雪碧图
- 横向排列会更小
- 禁止在 HTML 中缩放图片
- PNG logo
- WebP 格式 注意降级处理
- 多后缀方式兼容
- 指定 Accept 头支持 WebP 格式
- 小图标 base64 直接嵌入 HTML 文档
- 懒加载
- 图片渐进显示
CSS
- css 放在顶部
- 使用 link 而不是@import 加载样式
- css 通过文件的方式引入,不要写在 html 文档,以减小文档的大小,此外 css 文件可以被缓存
- 压缩 css
- 硬件加速
- transform: translateZ(0); // 仅开启硬件加速
- transform: translate3d(0, 0, 0); // 3d 变换开启硬件加速
- perspective: 1000
注意:使用 3D 硬件加速提升动画性能时,最好给元素增加一个 z-index 属性,人为干扰复合层的排序,可以有效减少 chrome 创建不必要的复合层,提升渲染性能,移动端优化效果尤为明显。
硬件加速最好只用在 animation 或者 transform 上。不要滥用硬件加速,因为这样会增加性能的消耗(内存的使用),如果滥用反而会使动画变得更加卡,就得不偿失了。
JavaScript
- 脚本放在底部
- js 和 css 通过文件的方式引入,不要写在 html 中,原因:
- 减小了 HTML 文档的大小
- js 和 css 会被缓存
- 压缩 js
- 删除重复的脚本
- 减少 DOM 的访问
- 缓存频繁访问的 DOM
- 在 DOM 树外更新节点,然后添加到 DOM 树,比如 DocumentFragment
- 动画能用 CSS 实现的不用 js 实现
- 防抖/节流
- 减少重绘重排
3. cookie 优化
- 消除不必要的 cookie
- 尽可能减小 cookie 的大小
- 注意设置 cookie 到合适的域名级别,则其它子域名不会被影响
- 正确设置 Expires 日期
- 静态资源请求没有 cookie,比如将静态资源放在全新的域下
4. HTTP 优化
- 最小化请求数
- 预加载资源
- 缓存策略
5. performance API 的使用
window.performance
const timingInfo = window.performance.timing
// TCP连接耗时
timingInfo.connectEnd - timingInfo.connectStart
// DNS查询耗时
timingInfo.domainLookupEnd - timingInfo.domainLookupStart
// 获得首字节耗费时间,也叫TTFB
timingInfo.responseStart - timingInfo.navigationStart
// domReady时间(与前面提到的DomContentLoad事件对应)
timingInfo.domContentLoadedEventStart - timingInfo.navigationStart
// DOM资源下载
timingInfo.responseEnd - timingInfo.responseStart
web 安全
XSS(跨站脚本攻击)
跨站脚本攻击(XSS):为什么 Cookie 中有 HttpOnly 属性?
CSRF(跨站伪造攻击)
错误监控及上报
一、类型及解决方式
1. 运行时错误
- try...catch
- window.onerror
- window.addEventListener('error')
2. 资源加载错误(图片)
- img.onerror
3. script Error(跨域代码)
原因:
跨域访问的 js 内部报错,浏览器处于安全考虑,不会报告具体的错误堆栈和行号,只抛出 script error 错误
解决:
- script 添加 crossorigin
- 服务端设置 Access-Control-allow-origin 为 * 或 访问域
二、错误上报
- ajax
- image 的 src((new Image()).src = '错误上报的请求地址') 使用图片发送 get 请求,上报信息,由于浏览器对图片有缓存,同样的请求,图片只会发送一次,避免重复上报
Webpack
loader 和 plugin 区别及原理
Git
git pull 和 git fetch 的区别?
- fetch:相当于是从远程获取最新版本到本地,不会自动 merge
- git pull:相当于是从远程获取最新版本并 merge 到本地
2. git rebase
场景题
1. 音乐播放器
2. 虚拟列表
3. 数据预加载
未完待续...
网友评论