0%

Class组件和函数组件的区别

Class组件

  • 有组件实例
  • 有生命周期
  • 有state和setState

函数组件

  • 没有组件实例
  • 没有生命周期
  • 没有state和setState,只能接收props
  • 函数组件是一个纯函数,执行完立即销毁,无法存储state

Class组件存在的问题

  • 大型组件很难拆分和重构,变得难以测试
  • 相同业务逻辑分散到各个方法中,可能会变得混乱
  • 复用逻辑可能变得复杂,比如 HOC、Render Props

所以 react 中更提倡函数式编程,因为函数更灵活,更易拆分,但函数组件太简单,所以出现了hook,hook就是用来增强函数组件功能的。

useState为什么不能放到条件语句里面?

react通过单链表来管理hooks。update阶段,hooks函数执行的顺序是不变的,就可以根据这个链表拿到当前hooks对应的Hook对象。如果将useState写在条件判断中,可能会导致顺序错乱,导致当前hooks拿到的不是自己对应的Hook对象。

实现一个Promise.all

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function promiseAll(promises){
return new Promise((resolved, rejected) => {
let resultCount = 0;
let results = new Array(promises.length);

for(let i = 0; i < promises.length; i++){
promises[i].then(value => {
resultCount++
results[i] = value
if(resultCount === promises.length){
return resolved(results)
}
}, error => {
rejected(error)
})
}
})
}

使用Redux的好处,以及和Mobx的区别

Redux的三大优势:

  1. 单一数据源
  2. 状态是只读的
  3. 状态的改变只能通过纯函数改变

Redux和Mobx区别:

  1. Redux将数据保存在单一的store中;而Mobx将数据保存在分散的多个store中
  2. Redux使用简单对象保存数据,需要手动处理变化后的操作;Mobx使用observable保存数据,数据变化后自动处理响应的操作。
  3. Redux使用的是不可变状态,意味着状态只是只读的,不能直接去修改它,而是应该通过纯函数改变返回一个新的状态;Mobx中的状态是可变的,可以直接对其进行修改
  4. Redux比较复杂,因为其中的函数式编程思想掌握起来不是那么容易,同时需要借助一系列的中间件来处理异步和副作用;Mobx相对比较简单,在其中有很多的抽象,使用的更多是面向对象的思维
  5. Redux提供可以进行时间回溯的开发工具,同时其纯函数以及更少的抽象,调试比较容易;Mobx中有更多的抽象和封装,调试起来比较复杂,同时结果也更难以预测

React SSR是怎么实现的?

所谓同构,通俗的讲,就是一套 React 代码在服务器上运行一遍,到达浏览器又运行一遍。 服务端渲染完成页面结构,客户端渲染绑定事件。

  • 服务端执行流程:在服务端使用react-dom/server下的renderToString将React组件转化为string,拼接在html中进行返回。此时html中不包含元素对应的事件。打包时把react-dom下的hydrate的逻辑打包到js中,拼接在html中作为script标签返回,提供给客户端运行使用
  • 浏览器执行流程:请求html,渲染html返回的页面内容并下载js文件,此时页面显示元素但不可交互,运行js中的ReactDom.hydrate给页面元素绑定事件,页面可交互。

有用过代码规范相关的吗?Eslint 和 Prettier 冲突怎么解决?

https://www.jianshu.com/p/b3a693cdcee9

实现一个数组转树形结构的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const data = [
{ id: 1, text: 't1', parentId: 0 },
{ id: 11, text: 't11', parentId: 1 },
{ id: 12, text: 't12', parentId: 1 },
{ id: 2, text: 't2', parentId: 0 },
{ id: 21, text: 't21', parentId: 2 },
{ id: 3, text: 't3', parentId: 0 }
]

function arrToTree(data, id, parentId, children){
let cloneData = JSON.parse(JSON.stringify(data))
return cloneData.filter(father => {
let newArr = cloneData.filter(child => {
return father[id] === child[parentId]
})
father[children] = newArr
return father[parentId] === 0
})
}

const treeData = arrToTree(data, 'id', 'parentId', 'children')

React性能优化

  • 使用React.Memo来缓存组件

    提升应用程序性能的一种方法是实现memoization。Memoization是一种优化技术,主要通过存储昂贵的函数调用的结果,并在再次发生相同的输入时返回缓存的结果,以此来加速程序。父组件的每次状态更新,都会导致子组件重新渲染,即使传入子组件的状态没有变化,为了减少重复渲染,我们可以使用React.memo来缓存组件,这样只有当传入组件的状态值发生变化时才会重新渲染。如果传入相同的值,则返回缓存的组件。示例如下:

    1
    2
    3
    4
    5
    export default React.memo((props) => {
    return (
    <div>{props.value}</div>
    )
    });
  • 使用useMemo缓存大量的计算

    有时渲染是不可避免的,但如果您的组件是一个功能组件,重新渲染会导致每次都调用大型计算函数,这是非常消耗性能的,我们可以使用新的useMemo钩子来“记忆”这个计算函数的计算结果。这样只有传入的参数发生变化后,该计算函数才会重新调用计算新的结果。通过这种方式,您可以使用从先前渲染计算的结果来挽救昂贵的计算耗时。总体目标是减少JavaScript在呈现组件期间必须执行的工作量,以便主线程被阻塞的时间更短。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 避免这样做
    function Component(props) {
    const someProp = heavyCalculation(props.item);
    return <AnotherComponent someProp={someProp} />
    }

    // 只有 `props.item` 改变时someProp的值才会被重新计算
    function Component(props) {
    const someProp = useMemo(() => heavyCalculation(props.item), [props.item]);
    return <AnotherComponent someProp={someProp} />
    }
  • 避免使用内联对象

    使用内联对象时,react会在每次渲染时重新创建对此对象的引用,这会导致接收此对象的组件将其视为不同的对象,因此,该组件对于prop的浅层比较始终返回false,导致组件一直重新渲染。许多人使用的内联样式的间接引用,就会使组件重新渲染,可能会导致性能问题。为了解决这个问题,我们可以保证该对象只初始化一次,指向相同引用。另外一种情况是传递一个对象,同样会在渲染时创建不同的引用,也有可能导致性能问题,我们可以利用ES6扩展运算符将传递的对象解构。这样组件接收到的便是基本类型的props,组件通过浅层比较发现接受的prop没有变化,则不会重新渲染。示例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // Don't do this!
    function Component(props) {
    const aProp = { someProp: 'someValue' }
    return <AnotherComponent style={{ margin: 0 }} aProp={aProp} />
    }

    // Do this instead :)
    const styles = { margin: 0 };
    function Component(props) {
    const aProp = { someProp: 'someValue' }
    return <AnotherComponent style={styles} {...aProp} />
    }
  • 避免使用匿名函数

    虽然匿名函数是传递函数的好方法(特别是需要用另一个prop作为参数调用的函数),但它们在每次渲染上都有不同的引用。这类似于上面描述的内联对象。为了保持对作为prop传递给React组件的函数的相同引用,您可以将其声明为类方法(如果您使用的是基于类的组件)或使用useCallback钩子来帮助您保持相同的引用(如果您使用功能组件)。当然,有时内联匿名函数是最简单的方法,实际上并不会导致应用程序出现性能问题。这可能是因为在一个非常“轻量级”的组件上使用它,或者因为父组件实际上必须在每次props更改时重新渲染其所有内容。因此不用关心该函数是否是不同的引用,因为无论如何,组件都会重新渲染。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 避免这样做
    function Component(props) {
    return <AnotherComponent onChange={() => props.callback(props.id)} />
    }

    // 优化方法一
    function Component(props) {
    const handleChange = useCallback(() => props.callback(props.id), [props.id]);
    return <AnotherComponent onChange={handleChange} />
    }

    // 优化方法二
    class Component extends React.Component {
    handleChange = () => {
    this.props.callback(this.props.id)
    }
    render() {
    return <AnotherComponent onChange={this.handleChange} />
    }
    }
  • 延迟加载不是立即需要的组件

    延迟加载实际上不可见(或不是立即需要)的组件,React加载的组件越少,加载组件的速度就越快。因此,如果您的初始渲染感觉相当粗糙,则可以在初始安装完成后通过在需要时加载组件来减少加载的组件数量。同时,这将允许用户更快地加载您的平台/应用程序。最后,通过拆分初始渲染,您将JS工作负载拆分为较小的任务,这将为您的页面提供响应的时间。这可以使用新的React.Lazy和React.Suspense轻松完成。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 延迟加载不是立即需要的组件
    const MUITooltip = React.lazy(() => import('@material-ui/core/Tooltip'));
    function Tooltip({ children, title }) {
    return (
    <React.Suspense fallback={children}>
    <MUITooltip title={title}>
    {children}
    </MUITooltip>
    </React.Suspense>
    );
    }

    function Component(props) {
    return (
    <Tooltip title={props.title}>
    <AnotherComponent />
    </Tooltip>
    )
    }
  • 调整CSS而不是强制组件加载和卸载

    渲染成本很高,尤其是在需要更改DOM时。每当你有某种手风琴或标签功能,例如想要一次只能看到一个项目时,你可能想要卸载不可见的组件,并在它变得可见时将其重新加载。如果加载/卸载的组件“很重”,则此操作可能非常消耗性能并可能导致延迟。在这些情况下,最好通过CSS隐藏它,同时将内容保存到DOM。尽管这种方法并不是万能的,因为安装这些组件可能会导致问题(即组件与窗口上的无限分页竞争),但我们应该选择在不是这种情况下使用调整CSS的方法。另外一点,将不透明度调整为0对浏览器的成本消耗几乎为0(因为它不会导致重排),并且应尽可能优先于更该visibility 和 display。有时在保持组件加载的同时通过CSS隐藏可能是有益的,而不是通过卸载来隐藏。对于具有显著的加载/卸载时序的重型组件而言,这是有效的性能优化手段。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 避免对大型的组件频繁对加载和卸载
    function Component(props) {
    const [view, setView] = useState('view1');
    return view === 'view1' ? <SomeComponent /> : <AnotherComponent />
    }

    // 使用该方式提升性能和速度
    const visibleStyles = { opacity: 1 };
    const hiddenStyles = { opacity: 0 };
    function Component(props) {
    const [view, setView] = useState('view1');
    return (
    <React.Fragment>
    <SomeComponent style={view === 'view1' ? visibleStyles : hiddenStyles}>
    <AnotherComponent style={view !== 'view1' ? visibleStyles : hiddenStyles}>
    </React.Fragment>
    )
    }
  • 使用React.Fragment避免添加额外的DOM

  • 虚拟化长列表

    react-window 或 react-virtualized

  • 不可变数据

    Immer 或 immutability-helper

  • 使用生产版本

实现一个深拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function deepClone(obj){
function isObject(o){
return (typeof o === 'object' || typeof o === 'function') && o !== null
}

if(!isObject(obj)){
throw new Error('非对象')
}

let isArray = Array.isArray(obj)
let newObj = isArray ? [...obj] : {...obj}
Reflect.ownKeys(newObj).forEach(key => {
newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
})

return newObj
}

微前端是怎么实现的?怎么独立部署?子应用通信怎么做?

Webpack构建流程

  1. 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler
  2. 编译:从 Entry 出发,针对每个 Module 串行调用对应的 Loader 去翻译文件的内容,再找到该 Module 依赖的 Module,递归地进行编译处理
  3. 输出:将编译后的 Module 组合成 Chunk,将 Chunk 转换成文件,输出到文件系统中

在webpack运行的生命周期中会广播很多事件,plugin可以监听这些事件,在合适的时机通过webpack提供的api改变输出结果。

Loader 和 Plugin 的原理和区别

  • Loader用于对模块文件进行编译转换和加载处理,在module.rules数组中进行配置,它用于告诉Webpack在遇到哪些文件时使用哪些Loader去加载和转换。
  • Plugin用于扩展Webpack功能,实现原理是在构建流程里注入钩子函数,在合适的时机通过webpack提供的api改变输出结果。在plugins数组中进行配置。

webpack 怎么做分包?

webpack 性能优化

react diff的复杂度,以及react diff的原理?

react hooks的优缺点?

从输入url到页面渲染经过了哪些步骤?

知道BFC吗?使用场景有哪些?

BFC 即 Block Formatting Contexts (块级格式化上下文)。
具有 BFC 特性的元素可以看作是隔离了的独立容器,容器里面的元素不会在布局上影响到外面的元素,并且 BFC 具有普通容器所没有的一些特性。通俗一点来讲,可以把 BFC 理解为一个封闭的大箱子,箱子内部的元素无论如何翻江倒海,都不会影响到外部。

怎么判断是否是数组?

用instanceof判断

1
2
const a = [];
console.log(a instanceof Array); //true

用Object的toString方法判断

1
2
3
const a = [];
Object.prototype.toString.call(a); // "[object Array]"
Object.prototype.toString.apply(a); // "[object Array]"

用Array对象的isArray方法判断

1
2
const a = [];
Array.isArray(a); //true

页面卡顿怎么去定位?

数组有10万个数据,取第一个和取第10万个的耗时多久?

工作中遇到最难的问题?

防抖和节流

防抖定义

防抖就是要延迟执行,你一直操作触发事件一直不执行,当你停止操作等待多少秒后才执行。

也就是说不管事件触发频率有多高,一定在事件触发 n 秒后执行。如果在事件触发的 n 秒又触发了这个事件,那就以新事件的事件为准,n 秒后才执行。总之,要等你触发完事件 n 秒内不再触发事件,它才执行。

手写防抖

根据定义,我们知道要在时间 n 秒后执行,那么我们就用定时器来实现:

1
2
3
4
5
6
7
8
9
function debounce(func, wait) {
let timer = null;
return function (...args){
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}

代码很简单,即当还在触发事件时,就清除 timer,使其在 n 秒后执行,但此写法首次不会立即执行,为其健壮性,需加上判断是否第一次执行的第三个参数 flag,判断其是否立即执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
function debounce(event, wait, flag) {
let timer = null;
return function (...args) {
clearTimeout(timer)
if (!timer && flag) {
event.apply(this, args)
} else {
timer = setTimeout(() => {
event.apply(this, args)
}, wait)
}
}
}

防抖场景

窗口大小变化,调整样式

1
window.addEventListener('resize', debounce(handleResize, 200))

搜索框,输入后300毫秒搜索

1
debounce(fetchSelectData, 300)

表单验证,输入 1000 毫秒后验证

1
debounce(validator, 1000)

节流定义
顾名思义,一节一节的流,就好似控制水阀,在事件不断触发的过程中,固定时间内执行一次事件。

手写节流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function throttle(func, wait) {
let pre = 0, timer = null;
return function (...args) {
if (new Date() - pre > wait) {
clearTimeout(timer);
timer = null;
pre = new Date();
func.apply(this, args)
} else {
timer = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
}

节流场景

scroll 滚动

1
window.addEventListener('scroll', throttle(handleScroll, 200))

input 动态搜索

1
throttle(fetchInput, 300)

http2的相关特性?

  • 二进制分帧(HTTP/2 将请求和响应数据分割为更小的帧,并且它们采用二进制编码)
  • 多路复用
  • 服务器推送
  • 头部压缩

viewport和移动端布局方案?

实现一个compose函数

1
2
3
4
5
6
7
8
9
function compose(...fns) { // fns是传入的函数
const fn = fns.pop();
return (...args) => {
fn(...args);
if (fns.length > 0) {
compose(...fns);
}
};
}

React Fiber?

React内部实现的一套状态更新机制。支持任务不同优先级,可中断与恢复,并且恢复后可以复用之前的中间状态。

怎么优化h5加载速度?怎么实现h5页面秒开?

https://segmentfault.com/a/1190000041701111

js bridge通信原理?

useReducer比redux好在哪里?

HTTP 和 HTTPS 的区别

https://www.51cto.com/article/701195.html

HTTP 常见的状态码

1 表示消息
2 表示成功
3 表示重定向
4 表示请求错误
5 表示服务器错误

1xx(代表请求已被接受,需要继续处理。这类响应是临时响应,只包含状态行和某些可选的响应头信息,并以空行结束)

  • 100(客户端继续发送请求,这是临时响应):这个临时响应是用来通知客户端它的部分请求已经被服务器接收,且仍未被拒绝。客户端应当继续发送请求的剩余部分,或者如果请求已经完成,忽略这个响应。服务器必须在请求完成后向客户端发送一个最终响应
  • 101:服务器根据客户端的请求切换协议,主要用于websocket或http2升级

2xx(代表请求已成功被服务器接收、理解、并接受)

  • 200(成功):请求已成功,请求所希望的响应头或数据体将随此响应返回
  • 204(无内容):服务器成功处理请求,但没有返回任何内容

3xx(表示要完成请求,需要进一步操作。 通常,这些状态代码用来重定向)

  • 301(永久移动):请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置
  • 302(临时移动): 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求
  • 304:协商缓存,告诉客户端有缓存,直接使用缓存中的数据,返回页面的只有头部信息,是没有内容部分

4xx(代表了客户端看起来可能发生了错误,妨碍了服务器的处理)

  • 400(错误请求): 服务器不理解请求的语法
  • 401(未授权): 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应
  • 403(禁止): 服务器拒绝请求
  • 404(未找到): 服务器找不到请求的网页
  • 405(方法禁用): 禁用请求中指定的方法
  • 408(请求超时): 服务器等候请求时发生超时

5xx(表示服务器无法完成明显有效的请求。这类状态码代表了服务器在处理请求的过程中有错误或者异常状态发生)

  • 500(服务器内部错误):服务器遇到错误,无法完成请求
  • 501(尚未实施):服务器不具备完成请求的功能。 例如,服务器无法识别请求方法时可能会返回此代码
  • 502(错误网关): 服务器作为网关或代理,从上游服务器收到无效响应
  • 503(服务不可用): 服务器目前无法使用(由于超载或停机维护)
  • 504(网关超时): 服务器作为网关或代理,但是没有及时从上游服务器收到请求

好处

  • 解决命名冲突
  • 提供复用性
  • 提高代码可维护性

立即执行函数

1
2
3
4
(function(globalVariable){
globalVariable.test = function(){}
// ...声明各种变量、函数都不会污染全局作用域
})(globalVariable)

AMD和CMD

1
2
3
4
5
6
7
8
9
10
11
12
13
// AMD(require.js)
define(['./a', './b'], function(a, b){
// 加载完毕可以使用
a.do()
b.do()
})

// CMD(sea.js)
define(function(require, exports, module){
// 可以把require写在函数体的任意地方实现延迟加载
var a = require('./a')
a.do()
})

CommonJS

1
2
3
4
5
// 引入模块
const path = require('path')
// 导出模块
const path = () => {}
module.exports = path

特点:

  • require()是同步加载模块
  • 是基于值的拷贝
  • node环境中默认使用CommonJS规范

ES Module

1
2
3
4
5
6
// 引入模块
import path from 'path'
import { doSomeThing } from 'path'
// 导出模块
export const doSomeThing = () => {}
export default path

特点:

  • import是异步加载模块
  • 基于值的引用
  • ES Module会编译成 require/exports 来执行

语法

1
let p = new Proxy(target, handler)

参数

  1. target: 需要使用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组、函数、甚至另一个代理)
  2. handler: 一个对象,其属性是当执行一个操作时定义代理的行为的函数(可以理解为某种触发器)

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let xiaoming = {
name: '小明',
age: 30
}
xiaoming = new Proxy(xiaoming, {
get(target, key){
let result = target[key]
if(key === 'age') result += '岁'
return result
},
set(target, key, value){
if(key === 'age' && typeof value !== 'number'){
throw Error('age字段必须是number类型')
}
return Reflect.set(target, key, value)
}
})
console.log(`我叫${xiaoming.name} 我今年${xiaoming.age}了`) // 我叫小明,我今年30岁了
  1. 首先创建了一个test对象,里面有name属性
  2. 然后使用Proxy将其包装起来,在返回给test
  3. 此时test已经成为了一个Proxy实例,我们对其的操作,都会被Proxy拦截

特点

  1. 三种状态:分别是等待中(pending), 完成了(resolved),拒绝了(rejected)
  2. 状态一旦从等待中变成其他状态就永远不能更改状态
  3. 状态一旦改变不可取消

实现一个简易版 Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'

function MyPromise(fn){
const that = this
that.state = PENDING
that.value = null
that.resolvedCallbacks = []
that.rejectedCallbacks = []

function resolve(value){
if(that.state === PENDING){
that.state = RESOLVED
that.value = value
that.resolvedCallbacks.map(cb => cb(that.value))
}
}

function reject(value){
if(that.state === PENDING){
that.state = REJECTED
that.value = value
that.rejectedCallbacks.map(cb => cb(that.value))
}
}

try{
fn(resolve, reject)
}catch(e){
reject(e)
}
}

MyPromise.prototype.then = function(onFulfilled, onRejected){
const that = this
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
onRejected = typeof onFulfilled === 'function' ? onRejected : err => { throw err }

if(that.state === PENDING){
that.resolvedCallbacks.push(onFulfilled)
that.rejectedCallbacks.push(onRejected)
}

if(that.state === RESOLVED){
onFulfilled(that.value)
}

if(that.state === REJECTED){
onRejected(that.value)
}
}

new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 0)
}).then(value => {
console.log(value)
})

缓存位置

从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络

Service Worker

Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。

Memory Cache

Memory Cache 也就是内存中的缓存,读取内存中的数据肯定比磁盘快。

Disk Cache

Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。

Push Cache

Push Cache 是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。并且缓存时间也很短暂,只在会话(Session)中存在,一旦会话结束就被释放。

缓存策略

强缓存

  • Expires

    1
    Expires: Wed, 22 Oct 2018 08:41:00 GMT

    Expires 是 HTTP/1 的产物,表示资源会在 Wed, 22 Oct 2018 08:41:00 GMT 后过期,需要再次请求。并且 Expires 受限于本地时间,如果修改了本地时间,可能会造成缓存失效。

  • Cache-Control

    1
    Cache-control: max-age=30

    Cache-Control 出现于 HTTP/1.1,优先级高于 Expires 。该属性值表示资源会在 30 秒后过期,需要再次请求。

协商缓存

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

但是 Last-Modified 存在一些弊端:

  • 如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源
  • 因为 Last-Modified 只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源

因为以上这些弊端,所以在 HTTP / 1.1 出现了 ETag 。

  • ETag 和 If-None-Match
    ETag 类似于文件指纹,If-None-Match 会将当前 ETag 发送给服务器,询问该资源 ETag 是否变动,有变动的话就将新的资源发送回来。并且 ETag 优先级比 Last-Modified 高。

实际场景应用缓存策略

频繁变动的资源

对于频繁变动的资源,首先需要使用 Cache-Control: no-cache 使浏览器每次都请求服务器,然后配合 ETag 或者 Last-Modified 来验证资源是否有效。这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小。

代码文件

这里特指除了 HTML 外的代码文件,因为 HTML 文件一般不缓存或者缓存时间很短。

一般来说,现在都会使用工具来打包代码,那么我们就可以对文件名进行哈希处理,只有当代码修改后才会生成新的文件名。基于此,我们就可以给代码文件设置缓存有效期一年 Cache-Control: max-age=31536000,这样只有当 HTML 文件中引入的文件名发生了改变才会去下载最新的代码文件,否则就一直使用缓存。

原始数据类型

  • boolean
  • null
  • undefined
  • null
  • string
  • symbol

注意:
1、原始类型存储的都是值,是没有函数可以调用的
2、typeof null输出object,这是JS存在的一个悠久Bug

对象类型

在JS中,除了原始类型,其他都是对象类型。对象类型和原始类型不同的是,原始类型存储的是值,对象类型存储的是地址(指针)。当你创建了一个对象类型的时候,计算机会在内存中开辟一个空间来存放值,但是我们需要找到这个空间,这个空间会拥有一个地址(指针)。

typeof vs instanceof

typeof

typeof 对于原始类型来说,除了null都是可以显示正确的类型

1
2
3
4
5
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol // 'symbol'

typeof 对于对象来说,除了函数都会显示object,所以 typeof 并不能准确判断变量到底是什么类型

1
2
3
typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'

instanceof

如果我们想判断一个对象的正确类型,这时候可以考虑使用 instanceof,因为内部机制是通过原型链来判断的

1
2
3
4
5
6
7
8
9
const Person = function() {}
const p1 = new Person()
p1 instanceof Person // true

var str = 'hello world'
str instanceof String // true

var str1 = new String('hello world')
str1 instanceof String // true

对于原始类型来说,想直接通过 instanceof 来判断是不行的,当然我们还是有办法让 instanceof 判断原始类型的

类型转换

this

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo(){
console.log(this.a)
}
var a = 1
foo()

const obj = {
a: 2,
foo: foo
}
obj.foo()

const c = new foo()
  • 对于直接调用foo来说,不管foo函数被放在了什么地方,this一定是window
  • 对于obj.foo()来说,谁调用了函数,谁就是this,所以在这个场景下foo函数中this就是obj对象
  • 对于new方式来说,this被永远绑定在了c上面,不会被任何方式改变this

箭头函数中的this

1
2
3
4
5
6
7
8
function a(){
return () => {
return () => {
console.log(this)
}
}
}
console.log(a()())

箭头函数是没有this的,箭头函数中的this只取决包裹箭头函数的第一个普通函数的this。在这个例子中,因为包裹箭头函数的第一个普通函数是a,所以此时的this是window。另外对箭头函数使用bind这类函数是无效的。

最后这种情况就是bind这些改变上下文的API了,对于这些函数来说,this取决于第一个参数,如果第一个参数为空,那么就是window。

1
2
3
let a = {}
let fn = function() { console.log(this) }
fn.bind().bind(a) // window

可以从上述代码中发现,不管我们给函数bind几次,fn中的this永远由第一次bind决定,所以结果永远是widnow。

以上就是this的规则了,但是可能会发生多个规则同事出现的情况,这时候不同的规则之间会根据优先级最高的来决定this最终指向哪里。

new —> bind —> obj.foo() -> foo()

闭包

闭包的定义:函数A内部有个函数B,函数B可以访问函数A中的变量,那么函数B就是闭包

1
2
3
4
5
6
7
8
function A(){
let a = 1
window.B = function(){
console.log(a)
}
}
A()
B() // 1

在JS中,闭包存在的意义就是让我们可以间接访问函数内部的变量。

经典面试题,循环中使用闭包解决var定义函数的问题

1
2
3
4
5
for(var i = 0;i <= 5; i++){
setTimeout(function timer(){
console.log(i)
}, i * 1000)
}

首先因为setTimeout是个异步函数,所以会等循环全部执行完毕,这时候i就是6了,所以会输出一堆6。

解决办法有三种,第一种是使用闭包方式

1
2
3
4
5
6
7
for(var i = 0; i <= 5; i++){
;(function(j){
setTimeout(function timer(){
console.log(j)
}, j * 1000)
})(i)
}

在上述代码中,首先使用了立即执行函数将传入函数内部,这时候值就被固定在了参数j上面不会改变,当下次执行timer这个闭包的时候,就可以使用外部函数的变量j,从而达到母的。

第二种就是使用setTimeout的第三个参数,这个参数会被当成timer函数的参数传入。

1
2
3
4
5
for(var i = 1; i <= 5; i++){
setTimeout(function timer(j){
console.log(j)
}, i * 1000, i)
}

第三种就是使用let定义i来解决问题,这个也是最为推荐的方式。

1
2
3
4
5
for(let i = 0; i <= 5; i++){
setTimeout(function timer(){
console.log(i)
}, i * 1000)
}

深浅拷贝

我们了解了对象在赋值的过程中其实是复制了地址,从而会导致改变了一方其他也都被改变的情况。通常在开发过程中我们不希望出现这样的情况,我们可以使用浅拷贝来解决这个情况。

1
2
3
4
5
6
let a = {
age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2

浅拷贝

Object.assign

只会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址,所以并不是深拷贝

1
2
3
4
5
6
let a = {
age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1

展开运算符(…)

1
2
3
4
5
6
let a = {
age: 1
}
let b = {...a}
a.age = 2
console.log(b.age) // 1

通常浅拷贝就能解决大部分问题了,但是当我们遇到如下情况就可能需要使用到深拷贝了

1
2
3
4
5
6
7
8
9
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = {...a}
a.jobs.first = 'native'
console.log(b.jobs.first) // native

浅拷贝只解决了第一层的问题,如果接下去的值还有对象的话,那么就又回到最开始的话题了,两者享有相同的地址。要解决这个问题,我们就得使用深拷贝了。

深拷贝

这个问题通常可以通过JSON.parse(JSON.stringify(object))来解决

1
2
3
4
5
6
7
8
9
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE

但是该方法也是有局限性的:

  • 会忽略undefined
  • 会忽略symbol
  • 不能序列化函数
  • 不能解决循环引用的对象

在遇到函数、undefined或者symbol的时候,该对象也不能正常的序列化

1
2
3
4
5
6
7
8
let a = {
age: undefined,
sex: Symbol('male'),
jobs: function(){},
name: 'zhj'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // {name: 'zhj'}

手写实现简易版深拷贝

实现一个深拷贝是很困难的,需要我们考虑好多种边界情况,比如原型链如何处理、DOM如何处理等等,推荐使用lodash深拷贝函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function deepClone(obj){
function isObject(o){
return (typeof o === 'object' || typeof o === 'function') && o !== null
}

if(!isObject(obj)){
throw new Error('非对象')
}

let isArray = Array.isArray(obj)
let newObj = isArray ? [...obj] : {...obj}
Reflect.ownKeys(newObj).forEach(key => {
newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
})

return newObj
}

let obj = {
a: [1,2,3],
b: {
c: 2,
d: 3
}
}
let newObj = deepClone(obj)
newObj.b.c = 1
console.log(obj.b.c) // 2

原型和原型链

每个实例对象都有一个私有属性proto,指向它的构造函数的原型对象(prototype)。原型对象也有自己的proto,层层向上直到一个对象的原型对象为null。这一层层原型就是原型链。

继承(原型继承和Class继承)

组合继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Parent(value){
this.val = value
}
Parent.prototype.getValue = function(){
console.log(this.val)
}
function Child(value){
Parent.call(this, value)
}
Child.prototype = new Parent()

const child = new Child(1)
console.log(child.getValue()) // 1
console.log(child instanceof Parent) // true

原理:
1、子类的构造函数中通过Parent.call(this)继承父类中的属性
2、改变子类的原型为new Parent()类继承父类中的函数

优点:
1、构造函数可以传参,不会与父类引用属性共享
2、可以复用父类的函数

缺点:继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费。

寄生组合继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Parent(value){
this.val = value
}
Parent.prototype.getValue = function(){
console.log(this.val)
}
function Child(value){
Parent.call(this, value)
}
Child.prototype = Object.create(Parent.prototype, {
constructor: {
value: Child,
enumerable: false,
writable: true,
configurable: true
}
})

const child = new Child(1)
console.log(child.getValue()) // 1
console.log(child instanceof Parent) // true

原理:
1、将父类的原型赋值给了子类
2、将构造函数设置为子类

优点:
1、解决了无用的父类属性问题
2、还能正确找到子类的构造函数

Class 继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Parent{
constructor(value){
this.val = value
}
getValue(){
console.log(this.val)
}
}
class Child extends Parent{
constructor(value){
super(value)
}
}

const child = new Child(1)
console.log(child.getValue()) // 1
console.log(child instanceof Parent) // true

数组展平(递归)

展平一个数组,[[1,2],3,[[[4]],5]] => [1,2,3,4,5]

1
2
3
4
5
function flatten(arr){
return [].concat(
...arr.map(x => Array.isArray(x) ? flatten(x) : x)
)
}

函数节流

document.addEventListener(‘scroll’, throttle(console.log(‘滚动了’)))
过滤掉重复的滚动事件

1
2
3
4
5
6
7
8
9
function throttle(fn, delay = 60){
let lock = false
return (...args) => {
if(lock) return;
fn(...args);
lock = true;
setTimeout(() => {lock = false}, delay)
}
}

过滤掉重复的验证事件(用户输入停止后300ms触发验证)

1
2
3
4
5
6
7
function throttle(fn, delay, timer = null){
return (...args) => {
clearInterval(timer)
timer = setTimeout(fn.bind(null, ...args), delay)
// timer = setTimeout((...args) => fn(...args), delay)
}
}

柯里化

1
2
3
4
5
const curry = func => {
const g = (...allArgs) => allArgs.length >= func.length ?
func(...allArgs) : (...args) => g(...allArgs, ...args);
return g;
}

重要的Math函数

1
2
3
4
5
6
7
8
9
Math.abs 求绝对值
Math.ceil 向上取整
Math.floor 向下取整
Math.max 求最大值
Math.min 求最小值
Math.random 0~1之间的随机数
Math.sqrt 平方根
Math.sign 求数值的符号
Math.pow 求幂

分页计算

在一个分页表格中,给定每页显示条数(pageSize)和元素的序号(index),求页码

1
const pageNo = Math.ceil((index+1)/pageSize)

数组最大值

const A = [1,2,3,4,5]

1
const max = Math.max(...A)

生成20~30的随机数

const min = 20, max = 30;

1
Math.round(min + Math.random() * (max-min))

判断一个数是否时素数

1
2
3
4
5
6
7
8
9
10
11
12
function is_prime(n){
if(n <= 1) return;
const N = Math.floor(Math.sqrt(n));
let is_prime = true;
for(let i = 2; i <= N; i++){
if(n % i === 0){
is_prime = false
break;
}
}
return is_prime
}

数组相关操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Array.length 长度  [1,2,3].length -> 3
indexOf 获取元素的序号 [1,2,3].indexOf(2) -> 2
Array.isArray 判断是否是数组 Array.isArray([]) -> true
forEach 遍历
push/pop/shift/unshift 入栈、出栈、入队、出队
map 映射-11 [1,2,3].map(x => x*2) -> [2,4,6]
reduce 聚合-多对1 [1,2,3].map((x, y) => x+y) -> 6
filter 筛选 [1,2,3].filter(x => x > 2) -> [3]
Array.from 创建数组
concat 合并数组 [1,2].concat([3,4]) -> [1,2,3,4]
slice 剪切
splice 删除/插入/替换
reduceRight 从右到左reduce
sort 排序
every 所有元素符合某个条件 [1,2,3].every(x => x>0) -> true

Vue 响应式原理

Vue中的三个核心类:

  1. Observer: 给对象的属性添加getter和setter, 用于依赖收集派发更新
  2. Dep: 用于收集当前响应式对象的依赖关系,每个响应式对象都有dep实例。dep.subs = watcher[],当数据发生变更的时候通过dep.notify()通知各个watcher。
  3. Watcher: 观察者对象,render watcher, computed watcher, user watcher
  • 依赖收集
  1. initState, 对computed属性初始化时,就会触发computed watcher依赖收集
  2. initState, 对监听属性初始化时,触发user watcher依赖收集
  3. render, 触发render watcher依赖收集
  • 派发更新
  1. 组件中对响应的数据进行了修改,会触发setter逻辑
  2. dep.notify()
  3. 遍历所有subs,调用每个watcher的update方法

总结原理:当创建vue实例时,vue会遍历data里的属性,Object.defineProperty为属性添加getter和setter对数据的读取进行劫持。

getter: 依赖收集
setter: 派发更新

每个组件的实例都会有对应的watcher实例

计算属性的实现原理

computed watcher, 计算属性的监听器

computed watcher 持有一个dep实例,通过dirty属性标记计算属性是否需要重新求值。

当computed的依赖值改变后,就会通知订阅的watcher进行更新,对于computed watcher会将dirty属性设置为true,并且进行计算属性方法的调用。

  1. computed 所谓的缓存是指什么?

计算属性是基于它的响应式依赖进行缓存的,只有依赖发生改变的时候才会重新求值。

  1. 那computed缓存存在的意义是什么?或者你经常在什么时候使用?

比如计算属性方法内部操作非常的耗时,遍历一个极大的数组,计算一次可能要耗时1s

类型转换,格式转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const largeArray = [
{...},
{...},
] // 10w

data: {
id: 1
}

computed: {
currentItem: function(){
return largeArray.find(item => item.id === this.id)
}
stringId: function(){
return String(this.id)
}
}
  1. 以下情况,computed可以监听到数据的变化吗?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template
{{ storageMsg }}

computed: {
storageMsg: function(){
return sessionStorage.getItem('xxx')
},
time: function(){
return Date.now()
}
}

created(){
sessionStorage.setItem('xxx', 111)
}

onClick(){
sessionStorage.setItem('xxx', Math.random())
}

答案:不会。

Vue.nextTick的原理

1
2
3
4
5
6
Vue.nextTick(() => {
// TODO
})

await Vue.nextTick()
// TODO

Vue是异步执行dom更新的,一旦观察到数据的变化,把同一个event loop中观察数据变化的watcher推送进这个队列。在下一次事件循环时,Vue清空异步队列,进行dom的更新

异步队列执行顺序
Promise.then -> MutationObserver -> setImmediate -> setTimeout

vm.someData = ‘new value’, dom并不会马上更新,而是在异步队列被清除时才会更新dom.

事件循环执行顺序
宏任务 -> 微任务队列 -> UI render -> 宏任务

一般什么时候会用到nextTick呢?

在数据变化后要执行某个操作,而这个操作依赖因你数据改变而改变的dom,这个操作就应该被放到vue.nextTick回调中。

1
2
3
4
5
6
7
8
9
<template>
<div v-if="loaded" ref="test"></div>
</template>

async showDiv(){
this.loaded = true;
await Vue.nextTick();
this.$refs.test.xxx(); // 才能获取到ref
}

手写一个简单的vue, 实现响应式更新

  1. 新建一个目录
  • index.html 主页面
  • vue.js Vue主文件
  • compiler.js 编译模板,解析指令,v-model v-html
  • dep.js 收集依赖关系,存储观察者 // 以发布订阅的形式实现
  • observer.js 数据劫持
  • watcher.js 观察者对象类
  1. index.html
1
2
3
4
5
6
7
8
<!DOCTYPE html>
<html lang="cn">
<head>My Vue</head>
<body>
<div id="app"></div>
<script src="./index.js" type="module"></script>
</body>
</html>
  1. 初始化vue class, 新建vue.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 包括vue构造函数,接收各种配置参数等等
*/
export default class Vue{
constructor(options={}){
this.$options = options;
this.$data = options.data;
this.$methods = options.methods;

this.initRootElement(options);
// 利用Object.defineProperty将data的属性注入到vue实例中
this._proxyData(this.$data);
}
// 获取更元素,并存储到vue实例。简单检查一下传入的el是否合规
initRootElement(options){
if(typeof options.el === 'string'){
this.$el = document.querySelector(options.el);
}else if(options.el instanceof HTMLElement){
this.$el = options.el;
}

if(!this.$el){
throw new Error('传入的el不合法,请传入css selector或者HTMLElement')
}
}
_proxyData(data){
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get(){
return data[key];
},
set(newValue){
if(data[key] === newValue){
return;
}
data[key] = newValue
}
})
})
}
}
  1. 验证一下,新建index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Vue from './myvue/vue.js';

const vm = new Vue({
el: '#app',
data: {
msg: 'hello world'
},
methods: {
handle(){
alert(111)
}
}
})

console.log(vm)
  1. vue里可以通过this来获取data里的属性

排序算法

1. 冒泡排序

冒泡排序

普通版冒泡排序

1
2
3
4
5
6
7
8
9
10
11
function BubbleSort(array){
let len = array.length;
for(let i = 0; i < len; i++){
for(let j = 0; j < len-i-1; j++){
if(array[j] > array[j+1]){
[array[j], [array[j+1]] = [array[j+1], array[j]];
}
}
}
return array;
}

优化版冒泡排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function BubbleSort(originalArray){
const array = [...originalArray];
let swapped;
for(let i = 0; i < array.length; i++){
swapped = true;
for(let j = 0; j < array.length - i - 1; j++){
if(array[j] > array[j+1]){
[array[j], array[j+1]] = [array[j+1], array[j]];
swapped = false;
}
}
if(swapped){
break;
}
}
return array;
}

2. 选择排序

选择排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function SelectionSort(originalArray){
const array = [...originalArray];
let len = array.length;
for(let i = 0; i < len - 1; i++){
let minIndex = i;
for(let j = i + 1; j < len; j++){
if(array[j] < array[minIndex]){
minIndex = j;
}
}
if(minIndex !== i){
[array[minIndex], array[i]] = [array[i], array[minIndex]];
}
}
return array;
}

3. 插入排序

插入排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function InsertionSort(originalArray){
const array = [...originalArray];
let len = array.length;
for(let i = 0; i < len; i++){
let temp = array[i]
let j = i - 1;
while(j >= 0 && array[j] > temp){
array[j+1] = array[j];
j--;
}
array[j+1] = temp;
}
return array;
}

4. 归并排序

并归排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function MergeSort(array){
let len = array.length;
if(len <= 1){
return array;
}
let num = Math.floor(len / 2);
let left = MergeSort(array.slice(0, num));
let right = MergeSort(array.slice(num, len));
return merge(left, right);

function merge(left, right){
let [l, r] = [0, 0];
let result = [];
while(l < left.length && r < right.length){
if(left[l] < right[r]){
result.push(left[l]);
l++;
}else{
result.push(right[r]);
r++;
}
}
result = result.concat(left.slice(l, left.length));
result = result.concat(right.slice(r, right.length));
return result;
}
}

快速排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function QuickSort(array){
const len = array.length
if(len <= 1){
return array; // 如果只有一个数,就直接返回
}

let num = Math.floor(len / 2); // 找到中间数的索引值,如果是浮点数,则向下取整
let numValue = array.splice(num, 1); // 找到中间数的值
let left = []
let right = []
for(let i = 0; i < len; i++){
if(array[i] < numValue){
left.push(array[i]) // 基准点的左边数传到左边数组
}else{
right.push(array[i]) // 基准点的右边数传到右边数组
}
}
return QuickSort(left).concat([numValue, QuickSort(right)]);
}

面向对象编程思想

一:面向过程:注重解决问题的步骤,分析问题需要的每一步,实现函数依次调用;
二:面向对象:是一种程序设计的思想。将数据和处理数据的程序封装到对象中;
三:面向对象特性:抽象、继承、封装、多态;
四:面向对象优点:提高代码的复用性及维护性;

对象的创建

工厂模式

new运算符

构造函数

原型prototype

构造函数继承

原型的继承

原型链

包装对象

面向对象和面向过程编程

类和对象概念