1 react的key值给组件作为唯一标识,类似身份证,当数组发生增删改查的时候可以通过这个 标识来
去对比虚拟dom的更改前后,如果相同标识的数据或者属性没有改变的话,讲直接略过,对比有改变的
然后直接改变此项
2 如果给组件添加key,如果key值改变的时候,组件将会重新出创建会执行 挂在阶段的生命周期
constructor
static getDerivedStateFromProps()
render(){}
componentDidUpdate
知道react里面的createPortal么?说说使用场景。之前react没有这个的时
候是怎么实现的,有他没他的区别
react.createPortal 这个方法是来使用做弹窗Modal组件的,在没有这个组件之前我们可以自己定义组件,然
后去实现Modal效果
Modal.js
const styles = {
modal: {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.3)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}
}
class Modal extends Component {
constructor(props) {
super(props);
}
render() {
return (
{this.props.children}
);
}
}
react.createPortal 这个来制作弹窗组件,它 在Modal 组件位置进行 fixed 定位,可以任意的挂载到某
个dom元素上,使用后的管理更方便,但是注意需要预留html的挂载元素react全家桶
真正意义上的react全家桶,其实指的是react,react-dom,react-native。
因为react核心库只是vDom的操作,无关dom(dom操作在react-dom中)——这意味着它天生就可以做到跨
平台。
注意这里有误区,react-router,react-router-dom,react-dux只是社区的一些使用较多的解决方案,事实
上它们各有缺陷。
如何理解fiber?
截止到当前版本(react17.0.2),react尚存在一个明显的缺陷——这是大多数vDom框架都需要面对的——
“diff”。
要知道,render前后的两颗vDom tree进行diff,这个过程是不可中断的(以tree为单位,不可中断),这将
造成当diff的两颗tree足够庞大的时候,js线程会被diff阻塞,无法对并发事件作出回应。
为了解决这个问题,react将vDom节点上添加了链表节点的特性,将其改造成了fiber节点(其实就是
vdom节点结合了链表节点的特性),目的是为了后面的Fiber架构的实现,以实现应对并发事件的“并发
模式”。
事实上,原计划是17版本上的,但最终定期在了18版本。
时间分片,任务分炼
fiber节点的改造是为了将diff的单位从不可中断的tree降级到可中断的单一vDom。这意味着每次一个
vDom节点比对过后,可以轮训是否发生了优先级更高的并发事件(react将用户想更快得到反馈的事进行
了优先级排序,比如js动画等)。
每个vDom的比对是一个时间片单位,因此基于fiber节点的Fiber架构,存在这样的特性:
“时间分片,任务分炼,异步渲染。”
hooks
你了解hooks吗?谈谈你对hooks的理解?聊聊hooks?
react-hooks提供给了函数式组件以业务逻辑抽离封装的能力,从单一组件单位进行UI与业务逻辑的分
离,以此来保证函数式组件的UI纯净。
在此之前,我们只能通过状态提升的方式,将业务逻辑提升到容器组件的方式。
hooks使用规则?
只可以在顶层调用栈调用hooks
不可以在循环/条件/嵌套里调用hooks
只可以在react function或自定义hooks里调用hooks
原因见hooks调用记录表底层
hooks调用记录表底层_单链表结构
hooks的引用记录,是用单链表结构实现的;(可在力扣/leetcode网找一下实现单链表的算法)
每一个链表节点(node)都有一个next的指针指向下一个node;
如果中间有缺失,那么就无法找到下一个next;
因此hooks不可以在条件里调用(有可能丢失链表node,导致hooks遍历中断);useEffect与副作用?什么是副作用?聊聊useEffect?
副作用函数
我们将跟UI渲染无关的业务逻辑称之为副作用。
useEffect是react在函数式组件里提供的副作用解决方案,它接受两个参数。
第一个是必传参数,类型为函数。
我们写在此函数中的业务逻辑代码就是我们所说的副作用。
消除副作用函数
默认情况下,useEffct会在每次render后执行传入的副作用函数。
然而有的时候我们需要在执行副作用前先去除掉上次render添加的副作用效果,我们可以在副作用函
数里再返回一个函数———这个函数就是消除副作用,它会在每次reRender前和组件卸载时去执行。
监听器
与 useMemo, useCallback等一样,useEffect可以传入一个监听数组参数,这意味着副作用只有在数
组中的监听值变化时才会执行。
我们借助它的这个参数特性,可以模拟类组件生命周期钩子————比如componentDidMount等。
生命周期模拟?
useEffect的本质作用,其实是将函数式组件中的业务逻辑副作用进行抽离封装。模拟生命周期并不是它
的核心思想,但它当然能做到这个事:
https://github.com/melodyWxy/melody-core-utils/blob/master/src/lifeHooks/index.ts
useLayoutEffect?
与useEffect的区别是,useLayoutEffect中副作用的执行时间在组件render之前,这意味着它适用于比组件
render更紧急的副作用。
正因为如此,多数情况下我们不会选择使用它,因为我们并不想阻塞render。
useRef
变量值存储功能
首先,函数式组件每次render就会重新执行一次,这意味着我们在函数组件中定义的变量都会重
置,无法保留变更。
useRef可以解决这个问题——它会形成一个闭包作用域来记录这个值。
ref的通用功能
当然,我们也可以用它来标记dom和类组件,从而获取对应的实例。
useCallback
同样的,因为函数式组件每次render就会重新执行一次,因此我们在函数式组件中定义的方法都会重新
定义————多数情况下这是不必要的,因此我们可以用useCallback来缓存它。
当然,我们也可以基于它第二个参数——一个监听数组,来控制它在何种时机进行重定义。
useMemo
同样的,因为函数式组件每次render就会重新执行一次,因此我们在定义的一些变量或者计算过后的变
量,都会重新定义————多数情况下这是不必要的,因此我们可以用useCallback来缓存它。
当然,我们也可以基于它第二个参数——一个监听数组,来控制它在何种时机进行重定义。
reduxFlux
Flux 是一种 架构思想,简单来讲 就是:
状态提升到全局Store + Store内容分发给组件 + 组件交互updateStore然后更新组件。
就是这样一种 通过全局store同一管理数据(从而实现组件间数据共享)的架构思想。
redux与react-redux
基于这种思想,社区里已有各种各样成型的解决方案,其中,redux是通用性比较强的方案(兼容原生和
绝大部分框架);
但它的使用较为麻烦,在react项目里,一般用 redux + react-redux + 配置中间件(根据项目需要配置) 这样
的方式。
比如 dva = redux + react-redux + redux-saga + router (耦合了router也是dva的诟病所在);
在hooks出现之后,react提供了 useContext这样的钩子,实现了redux的功能。
react-redux 的核心:Provider 与 connect
Provider和connect的底层原理
Provider容器组件底层: 把store打入 contextconnect;
高阶组件底层:
从context拿到store,然后打入到参数组件的props;
什么是高阶组件?
高阶组件:接受一个组件为参数,return 出一个新的组件;
高阶函数:就是一个函数,接受一个函数为参数。
redux的核心:
createStore 与 disPatchcreateStore:
创建store
disPatch:
更新store的方法disPatch的规范/更新store的流程
react-redux流程
状态提升到全局与分发
createStore创建全局状态仓库
Provider和connnect进行状态的分发
状态更新
disPatch 一个 action (
action就是一个对象,形如{type:'xxx',...prarams}),基于action.type,触发
对应的reducer(
reducer是具体更新store的方法,纯函数)逻辑,从而更新store;
react组件的概念(高阶组件,受控组件,非受控组件)
高阶组件:
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它
是一种基于 React 的组合特性而形成的设计模式。
一 属性代理
1. 属性代理是最常见的实现方式,它本质上是使用组合的方式,通过将组件包装在容器组件中实现功能。
2. 属性代理方式实现的高阶组件和原组件的生命周期关系完全是React父子组件的生命周期关系,所以该方式实现的
高阶组件会影响原组件某些生命周期等方法。操作 props
抽象 state
获取 refs 引用
获取原组件的 static 方法
通过 props 实现条件渲染
二 反向继承
受控组件
1. 受控组件通过props获取其当前值,并通过回调函数(比如onChange)通知变化
2. 表单状态发生变化时,都会通知React,将状态交给React进行处理,比如可以使用useState存储
3. 受控组件中,组件渲染出的状态与它的value或checked属性相对应
4. 受控组件会更新state的流程
非受控组件
非受控组件将数据存储在DOM中,而不是组件内,这比较类似于传统的HTML表单元素。
1. 非受控组件的值不受组件自身的state和props控制
2. 非受控组件使用ref从DOM中获取元素数据
react类组件中的生命周期,整体分几步,常用的有哪些
三大阶段,如图
实例期
存在期
销毁期
render里面为什么不能使用setState
setState会重新执行render函数,如果添加setState会导致死循环
子组件中修改状态时会不会影响父组件,会不会触发生命周期
如果父组件中没有依赖子组件的状态不会
如果父组件中依赖了子组件的状态,因为要执行setState会重新触发生命周期
反向继承指的是使用一个函数接受一个组件作为参数传入,并返回一个继承了该传入组件的类组件,且在返回组件的
render() 方法中返回 super.render() 方法,最简单的实现如下:父组件改变时会不会影响子组件
会,因为会重新执行render函数,如果不想更新子组件可以在shouldComponentUpdate中终止掉
react页面跳转路由传参方法
跳转路由,并向路由组件传递params参数
{tabItem.title}
跳转路由,并向路由组件传递search参数
{tabItem.title}
跳转路由,并向路由组件传递state参数
{tabItem.title}
使用redux时,setState修改状态的过程是异步还是同步
类组件setState是同步还是异步
1. 使用合成事件 setState是异步的,想要拿到最新数据需要去回调中
2. 使用原生事件 setState是同步的
传值过程中,子传值能不能改变父组件,为什么?
不能,props为只读状态 单向数据流
hooks中常用的api有哪些
useState()
userContext()
userReducer()
useEffect()
useeffect中第二个参数有什么作用
如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组(
[])作为第二个
参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。
react常用的优化手段有哪些
1. 属性传递优化
2. 多组件优化
3. Key
4. memo
5. purecomponent
6. 生命周期
7. 虚拟列表
8. 使用纯组件
9. 懒加载组件
10. 使用 React Fragments 避免额外标记
11. 不要使用内联函数定义
12. 避免 componentWillMount() 中的异步请求
13. 在 Constructor 的早期绑定函数
14. 优化 React 中的条件渲染15. 不要在 render 方法中导出数据
16. 为组件创建错误边界
17. 组件的不可变数据结构
为什么要用redux,怎么修改数据
使用Redux的主要优势之一是它可以帮你处理应用的共享状态。如果两个组件需要访问同一状态,该怎么
办?这种两个组件同时需要访问同一状态的现象称为“共享状态”。你可以将该状态提升到附近的父组
件,但是如果该父组件在组件树中向上好几个组件的位置,那么将状态当做属性向下一个一个地传递,
这项工作很快就会变得乏味。此外,在该父组件和该子组件之间的组件甚至根本不需要访问该状态!
在构建网络应用时,Redux不仅使我们能够以有条理的方式存储数据,而且使我们能够在应用的任何位置
快速获取该数据。只需告诉Redux到底哪个组件需要哪个数据,它就会为你处理后续一切工作!
借助Redux,你可以控制状态改变的时间、原因和方式。
useEffect是干什么的
该 Hook 接收一个包含命令式、且可能有副作用代码的函数。
在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行
其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。
使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。你可以把 effect
看作从 React 的纯函数式世界通往命令式世界的逃生通道。
useEffect能完全模拟componentDidUpdate吗
不可以。 useEffect 并不完全等同于 componentWillUnMount 。当 useEffect 模拟 componentDidMount 和
componentDidUpdate 的时候,返回的函数并不完全等同于 componentWillUnMount。
useLayoutEffect和useEffect的区别
useEffect和useLayoutEffect作为组件的副作用,本质上是一样的。共用一套结构来存储effect链表。整体
流程上都是先在render阶段,生成effect,并将它们拼接成链表,存到fiber.updateQueue上,最终带到
commit阶段被处理。他们彼此的区别只是最终的执行时机不同,一个异步一个同步,这使得useEffect不
会阻塞渲染,而useLayoutEffect会阻塞渲染。
react和vue的数据流,底层原理
react路由和vue路由底层实现原理
eact-router和vue-router实现原理大同小异,都是通过两种方式,hash和history,首先看一下什么是hash
和history:
hash —— 即地址栏 URL 中的 # 符号(此 hash 不是密码学里的散列运算)。比如这个 URL:
http://www.abc.com/#/hello,hash 的值为 #/hello。它的特点在于:hash 虽然出现在 URL 中,但不
React遵循从上到下的数据流向,即单向数据流。
1、单向数据流并非‘单向绑定’,甚至单向数据流与绑定没有‘任何关系’。对于React来说,单向数据流(从上到下)
与单一数据源这两个原则,限定了React中要想在一个组件中更新另一个组件的状态
(类似于Vue的平行组件传参,或者是子组件向父组件传递参数),需要进行状态提升。即将状态提升到他们最近的祖
先组件中。子组件中Change了状态,触发父组件状态的变更,父组件状态的变更,
影响到了另一个组件的显示(因为传递给另一个组件的状态变化了,这一点与Vue子组件的$emit()方法很相似)。
2、Vue也是单向数据流,只不过能实现双向绑定。
3、单向数据流中的‘单向’-- 数据从父组件到子组件这个流向叫单向。
4、绑定的单双向:View层与Module层之间的映射关系。会被包括在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面。
history —— 利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。(需要特定
浏览器支持)这两个方法应用于浏览器的历史记录栈,在当前已有的 back、forward、go 的基础之
上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但
浏览器不会立即向后端发送请求。
1. hash模式(
vue-router默认hash模式)
hash模式背后的原理是onhashchange事件。
(
localtion是js里管理地址栏的内置对象,是window对象的一部分,可通过window.localtion访问,在
w3cshool里的详细介绍)
由于hash发生变化的url都会被浏览器记录下来,使得浏览器的前进后退都可以使用了,尽管浏览器没有
亲求服务器,但是页面状态和url关联起来。后来人们称其为前端路由,成为单页应用标配。
比如http://www.abc.com/#/index,hash值为#/index。hash模式的特点在于hash出现在url中,但是不会
被包括在HTTP请求中,对后端没有影响,不会重新加载页面。
2.history模式
当使用history模式时,url就像正常的url,例如http://abc.com/user/id相比hash模式更加好看。特别注意,
history模式需要后台配置支持。如果后台没有正确配置,访问时会返回404。
其中一事件触发用到了 onpopstate。
通过history api,我们丢弃了丑陋的#,但是有一个缺点,当刷新时,如果服务器中没有相应的相应或者
资源,会分分钟刷出一个404来(刷新需要请求服务器)。所以history模式不怕前进,不怕后退,就怕刷
新。
下述案例为实现的react-router的代码,来了解router大致实现原理
原理:
在dom渲染完成之后,给window 添加 “hashchange” 事件监听页面hash的变化,并且在state属性
之中添加了 route属性,代表当前页面的路由。
1、当点击连接 页面hash改变时,触发绑定在window 上的 hashchange 事件,
2、在 hashchange 事件中改变组件的 state中的 route 属性,(
react组件的state属性改变时,自动重新渲
染页面)
3、页面 随着 state 中的route属性改变自动 根据 不停的hash 给 Child 变量赋值不通的组件,进行渲染
核心代码:
window.οnhashchange=function(){
let hash=location.hash.slice(1);
document.body.style.color=hash;
}
import React from 'react'
import { render } from 'react-dom'
const About = function () {
return
}
const Inbox = function () {
return
}
const Home = function () {
return
}
class App extends React.Component {axios守卫(请求和响应守卫)
其实这个问题问的是axios请求封装种的请求拦截器和响应拦截器,比如如下的封装代码案例
state = {
route: window.location.hash.substr(1)
}
componentDidMount() {
window.addEventListener('hashchange', () => {
this.setState({
route: window.location.hash.substr(1)
})
})
}
render() {
let Child
switch (this.state.route) {
case '/about': Child = About; break;
case '/inbox': Child = Inbox; break;
default: Child = Home;
}
return (
)
}
}
render(
import axios from 'axios'
import router from './../router'
// development 开发环境 production 生产环境
const isDev = process.env.NODE_ENV === 'development'
// baseURL 会自动加到所有的请求之前
const request = axios.create({
// http://121.89.205.189/api/pro/list ==> /pro/list
baseURL: isDev ? 'http://121.89.205.189/api' : 'http://121.89.205.189/api',
timeout: 6000
})
// 请求拦截器
request.interceptors.request.use(config => {
// 传入token验证登录状态
// 如果需要设置其他的信息也可以在这里
config.headers.common.token = localStorage.getItem('token')dva之前有了解吗
这个其实是支付宝整合的一套框架,集成了 (redux + react-router + redux-saga 等)的一层轻量封装。dva
是 framework,不是 library,类似 emberjs。
他最核心的是提供了 app.model 方法,用于把 reducer, initialState, action, saga 封装到一起。
namespace - 对应 reducer 在 combine 到 rootReducer 时的 key 值
state - 对应 reducer 的 initialState
subscription - [email protected] 的新概念,在 dom ready 后执行,这里不展开解释
effects - 对应 saga,并简化了使用
reducers - 相当于数据模型
return config
}, err => Promise.reject(err))
// 响应拦截器封装
request.interceptors.response.use(response => {
// 如果验证未登录,可以跳转到登录页面
if (response.data.code === '10119') {
router.push('/login')
}
return response
}, err => Promise.reject(err))
export default request
app.model({
namespace: 'products',
state: {
list: [],
loading: false,
},
subscriptions: [
function(dispatch) {
dispatch({type: 'products/query'});
},
],
effects: {
['products/query']: function*() {
yield call(delay(800));
yield put({
type: 'products/query/success',
payload: ['ant-tool', 'roof'],
});
},
},
reducers: {
['products/query'](state) {
return { ...state, loading: true, };
},
['products/query/success'](state, { payload }) {
return { ...state, loading: false, list: payload };
},
},
});使用了Ant design ,如何自定义样式
方式1:添加global
可以在样式的外部添加:global
方式2:定制主题
可以参考链接https://ant.design/docs/react/customize-theme-cn
我们以 webpack@4 为例进行说明,以下是一个 webpack.config.js 的典型例子,对 less-loader 的
options 属性进行相应配置。
注意:
1. less-loader 的处理范围不要过滤掉 node_modules 下的 antd 包。
// 如何创建一个dva项目
$ npm install dva-cli -g
$ dva new myapp && cd myapp
$ npm start
:global{
.account-form{
background-color: red;
}
}
// webpack.config.js
module.exports = {
rules: [{
test: /\.less$/,
use: [{
loader: 'style-loader',
}, {
loader: 'css-loader', // translates CSS into CommonJS
}, {
loader: 'less-loader', // compiles Less to CSS
+ options: {
+ lessOptions: { // 如果使用less-loader@5,请移除 lessOptions 这一级直接配置选项。
+ modifyVars: {
+ 'primary-color': '#1DA57A',
+ 'link-color': '#1DA57A',
+ 'border-radius-base': '2px',
+ },
+ javascriptEnabled: true,
+ },
+ },
}],
// ...other rules
}],
// ...other config
}2. lessOptions 的配置写法在 [email protected] 里支持。
React 组件传值的方法
1. 父组件传递到子组件传值。又称正向传值 采用props进行数据的传递
2. 子组件传递到父组件传值。又称逆向传值 通过在父组件引入的子组件中传递一个函数并传参,子
组件去触发这个函数更改参数完成数据更新
3. 同级组件传值。又称同胞传值 通过一款插件pubsub-js 这个可以采用发布订阅的方式实现组件间的
传值 a组件通过publish进行发布数据。b组件通过subscribe订阅数据
4. 跨层级传值 通过上下文对象的形式进行数据的传递 通过Provider生产数据 需要接受的组件使用
consumer使用数据
5. redux也可以完成上述传值方式
useEffect模拟生命周期
模拟componentDidMount
hooks 模拟 componentDidMount
useEffect 拥有两个参数,第一个参数作为回调函数会在浏览器布局和绘制完成后调用,因此它不会阻碍
浏览器的渲染进程。
第二个参数是一个数组
当数组存在并有值时,如果数组中的任何值发生更改,则每次渲染后都会触发回调。
当它不存在时,每次渲染后都会触发回调。
当它是一个空列表时,回调只会被触发一次,类似于 componentDidMount。
模拟componentDidUpdate
没有第二个参数代表监听所有的属性更新
监听多个属性的变化需要将属性作为数组传入第二个参数。
模拟componentWillUnmount
通常,组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源 useEffect函数返回的函数可以表示
组件卸载了
useEffect(()=>{console.log('第一次渲染时调用')},[])
useEffect(()=>{console.log('任意属性该改变')})
useEffect(()=>{console.log('n变了')},[n,m])
useEffect(()=>{
const timer = setTimeout(()=>{
...
},1000)
return()=>{
console.log('组件卸载了')
clearTimerout(timer)
}
})hooks模拟生命周期
在 React 16.8 之前,函数组件只能是无状态组件,也不能访问 react 生命周期。hook 做为 react 新增特
性,可以让我们在不编写 class 的情况下使用 state 以及其他的 react 特性,例如生命周期。接下来我们
便举例说明如何使用 hooks 来模拟比较常见的 class 组件生命周期。
1. hooks 模拟 componentDidMount
useEffect 拥有两个参数,第一个参数作为回调函数会在浏览器布局和绘制完成后调用,因此它不
会阻碍浏览器的渲染进程。
第二个参数是一个数组
当数组存在并有值时,如果数组中的任何值发生更改,则每次渲染后都会触发回调。
当它不存在时,每次渲染后都会触发回调。
当它是一个空列表时,回调只会被触发一次,类似于 componentDidMount。
2. hooks 模拟 shouldComponentUpdate
React.memo 包裹一个组件来对它的 props 进行浅比较,但这不是一个 hooks,因为它的写法和
hooks 不同,其实React.memo 等效于 PureComponent,但它只比较 props。
3. hooks 模拟 componentDidUpdate
没有第二个参数代表监听所有的属性更新
监听多个属性的变化需要将属性作为数组传入第二个参数。
4. hooks 模拟 componentWillUnmount
通常,组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源 useEffect函数返回的函数可
以表示组件卸载了
useEffect(()=>{console.log('第一次渲染时调用')},[])
const MyComponent = React.memo(
_MyComponent,
(prevProps, nextProps) => nextProps.count !== prevProps.count
)
useEffect(()=>{console.log('任意属性该改变')})
useEffect(()=>{console.log('n变了')},[n,m])
useEffect(()=>{
const timer = setTimeout(()=>{
...
},1000)
return()=>{
console.log('组件卸载了')
clearTimerout(timer)
}
})react框架怎么做优化
1.render里面尽量减少新建变量和bind函数,传递参数时尽量减少传递参数的数量
第一种是在构造函数中绑定this,会在 构造函数 实例化的时候执行一次
第二种是在render()函数里面绑定this,在每次render()的时候都会重新执行一遍
第三种就是使用箭头函数,每一次render()的时候,都会生成一个新的箭头函数,即使两个箭头函数的内
容是一样的。
2.shouldComponentUpdate是决定react组件什么时候能够不重新渲染的函数,但是这个函数默认的实现
方式就是简单的返回一个true。也就是说,默认每次更新的时候都要调用所用的生命周期函数,包括
render函数,重新渲染。
为了不做不必要的渲染,需要使用shouldComponentUpdate 加以判断
最新的react中,react给我们提供了React.PureComponent
3.使用key进行组件的唯一标识
react的虚拟dom怎么实现
虚拟DOM可以看做一棵模拟了DOM树的JavaScript对象树。在传统的 Web 应用中,我们往往会把数据的
变化实时地更新到用户界面中,于是每次数据的微小变动都会引起 DOM 树的重新渲染。虚拟DOM的目的
是将所有操作累加起来,统计计算出所有的变化后,统一更新一次DOM。
React会先将你的代码转换成一个javascript对象,然后再把这个javascript对象转换成真实的DOM。而这个
javascript对象就是所谓的虚拟DOM。
1.基于babel-preset-react-app把JSX变成 React.createElement(...) 。可以理解为JSX只是
React.createElement的语法糖。
经过babel转换成下面的样子:
2.执行 React.createElement(...) 会返回一个javascript对象,这个对象就是所谓的虚拟DOM。
createElement会返回一个虚拟对象 let VitrualDomObj = React.createElement(...)
3.把这个虚拟DOM通过render函数转换成真实的DOM
123
456
React.createElement("div", {
className: "box",
style: {
color: '#f00'
},
index: 0
}, "123", React.createElement("p", null, "456")
);
myrender( VitrualDomObj , document.getElementById('root'));redux和mobx的区别?
1.共同点:
两者都是为了解决状态不好管理,无法有效同步的问题而产生的工具。
都是用来统一管理应用状态的工具
某一个状态只有一个可靠的数据来源
操作更新的方式是统一的,并且是可控的
都支持store与react组件,如react-redux,mobx-react;
2.不同点:
Redux每一次的dispatch都会从根reducer到子reducer嵌套递归的执行,所以效率相对较低;而Mobx的内
部使用的是依赖收集,所以不会有这个问题,执行的代码较少,性能相对更高;
Redux核心是不可变对象,在Reducer中的操作都要比较小心,注意不能修改到state的属性,返回时必须
是一个全新的对象;而Mobx采用不存在这个问题,操作比较随意;
Redux中写法固定,模板代码较多,Mobx中写法比较随意,但是因为写法随意的原因,如果没有规范性
的话,维护性则不会像Redux那么高;
正因为Redux中的reducer更新时,每次return的都是不可变对象,所以时间旅行操作相对容易,而Mobx
在这方面不占优势
Redux更加的轻量,但是一般来说都会配合中间件进行使用
react中跨级组件通信中的context是不推荐使用的,为什么?不使用redux
还有其它方案吗?
1、Context目前还处于实验阶段,可能会在后面的发行版本中有很大的变化,事实上这种情况已经发生
了,所以为了避免给今后升级带来大的影响和麻烦,不建议在app中使用context。
2、尽管不建议在app中使用context,但是独有组件而言,由于影响范围小于app,如果可以做到高内
聚,不破坏组件树之间的依赖关系,可以考虑使用context
3、对于组件之间的数据通信或者状态管理,有效使用props或者state解决,然后再考虑使用第三方的成
熟库进行解决,以上的方法都不是最佳的方案的时候,在考虑context。
4、context的更新需要通过setState()触发,但是这并不是很可靠的,Context支持跨组件的访问,但是如
果中间的子组件通过一些方法不影响更新,比如 shouldComponentUpdate() 返回false 那么不能保证
Context的更新一定可以使用Context的子组件,因此,Context的可靠性需要关注。
其他的解决方案可以使用
1. mobx
2. useContext + useReducer
react-router底层实现原理
顶层Router订阅history,history变化时,Router调用setState将location向下传递,并设置到
RouterContext。Route组件匹配context中的location决定是否显示。Switch选择最先匹配到的显示,利用
props children。Link组件阻止a标签默认事件,并调用history.push。NavLink通过匹配context中的location
决定是否为active状态。Redirect组件匹配context里的location决定是否调用history.push(to),Switch组件
会匹配location和from决定是否发起Redirect。react为什么是单向数据流,react有什么特性
单向数据流就是:数据在某个节点被改动后,只会影响一个方向上的其他节点。数据只会影响到下一个
层级的节点,不会影响上一个层级的节点。
在react中Props 为只读的,而不是可更改的。这也就是数据更新不能直接通过 this.state 操作,想要更
新,就需要通过 React 提供的专门的 this.setState() 方法来做。
单向数据流其实就是一种框架本身对数据流向的限制。保证数据的可控性。
react使用的是jsx,模板的好处是什么?
JSX 是 JavaScript 的一种扩展,为函数调用和对象构造提供了语法糖,特别是 React.createElement()。
JSX 看起来可能更像是模板引擎或 HTML,但它不是。JSX 生成 React 元素,同时允许你充分利用
JavaScript 的全部功能。
JSX 是编写 React 组件的极好方法,有以下优点:
改进的开发人员体验:代码更易读,因为它们更加形象,感谢类 XML 语法,从而可以更好地表示嵌套的
声明式结构。
更具生产力的团队成员:非正式开发人员可以更容易地修改代码,因为 JSX 看起来像HTML,这正是他们
所熟悉的。
更少的语法错误:开发人员需要敲入的代码量变得更少,这意味着他们犯错的概率降低了,能够减少重
复性伤害。
antd常用的组件
常用组件有:Table、Form、Grid、Button、Menu、Upload、DatePicker、Pagination等。
antd弹出层的原理
弹出层组件使用了 ReactDOM.createPortal(child, container) 来实现的。
React Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。
类组件和函数组件的区别
1. 函数组件看似只是一个返回值是DOM结构的函数,其实它的背后是无状态组件(
Stateless
Components)的思想。函数组件中,你无法使用State,也无法使用组件的生命周期方法,这就决
定了函数组件都是展示性组件(
Presentational Components),接收Props,渲染DOM,而不关注
其他逻辑。
2. 函数组件中没有this。所以你再也不需要考虑this带来的烦恼。而在类组件中,你依然要记得绑定
this这个琐碎的事情。如示例中的sayHi。
3. 函数组件更容易理解。当你看到一个函数组件时,你就知道它的功能只是接收属性,渲染页面,
它不执行与UI无关的逻辑处理,它只是一个纯函数。而不用在意它返回的DOM结构有多复杂。
4. 性能。目前React还是会把函数组件在内部转换成类组件,所以使用函数组件和使用类组件在性能
上并无大的差异。但是,React官方已承诺,未来将会优化函数组件的性能,因为函数组件不需要
考虑组件状态和组件生命周期方法中的各种比较校验,所以有很大的性能提升空间。
5. 函数组件迫使你思考最佳实践。这是最重要的一点。组件的主要职责是UI渲染,理想情况下,所
有的组件都是展示性组件,每个页面都是由这些展示性组件组合而成。如果一个组件是函数组
件,那么它当然满足这个要求。所以牢记函数组件的概念,可以让你在写组件时,先思考这个组
件应不应该是展示性组件。更多的展示性组件意味着更多的组件有更简洁的结构,更多的组件能
被更好的复用。在React组件复用与组合 中我们会提到,应当避免在底层的展示性组件中混入对于状态的管理,而应该将
状态托管于某个高阶组件或者其他的状态容器中。利用函数式声明组件可以彻底保证不会在组件中进行
状态操作。
双向数据绑定和单向数据流的优缺点
1. 单向数据流 数据流动方向可以跟踪,流动单一,追查问题的时候可以跟快捷。缺点就是写起来不
太方便。要使UI发生变更就必须创建各种action来维护对应的state
2. 单向数据流其实是没有状态的, 这使得单向绑定能够避免状态管理在复杂度上升时产生的各种问题,
程序的调试会变得相对容易, 但强制要求编码时设计思维的一致性。
3. 双向流动值和UI双绑定,这种好处大家都懂。但是由于各种数据相互依赖相互绑定,导致数据问
题的源头难以被跟踪到,子组件修改父组件,兄弟组件互相修改有有违设计原则。 但好处就是 太
方便使用了。
4. 双向数据流是自动管理状态的, 但是在实际应用中会有很多不得不手动处理状态变化的逻辑, 使得
程序复杂度上升, 难以调试, 但程序各个部分自成一体, 不强制要求一致的编码。
如果你的程序需要一个统一的数据源, 应该选择单向数据流, 所有的数据变化都是可观测的, 数据自
上而下流动, 即使出问题也很容易找到源头.
5. 如果你的程序本身有多个数据源, 或者是程序的逻辑本身会产生很多的副作用, 应该选择双向绑定
的程序, 将大项目分化为小项目, 逐个击破。
6. 单向绑定使得数据流也是单向的,对于复杂应用来说这是实施统一的状态管理(如redux)的前
提。
双向绑定在一些需要实时反应用户输入的场合会非常方便(比如多级联动菜单)。但通常认为复
杂应用中这种便利比不上引入状态管理带来的优势。
setState是同步还是异步
1. 在 React 的 setState 函数实现中,会根据一个变量 isBatchingUpdates 判断是直接更新 this.state 还
是放到一个updateQueue中延时更新,而 isBatchingUpdates 默认是 false,表示 setState 会同步更
新 this.state;但是,有一个函数 batchedUpdates,该函数会把 isBatchingUpdates 修改为 true,而
当 React 在调用事件处理函数之前就会先调用这个 batchedUpdates将isBatchingUpdates修改为
true,这样由 React 控制的事件处理过程 setState 不会同步更新 this.state,而是异步的。
2. 所以说setstate本身是同步的,一旦走了react内部的合并逻辑,放入了updateQueue队列中就变成
异步了,而代码中的函数是react控制的,内部会走合并逻辑,所以这里的setState 不但是合并的也
是异步的
3. 上面说了,setState是异步的原因是因为走了react内部的合并逻辑,那只要能绕过react内部的合并
逻辑,不让它进入到updateQueue中不就变成同步了吗?因为setState()本身就是同步的
4. 利用setTimeout绕过react内部的合并逻辑
5. 异步的情况:
由React控制的事件处理函数,以及生命周期函数调用setState时表现为异步 。大部分开发中用到
的都是React封装的事件,比如onChange、onClick、onTouchMove等(合成事件中),这些事件处
理函数中的setState都是异步处理的。
同步的情况:
React控制之外的事件中调用setState是同步更新的。比如原生js绑定的事件,setTimeout/setInterval,
ajax,promise.then内等 React 无法掌控的 APIs情况下,setState是同步更新state的
redux和context+hooks优缺点
数据流对比
reduxhooks简单分析
redux 的数据流程图画得比较简单,理解大概意思就好,毕竟它不是我要说的重点,和 hooks 的数
据流程相比其实是大同小异。
从 hooks 数据流能大致看出来,我们设计好 store 后,通过对应的 hooks 函数生成每个 store 的
Provider 和 Context。我们把所有的单个 Provider 糅合为一个整体的 Providers,作为所有 UI 的父组
件。
在任何一个子 UI 组件内部,通过 hooks 函数得到对应 store 的 state、dispatch。UI 组件内,通过主
动调用 dispatch 发送 action,然后经过 store 的数据处理中心 reducer,就能触发相应的数据改变。
这个数据流程和 redux 几乎一模一样。
相同点
统一 store 数据管理
支持以发送 action 来修改数据
支持 action 处理中心:reducer
异同点
hooks UI 层获取 store 和 dispatch 不需要用 HOC 依赖注入,而是用 useContext
redux 在 action 之后改变视图本质上还是 state 注入的方式修改的组件内部 state,而 hooks 则是一
对一的数据触发
hooks 的 reducer 来自于 useReducer
hooks 还没有 middleware 的解决方案
react的生命周期
react旧版生命周期包含三个过程1、挂载过程
constructor()
componentWillMount()
componentDidMount()
2、更新过程
componentWillReceiveProps(nextProps)
shouldComponentUpdate(nextProps,nextState)
componentWillUpdate (nextProps,nextState)
render()
componentDidUpdate(prevProps,prevState)
3、卸载过程
componentWillUnmount()
其具体作用分别为:
1、constructor()
完成了React数据的初始化。2、componentWillMount()
组件已经完成初始化数据,但是还未渲染DOM时执行的逻辑,主要用于服务端渲染。
3、componentDidMount()
组件第一次渲染完成时执行的逻辑,此时DOM节点已经生成了。
4、componentWillReceiveProps(nextProps)
接收父组件新的props时,重新渲染组件执行的逻辑。
5、shouldComponentUpdate(nextProps, nextState)
在setState以后,state发生变化,组件会进入重新渲染的流程时执行的逻辑。在这个生命周期中return
false可以阻止组件的更新,主要用于性能优化。
6、componentWillUpdate(nextProps, nextState)
shouldComponentUpdate返回true以后,组件进入重新渲染的流程时执行的逻辑。
7、render()
页面渲染执行的逻辑,render函数把jsx编译为函数并生成虚拟dom,然后通过其diff算法比较更新前后的
新旧DOM树,并渲染更改后的节点。
8、componentDidUpdate(prevProps, prevState)
重新渲染后执行的逻辑。
9、componentWillUnmount()
组件的卸载前执行的逻辑,比如进行“清除组件中所有的setTimeout、setInterval等计时器”或“移除所有组
件中的监听器removeEventListener”等操作。
react新版生命周期
react16.4后使用了新的生命周期,使用getDerivedStateFromProps代替了旧的componentWillReceiveProps
及componentWillMount。使用getSnapshotBeforeUpdate代替了旧的componentWillUpdate。
使用getDerivedStateFromProps(nextProps, prevState)的原因:
旧的React中componentWillReceiveProps方法是用来判断前后两个 props 是否相同,如果不同,则将新的
props 更新到相应的 state 上去。在这个过程中我们实际上是可以访问到当前props的,这样我们可能会对
this.props做一些奇奇怪怪的操作,很可能会破坏 state 数据的单一数据源,导致组件状态变得不可预
测。而在 getDerivedStateFromProps 中禁止了组件去访问 this.props,强制让开发者去比较 nextProps 与
prevState 中的值,以确保当开发者用到 getDerivedStateFromProps 这个生命周期函数时,就是在根据当
前的 props 来更新组件的 state,而不是去访问this.props并做其他一些让组件自身状态变得更加不可预测
的事情。
使用getSnapshotBeforeUpdate(prevProps, prevState)的原因:
在 React 开启异步渲染模式后,在执行函数时读到的 DOM 元素状态并不总是渲染时相同,这就导致在
componentDidUpdate 中使用 componentWillUpdate 中读取到的 DOM 元素状态是不安全的,因为这时的
值很有可能已经失效了。
而getSnapshotBeforeUpdate 会在最终的 render 之前被调用,也就是说在 getSnapshotBeforeUpdate 中读
取到的 DOM 元素状态是可以保证与componentDidUpdate 中一致的。
React-redux中有个Context使用过吗?用来干什么的
这里再次提及下Redux的Store上的3个api,
虽然 Redux 应用全局就一个 Store 这样的直接导入依然有问题,因为在组件中直接导人 Store 是非常不利
于组件复用的 。
所以,一个应用中,最好只有一个地方需要直接导人 Store ,这个位置当然应该是在调用最顶层 React 组
件的位置 。 但是这样需要把Store,从顶层一层层往下传递,首先我们想到的就是props(父子组件通信
方案)。这种方法有一个很大的缺陷,就是从上到下,所有的组件都要帮助传递这个 props 。
设想在一个嵌套多层的组件结构中,只有最里层的组件才需要使用 store ,但是为了把 store 从最外层传
递到最里层,就要求中间所有的组件都需要增加对这个 store prop 的支持,即使根本不使用它,这无疑
增加程序的耦合度,复杂度和不可维护性 。
React 提供了一个 叫 Context 的功能,能够完美地解决这个问题 。
redux-thunk是干什么的,为什么要用这个?具体怎么实现
1. 什么是thunk?
thunk可以理解为函数的另一种称呼,但不是以前我们说的简单函数。Thunk代表的是一个被其他
函数返回的函数。
2. 那么它是怎么作用在redux上的呢?
Redux中有几个概念:”actions”,”action creators”,”reducer”,“middleware”
Actions就是普通对象,并且一定有一个type属性,除了这个,你可以在对象中自定义你想要描述该action
的数据。
每次都要手写action对象会很麻烦,redux有一个“action creators”的概念来简化。
store.getState() // 获得 store 上存储的所有状态
store.dispatch() // View 发出 Action 的唯一方法
store.subscribe() // 订阅 store上State 发生变化
// 1. plain object
// 2. has a type
// 3. whatever else you want
{
type: "USER_LOGGED_IN",
username: "dave"
}function userLoggedIn() {
return {
type: 'USER_LOGGED_IN',
username: 'dave'
};
}
你可以通过调用userLoggedIn函数来生成这个action,如果你在app中多次dispatch同样的action,使用
action creators会大大简化工作。
1. Action可以做点什么吗?
Action虽然叫action,但其实什么都不做,只是对象而已。它们可以自己做点什么吗?比如像调用
api或是触发其他actions?
因为reducer必须是纯函数,我们不能在reducer里调用api或是触发其他actions。
如果我们希望action做些什么,就要把代码放进一个函数中去,这个函数就是thunk。
Action creators返回一个函数而不是一个action对象。
function getUser() {
return function() {
return axios.get('/current_user');
};
}
1. 那么怎么让Redux处理actions是函数的情况呢?
我们就引入了redux-thunk,它是一个中间件,每次action都会经历它,如果action是一个函数,就
调用这个函数,这就是redux-thunk做的事。
传递给thunk函数的就是两个参数:
Dispatch,如果它们需要,可以dispatch新的action。
getState,它们可以获取最新的state。
function logOutUser() {
return function(dispatch, getState) {
return axios.post('/logout').then(function() {
// pretend we declared an action creator
// called 'userLoggedOut', and now we can dispatch it
dispatch(userLoggedOut());
});
};
}
1. 如何使用redux-thunk
安装redux-thunk包
npm install --save redux-thunk
导入redux-thunk并插入redux怎么用 React-redux 解决数据发生改变就会刷新页面这个问题
通常情况下,store 状态数据发生改变,会刷新页面这个问题是由于设计导致的,可能是组件定义时的问
题。建议不要将页面中使用的组件都包装到一个大的容器组件中(如图1所示),而是将各相关的组件包
装到一个较小的容器组件中(如图2所示):
图1 图2
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);图1中,当容器组件中订阅的 store 发生变化时,所有组件都会重新渲染。在图2中,由于将相关组件包装
到了较小的容器组件中,当 container2 容器中订阅的 store 发生变化时,仅相关的组件重新渲染,其它组
件不会导致重新渲染。
一般什么时候使用redux
在遇到如下情况时,可以考虑使用 redux:
某个状态需要在全局使用或共享(例如角色权限等信息)
state并不总是以单向的方式线性流动
UI 可以根据应用程序状态显著变化
许多不相关的组件以相同的方式更新状态
状态树结构复杂
redux有什么优缺点
优点:
纯粹,清晰易懂,让状态可预测,方便测试
缺点:
繁琐,要写大量的 action creator、reducer,所以即便想进行一个小的状态变化也会需要更改好几个
地方
需要写大量 switch case 分支判断,造成不小的负担
状态是存在于顶层的一棵树,通过 connect 和组件连在一起,单个组件是无法独立工作的。
对于各种异步的副作用,redux 本身是无能为力的,需要使用中间件
类组件有状态,为什么还要用 redux
类组件有 state,但 React 推荐组件间传递数据使用 props 自上而下流动,如果涉及到跨组件层级的通
信,涉及到全局组件状态数据的共享及更新等,在各组件中都添加 props 来传递数据会显得非常繁琐,
利用 redux 可以简化状态更新的繁琐度,提供统一的状态更新方式,使得状态更新可预测,也方便调试
与维护。
shouldComponentUpdate 的作用
此方法仅作为性能优化的方式而存在,根据 shouldComponentUpdate() 的返回值,判断 React 组件
的输出是否受当前 state 或 props 更改的影响,返回 false 以告知 React 可以跳过更新。
示例:这是一个组件的子树。每个节点中, SCU 代表 shouldComponentUpdate 返回的值,而 vDOMEq 代
表返回的 React 元素是否相同。最后,圆圈的颜色代表了该组件是否需要被调停。
节点 C2 的 shouldComponentUpdate 返回了 false ,React 因而不会去渲染 C2,也因此 C4 和 C5 的
shouldComponentUpdate 不会被调用到。
对于 C1 和 C3, shouldComponentUpdate 返回了 true ,所以 React 需要继续向下查询子节点。这里
C6 的 shouldComponentUpdate 返回了 true ,同时由于渲染的元素与之前的不同使得 React 更新了
该 DOM。
最后一个有趣的例子是 C8。React 需要渲染这个组件,但是由于其返回的 React 元素和之前渲染的相
同,所以不需要更新 DOM。
显而易见,你看到 React 只改变了 C6 的 DOM。对于 C8,通过对比了渲染的 React 元素跳过了渲染。而
对于 C2 的子节点和 C7,由于 shouldComponentUpdate 使得 render 并没有被调用。因此它们也不
需要对比元素了。
react 中 ref 的作用
ref 是 React 提供的用来操纵 React 组件实例或者操作 DOM 元素的技术。
适合使用 ref 的几种情况:
管理焦点,文本选择或媒体播放
触发强制动画
集成第三方 DOM 库
示例1:
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
// 创建一个 ref 来存储 textInput 的 DOM 元素
this.textInput = React.createRef();
this.focusTextInput = this.focusTextInput.bind(this);
}
focusTextInput() {
// 直接使用原生 API 使 text 输入框获得焦点
// 注意:我们通过 "current" 来访问 DOM 节点
this.textInput.current.focus();这个例子是为 DOM 元素添加 ref ,使用 ref 去存储 DOM 节点的引用,React 会在组件挂载时给 ref
对象的 current 属性传入 DOM 元素,并在组件卸载时传入 null 值。 ref 会在
componentDidMount 或 componentDidUpdate 生命周期钩子触发前更新。
示例2:
这个示例是包装上面的 CustomTextInput ,来模拟它挂载之后立即被点击的操作,我们可以使用 ref
来获取这个自定义的 input 组件并手动调用它的 focusTextInput 方法。
解释 redux 的工作流程
Redux工作流程:
1. 在 View 中派发 action(
dispatch(action))
2. store 接收到派发的 action 对象
3. 在 store 中,调用 reducer(),并传递之前的 state 与 action 作为参数
4. 在 reducer 中按照 action.type 判断,运算并返回新的 state 给 store
5. store 中更新最新的 state
6. 执行由 subscribe() 注册的监听任务(这些任务订阅了 store 的变化)
}
render() {
// 告诉 React 我们想把 ref 关联到
// 构造器里创建的 `textInput` 上
return (
type="text"
ref={this.textInput} />
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
);
}
}
class AutoFocusTextInput extends React.Component {
constructor(props) {
super(props);
this.textInput = React.createRef();
}
componentDidMount() {
this.textInput.current.focusTextInput();
}
render() {
return (
);
}
}redux 数据流向,比如点击按钮到状态修改,数据如何流向的
由上图我们可以看到,redux 数据流向大致步骤(以按钮点击修改状态为例):
1. UI 首次呈现时,访问 redux 存储的当前状态 state,并使用该数据来决定初始呈现内容:显示提现
$0 元。
2. 在视图中触发点击事件:提现
3. 事件处理程序中,触发 action 并传递给 store:dispatch(action)
4. store 使用前一个 state 和接收到的 action 经 reducer 函数处理,并将返回值保存为新的 state
5. UI 根据新状态重新呈现
说一下 redux 的使用流程,dispash 是从哪里解构出来的
redux 使用流程:
1. 创建 reducer() 纯函数,用于同步更新状态
2. 将各独立的 reducer 合并为一个完整的根 reducer
3. 创建 action creator 函数,用于生成 action 对象
4. 基于根 reducer 创建 store,如果有异步更新状态数据操作,应用中间件(如 redux-thunk)
5. 订阅 store 中的状态变化
6. 在 UI 中根据 store 的状态初始渲染
7. 在 UI 中触发 action 到 store 中更新状态
如果我们要在 View 中实现 store 中的状态更新,则需要 dispatch(action),这个 dispatch() 方法是从
创建的 store 中解构出来的,在 store 中除了 dispatch 外,还可以解构诸如 getState、subscribe 等属性。
React性能为什么强于vue
对于框架之间我们并不是特别在意谁强谁弱, 而是要搞清楚框架真正值得我们学习的点:
像React框架, 它在 架构上融合了数据驱动视图、组件化、函数式编程、面向对象、Fiber 等经典设计“哲
学”, 在底层技术选型上涉及了 JSX、虚拟 DOM等经典解决方案,在周边生态上至少涵盖了状态管理
和前端路由两 大领域的最佳实践。此外,它还自建了状态管理机制与事件系统,创造性地在前端框架
中引入了 Hooks 思 想...... React 十年如一日的稳定输出背后,有太多值得我们去吸收和借鉴的东西.
1. npm源码包大小, react 291kb, vue 2.97mb 更小的源码包, 更少的cpu消耗.
2. 基于虚拟DOM, 减少重绘次数(将多次数据操作汇集成一次 DOM 更新); 减少手动操作 DOM 操作(不用
再像以前写 jQuery 那样,先获取 DOM 元素,再设置属性)3. Fiber算法, React16提出了Fiber架构,其能够将任务分片,划分优先级,同时能够实现类似于操作
系统中对线程的抢占式调度,非常强大, 对于因为JavaScript的单线程特性,单个同步任务耗时太
长,出现卡顿的问题就可以得到解决, 这进一步弥补了React在组件更新机制方面的缺陷.
4. Hooks, 创造性地在前端框架中引入了 Hooks , 这使得程序员有了除class component以外的界面构建
方式, Hooks让复用代码这件事变得更容易, 结合函数式组件整体风格更清爽,更优雅, 更少的代码
量, 这也使得项目更容易阅读和维护.
5. 结合shouldComponentUpdate等方法, 可以避免不必要的组件更新, 实现更少的cpu消耗, 程序员可以
从代码的角度介入到组件更新效率的控制过程中.
vue-router和react-router的区别
路由配置语法的差异
vue-router是全局配置方式,react-router是全局组件方式。
vue-router仅支持对象形式的配置,react-router支持对象形式和JSX语法的组件形式配置。
vue-router任何路由组件都会被渲染到位置,react-router子组件作为children被传入父组件,而
根组件被渲染到位置。
使用方式上的不同
vue-router通过路由配置表 + router-link, router-view控制路由.
react-router通过Link,NavLink, Route,Switch,Redirect等路由组件控制路由,
获取参数的方式不同
vue-router借助this.$router获取全局路由的实例,this.$route获取当前路由信息
react-router借助this.props.location, this.props.match获取路由参数
三、小程序
简单谈谈微信小程序
在结构和样式方面,小程序提供了一些常用的标签与控件,比如:
view,小程序主要的布局元素,类似于html标签的div,你也完全可以像控制div那样去控制view。
scroll-view,你要滚动内容的话,没必要用view去做overflow,scroll-view提供了更为强大的功能,通过参
数的调整,你可以控制滚动方向,触发的事件等等
配置文件app.json平级的还有一个app.js文件,是小程序的脚本代码。我们可以在这个文件中监听并处理
小程序的生命周期函数、声明全局变量,在每个page目录里的js做当前页面的业务操作。但是小程序的
页面的脚本逻辑是在JsCore中运行,JsCore是一个没有窗口对象的环境,所以不能在脚本中使用
window,也无法在脚本中操作组件,所以我们常用的zepto/jquery 等类库也是无法使用的。
另一个app.wxss文件,这个是全局的样式,所有的页面都会调用到,每个项目目录下面的wxss是局部样
式文件,不会和其他目录产生污染,可以放心使用样式名。
他提供的WXSS(WeiXin Style Sheets)是一套样式语言,具有 CSS 大部分特性,可以看作一套简化版的css。
同时为了更适合开发微信小程序,还对 CSS 进行了扩充以及修改,直接帮我们把适配的一部分工作都做
了,比如他的rpx(
responsive pixel),可以根据屏幕宽度进行自适应,规定屏幕宽为750rpx。如在
iPhone6 上,屏幕宽度为375px,共有750个物理像素,则750rpx = 375px = 750物理像素,1rpx = 0.5px = 1
物理像素。在调用微信生态系统功能时,微信小程序提供了相应的api,比如你要修改一个头像,可以使用
wx.chooseImage等
小程序的原生组件有哪些
以微信小程序为例,可以分成容器组件、基础组件、表单组件、媒体组件、开放能力组件等
小程序的安卓版和ios版是怎么开发出来
小程序开发基于html、css、javascript,与web开发一样具有跨平台特性,一次开发即可在安卓和iOS等平
台访问,但与普通web开发不同,小程序运行环境并不是浏览器,而是依附于各自的软件App,如微信小
程序必须在微信中访问,支付宝小程序必须在支付宝中访问等,小程序的开发流程也有所不同,需要经
过申请小程序帐号、安装小程序开发者工具、配置项目、开发、调试、上线发布等过程方可完成
uni-app弹窗被覆盖怎么解决
如果弹窗被别的内容覆盖,且设置很大的z-index也无法解决,这种情况多半是被一些如 map 、
video 、 textarea 、 canvas 等原生组件遮盖,因为原生组件层级高于前端组件,我们可以使用
cover-view 组件解决
小程序生命周期
onReady 生命周期函数--监听页面初次渲染完成
onShow 生命周期函数--监听页面显示
onHide 生命周期函数--监听页面隐藏
onUnload 生命周期函数--监听页面卸载
onPullDownRefresh 页面相关事件处理函数--监听用户下拉动作
onReachBottom 页面上拉触底事件的处理函数
onShareAppMessage 用户点击右上角转发
onPageScroll 页面滚动触发事件的处理函数
onTabItemTap 当前是 tab 页时,点击 tab 时触发
小程序路由跳转
1. 通过组件navigator跳转,设置url属性指定跳转的路径,设置open-type属性指定跳转的类型(可
选),open-type的属性有 redirect, switchTab, navigateBack
// redirect 对应 API 中的 wx.redirect 方法
前页打开
// navigator 组件默认的 open-type 为 navigate
// switchTab 对应 API 中的 wx.switchTab 方法
// reLanch 对应 API 中的 wx.reLanch 方法
闭所有页面,打开到应用内的某个页面2. 通过api跳转,wx.navigateTo() , wx.navigateBack(), wx.redirectTo() , wx.switchTab(), wx.reLanch()
小程序的兼容问题有哪些
遇到的如下:
1,ios下的zIndex层级问题,主要发生在iphone7和iphoneX下 绝对定位必须有一个共同的父元素。
2,左右边框不生效 当边框的宽度设置为奇数的时候,可能会不生效 解决方法:将宽度设置为偶数
的时候,在ios下就可以解决
3,还有尽量不要用margin-bottom ,当元素是在整个页面的最底部的时候,在ios下可能margin-
bottom会失效,所以建议,都使用padding-bottom
new Date跨平台兼容性问题
在Andriod使用new Date(“2018-05-30 00:00:00”)木有问题,但是在ios下面识别不出来。
因为IOS下面不能识别这种格式,需要用2018/05/30 00:00:00格式。可以使用正则表达式对做字符串替
换,将短横替换为斜杠。var iosDate= date.replace(/-/g, '/');。
wx.getUserInfo()接口更改问题
微信小程序最近被吐槽最多的一个更改,就是用户使用wx.getUserInfo(开发和体验版)时不会弹出授
权,正式版不受影响。现在授权方式是需要引导用户点击一个授权按钮,然后再弹出授权。
// navigateBack 对应 API 中的 wx.navigateBack 方法
页面或多级页面
wx.navigateTo({
url: 'page/home/home?user_id=1' // 页面 A
})
wx.navigateTo({
url: 'page/detail/detail?product_id=2' // 页面 B
})
// 跳转到页面 A
wx.navigateBack({
delta: 2 //返回指定页面
})
// 关闭当前页面,跳转到应用内的某个页面。
wx.redirectTo({
url: 'page/home/home?user_id=111'
})
// 跳转到tabBar页面(在app.json中注册过的tabBar页面),同时关闭其他非tabBar页面。
wx.switchTab({
url: 'page/index/index'
})
// 关闭所有页面,打开到应用内的某个页面。
wx.reLanch({
url: 'page/home/home?user_id=111'
})小程序框架都掌握哪一些,uniapp都会哪一些,平时开发遇到的困难
Taro
uni-app
WeUI
mpvue
iView Weapp
开发uni-app遇到的坑
上传图片
小程序时必须要写header:{“Content-Type”: “multipart/form-data”}, h5是必须省略
uni-app h5 端的ios图片不能加载问题
uni-app h5端 ios只能加载https的图片
uni-app 使用deep 穿透微信小程序生效 h5无作用
需要在methods同级下加一个 :
options: { styleIsolation: ‘shared’ },
uni-app post请求如何传递数组 参数
在开发中我们接口上传图片是post请求 无法传递一个数组 解决方法如下
我们可以把数据转换成字符串 然后拼接到请求地址后后面
拼接字符串格式:image[]=arr[0]&image[]=arr[1]
小程序怎么获取手机号
1. 准备一个button组件, 将 button 组件 open-type 的值设置为 getPhoneNumber ,当用户点击并
同意之后,可以通过 bindgetphonenumber 事件回调获取到动态令牌 code ;
2. 接着把 code 传到开发者后台,并在开发者后台调用微信后台提供的
phonenumber.getPhoneNumber 接口,消费 code 来换取用户手机号。每个 code 有效期为5分
钟,且只能消费一次。
imgURlClick(imgArr){
return '?images[]='+imgArr.join('&images[]=')
}
Page({
getPhoneNumber (e) {
console.log(e.detail.code)
}
})
getPhoneNumber: function (e) {
var that = this;注: getPhoneNumber 返回的 code 与 wx.login 返回的 code 作用是不一样的,不能混用.
注:从基础库 2.21.2 开始,对获取手机号的接口进行了安全升级, 需要用户主动触发才能发起获取
手机号接口,所以该功能不由 API 来调用,需用 button 组件的点击来触发。另外,新版本接口不
再需要提前调用 wx.login 进行登录.
小程序的登录流程
登陆流程:
console.log(e.detail.errMsg == "getPhoneNumber:ok");
if (e.detail.errMsg == "getPhoneNumber:ok") {
wx.request({
url: 'http://localhost/index/users/decodePhone',
data: {
encryptedData: e.detail.encryptedData,
iv: e.detail.iv,
sessionKey: that.data.session_key,
uid: "",
},
method: "post",
success: function (res) {
console.log(res);
}
})
}
}首次登录
调用小程序api接口 wx.login() 获取 临时登录凭证code ,这个code是有过期时间的.
将这个 code 回传到开发者服务器(就是请求开发者服务器的登录接口,通过凭证进而换取用
户登录态信息,包括用户的唯一标识(
openid)及本次登录的会话密钥(
session_key)等)
拿到开发者服务器传回来的会话密钥(
session_key)之后,前端需要保存起来.
再次登录的时候,就要判断存储的session_key是否过期了
获取缓存中的session_key, wx.getStorageSync('sessionKey')
如果缓存中存在session_key,那么调用小程序api接口 wx.checkSession() 来判断登录态是
否过期,回调成功说明当前 session_key 未过期,回调失败说明 session_key 已过期。登录态过
期后前端需要再调用 wx.login()获取新的用户的code,然后再向开发者服务器发起登录请求.
一般在项目开发,开发者服务器也会对用户的登录态做过期限制,所以这时在判断完微信服务
器中登录态如果没有过期之后还要判断开发者服务器的登录态是否过期。(请求开发者服务器
给定的接口进行请求判断就好)
参考官网: 小程序登陆流程 登陆态过期检测
wx.setStorageSync('sessionKey', 'value')小程序如果版本更新了怎么通知用户
当小程序发布新的版本后,用户如果之前访问过该小程序,通过已打开的小程序进入(未手动删除),
则会弹出提示,提醒用户更新新的版本。用户点击确定就可以自动重启更新,点击取消则关闭弹窗,不
再更新.
核心步骤:
打开小程序, 检查小程序是否有新版本发布
小程序有新版本,则静默下载新版本,做好更新准备
新的版本已经下载好,调用 applyUpdate 应用新版本并重启小程序
更新版本的模拟测试
微信开发者工具上可以通过「编译模式」下的「下次编译模拟更新」开关来调试.
点击编译模式设置下拉列表,然后点击“添加编译模式”,在自定义编译条件弹窗界面,点击下次编
译时模拟更新,然后点击确定,重新编译就可以了.
注: 需要注意的是,这种方式模拟更新一次之后就失效了,后边再测试仍需要对这种编译模式进行
重新设置才可以.
核心代码如下:
updateManager.onCheckForUpdate(function (res) {})
updateManager.onUpdateReady(function () {})
updateManager.applyUpdate()
App({
onLaunch: function(options) {
this.autoUpdate()
},
autoUpdate:function(){
var self=this
// 获取小程序更新机制兼容
if (wx.canIUse('getUpdateManager')) {
const updateManager = wx.getUpdateManager()
//1. 检查小程序是否有新版本发布
updateManager.onCheckForUpdate(function (res) {
// 请求完新版本信息的回调
if (res.hasUpdate) {
//2. 小程序有新版本,则静默下载新版本,做好更新准备
updateManager.onUpdateReady(function () {
wx.showModal({
title: '更新提示',
content: '新版本已经准备好,是否重启应用?',
success: function (res) {
if (res.confirm) {
//3. 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
updateManager.applyUpdate()
} else if (res.cancel) {
//不应用
}
}
})
})
updateManager.onUpdateFailed(function () {小程序嵌入H5页面怎么做
解决方式 :web-view
webview 指向网页的链接。可打开关联的公众号的文章,其它网页需登录小程序管理后台配置业务域
名。
具体实现步骤:
登陆小程序管理后台, 配置服务器域名( h5页面所在的域名 )
在小程序里面嵌入h5
在小程序里面定义一个你想要的H5入口
新建一个页面,放置 webview , src指向h5网页的链接.
注: 实际开发中在h5页面中有可能需要向小程序发送消息, 实现h5页面和小程序页面的通信
需要使用postMessage向小程序发送消息, 在h5中postMessage 注意,key必须叫做data,否则取不到.
小程序的生命周期函数有哪些?分别有什么作用?
小程序的生命周期函数大体分为三类:
属性
说明
onLaunch
监听小程序初始化, 全局只触发一次
onShow
监听小程序启动或切前台。
onHide
监听小程序切后台。
小程序应用的生命周期
// 新的版本下载失败
wx.showModal({
title: '已经有新版本了哟~',
content: '新版本已经上线啦~,请您删除当前小程序,重新搜索打开哟~',
})
})
}
})
} else {
// 如果希望用户在最新版本的客户端上体验您的小程序,可以这样子提示
wx.showModal({
title: '提示',
content: '当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试。'
})
}
}
})
新页面
属性
说明
onLoad
监听页面加载, 获取其他页面传过来的参数, 发起网络请求
onShow
监听页面显示
onReady
监听页面初次渲染完成
onHide
监听页面隐藏
onUnload
监听页面卸载
小程序页面的生命周期
参考官网: 页面的生命周期
定义段
描述
created
在组件实例刚刚被创建时执行,注意此时不能调用 setData )
attached
在组件实例进入页面节点树时执行)
ready
在组件布局完成后执行)
moved
在组件实例被移动到节点树另一个位置时执行)
detached
在组件实例被从页面节点树移除时执行)
小程序组件的生命周期
四、webpack
webpack了解吗,讲一讲原理,怎么压缩代码
1. 需要读到入口文件里面的内容。
2. 分析入口文件,递归的去读取模块所依赖的文件内容,生成AST语法树。
3. 根据AST语法树,生成浏览器能够运行的代码
webpack怎么配置
主要配置5个核心文件
1. mode:通过选择 `development`, `production` 或 `none` 之中的一个,来设置 `mode` 参数,你可
以启用 webpack 内置在相应环境下的优化。
2. entry:**入口起点(entry point)** 指示 webpack 应该使用哪个模块,来作为构建其内部 [依赖图
(dependency graph)](https://webpack.docschina.org/concepts/dependency-graph/) 的开始。进
入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
3. output:**output** 属性告诉 webpack 在哪里输出它所创建的 *bundle*,以及如何命名这些文件。
主要输出文件的默认值是 `./dist/main.js`,其他生成文件默认放置在 `./dist` 文件夹中。
4. loader:webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。
**loader** 让 webpack 能够去处理其他类型的文件,并将它们转换为有效 [模块]
(https://webpack.docschina.org/concepts/modules),以供应用程序使用,以及被添加到依赖图中。
5. plugin:loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资
源管理,注入环境变量。webpack怎么打包
初始化参数 :解析webpack配置参数,合并shell传入和webpack.config.js文件配置的参数,形成最后的配
置结果;
开始编译 :上一步得到的参数初始化compiler对象,注册所有配置的插件,插件 监听webpack构建生命
周期的事件节点,做出相应的反应,执行对象的run方法开始执行编译;
确定入口 :从配置的entry入口,开始解析文件构建AST语法树,找出依赖,递归下去;
编译模块 :递归中根据文件类型和loader配置,调用所有配置的loader对文件进行转换,再找出该模块
依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
完成模块编译并输出 :递归完事后,得到每个文件结果,包含每个模块以及他们之间的依赖关系,根据
entry或分包配置生成代码块chunk;
输出完成 :输出所有的chunk到文件系统;
vue打包内存过大,怎么使用webpack来进行优化
开放式题目
打包优化的目的
1、优化项目启动速度,和性能
2、必要的清理数据
3、性能优化的主要方向
cdn加载
-压缩js
减少项目在首次加载的时长(首屏加载优化)
4、目前的解决方向
cdn加载不比多说,就是改为引入外部js路径
首屏加载优化方面主要其实就两点
第一:
尽可能的减少首次加载的文件体积,和进行分布加载
第二:
首屏加载最好的解决方案就是ssr(服务端渲染),还利于seo
但是一般情况下没太多人选择ssr,因为只要不需要seo,ssr更多的是增加了项目开销和技术难度
的。
1、路由懒加载
在 Webpack 中,我们可以使用动态 import语法来定义代码分块点 (split point):
import(’./Fee.vue’)
// 返回 Promise如果您使用的是 Babel,你将需要添加 syntax-dynamic-import 插件,才能使 Babel
可以正确地解析语法。
结合这两者,这就是如何定义一个能够被 Webpack 自动代码分割的异步组件。
const Fee = () => import('./Fee.vue')
在路由配置中什么都不需要改变,只需要像往常一样使用 Foo:
const router = new VueRouter({
routes: [
{ path: '/fee', component: Fee }
]
})2、服务器和webpack打包同时配置Gzip
Gzip是GNU zip的缩写,顾名思义是一种压缩技术。它将浏览器请求的文件先在服务器端进行压缩,
然后传递给浏览器,浏览器解压之后再进行页面的解析工作。在服务端开启Gzip支持后,我们前端
需要提供资源压缩包,通过Compression-Webpack-Plugin插件build提供压缩
需要后端配置,这里提供nginx方式:
http:{
gzip on; #开启或关闭gzip on off
gzip_disable "msie6"; #不使用gzip IE6
gzip_min_length 100k; #gzip压缩最小文件大小,超出进行压缩(自行调节)
gzip_buffers 4 16k; #buffer 不用修改
gzip_comp_level 8; #压缩级别:1-10,数字越大压缩的越好,时间也越长
gzip_types text/plain application/x-javascript text/css application/xml
text/javascript application/x-httpd-php image/jpeg image/gif image/png; # 压缩文件
类型
}
// 安装插件
$ cnpm i --save-dev compression-webpack-plugin
// 在vue-config.js 中加入
const CompressionWebpackPlugin = require('compression-webpack-plugin');
const productionGzipExtensions = [
"js",
"css",
"svg",
"woff",
"ttf",
"json",
"html"
];
const isProduction = process.env.NODE_ENV === 'production';
.....
module.exports = {
....
// 配置webpack
configureWebpack: config => {
if (isProduction) {
// 开启gzip压缩
config.plugins.push(new CompressionWebpackPlugin({
algorithm: 'gzip',
test: /\.js$|\.html$|\.json$|\.css/,
threshold: 10240,
minRatio: 0.8
}))
}
}
}
3、优化打包chunk-vendor.js文件体积过大
当我们运行项目并且打包的时候,会发现chunk-vendors.js这个文件非常大,那是因为webpack将所有的
依赖全都压缩到了这个文件里面,这时我们可以将其拆分,将所有的依赖都打包成单独的js。
// 在vue-config.js 中加入.....
module.exports = {
....
// 配置webpack
configureWebpack: config => {
if (isProduction) {
// 开启分离js
config.optimization = {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
maxInitialRequests: Infinity,
minSize: 20000,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name (module) {
// get the name. E.g. node_modules/packageName/not/this/part.js
// or node_modules/packageName
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)
([\\/]|$)/)[1]
// npm package names are URL-safe, but some servers don't like @
symbols
return `npm.${packageName.replace('@', '')}`
}
}
}
}
};
}
}
}
// 至此,你会发现原先的vender文件没有了,同时多了好几个依赖的js文件
4、启用CDN加速
用Gzip已把文件的大小减少了三分之二了,但这个还是得不到满足。那我们就把那些不太可能改动的代
码或者库分离出来,继续减小单个chunk-vendors,然后通过CDN加载进行加速加载资源。
// 修改vue.config.js 分离不常用代码库
// 如果不配置webpack也可直接在index.html引入
module.exports = {
configureWebpack: config => {
if (isProduction) {
config.externals = {
'vue': 'Vue',
'vue-router': 'VueRouter',
'moment': 'moment'
}
}
}
}
// 在public文件夹的index.html 加载
5、完整vue.config.js代码
const path = require('path')
// 在vue-config.js 中加入
// 开启gzip压缩
const CompressionWebpackPlugin = require('compression-webpack-plugin');
// 判断开发环境
const isProduction = process.env.NODE_ENV === 'production';
const resolve = dir => {
return path.join(__dirname, dir)
}
// 项目部署基础
// 默认情况下,我们假设你的应用将被部署在域的根目录下,
// 例如:https://www.my-app.com/
// 默认:'/'
// 如果您的应用程序部署在子路径中,则需要在这指定子路径
// 例如:https://www.foobar.com/my-app/
// 需要将它改为'/my-app/'
// iview-admin线上演示打包路径: https://file.iviewui.com/admin-dist/
const BASE_URL = process.env.NODE_ENV === 'production'
? '/'
: '/'
module.exports = {
//webpack配置
configureWebpack:config => {
// 开启gzip压缩
if (isProduction) {
config.plugins.push(new CompressionWebpackPlugin({
algorithm: 'gzip',
test: /\.js$|\.html$|\.json$|\.css/,
threshold: 10240,
minRatio: 0.8
}));
// 开启分离js
config.optimization = {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
maxInitialRequests: Infinity,
minSize: 20000,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name (module) {
// get the name. E.g. node_modules/packageName/not/this/part.js
// or node_modules/packageName
const packageName = module.context.match(/[\\/]node_modules[\\/]
(.*?)([\\/]|$)/)[1]// npm package names are URL-safe, but some servers don't like @
symbols
return `npm.${packageName.replace('@', '')}`
}
}
}
}
};
// 取消webpack警告的性能提示
config.performance = {
hints:'warning',
//入口起点的最大体积
maxEntrypointSize: 50000000,
//生成文件的最大体积
maxAssetSize: 30000000,
//只给出 js 文件的性能提示
assetFilter: function(assetFilename) {
return assetFilename.endsWith('.js');
}
}
}
},
// Project deployment base
// By default we assume your app will be deployed at the root of a domain,
// e.g. https://www.my-app.com/
// If your app is deployed at a sub-path, you will need to specify that
// sub-path here. For example, if your app is deployed at
// https://www.foobar.com/my-app/
// then change this to '/my-app/'
publicPath: BASE_URL,
// tweak internal webpack configuration.
// see https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md
devServer: {
host: 'localhost',
port: 8080, // 端口号
hotOnly: false,
https: false, // https:{type:Boolean}
open: true, //配置自动启动浏览器
proxy:null // 配置跨域处理,只有一个代理
},
// 如果你不需要使用eslint,把lintOnSave设为false即可
lintOnSave: true,
css:{
loaderOptions:{
less:{
javascriptEnabled:true
}
},
extract: true,// 是否使用css分离插件 ExtractTextPlugin
sourceMap: false,// 开启 CSS source maps
modules: false// 启用 CSS modules for all css / pre-processor files.
},
chainWebpack: config => {
config.resolve.alias
.set('@', resolve('src')) // key,value自行定义,比如.set('@@',
resolve('src/components'))
.set('@c', resolve('src/components'))webpack打包用过什么插件
文件处理上
1、HtmlWebpackPlugin
包名:html-webpack-plugin
该插件将为你生成一个 HTML 文件, 在 body 中使用 script 标签引入你所有 webpack 生成的
bundle。
官方传送门
2、CleanWebpackPlugin
包名:clean-webpack-plugin
用于在打包前清理上一次项目生成的 bundle 文件。默认情况下,此插件将删除webpack的
Output.Path 目录中的所有文件,以及在每次成功重建后所有未使用的webpack资源
(
assets)。如果使用的webpack版本是 4 以上的,默认 清理
件。
官方传送门
3、MiniCssExtractPlugin
包名:mini-css-extract-plugin
将 css 成生文件,而非内联 。该插件的主要是为了抽离 css 样式,防止将样式打包在 js 中引起页面
样式加载错乱的现象。支持按需加载 css 和 sourceMap
官方传送门
4、HotModuleReplacementPlugin
包名:HotModuleReplacementPlugin
由 webpack 自带 。在对CSS / JS文件进行修改时,可以立即更新浏览器(部分刷新)。依赖于
webpack-dev-server
官方传送门
5、ImageminPlugin
包名:imagemin-webpack-plugin
},
// 打包时不生成.map文件
productionSourceMap: false
// 这里写你调用接口的基础路径,来解决跨域,如果设置了代理,那你本地开发环境的axios的baseUrl要写
为 '' ,即空字符串
// devServer: {
// proxy: 'localhost:3000'
// }
}批量压缩图片。
官方传送门
6、PurifyCSSPlugin
包名:purifycss-webpack
从CSS中删除未使用的选择器(删除多余的 css 代码)。和 extract-text-webpack-plugin 一起
使用
官方传送门
7、OptimizeCSSAssetsPlugin
包名:optimize-css-assets-webpack-plugin
压缩css文件。
官方传送门
8、CssMinimizerPlugin
包名:
css-minimizer-webpack-plugin
压缩css文件。用于 webpack 5 。
官方传送门
9、UglifyJsPlugin
包名:uglifyjs-webpack-plugin
压缩js文件。
官方传送门
10、ProvidePlugin
包名:ProvidePlugin
由 webpack 自带 。自动加载模块,而不必在任何地方 import 或 require 它们。例如:new
webpack.ProvidePlugin({$: 'jquery',React: 'react'})
官方传送门
11、SplitChunksPlugin
包名:-
在 webapck配置 中的 optimization 字段中配置。 cacheGroups 是关键,将文件提取打包成公
共模块,像 抽取 node_modules 里的文件。
官方传送门
12、CompressionPlugin
包名:compression-webpack-plugin
启用 gzip 压缩。
官方传送门13、CopyWebpackPlugin
包名:
copy-webpack-plugin
将已存在的单个文件或整个目录复制到构建目录中。多用于 将静态文件 因在打包时 webpack 并不
会帮我们拷贝到 dist 目录 拷贝到 dist 目录
官方传送门
14、DefinePlugin
包名:DefinePlugin
由 webpack 自带 。设置全局变量。如:new webpack.DefinePlugin({'process.env.NODE_ENV':
JSON.stringify('production')})
官方传送门
打包速率上
1、DllPlugin
包名:DllPlugin
由 webpack 自带 。 dllplugin 和 dllreferenceplugin 提供了拆分捆绑包的方法,这些方式
可以大大提高构建时间性能。
官方传送门
2、DLLReferencePlugin
包名:DLLReferencePlugin
由 webpack 自带 。它引用了仅需要预先构建的依赖项的 DLL-only-Bundle 。
官方传送门
3、ParallelUglifyPlugin
包名:webpack-parallel-uglify-plugin
开启多个子进程,把对多个文件压缩的工作分别给多个子进程去完成。减少构建时间。
官方传送门
4、HappyPack
包名:happypack
让 webpack 把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。提
升 构建 速度
官方传送门
说说gulp和webpack的区别
开放式题目Gulp强调的是前端开发的工作流程。我们可以通过配置一系列的task,定义task处理的事务(例如文件压
缩合并、雪碧图、启动server、版本控制等),然后定义执行顺序,来让Gulp执行这些task,从而构建项
目的整个前端开发流程。通俗一点来说,“Gulp就像是一个产品的流水线,整个产品从无到有,都要受流
水线的控制,在流水线上我们可以对产品进行管理。”
Webpack是一个前端模块化方案,更侧重模块打包。我们可以把开发中的所有资源(图片、js文件、css
文件等)都看成模块,通过loader(加载器)和plugins(插件)对资源进行处理,打包成符合生产环境
部署的前端资源。 Webpack就是需要通过其配置文件(Webpack.config.js)中 entry 配置的一个入口文件
(
JS文件),然后在解析过程中,发现其他的模块,如scss等文件,再调用配置的loader或者插件对相关
文件进行解析处理。
虽然Gulp 和 Webpack都是前端自动化构建工具,但看2者的定位就知道不是对等的。Gulp严格上讲,模
块化不是他强调的东西,旨在规范前端开发流程。Webpack更明显的强调模块化开发,而那些文件压缩
合并、预处理等功能,不过是他附带的功能。
五、Typescript
了解过TS吗?
ts是一种基于静态类型检查的强类型语言那当然js就是一种弱类型语言
由于我们在浏览器中不能编译ts语言所以我们需要安装编译器
安装下载
使用npm install -g typescript进行下载
使用tsc进行检测是否安装成功
在文件中间一个js文件,然后在文件中见一个ts文件,但是直接去使用的时候会报错,需要在终端中使用
tsc ./js/hello.ts,这样之后可以在当前的就是文件中自动编译一个同名js文件。
ts支持的数据类型
数组
元组Tuple
规定元素类型和规定元素数量和顺序的数组
特点:不要越界访问
定义的是什么类型写的就是什么类型,可以使用数组的下标取值,但是如果使用数组的push方法的话,
虽然输出的数组中有,但是取值的话会报错可以打印出来但不建议这样写,这就说了元组的一个越界问
题
let num:number=20
console.log(num)
console.log("str")
let arr:number[]=[1,2,3,4,5]
//将let定义为一个数组,每一项都是number
let arr:number[]=[1,2,3,4,5,"str"] //报错不能将类型string分配给类型number
let arr1:Array
//通过给范型添加多类型,让数组支持多种数据格式
let tu:[number,string]
tu=[1,"str"]枚举
1.有限的可列举出来的命名和索引对应的类型
2枚举类型的优势:语义化可维护性
3原理:反向映射,互相指向
代表任意类型:
接口
跟另一个事物之间的一个媒介
使用ts写一个对象属性约束
使用{useName:string, password:number}约束传入的userData参数(并将password置为可选参数)
//定义了一个枚举
enum user{
admin,
guest,
develoment,
pm
}
console.log(user)
//使用user类型来定义枚举变量any
let t:any=10
t="str"
t=true
interface userInfo{
name:string;
age:number;
address?:string//问号代表该属性可添加可不添加
}
function getUserInfo(u:userInfo){
console.log(u.name) //张三
}
let user1 = {name:"张三",age:24,address:"北京"}
getUserInfo(user1)
class User{
useName:string;
password?:number|undefined;
//使用{useName:string, password?:number|undefined}约束传入的userData参数
constructor(userData:{useName:string, password?:number|undefined}){
this.useName=userData.useName;
if(userData.password)
this.password=userData.password;
}
}
let u1=new User( {useName:"小猪猪", password:12233} );
let u2=new User( {useName:"大猪猪"} )
console.log(u1);
console.log(u2);说一下typescript中的泛型
泛型(
Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定
类型的一种特性。
通俗理解:泛型就是解决类 接口 方法的复用性、以及对不特定数据类型的支持
function getData(value:string):string{
return value //只能返回string类型的数据
}
//想要同时返回string和number类型
//1.通常: 代码冗余
function getData1(value:string):string{
return value
}
function getData2(value:number):number{
return value
}
//2.用any(可以解决,但是放弃了类型检查)
function getData(value:any):any{
return value //什么类型都可以
}
//传入什么返回什么。比如传入number类型,必须返回number类型。传入string必须返回string类型。用any就
可以不一致。
//泛型,可以支持不特定的数据类型
//要求:传入参数和返回的参数一致
//这里的T指泛型,也可以用任意字母取代,但是前后要一致
function getData
return value
}
function getData
return 'xxxx' //错误写法。不能将任何任性分配给T
}
//调用
getData
getData
//可以调用的时候传一个泛型,返回的时候返回其他的类型(用的不多)
function getData
return 'xxx'
}
//调用
getData
getData
//定义泛型是什么类型,就要传入什么类型的参数如何在TS中对函数的返回值进行类型约束
ts中函数参数的类型定义
函数的参数可能是一个,也可能是多个,有可能是一个变量,一个对象,一个函数,一个数组等等。
1.函数的参数为单个或多个单一变量的类型定义
修改后:
2.函数的参数为数组的类型定义
修改后:
3.函数的参数为对象的类型定义
修改后:
4.函数的参数为函数的类型定义
function fntA(one, two, three) {
// 参数 "two" 隐式具有 "any" 类型,但可以从用法中推断出更好的类型。
return one + two + three
}
const aResult = fntA(1, '3', true)
function fntA(one: number, two: string, three: boolean) {
return one + two + three
}
const aResult1 = fntA(1, '3', true)
// 如果函数的参数为单个或者多个变量的时候,只需要为这些参数进行静态类型下的基础类型定义就行
function fntB(arr) {
//参数 "arr" 隐式具有 "any" 类型,但可以从用法中推断出更好的类型。
return arr[0]
}
const bResult = fntB([1, 3, 5])
function fntB(arr: number[]) {
return arr[0]
}
const bResult1 = fntB([1, 3, 5])
// 如果参数是数组时,只需要为这些变量进行对象类型下的数组类型定义
function fntC({ one, two }) {
return one + two
}
const cResult = fntC({ one: 6, two: 10 })
function fntC({ one, two }: { one: number, two: number }) {
return one + two
}
const cResult1 = fntC({ one: 6, two: 10 })
// 如果参数是对象,只需要为这些变量进行对象类型下的对象类型定义function fntD(callback) {
//参数 "callback" 隐式具有 "any" 类型,但可以从用法中推断出更好的类型
callback(true)
}
function callback(bl: boolean): boolean {
console.log(bl)
return bl
}
const dResult = fntD(callback)
修改后:
function fntD(callback: (bl: boolean) => boolean) {
callback(true)
}
function callback(bl: boolean): boolean {
console.log(bl)
return bl
}
const dResult = fntD(callback)
// 如果参数是函数,只需要为参数进行对象类型下的函数类型定义即可
ts中函数返回值的类型定义
当函数有返回值时,根据返回值的类型在相应的函数位置进行静态类型定义即可
返回数字:
function getTotal2(one: number, two: number): number {
return one + two;
}
const total2 = getTotal(1, 2);
// 返回值为数字类型
返回布尔值
function getTotal2(one: number, two: number): boolean {
return Boolean(one + two);
}
const total2 = getTotal(1, 2);
// 返回值为布尔类型
返回字符串
function getTotal2(one: string, two: string): string{
return Bone + two;
}
const total2 = getTotal('1', '2');
// 返回值为字符串
返回对象
function getObj(name: string, age: number): { name: string, age: number } {
return {name,age}
}
getObj('小红',16)
// 返回值为对象返回数组
函数返回值为underfinde,仅仅时为了在内部实现某个功能,我们就可以给他一个类型注解void,代表没
有任何返回值,
修改后:
当函数没有返回值时
ts和js相比有什么区别
在ts中完全可以使用js
ts是js的超集,并且ts比js多了一个类型检查功能
RX了解吗?
Rx 中强大的地方在于两处
管道思想,通过管道,我们订阅了数据的来源,并在数据源更新时响应 。
强大的操作符,通过操作符对流和流中的数据转换,拼接,以形成我们想要的数据模型 。
在 Rx 中,我们先预装好管道,通过管道流通数据 。这些管道的来源多种, create ,from, fromEvent,
of .., 通过操作符将管道 拼接,合并,映射...形成最终的数据模型 。
对于管道来说,有两点非常重要
. 管道是懒执行的,只有订阅器 observer subscribe了 数据管道,这个管道才会有数据流通 。
. 整个节点组成一个完整的管道,订阅了后面的管道节点,也会同时订阅之前的管道节点 ,每个节点接
受之前的值,并发出新值
Rx 如此高效和强大,得益于其强大的操作符 。
主要包含下面几类
function getArr(arr: number[]) :number[]{
let newArr = [...arr]
return newArr
}
getArr([1,2,3,4])
// 返回值为数组
function sayName() {
console.log('hello,world')
}
function sayName1(): void {
console.log('无返回值')
}
// 因为总是抛出异常,所以 error 将不会有返回值
// never 类型表示永远不会有值的一种类型
function error(message: string): never {
throw new Error(message);
}创建操作符:
create, range, of, from, fromEvent, fromPromise, empty ..
组合 contact ,merge, startWith, zip ..
时间 delay , throttle, dobounceTime, interval ..
过滤:
filter, first, last, skip, distinct, take ..
转换:
buffer,map, mapTo, mergeMap, switch, switchMap, reduce, scan .
六、nodejs
说说对nodejs的了解
Node.js 是一个开源和跨平台的 JavaScript 运行时环境。 它几乎是任何类型项目的流行工具!
Node.js 在浏览器之外运行 V8 JavaScript 引擎(
Google Chrome 的内核)。 这使得 Node.js 的性能非
常好。
Node.js 应用程序在单个进程中运行,无需为每个请求创建新的线程。 Node.js 在其标准库中提供了
一组异步的 I/O 原语,以防止 JavaScript 代码阻塞,通常,Node.js 中的库是使用非阻塞范式编写
的,使得阻塞行为成为异常而不是常态。
当 Node.js 执行 I/O 操作时(比如从网络读取、访问数据库或文件系统),Node.js 将在响应返回时
恢复操作(而不是阻塞线程和浪费 CPU 周期等待)。
这允许 Node.js 使用单个服务器处理数千个并发连接,而
不会引入管理线程并发(这可能是错误的
重要来源)的负担。
Node.js 具有独特的优势,因为数百万为浏览器编写 JavaScript 的前端开发者现在无需学习完全不同
的语言,就可以编写除客户端代码之外的服务器端代码。
在 Node.js 中,可以毫无问题地使用新的 ECMAScript 标准,因为你不必等待所有用户更新他们的浏
览器,你负责通过更改 Node.js 版本来决定使用哪个 ECMAScript 版本,你还可以通过运行带有标志
的 Node.js 来启用特定的实验性功能。
nodejs如何写接口,返回参数如何处理,有多少种方法
nodejs的原生模块http可用于编写接口,但一般推荐使用express或koa2来设计restful api。
下面以 express 为例说明如何编写接口。
express响应客户端的方法有:end()/json()/render()/send()/jsonp()/sendFile()等。
websocket和http的区别
WebSocket 是 HTML5 中的协议,支持持久连续,http 协议不支持持久性连接。
Http1.0 和 HTTP1.1 都不支持持久性的链接,HTTP1.1 中的 keep-alive,将多个 http 请求合并为 1
个。
var express = require('express')
var app = express()
app.get('/', function (req, res) {
res.send('hello world')
})常见的 HTTP Method 有哪些? GET/POST 区别?
1、常见的HTTP方法
GET:获取资源
POST:传输资源
PUT:更新资源
DELETE:删除资源
HEAD:获得报文首部
2、GET/POST的区别
GET在浏览器回退时是无害的,而POST会再次提交请求
GET请求会被浏览器主动缓存,而POST不会,除非手动设置
GET请求参数会被完整保留在浏览器的历史记录里,而POST中的参数不会被保留
GET请求在URL中传送的参数是有长度限制的,而POST没有限制
GET参数通过URL传递,POST放在Request body中
GET请求只能进行 url 编码,而POST支持多种编码方式
GET产生的URL地址可以被收藏,而POST不可以
对参数的数据类型,GET只接受ASCII字符,而POST没有限制
GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息
说一说Tcp三次握手,四次挥手
1、三次握手
seq序号,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记
ack确认序号,只有ACK标志位为1时,确认序号字段才有效,ack=seq+1
2、四次挥手
关闭连接时,当服务器端收到FIN报文时,很可能并不会立即关闭链接,所以只能先回复一个ACK报文,
告诉客户端:”你发的FIN报文我收到了”,只有等到服务器端所有的报文都发送完了,我才能发送FIN报
文,因此不能一起发送,故需要四步握手。
阐述一下http1.0与http2.0的区别,及http和https区别
1、HTTP1.0和HTTP1.1的一些区别
缓存处理,HTTP1.0中主要使用Last-Modified,Expires 来做为缓存判断的标准,HTTP1.1则引入了更
多的缓存控制策略:ETag,Cache-Control…
带宽优化及网络连接的使用,HTTP1.1支持断点续传,即返回码是206(
Partial Content)
错误通知的管理,在HTTP1.1中新增了24个错误状态响应码,如409(
Conflict)表示请求的资源与资
源的当前状态发生冲突;410(
Gone)表示服务器上的某个资源被永久性的删除…
Host头处理,在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没
有传递主机名(
hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟
主机(Multi-homed Web Servers),并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都应
支持Host头域,且请求消息中如果没有Host头域会报告一个错误(
400 Bad Request)
长连接,HTTP1.1中默认开启Connection:
keep-alive,一定程度上弥补了HTTP1.0每次请求都要创
建连接的缺点
2、HTTP2.0和HTTP1.X相比的新特性
新的二进制格式(
Binary Format),HTTP1.x的解析是基于文本,基于文本协议的格式解析存在天
然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1
的组合,基于这种考虑HTTP2.0的协议解析决定采用二进制格式,实现方便且健壮header压缩,HTTP1.x的header带有大量信息,而且每次都要重复发送,HTTP2.0使用encoder来减少
需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又
减小了需要传输的大小
服务端推送(
server push),例如我的网页有一个sytle.css的请求,在客户端收到sytle.css数据的同
时,服务端会将sytle.js的文件推送给客户端,当客户端再次尝试获取sytle.js时就可以直接从缓存中
获取到,不用再发请求了
node.js如何导出页面数据形成报表
生成报表并下载是作为web应用中的一个传统功能,在实际项目中有广范的应用,实现原理也很简单,
以NodeJS导出数据的方法为例,就是拿到页面数据对应id,然后根据id查询数据库,拿到所需数据后格
式化为特点格式的数据,最后导出文件。
在nodejs中,也提供了很多的第三方库来实现这一功能,以 node-xlsx 导出excel文件为例,实现步骤如
下:
1. 下载 node-xlsx
2. 编写接口
以下代码使用 express 编写,调用接口实现下载功能
npm install node-xlsx
const fs = require('fs');
const path = require('path');
const xlsx = require('node-xlsx');
const express = require('express');
const router = express.Router();
const mongo = require('../db');
router.post('/export', async (req, res) => {
const { ids } = req.body;
const query = {
_id: { $in: ids }
}
// 查询数据库获取数据
let result = await mongo.find(colName, query);
// 设置表头
const keys = Object.keys(Object.assign({}, ...result));
rows[0] = keys;
// 设置表格数据
const rows = []
result.map(item => {
const values = []
keys.forEach((key, idx) => {
if (item[key] === undefined) {
values[idx] = null;
} else {
values[idx] = item[key];
}
})
rows.push(values);协商缓存和强缓存
浏览器缓存主要分为强强缓存(也称本地缓存)和协商缓存(也称弱缓存)。浏览器在第一次请求发生
后,再次发送请求时:
浏览器请求某一资源时,会先获取该资源缓存的header信息,然后根据header中的 Cache-
Control 和 Expires 来判断是否过期。若没过期则直接从缓存中获取资源信息,包括缓存的
header的信息,所以此次请求不会与服务器进行通信。这里判断是否过期,则是强缓存相关。后面
会讲 Cache-Control 和 Expires 相关。
如果显示已过期,浏览器会向服务器端发送请求,这个请求会携带第一次请求返回的有关缓存的
header字段信息,比如客户端会通过 If-None-Match 头将先前服务器端发送过来的Etag发送给服务
器,服务会对比这个客户端发过来的Etag是否与服务器的相同,若相同,就将 If-None-Match 的值
设为false,返回状态304,客户端继续使用本地缓存,不解析服务器端发回来的数据,若不相同就
将 If-None-Match 的值设为true,返回状态为200,客户端重新机械服务器端返回的数据;客户端
还会通过 If-Modified-Since 头将先前服务器端发过来的最后修改时间戳发送给服务器,服务器
端通过这个时间戳判断客户端的页面是否是最新的,如果不是最新的,则返回最新的内容,如果是
最新的,则返回304,客户端继续使用本地缓存。
强缓存
强缓存是利用http头中的 Expires 和 Cache-Control 两个字段来控制的,用来表示资源的缓存时间。
强缓存中,普通刷新会忽略它,但不会清除它,需要强制刷新。浏览器强制刷新,请求会带上 Cache-
Control:no-cache 和 Pragma:no-cache
Expires
Expires 是http1.0的规范,它的值是一个绝对时间的GMT格式的时间字符串。如我现在这个网页的
Expires 值是: expires:Fri, 14 Apr 2017 10:47:02 GMT 。这个时间代表这这个资源的失效时
间,只要发送请求时间是在 Expires 之前,那么本地缓存始终有效,则在缓存中读取数据。所以这种方
式有一个明显的缺点,由于失效的时间是一个绝对时间,所以当服务器与客户端时间偏差较大时,就会
导致缓存混乱。如果同时出现 Cache-Control:max-age 和 Expires ,那么 max-age 优先级更高。如我
主页的response headers部分如下:
Cache-Control
Cache-Control是在http1.1中出现的,主要是利用该字段的max-age值来进行判断,它是一个相对时间,例
如Cache-Control:max-age=3600,代表着资源的有效期是3600秒。cache-control除了该字段外,还有下面
几个比较常用的设置值:
});
let data = xlsx.build([{ name: "商品列表", data: rows }]);
const downloadPath = path.join(__dirname, '../../public/download');
const filePath = `${downloadPath}/goodslist.xlsx`;
fs.writeFileSync(filePath, data);
res.download(filePath, `商品列表.xlsx`, (err) => {
console.log('download err', err);
});
})
cache-control:max-age=691200
expires:Fri, 14 Apr 2017 10:47:02 GMTno-cache:不使用本地缓存。需要使用缓存协商,先与服务器确认返回的响应是否被更改,如果之
前的响应中存在ETag,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下
载。
no-store:直接禁止游览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都
会下载完整的资源。
public:可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器。
private:只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。
Cache-Control与Expires可以在服务端配置同时启用,同时启用的时候Cache-Control优先级高。
协商缓存
协商缓存就是由服务器来确定缓存资源是否可用,所以客户端与服务器端要通过某种标识来进行通信,
从而让服务器判断请求资源是否可以缓存访问。
普通刷新会启用弱缓存,忽略强缓存。只有在地址栏或收藏夹输入网址、通过链接引用资源等情况下,
浏览器才会启用强缓存,这也是为什么有时候我们更新一张图片、一个js文件,页面内容依然是旧的,
但是直接浏览器访问那个图片或文件,看到的内容却是新的。
这个主要涉及到两组header字段: Etag 和 If-None-Match 、 Last-Modified 和 If-Modified-
Since 。上面以及说得很清楚这两组怎么使用啦~复习一下:
Etag 和 If-None-Match
Etag/If-None-Match返回的是一个校验码。ETag可以保证每一个资源是唯一的,资源变化都会导致ETag变
化。服务器根据浏览器上送的If-None-Match值来判断是否命中缓存。
与Last-Modified不一样的是,当服务器返回304 Not Modified的响应时,由于ETag重新生成过,response
header中还会把这个ETag返回,即使这个ETag跟之前的没有变化。
Last-Modify/If-Modify-Since
浏览器第一次请求一个资源的时候,服务器返回的header中会加上Last-Modify,Last-modify是一个时间
标识该资源的最后修改时间,例如Last-Modify: Thu,31 Dec 2037 23:59:59 GMT。
当浏览器再次请求该资源时,request的请求头中会包含If-Modify-Since,该值为缓存之前返回的Last-
Modify。服务器收到If-Modify-Since后,根据资源的最后修改时间判断是否命中缓存。
如果命中缓存,则返回304,并且不会返回资源内容,并且不会返回Last-Modify。
为什么要有Etag
你可能会觉得使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要Etag
呢?HTTP1.1中Etag的出现主要是为了解决几个Last-Modified比较难解决的问题:
一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不
希望客户端认为这个文件被修改了,而重新GET;
某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-
Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);
某些服务器不能精确的得到文件的最后修改时间。
Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-
Modified,最后才决定是否返回304。分类
分类描述
1**
信息,服务器收到请求,需要请求者继续执行操作
2**
成功,操作被成功接收并处理
3**
重定向,需要进一步的操作以完成请求
4**
客户端错误,请求包含语法错误或无法完成请求
5**
服务器错误,服务器在处理请求的过程中发生了错误
http常用状态码有哪一些,说一说他们的作用
常用的状态码:
200表示成功,301表示重定向,404表示资源未找到,500表示服务器内部错误
状态码分类:
http状态码列表状
态
码
状态码英文名
称
中文描述
100
Continue
继续。客户端应继续其请求
101
Switching
Protocols
切换协议。服务器根据客户端的请求切换协议。只能切换到更高级的协
议,例如,切换到HTTP的新版本协议
200
OK
请求成功。一般用于GET与POST请求
201
Created
已创建。成功请求并创建了新的资源
202
Accepted
已接受。已经接受请求,但未处理完成
203
Non-
Authoritative
Information
非授权信息。请求成功。但返回的meta信息不在原始的服务器,而是一
个副本
204
No Content
无内容。服务器成功处理,但未返回内容。在未更新网页的情况下,可
确保浏览器继续显示当前文档
205
Reset Content
重置内容。服务器处理成功,用户终端(例如:浏览器)应重置文档视
图。可通过此返回码清除浏览器的表单域
206
Partial
Content
部分内容。服务器成功处理了部分GET请求
300
Multiple
Choices
多种选择。请求的资源可包括多个位置,相应可返回一个资源特征与地
址的列表用于用户终端(例如:浏览器)选择
301
Moved
Permanently
永久移动。请求的资源已被永久的移动到新URI,返回信息会包括新的
URI,浏览器会自动定向到新URI。今后任何新的请求都应使用新的URI代
替
302
Found
临时移动。与301类似。但资源只是临时被移动。客户端应继续使用原有
URI
303
See Other
查看其它地址。与301类似。使用GET和POST请求查看
304
Not Modified
未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何
资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户
端希望只返回在指定日期之后修改的资源
305
Use Proxy
使用代理。所请求的资源必须通过代理访问
306
Unused
已经被废弃的HTTP状态码
307
Temporary
Redirect
临时重定向。与302类似。使用GET请求重定向
400
Bad Request
客户端请求的语法错误,服务器无法理解状
态
码
状态码英文名
称
中文描述
401
Unauthorized
请求要求用户的身份认证
402
Payment
Required
保留,将来使用
403
Forbidden
服务器理解请求客户端的请求,但是拒绝执行此请求
404
Not Found
服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设
计人员可设置"您所请求的资源无法找到"的个性页面
405
Method Not
Allowed
客户端请求中的方法被禁止
406
Not
Acceptable
服务器无法根据客户端请求的内容特性完成请求
407
Proxy
Authentication
Required
请求要求代理的身份认证,与401类似,但请求者应当使用代理进行授权
408
Request Time-
out
服务器等待客户端发送的请求时间过长,超时
409
Conflict
服务器完成客户端的 PUT 请求时可能返回此代码,服务器处理请求时发
生了冲突
410
Gone
客户端请求的资源已经不存在。410不同于404,如果资源以前有现在被
永久删除了可使用410代码,网站设计人员可通过301代码指定资源的新
位置
411
Length
Required
服务器无法处理客户端发送的不带Content-Length的请求信息
412
Precondition
Failed
客户端请求信息的先决条件错误
413
Request Entity
Too Large
由于请求的实体过大,服务器无法处理,因此拒绝请求。为防止客户端
的连续请求,服务器可能会关闭连接。如果只是服务器暂时无法处理,
则会包含一个Retry-After的响应信息
414
Request-URI
Too Large
请求的URI过长(URI通常为网址),服务器无法处理
415
Unsupported
Media Type
服务器无法处理请求附带的媒体格式
416
Requested
range not
satisfiable
客户端请求的范围无效
417
Expectation
Failed
服务器无法满足Expect的请求头信息状
态
码
状态码英文名
称
中文描述
500
Internal Server
Error
服务器内部错误,无法完成请求
501
Not
Implemented
服务器不支持请求的功能,无法完成请求
502
Bad Gateway
作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到
了一个无效的响应
503
Service
Unavailable
由于超载或系统维护,服务器暂时的无法处理客户端的请求。延时的长
度可包含在服务器的Retry-After头信息中
504
Gateway
Time-out
充当网关或代理的服务器,未及时从远端服务器获取请求
505
HTTP Version
not supported
服务器不支持请求的HTTP协议的版本,无法完成处理
网络攻击方案有哪些,自己有写过什么安全性方面的东西吗?
1. iframe
2. opener
3. CSRF(跨站请求伪造)
4. XSS(跨站脚本攻击)
5. ClickJacking(点击劫持)
6. HSTS(HTTP严格传输安全)
7. CND劫持
很难通过技术手段完全避免 XSS,但我们可以总结以下原则减少漏洞的产生:
利用模板引擎 开启模板引擎自带的 HTML 转义功能。例如:
在 ejs 中,尽量使用 <%= data %> 而
不是 <%- data %> ; 在 doT.js 中,尽量使用 {{! data } 而不是 {{= data } ; 在 FreeMarker
中,确保引擎版本高于 2.3.24,并且选择正确的 freemarker.core.OutputFormat 。
避免内联事件 尽量不要使用 onLoad="onload('{{data}}')" 、
onClick="go('{{action}}')" 这种拼接内联事件的写法。在 JavaScript 中通过
.addEventlistener() 事件绑定会更安全。
避免拼接 HTML 前端采用拼接 HTML 的方法比较危险,如果框架允许,使用 createElement 、
setAttribute 之类的方法实现。或者采用比较成熟的渲染框架,如 Vue/React 等。
时刻保持警惕 在插入位置为 DOM 属性、链接等位置时,要打起精神,严加防范。
增加攻击难度,降低攻击后果 通过 CSP、输入长度配置、接口安全措施等方法,增加攻击的难度,
降低攻击的后果。
主动检测和发现 可使用 XSS 攻击字符串和自动扫描工具寻找潜在的 XSS 漏洞。静态资源部署到哪?
存放静态资源文件到服务器
但这种形式在性能上也有缺陷:
受地理环境影响,离服务器越远资源加载越慢
频繁请求资源对服务器压力较大
存放静态资源文件到CDN
为了进一步提升性能,可以把动态网页(
index.html)和静态资源(
js、css、image...)分开部署。
静态资源被放置于 CDN 上.
但是 CDN 也有缓存策略:新的静态资源发布后,需要一定的时间去覆盖各个边缘站点的旧资源。
若某客户端获得了新的动态网页,但是附近的 CDN 节点尚未更新最近发布的静态资源,客户端即
便放弃本地缓存,它加载的依旧是位于 CDN 上的“脏数据”。怎么办呢?干脆把文件名也给改了——
让摘要信息成为文件名的一部分!具体实现可以仰仗 webpack,将 output.filename 设为
[name].[contenthash].js ,输出文件和 html 模版都会帮你更改好.
用摘要信息重命名后的资源文件,与旧资源就不同名了,不再需要以覆盖旧文件的形式主动更新各
个地区的边缘站点。新版本发布后,浏览器首次请求资源,若 CDN 不存在该资源,便会向就近的
边缘站点加载该文件,同时更新 CDN 缓存;这就彻底避免了 CDN 脏数据的问题.
说说你对nodejs的了解
我们从以下几方面来看nodejs.
什么是nodejs?
Node.js 是一个开源与跨平台的 JavaScript 运行时环境, 在浏览器外运行 V8 JavaScript 引擎
(
Google Chrome 的内核),利用事件驱动、非阻塞和异步输入输出模型等技术提高性能.
可以理解为 Node.js 就是一个服务器端的、非阻塞式I/O的、事件驱动的 JavaScript 运行环境
非阻塞异步
Nodejs 采用了非阻塞型 I/O 机制,在做 I/O 操作的时候不会造成任何的阻塞,当完成之
后,以时间的形式通知执行操作
例如在执行了访问数据库的代码之后,将立即转而执行其后面的代码,把数据库返回结果的处
理代码放在回调函数中,从而提高了程序的执行效率
事件驱动
事件驱动就是当进来一个新的请求的时,请求将会被压入一个事件队列中,然后通过一个循环
来检测队列中的事件状态变化,如果检测到有状态变化的事件,那么就执行该事件对应的处理
代码,一般都是回调函数
优缺点
优点:
处理高并发场景性能更佳
适合I/O密集型应用,指的是应用在运行极限时,CPU占用率仍然比较低,大部分时间是在做
I/O硬盘内存读写操作
因为 Nodejs 是单线程,带来的缺点有:
不适合CPU密集型应用
只支持单核CPU,不能充分利用CPU
可靠性低,一旦代码某个环节崩溃,整个系统都崩溃
应用场景借助 Nodejs 的特点和弊端,其应用场景分类如下:
善于 I/O ,不善于计算。因为Nodejs是一个单线程,如果计算(同步)太多,则会阻塞这个
线程
大量并发的I/O,应用程序内部并不需要进行非常复杂的处理
与 websocket 配合,开发长连接的实时交互应用程序
具体场景可以表现为如下:
第一大类:用户表单收集系统、后台管理系统、实时交互系统、考试系统、联网软件、高并发
量的web应用程序
第二大类:基于web、canvas等多人联网游戏
第三大类:基于web的多人实时聊天客户端、聊天室、图文直播
第四大类:单页面浏览器应用程序
第五大类:操作数据库、为前端和移动端提供基于 json 的API
七、GIT
git经常用哪些指令
产生代码库
新建一个git代码库
下载远程项目和它的整个代码历史
配置
显示配置
编辑配置
设置用户信息
暂存区文件操作
增加文件到暂存区
在暂存区中删除文件
git init
git clone 远程仓库地址
git config --list [--global]
git config -e [--global]
git config [--global] user.name "名"
git config [--global] user.email "邮箱地址"
# 1.添加当前目录的所有文件到暂存区
git add .
# 2.添加指定目录到暂存区,包括子目录
git add [dir]
# 3.添加指定文件到暂存区
git add [file1] [file2] ...# 删除工作区文件,并且将这次删除放入暂存区
git rm [file1] [file2] ...
# 停止追踪指定文件,但该文件会保留在工作区
git rm --cached [file]
重命名暂存区文件
# 改名文件,并且将这个改名放入暂存区
git mv [file-original] [file-renamed]
代码提交
# 提交暂存区到仓库区
git commit -m [message]
分支操作
# 列出所有本地分支
git branch
# 列出所有远程分支
git branch -r
# 列出所有本地分支和远程分支
git branch -a
# 新建一个分支,但依然停留在当前分支
git branch [branch-name]
# 新建一个分支,并切换到该分支
git checkout -b [branch]
# 新建一个分支,指向指定commit
git branch [branch] [commit]
# 新建一个分支,与指定的远程分支建立追踪关系
git branch --track [branch] [remote-branch]
# 切换到指定分支,并更新工作区
git checkout [branch-name]
# 切换到上一个分支
git checkout -
# 建立追踪关系,在现有分支与指定的远程分支之间
git branch --set-upstream [branch] [remote-branch]
# 合并指定分支到当前分支
git merge [branch]
# 选择一个commit,合并进当前分支
git cherry-pick [commit]
# 删除分支
git branch -d [branch-name]
# 删除远程分支
git push origin --delete [branch-name]git branch -dr [remote/branch]
信息查看
# 显示有变更的文件
git status
# 显示当前分支的版本历史
git log
# 显示commit历史,以及每次commit发生变更的文件
git log --stat
# 搜索提交历史,根据关键词
git log -S [keyword]
# 显示某个commit之后的所有变动,每个commit占据一行
git log [tag] HEAD --pretty=format:%s
# 显示某个commit之后的所有变动,其"提交说明"必须符合搜索条件
git log [tag] HEAD --grep feature
# 显示过去5次提交
git log -5 --pretty --oneline
同步操作
# 增加一个新的远程仓库,并命名
git remote add [shortname] [url]
# 取回远程仓库的变化,并与本地分支合并
git pull [remote] [branch]
# 上传本地指定分支到远程仓库
git push [remote] [branch]
# 强行推送当前分支到远程仓库,即使有冲突
git push [remote] --force
# 推送所有分支到远程仓库
git push [remote] --all
撤销操作
# 恢复暂存区的指定文件到工作区
git checkout [file]
# 恢复某个commit的指定文件到暂存区和工作区
git checkout [commit] [file]
# 恢复暂存区的所有文件到工作区
git checkout .
# 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变
git reset [file]
# 重置暂存区与工作区,与上一次commit保持一致
git reset --hardgit出现代码冲突怎么解决
冲突合并一般是因为自己的本地做的提交和服务器上的提交有差异,并且这些差异中的文件改动,Git不
能自动合并,那么就需要用户手动进行合并
如我这边执行 git pull origin master
如果Git能够自动合并,那么过程看起来是这样的:
拉取的时候,Git自动合并,并产生了一次提交。
如果Git不能够自动合并,那么会提示:
这个时候我们就可以知道 README.MD 有冲突,需要我们手动解决,修改 README.MD 解决冲突:
# 重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变
git reset [commit]
# 重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致
git reset --hard [commit]
# 重置当前HEAD为指定commit,但保持暂存区和工作区不变
git reset --keep [commit]
# 新建一个commit,用来撤销指定commit
# 后者的所有变化都将被前者抵消,并且应用到当前分支
git revert [commit]可以看出来,在1+1=几的这行代码上产生了冲突,解决冲突的目标是保留期望存在的代码,这里保留
1+1=2,然后保存退出。
退出之后,确保所有的冲突都得以解决,然后就可以使用:
即可完成一次冲突的合并。
整个过程看起来是这样的:
你们团队是怎么管理git分支的
关于 Git 分支管理,每个团队在不同阶段都有自己的管理策略。我们团队以前采用的是版本分支管理策
略,也就是每次上线新版本都会创建一个新的版本分支,而新的需求开发也从当前最新的版本分支迁出
一个新的需求分支开发,线上 bug 则在版本分支上修改然后发布。
git add .
git commit -m "fixed conflicts"
git push origin master`如何实现Git的免密操作
现在的远程仓库都提供了基于SSH协议的Git服务,在使用SSH协议访问仓库之前,需要先配置好账户/仓
库的SSH公钥。
① 产生公私钥对
产生完毕后,公私玥对位于 c/Users/用户名/.ssh/ :
id_rsa:私钥(私有的钥匙,不能公开)
id_rsa.pub:公钥(可以公开的钥匙)
② 将公钥上传至远程仓库个人中心的设置里
以gitee为例,添加公钥请访问:https://gitee.com/profile/sshkeys
注:如果想用现在的免密登录,请使用ssh协议去关联本地与线上仓库。
八、其它
loadsh 了解过吗?
Lodash 是一个一致性、模块化、高性能的 JavaScript 实用工具库。
首先要明白的是lodash的所有函数都不会在原有的数据上进行操作,而是复制出一个新的数据而不改变
原有数据。类似immutable.js的理念去处理。
Lodash 通过降低 array、number、objects、string 等等的使用难度从而让 JavaScript 变得更简单。
Lodash 的模块化方法 非常适用于:
1. 遍历 array、object 和 string 对
2. 值进行操作和检测
3. 创建符合功能的函数
常用语法:cloneDeep深拷贝,uniq数组去重,compact 去除假值,filter和reject 过滤集合传入匿名函
数, join 将 array 中的所有元素转换为由 separator 分隔的字符串等
ssh-keygen -t rsa -C [email protected]是否用过混合APP开发
1.原生开发(NativeApp开发):像盖房子一样,先打地基然后浇地梁、房屋结构、一砖一瓦、钢筋水泥、
电路走向等,原生APP同理:通过代码从每个页面、每个功能、每个效果、每个逻辑、每个步骤全部用
代码写出来,一层层,一段段全用代码写出来
此种APP的数据都保存在本地,APP能及时调取,所以相应速度及流畅性有保障
2.混合开发(HTML5开发):这个就相当于一种框架开发,说白了就是网页;该模式通常由“HTML5云网站
+APP应用客户端”两部份构成,APP应用客户端只需安装应用的框架部份,而应用的数据则是每次打开
APP的时候,去云端取数据呈现给手机用户。
混合APP还有一种是套壳APP,套壳APP就是用H5的网页打包成APP,虽然是APP能安装到手机上,但是每
个界面,全部是网页
混合APP开发优势:
时间短:基本都是模版拿来直接套上或打包成APP,会节省很大一部分时间。
价格便宜:代码不需要重新写,界面不用重新设计,都是固定的,可替换的地方很少,自己随便都能换
上,所以价格相对便宜。
项目中的组件是如何使用的
可以使用第三方组件库以及自定义组件
vue项目中使用组件库为 vant / element-ui / iview / ant-design-vue
react常用组件库为 Ant Design / Ant Design Mobile
开发者也可以通过自定义实现组件的使用
hash和histoty的原理
hash模式:在浏览器中符号“#”,#以及#后面的字符称之为hash,用 window.location.hash 读取。特
点:hash虽然在URL中,但不被包括在HTTP请求中;用来指导浏览器动作,对服务端安全无用,
hash不会重加载页面。
history模式:history采用HTML5的新特性;且提供了两个新方法:
pushState(), replaceState()可以
对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更
window.location.href和history.push的区别
window.location.href 锚点跳转,不会加入路由栈。
history.push 向由路由栈中添加新的路由信息,点击浏览器返回按钮时返回到上一页。
商城项目中有写到调用微信,支付宝支付,简单讲述一下这个支付与后台
对接的过程,微信支付的原理
步骤1:用户在商户APP中选择商品,提交订单,选择微信支付。
步骤2:商户后台收到用户支付单,调用微信支付统一下单接口。参见【统一下单API】。
步骤3:统一下单接口返回正常的prepay_id,再按签名规范重新生成签名后,将数据传输给APP。
参与签名的字段名为appid,partnerid,prepayid,noncestr,timestamp,package。注意:package
的值格式为Sign=WXPay
步骤4:商户APP调起微信支付。api参见本章节【
app端开发步骤说明】
步骤5:商户后台接收支付通知。api参见【支付结果通知API】
步骤6:商户后台查询支付结果。,api参见【查询订单API】(查单实现可参考:支付回调和查单实
现指引)混合开发知道吗?你是怎么理解混合开发的,在项目中用到过混合开发吗?
在APP应用中使用WebView嵌套H5页面,即为混合开发。
Android、IOS、uniapp、ReactNative、小程序,都支持混合开发了。
平时工作中有是否有接触linux系统?说说常用到linux命令?
1、常用的目录操作命令
创建目录 mkdir <目录名称>
删除目录 rm <目录名称>
定位目录 cd <目录名称>
查看目录文件 ls ll
修改目录名 mv <目录名称> <新目录名称>
拷贝目录 cp <目录名称> <新目录名称>
2、常用的文件操作命令
创建文件 touch <文件名称> vi <文件名称>
删除文件 rm <文件名称>
修改文件名 mv <文件名称> <新文件名称>
拷贝文件 cp <文件名称> <新文件名称>
3、常用的文件内容操作命令
查看文件 cat <文件名称> head <文件名称> tail <文件名称>
编辑文件内容 vi <文件名称>
查找文件内容 grep '关键字' <文件名称>
echarts是什么,怎么用
echarts是一个基于 JavaScript 的开源可视化图表库,可以流畅的运行在 PC 和移动设备上,兼容绝大部
分的浏览器(
IE9/10/11,Chrome,Firefox,Safari等),底层依赖矢量图形库 ZRender,提供20 多种图表
和十几种组件,支持Canvas、SVG 双引擎并能一键切换,让移动端渲染更加流畅
echarts源自百度,现由Apache 开源社区维护,官方提供了完善的文档(
https://echarts.apache.org
),使用也非常简单,步骤如下:
1. 下载echarts
从 GitHub 获取
https://github.com/apache/echarts
从 npm 获取
从 CDN 获取
https://www.jsdelivr.com/package/npm/echarts
在线定制
https://echarts.apache.org//builder.html
2. 引入echarts
npm install echarts --save3. 添加一个div容器并设置宽高
注意:不设置宽高无法显示效果
4. 初始化一个 echarts 实例并通过 setOption 方法生成一个图表(以柱状图为例)
Hash和history的区别
hash与history一般指Vue-router或React-router中的路由模式,Vue-router中通过 mode 配置选项设置,
React-router中通过 HashRouter 与 BrowerRouter 组件设置,区别如下:
1. 最直观的区别就是在浏览器url中hash 带了一个很丑的 # ,而history是没有
2. hash即浏览器地址栏 URL 中的 # 符号,hash 虽然出现在 URL 中,但不会被包括在 HTTP 请求中,
因此改变 hash 不会重新加载页面
// 基于准备好的dom,初始化echarts实例
var myChart = echarts.init(document.getElementById('main'));
// 指定图表的配置项和数据
var option = {
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
legend: {
data: ['销量']
},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
},
yAxis: {},
series: [
{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}
]
};
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
3. history利用了 HTML5新增的 pushState() 和 replaceState() 方法,在已有 back() 、
forward() 、 go() 的基础上,提供了对历史记录进行修改的功能。调用 pushState() 和
replaceState() 时,虽然改变了当前的 URL,但浏览器不会向后端发送请求,但如何用户刷新
页面,会导致浏览器向服务器发起请求,如后端没有做出适当的响应,则会显示404页面
谈谈宏任务与微任务的理解,举一个宏任务与微任务的api
要理解宏任务(macrotask)与微任务(microtask),就必须了解javascript中的事件循环机制(
event
loop)以及js代码的运行方式
要了解js代码的运行方式,得先搞懂以下几个概念
JS是单线程执行
单线程指的是JS引擎线程
宿主环境
JS运行的环境,一般为浏览器或者Node
执行栈
是一个存储函数调用的栈结构,遵循先进后出的原则
JS引擎常驻于内存中,等待宿主将JS代码或函数传递给它执行,如何传递,这就是事件循环(
event
loop)所做的事情:当js执行栈空闲时,事件循环机制会从任务队列中提取第一个任务进入到执行栈执
行,优先提取微任务(microtask),待微任务队列清空后,再提取宏任务(macrotask),并不断重复该
过程
在实际应用中,宏任务(macrotask)与微任务(microtask)的API分别如下:
宏任务
setTimeout/setInterval
ajax
setImmediate (Node 独有)
requestAnimationFrame (浏览器独有)
I/O
UI rendering (浏览器独有)
微任务
process.nextTick (Node 独有)
Promise
Object.observe
MutationObserver
对Event loop的了解?
Javascript是单线程的,那么各个任务进程是按照什么样的规范来执行的呢?这就涉及到Event Loop的概
念了,EventLoop是在html5规范中明确定义的;
何为eventloop,javascript中的一种运行机制,用来解决浏览器单线程的问题
Event Loop是一个程序结构,用于等待和发送消息和事件。同步任务、异步任务、微任务、宏任务
javascript单线程任务从时间上分为同步任务和异步任务,而异步任务又分为宏任务(macroTask)和微
任务(microTask)宏任务:主代码块、setTimeOut、setInterval、script、I/O操作、UI渲染
微任务:promise、async/await(返回的也是一个promise)、process.nextTick
在执行代码前,任务队列为空,所以会优先执行主代码块,再执行主代码块过程中,遇到同步任务则立
即将任务放到调用栈执行任务,遇到宏任务则将任务放入宏任务队列中,遇到微任务则将任务放到微任
务队列中。
主线程任务执行完之后,先检查微任务队列是否有任务,有的话则将按照先入先出的顺序将先放进来的
微任务放入调用栈中执行,并将该任务从微任务队列中移除,执行完该任务后继续查看微任务队列中是
否还有任务,有的话继续执行微任务,微任务队列null时,查看宏任务队列,有的话则按先进先出的顺序
执行,将任务放入调用栈并将该任务从宏任务队列中移除,该任务执行完之后继续查看微任务队列,有
的话则执行微任务队列,没有则继续执行宏任务队列中的任务。
需要注意的是:每次调用栈执行完一个宏任务之后,都会去查看微任务队列,如果microtask queue不为
空,则会执行微任务队列中的任务,直到微任务队列为空再返回去查看执行宏任务队列中的任务
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})console.log('script end')
最终输出 :
script start
VM70:8 async2 end
VM70:17 Promise
VM70:27 script end
VM70:5 async1 end
VM70:21 promise1
VM70:24 promise2
undefined
VM70:13 setTimeout
解析:
在说返回结果之前,先说一下async/await,async 返回的是一个promise,而await则等待一个promise对象
执行,await之后的代码则相当于promise.then()
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
//等价于
new Promise(resolve =>
resolve(console.log('async2 end'))
).then(res => {
console.log('async1 end')
})
整体分析:先执行同步任务 script start -> async2 end -> await之后的console被放入微任务队列中 ->
setTimeOut被放入宏任务队列中 ->promise -> promise.then被放入微任务队列中 -> script end -> 同步任
务执行完毕,查看微任务队列 --> async1 end -> promise1 -> promise2 --> 微任务队列执行完毕,执行宏
任务队列打印setTimeout