前言
上篇和大家分享了浏览器,html, css和js部分的,下篇就和大家来见见剩余的部分。我的主要技术栈是Vue,所以问的Vue的题目会多一点,相对的其他就少一点了,并且认识和学习的可能不少很到位,希望大家多多指教。
从3月底裸辞到现在,我个人感觉受疫情的影响,很多公司招人都越来越严格了。如果单纯只会一个Vue,那远远是不够的,除了Vue的原理外,至少我们还应该对他的构建工具webpack有所了解。前段时间,我向字节面试官问,他对我这样的普通的2年程序员有什么建议?
我对他的回答总结两点。一是深度:不仅要明白是如何实现的,还是知道为什么可以这样实现,并且能做到举一反三。例如我们知道transform可以居中,并且避免回流和重绘,那么transform的原理有所什么?二是广度:了解一些前端外的知识,例如服务端,运维相关的。总之,就是我们要学的东西还很多😭。
最近,我也终于拿到了一份15k以上的offer。不枉费我辛辛苦苦巩固基础,总结了一个多月来的面试题。机会总是会有的,只是可能它在路上抛锚了,来得慢点。与大家共勉之。
五、Vue
5.1 Vue的生命周期
5.1.1 单个组件的生命周期
按创建,激活到更新,停止, 最后销毁的顺序。
- beforeCreated:
- created
- beforeMounted
- mounted
- activated
- beforeUpdated
- updated
- deactivated
- beforeDestory
- destoryed
如果有两行代码同时进行了停止和更新,无论代码顺序,生命周期的顺序都将是 deactivated -> beforeUpdate -> updated
其他:
- 错误捕获errorCaptured 错误捕获
5.1.2 父子组件的生命周期顺序
父beforeCreated -> 父created -> 父beforeMounted -> 子beforeCreated -> 子created -> 子beforeMounted -> 子mounted -> 父mounted
接着是下面的三种情况:
- 更新:
- 只更新父或子: 局部更新,父或子beforeUpdate -> updated
- 同时更新父和子: 父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated
- 销毁父组件
父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed
- 激活父组件
子activated -> 父activated -> 停止 -> 子deactivated -> 父deactivated
5.2 响应式原理
使用 defineReactive 函数将深度遍历一个对象(或循环遍历数组),将对象构建成响应式式对象。 明显的标志就是 ob 属性 实质是通过 Object.defineProperty 对属性(深度遍历)进行 setter 和 getter 拦截。
- get中主要做依赖收集 dep.depend() 【子属性也收集该依赖】
- set中主要做派发更新 (新的值才更新) dep.notify() 调用dep数组中每个渲染watcher的update方法更新DOM
源码路径:src/core/observer/index.js
5.2.1 响应式对象使用应该注意哪些点
- 对象的新增属性,数组的新增元素,因为不是响应式的,所以不会触发视图渲染。 此时应该使用 $set
- 改变某一下标的元素,因为Vue未实现监听,所以不会触发视图渲染。 此时应该使用 $set
- 删除对象的属性,数组下标的某一元素,确保删除属性能触发视图渲染。此时应该使用 $delete
$set( target, [index | property], value )
$delete( target, [index | property] )
5.3 v-model基本原理
- 首先在编译阶段,v-model被当成普通指令解析到el.directives,然后在解析v-model的时候,会根据节点的不同请求去执行不同的逻辑。
- 如果节点是select、checkbox, radio,则监听的是change事件
- 如果节点是input,textarea,则监听一般是input事件,在.lazy下的情况下是change事件。
- 如果节点是组件,则是使用自定义的回调函数
- 在运行的时候,通过相应事件的监听函数去更改数据。
v-model实质是一种语法糖,换成模板写法如下:
<input :value="sth" @input="sth = $event.target.value" />
源码路径src/platforms/web/compiler/directives/model.js
5.3.1 组件中使用v-model
export default {
model: {
prop: 'num', // 自定义属性名
event: 'addNum' // 自定义事件名
},
props: {
num: Number,
},
methods: {
add() {
this.$emit('addNum', this.num + 1)
}
}
}
5.3.2 vue2.0响应式的缺陷
- Object.defineProperty无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应;
Object.defineProperty本身是可以监控到数组下标的变化的,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性。具体我们可以参考 《记一次思否问答的问题思考:Vue为什么不能检测数组变动》这篇文章。简单说就是假设元素内容只有4个有意义的值,但是长度确实1000,我们不可能为1000个元素做检测操作。
- Object.defineProperty只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。
5.3.3 vue3.0为什么使用Proxy实现响应式
Vue3.0的响应式Proxy
- Proxy可以劫持整个对象,并返回一个新的对象。
- Proxy不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。
5.4 vue中通信方式
- props 和 $emit
- $parent 和 $children
- ref和refs
- $attr 和 $listener: v-bind="$attrs" v-on="$listeners"
- provide 和 inject: 实质就是递归父组件帮你寻找对应的provider
provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。 - vueBus: 中央事务总线,一个发布订阅中心
- vuex:状态树管理
5.5 nextTick的原理
- Vue.nextTick是在执行render渲染后运行的,即在render渲染后的下一次tick(event loop最开始的时候执行)
- Vue.nextTikc的降级顺序(优先使用) Promise.then(microtask) , MutationObserver(microtask) , setImmediate(task) , setTimeout(fn, 0)(task)
源码路径 src/core/util/next-tick.js
5.6 new Vue会做什么操作
Vue.prototype._init = function (options) {
const vm = this
// ...忽略,从第45行看起
if (process.env.NODE_ENV !== 'production') {
initProxy(vm) // 作用域代理,拦截组件内访问其它组件的数据
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm) // 建立父子组件关系,在当前实例上添加一些属性和生命周期标识。
initEvents(vm) // 用来存放除 @hook:生命周期钩子名称="绑定的函数"事件的对象。如: $on、 $emit等
initRender(vm) // 用于初始化 $slots、 $attrs、 $listeners
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props // 初始化 inject,一般用于更深层次的组件通信,相当于加强版子组件的 props。用于组件库开发较多
initState(vm) // 是很多选项初始化的汇总,包括: props、methods、data、computed和watch 等。
initProvide(vm) // resolve provide after data/props // 初始化 provide
callHook(vm, 'created')
// ...忽略
if (vm.$options.el) {
vm.$mount(vm.$options.el) // 挂载实例
}
}
}
源码路径 src/core/instance/init.js
5.7 vue的diff原理
主要执行的是patch函数。主要流程如下:
function patch (oldVnode, vnode, hydrating, removeOnly)
- 如果oldVnode不存在,即是新添加的节点,则创建vnode的DOM
- 如果不是真实的节点且是相同类型的节点,则进入结点diff,即patchVnode函数。否则会用新的节点替换老的。这里的相同类型指的是具有相同的key值和一些其他条件,例如标签相同等等
- 如果oldVnode === vnode,则认为没有变化 如果oldVnode的isAsyncPlaceholder属性为true时,跳过检查异步组件,return;
- 如果oldVnode跟vnode都是静态节点(实例不会发生变化),且具有相同的key,并且当vnode是克隆节点或是v-once指令控制的节点时,则把oldVnode.elm和oldVnode.child都复制到vnode上;
- 如果vnode不是文本节点或注释节点
- 如果vnode和oldVnode都有子节点并且两者的子节点不一致时,就调用updateChildren更新子节点
- 如果只有vnode有子节点,则调用addVnodes创建子节点
- 如果只有oldVnode有子节点,则调用removeVnodes把这些子节点都删除
- 如果vnode文本为undefined,则清空vnode.elm文本;
- 如果vnode是文本节点但是vnode.text != oldVnode.text时只需要更新vnode.elm的文本内容就可以。
- 在updateChildren主要是子节点数组对比,思路是通过首尾两端对比,如果是相同类型的节点也会使用patchVnode函数。
在做对比中key 的作用 主要是
- 决定节点是否可以复用
- 建立key-index的索引,主要是替代遍历,提升性能
源码路径 src/core/vdom/patch.js
5.8 computed 和 watcher
5.8.1 对比
- computed是计算属性,依赖其他属性计算,并且computed的值有缓存,只有当计算值发生变化才会返回内容。适合简单的逻辑。
注意 computed里无法使用非纯函数来响应,例如Date.now()
- watch主要用于监控vue实例的变化,它监控的变量当然必须在data里面声明才可以,它可以监控一个变量,也可以是一个对象。比较适合的场景是一个数据影响多个数据。
- watch支持异步。
5.8.2 watcher的分类
- 内部-watcher vue组件上的每一条数据绑定指令(例如{{myData}})和computed属性,通过compile最后都会生成一个对应的 watcher 对象。
- user--watcher 在watch属性中,由用户自己定义的,都属于这种类型,即只要监听的属性改变了,都会触发定义好的回调函数
- render-watcher 每一个组件都会有一个 render-watcher, function () {vm._update(vm._render(), hydrating);}, 当 data / computed中的属性改变的时候,会调用该 render-watcher 来更新组件的视图
watcher 也有固定的执行顺序,分别是: 内部-watcher -> user-watcher -> render-watcher
5.9 Vue指令
5.9.1 全局指令和局部指令
// 全局
Vue.directive('my-click', config)
// 局部
new Vue({
directives:{
focus: config // v-focus
}
}})
5.9.2 配置参数
一个指令定义对象可以提供如下几个钩子函数 (均为可选):
- bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
- inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
- update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新。
- componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
- unbind:只调用一次,指令与元素解绑时调用。
每个钩子函数都有四个参数el、binding、vnode 和 oldVnode
5.10 mixin
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。
全局和局部mixin
var mixin = {
data: function () {
return {
message: 'hello',
foo: 'abc'
}
}
}
<!-- 全局mixin -->
Vue.mixin(mixin)
<!-- 局部mixin -->
new Vue({
mixins: [mixin],
})
合并策略
- 钩子函数将合并成数组,且混入的函数先执行
- 其他的值为对象的将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
- 默认的合并策略可以使用下面的方面更改
Vue.config.optionMergeStrategies.myOption = function (toVal, fromVal) {
// 返回合并后的值
}
5.11 vue-router
5.11.1 路由模式
- HashHistory模式:实质是监听onhashchange事件 (window.location API - location.hash)
- HTML5History模式:实质是使用h5的 window.history API, 监听popstate事件(pushState, replaceState)。使用该模式,服务器和前端需要做好页面404的处理
- AbstractHistory模式:在不支持上面两种方式的环境下使用,如node环境,实际是使用数组模拟路由历史栈
源码: src/history目录下的js文件
5.11.2 导航守卫
- 全局守卫
router.beforeEach((to, from, next) => {})
router.afterEach((to, from) => {})
router.beforeResolve((to, from) => {}) // 与afterEach类似, 区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用
- 路由独享守卫
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {},
...
}
]
})
- 组件内守卫
const Foo = {
template: `...`,
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
}
5.11.3 vue-router如何注入
- 基于vue的插件机制,全局混入beforeCreated和destroyed的生命钩子
- 查找根实例上的route,注入到每个组件上,监听current变化
Vue.util.defineReactive(this, '_route', this._router.history.current)
- vue 原型上添加两个属性
route, 拦截get操作,限制set操作
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
- 注册全局组件RouterView 和 RouterLink
源码 src/install.js
5.12 vuex
5.12.1 核心概念
- state 数据
- getter 可看成数据的计算属性
- mutation 唯一更改数据的方法 通过 store.commit 使用相应的 mutation方法
- Action 支持异步的提交mutation 通过 store.dispatch 使用相应的Action方法
- module 数据分模块。如moduleA.xx
数据流 来自官网的图:

5.12.2 如何注入
在使用 Vue.use(vuex) 的时候会执行install 方法在(vue插件机制)。这个方法会混入一个minxin
Vue.mixin({
beforeCreate() {
const options = this.$options
// store injection
// 非根组件指向其父组件的$store,使得所有组件的实例,都指向根的store对象
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
})
源码路径 src/mixin.js
5.12.3 如何实现响应式
通过添加到data中实现响应式
store._vm = new Vue({
data: {
?state: state
},
computed // 这里是store的getter
})
源码路径:src/store.js resetStoreVM函数
5.13 vue应用
5.13.1 实现一个简易的render函数
实现一个createElement(A, B, C)函数,使得执行createElement后在页面上可生成以下元素
const A = 'div'
const B = {id: 'div'}
const C = [
createElement('input',{ name: 'name'}),
{
tag: 'p',
data: null,
children: 'text'
}
]
let el = createElement(A, B, C).render()
document.body.appendChild(el)
<div id="div">
<input name="name" />
<p>text</p>
</div>
解题
function createElement(tag = 'div', data = {}, children) {
return {
render: function() {
let el = document.createElement(tag)
for(let k in data) {
el.setAttribute(k, data[k])
}
if (Array.isArray(children) && children.length) {
children.map(child => {
child = child || {}
if (typeof child === 'function') {
child.render = child
}
if (!child.render) {
Object.assign(child, createElement(child.tag, child.data, child.children))
}
el.appendChild(child.render())
})
} else if (children){
let text = document.createTextNode(children.toString());
el.appendChild(text)
}
return el
}
}
六、React
6.1 生命周期
6.1.1 单个组件的生命周期
按照创建,更新,销毁的顺序
- componentwillMount(React17废弃)
- componentDidMount
- componentWillReceiveProps(React17替换成getDerivedStateFromProps)
- shouldComponentUpdate
- componentWillUpdate(React17替换成getSnapshotBeforeUpdate)
- componentDidUpdate
- componentWillUnmount
其他:
- 错误捕获componendDidCatch
6.1.2 父子组件的生命周期
大致和Vue相同,不同的是,react没有局部更新,更新父组件的同时也会更新子组件。
父componentWillMount -> 子componentWillMount -> 子componentDidMount -> 父componentDidMount
- 销毁:
父componentWillUnmount -> 子componentWillUnmount
- 更新
- 只更新子: 子shouldComponentUpdate -> 子componentWillUpdate -> 子componentDidUpdate
- 更新父或同时更新:父shouldComponentUpdate -> 父componentWillUpdate -> ==子componentWillReceiveProps== -> 子shouldComponentUpdate -> 子componentWillUpdate -> 子componentDidUpdate -> 父componentDidUpdate
6.1.3 react17 生命周期改动
- componentWillMount
- componentWillRecieveProps
- componentWIllUpdate
简单来说就是这三个生命周期函数容易被误解并滥用,可能会对异步渲染造成潜在的问题。 可用UNSAFE_xxx 来取消eslint的报错。
新增三个生命周期
- getDerivedStateFromProps(nextProps, prevState)
静态方法,所以不能使用this.setState。 用于替换componentWillReceiveProps,可以用来控制 props 更新 state 的过程;它返回一个对象表示新的 state;如果不需要更新,返回 null 即可
- getSnapshotBeforeUpdate(nextProps, prevState) 。用于替换componentWillUpdate
- componendDidCatch(error, info)。 新增,用于捕捉错误
6.2 react的通信方式
- props
- context。 可跨级通信,但不知道来源哪里不推荐使用。基于生产者消费者模式
- redux和react-redux
- 用js实现发布订阅模式
- React17 会废弃childContext 使用新API - createContext(),并提供Provider和consumer组件,类似Vue
6.3 setState
setState是React组件中用于更新数据和触发渲染的函数,他的用法如下
this.setState(newState:object | updater: Function, callback?)
updater = (state, prop) => {}
6.3.1 同步和异步setState
何时同步,何时异步?
- 在react的生命周期勾子或react事件监听回调中使用
- 其他情况,如定时器回调,原生事件监听回调,promise回调
为什么setState是异步的?
setState出发React的更新生命周期函数4个函数:shouldComponentUpdate,componentWillUpdate,render,componentDidUpdate。如果每一次setState 调用都走一圈生命周期,并拿render函数返回的结果会拿去做Virtual DOM比较和更新DOM树,这个就比较费时间。
目前React会将setState的效果放在队列中,积攒着一次引发更新过程。为的就是把 Virtual DOM 和 DOM 树操作降到最小,用于提高性能。
6.3.2 setState如何实现
ReactComponent.prototype.setState = function (partialState, callback) {
// 更新的操作会放在数组里
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}};
总体流程如下:
- 将state放入enqueueSetState队列中,并调用enqueueUpdate处理要更新的Component
- 如果组件当前正处于update中(isBatchingUpdates),则先将Component存入dirtyComponent中。否则调用batchedUpdates处理。
- batchedUpdates发起一次transaction.perform()事务
- 事务会更新isBatchingUpdates为false,循环遍历所有的dirtyComponents,调用updateComponent刷新组件,并执行它的pendingCallbacks, 也就是setState中设置的callback。
6.4 react的事件机制
6.4.1 合成事件
在react中使用jsx语法绑定的事件并不是原生事件,而是一种合成事件SyntheticEvent。例如SyntheticEvent, SyntheticKeyboardEvent, SyntheticFocusEvent等。他有以下特点:
- 默认的事件流是冒泡,如果以捕获的方式来触发事件的话,事件类型后面加一个后缀Capture
- 几乎所有的事件代理(delegate)到document,达到性能优化的目的,例如对于audio、video标签,存在一些媒体事件(例如onplay、onpause),只能在这些标签上进行事件绑定,绑定一个入口分发函数(dispatchEvent)
- 对于每种类型的事件,拥有统一的分发函数dispatchEvent
- 事件对象(event)是合成对象(SyntheticEvent),不是原生的。所以e.stopPropagation()方法阻止的知识合成事件流的传播。
6.4.2 如何实现
React 事件机制分为事件注册,和事件分发,两个部分。
- 组件加载 (mountComponent)、更新 (updateComponent) 的时候,调用 _updateDOMProperties 方法对 props 进行处理,将事件绑定在document上,并存储在EventPluginHub中(订阅发布中心)
- 回调统一是ReactEventListener的dispatch方法。通过_dispatchListeners里得到所有绑定的回调函数,然后循环执行里面的所有的回调函数
6.5 react16
- 新的核心算法 Fiber
- render可以返回数组,字符串
- 错误处理机制
- Portals组件 渲染外部的dom节点. createPortal API
- 更好 更快的服务端渲染 rendertoNodeStream 返回node的流
- 体积更小 MIT协议
6.6 Fiber
Fiber 可以提升复杂React 应用的可响应性和性能。Fiber 即是React新的调度算法。
- 每次有 state 的变化 React 重新计算,如果计算量过大,浏览器主线程来不及做其他的事情,比如 rerender 或者 layout,那例如动画就会出现卡顿现象。
- React 制定了一种名为 Fiber 的数据结构,加上新的算法,使得大量的计算可以被拆解,异步化,浏览器主线程得以释放,保证了渲染的帧率。从而提高响应性。
- React 将更新分为了两个时期:
- render/reconciliation: 可打断,React 在 workingProgressTree 上复用 current 上的 Fiber 数据结构来一步地(通过requestIdleCallback)来构建新的 tree,标记处需要更新的节点,放入队列中。
- commit: 不可打断。在第二阶段,React 将其所有的变更一次性更新到DOM上。
6.7 函数式组件,class组件,受控组件,高级组件的概念
- class组件:类组件不仅允许你使用更多额外的功能,如组件自身的状态和生命周期钩子,也能使组件直接访问 store 并维持状态
- 函数式组件:当组件仅是接收 props,并将组件自身渲染到页面时,该组件就是一个 '无状态组件(stateless component)',可以使用一个纯函数来创建这样的组件,即函数式组件
- 受控组件:在 HTML 中,类似 input, textarea 和 select 这样的表单元素会维护自身的状态,并基于用户的输入来更新。一个输入表单元素,它的值通过 React 的这种方式来控制,这样的元素就被称为"受控元素"。
- 高级组件HOC:高阶组件是一个以组件为参数并返回一个新组件的函数,例如redux的connect函数
6.8 react-router
6.8.1 路由模式
- BrowserHistory:h5历史模式
- HashHistory:h5 hash模式
- MemoryHistory:和abstract模式类似
- StaticRouter:一个永远不会改变位置的<Router>。这在服务器端渲染场景中非常有用
- NativeRouter:RN使用
6.9 redux
6.9.1 基本概念
- state: 数据,即状态
- Action: 一个纯对象,携带这个操作的类型和数据信息
- Action Creater: 一个函数,根据指定参数,来生成一个Action,目的是减少代码量
- Reducer: 一个纯函数,用来修改应用的状态,接收当前State和Action,返回一个新的State。
- 不得改写参数
- 不得调用系统的I/O的API
- 不得调用Date.now()或者Math.random()等不纯的方法,因为每次得到的结果会不一样
- 不能改变State,必须返回一个新的对象,具体可以使用{...obj}运算符或者Object.assign()来操作
- combineReducers: 一个函数,将多个小的Reducer合并成一个大的Reducer
- Store: 数据存储中心
- Store.getState() 获取Store当前的状态
- Store.dispatch() 分派一个Action,用来修改Store的状态,从View中发出Action的唯一方法
- Store.subscribe() 订阅一个监听器,当Store的状态发生改变的时候,执行函数
- Middlewares: 中间件, 中间件实际上就是一个拦截器,本质是一个函数,拦截所有的Action,并执行特定的操作
- compose函数,将[f1, f2, f3] => f1(f2(f3(x)))
6.9.2 数据流
Redux 应用中数据的生命周期遵循下面 4 个步骤:
- 调用 store.dispatch(action)。
- Redux store 调用传入的 reducer 函数。
- 根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
- Redux store 保存了根 reducer 返回的完整 state 树。

6.10 react应用
6.10.1 实现一个简单的redux
<!-- 数据中心 和 发布订阅调度中心 -->
/**
* 生成状态树
* @param {*} reducer 纯函数 接受dispatch的action,对state进行操作
* @param {*} enhancer 增强器,中间件
*/
export function createStore(reducer, enhancer) {
if (enhancer) {
return enhancer(createStore)(reducer)
}
let state = {}
let listeners = []
function getState() {
return state
}
function subscribe(listener) {
if (typeof listener !== 'function') {
throw new Error('listeners should be a function')
}
listeners.push(listener)
}
function dispatch(action) {
state = reducer(state, action)
listeners.forEach(f => f())
return action
}
dispatch({type: '@@zty/redux/init'}) // 初始化数据
return {getState, subscribe, dispatch}
}
// addGun = dispatch(addGun())
function bindActionCreator(creator, dispatch) {
return (...args) => dispatch(creator(...args))
}
// creators = {addGun, removeGun}
export function bindActionCreators(creators, dispatch) {
return Object.keys(creators).reduce((res, k) => {
res[k] = bindActionCreator(creators[k], dispatch)
return res
}, {})
}
// [f1, f2, f3] = f1(f2(f3()))
export function compose(...funs) {
if (!funs.length) {
return f => f
}
if (funs.length === 1) {
return funs[0]
}
return funs.reduce((res, fun) => (...args) => res(fun(...args)))
}
// 添加中间件
export function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args)
let dispatch = store.dispatch
const midApi = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
const middlewaresChain = middlewares.map(mw => mw(midApi))
// [f1, f2, f3] => f1(f2(f3)) 形成中间件链路
dispatch = compose(...middlewaresChain)(store.dispatch)
return {
...store,
dispatch
}
}
}
七、Node
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。 Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。
内置模块:
- events: 事件模块
- buffer: 二进制数据处理模块
- utils: 工具类模块
- fs: 文件模块
- path: 路径工具
- http: web模块
- ...
7.1 eventloop
7.1.1 基本流程
- timers:执行满足条件的setTimeout、setInterval回调。 [uv__run_timers函数]
- I/O callbacks:是否有已完成的I/O操作的回调函数,来自上一轮的poll残留。[uv__run_pending函数]
- idle,prepare:node内部特定的阶段,在I/O轮询开始前做一些特定的回调,可忽略 [uv__run_idle, uv__run_prepare函数]
- poll:轮询,等待还没完成的I/O事件,会因timers和超时时间等结束等待。[uv__io_poll(loop, timeout)函数]
- check:执行setImmediate的回调。[uv__run_check函数]
- close callbacks:关闭所有的closing handles,一些onclose事件,例如socket.on("close",func)。[ uv__run_closing_handles(loop)函数]
- 重复以上步骤。
7.1.2 经典例子
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
结果如下:
- 浏览器环境:time1,promise1,time2,promise2
- node11以下:time1,time2,promise1,promise2
- node11及以上:time1,promise1,time2,promise2
在 node 11 版本中,node 下 Event Loop 已经与浏览器趋于相同。我们可以用浏览器的微任务和宏任务解释,11版本前的timer,由于到期时间相近,会在timer阶段合并执行。所以打出time1后,打印time2。
7.2 koa 洋葱模型
koa洋葱式模型: koa各个中间件合并执行,结合next()形成一种串行机制,并且是支持异步。 如请求顺序进入1,2,3,4,响应顺序从4,3,2,1出来
- compose函数支持中间件和next
- async/await支持异步
洋葱模式实现了:
- context的保存和传递
- 中间件的管理和next的实现
八、Webpack
8.1 简介
本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle.
8.2 核心概念
- 入口(entry)
指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
可以通过在 webpack 配置中配置 entry 属性,来指定一个入口起点(或多个入口起点)。默认值为 ./src。
- 输出(output)
output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值为 ./dist。基本上,整个应用程序结构,都会被编译到你指定的输出路径的文件夹中。你可以通过在配置中指定一个 output 字段,来配置这些处理过程
- loader
loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块
在更高层面,在 webpack 的配置中 loader 有两个目标:
- test 属性,用于标识出应该被对应的 loader 进行转换的某个或某些文件。
- use 属性,表示进行转换时,应该使用哪个 loader。
注意:Webpack选择了compose方式,即从右到左执行loader
插件(plugins)
插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。
plugins需要暴露出一个class, 在new WebpackPlugin()的时候通过构造函数传入这个插件需要的参数,在webpack启动的时候会先实例化plugin再调用plugin的apply方法,插件需要在apply函数里监听webpack生命周期里的事件,做相应的处理
模式(mode)
通过选择 development 或 production 之中的一个,来设置 mode 参数,你可以启用相应模式下的 webpack 内置的优化
简单例子:
// 多个入口
module.exports = {
mode: 'production',
entry: {
index: ["./src/index.js"],
main: ["./src/main.js"]
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[hash:8].js'
},
module: {
rules: [{
test: /\.js$/, // 正则匹配文件名
exclude: '/node_modules/', // 排除
use: ['babel-loader']
}
},
plugins: [ // 插件
new copyWebpackPlugin([{
from: path.resolve(__dirname, 'public/static'),
to: path.resolve(__dirname, 'dist'),
ignore: ['index.html']
}])
}
8.3 基本流程
- 解析shell和config中的配置项,用于激活webpack的加载项和插件
- webpack初始化工作,包括构建compiler对象,初始化compiler的上下文,loader和file的输入输出环境
- 解析入口js文件,通过对应的工厂方法创建模块,使用acron生成AST树并且遍历AST,处理require的module,如果依赖中包含依赖则遍历build module,在遍历过程中会根据文件类型和loader配置找出合适的loader用来对文件进行转换
- 调用seal方法,封装,逐次对每一个module,chunk进行整理,生成编辑后的代码
8.4 模块打包
- 通过fs将模块读取成字符串,然后用warp包裹一下,使之成为一个字符串形式的的函数然后调用 vm.runInNewContext 这样类型的方法,这个字符串会变成一个函数。
- 这些模块的函数会被存放在数组里,然后进行解析执行。module和export都是传入的对象,webpack会实现require函数,去加载其他模块。
- 如果是异步模块,则会通过jsonp的形式去加载该模块打包好生成的chunk。 异步加载模块可以使用import和require.ensure函数,函数将会返回一个promise。
- 上面方法都是公共的,可以抽离成模板的js文件,webpack负责做依赖分析,并将模块读成函数填充入数组。(这里说的只是js的模块)
下面附上简易版的代码
<!-- 同步模块 -->
var moduleDepList = [
{'./moduleA': 1}, // module[0] 的依赖 他依赖moduleA 且 moduleA的下标在moduleList 中 为 1
{}
]
function require(id, parentId) {
var currentModlueId = parentId !== undefined ? moduleDepList[parentId][id] : id
var module = {exports: {}}
var moduleFunc = moduleList[currentModlueId]
moduleFunc(id => require(id, currentModlueId), module, module.exports)
return module.exports
}
<!-- 异步模块 -->
var cache = {}
window.__jsonp = function(chunkId, moduleFunc) {
var chunk = cache[chunkId]
var resolve = chunk[0]
var module = {exports: {}}
moduleFunc(require, module, module.exports)
resolve(module.exports)
}
require.ensure = function(chunkId, parentId) {
var currentModlueId = parentId !== undefined ? moduleDepList[parentId][chunkId] : chunkId
var currentChunk = cache[currentModlueId]
if (currentChunk === undefined) {
var $script = document.createElement('script')
$script.src = `chunk.${chunkId}.js`
document.body.appendChild($script)
var promise = new Promise(function(resolve) {
var chunkCache = [resolve] // 数组形式是为了保存promise
chunkCache.status = true // 异步模块加载中 如果有别的包 在 异步加载在模块 那么下面的
cache[chunkId] = chunkCache
})
cache[chunkId].push(promise)
return promise
}
if (currentChunk.status) {
return currentChunk[1] // 这里的promise 这里的就直接返回promise 这样模块只会加载一次
}
return currentChunk
}
8.5 热更新
- client 和 server 建立一个 websocket 通信
- 当有文件发生变动(如fs.watchFile)的时候,webpack编译文件,并通过 websocket 向client发送一条更新消息
- client 根据收到的hash值,通过ajax获取一个 manifest 描述文件
- client 根据manifest 获取新的JS模块的代码
- 当取到新的JS代码之后,会更新 modules tree,(installedModules)调用之前通过 module.hot.accept 注册好的回调,可能是loader提供的,也可能是你自己写的
manifest: 描述资源文件对应关系如下,打包后的文件拥有了hash值,所以需要进行映射。
{
"a.js": "a.41231243.js"
}
8.6 plugin
8.6.1 如何开发一个plugin
- 一个 JavaScript 命名函数。
- 在插件函数的 prototype 上定义一个 apply 方法。
- 指定一个绑定到 webpack 自身的事件钩子。
- 处理 webpack 内部实例的特定数据。
- 功能完成后调用 webpack 提供的回调。
tapable 工具,它提供了 webpack 插件接口的支柱
// 一个 JavaScript 命名函数。
function plugin() {};
// 在插件函数的 prototype 上定义一个 `apply` 方法。
plugin.prototype.apply = function(compiler) {
// 指定一个挂载到 webpack 自身的事件钩子。
compiler.plugin('webpacksEventHook', function(compilation, callback) {
callback();
});
// 使用taptable的写法
//基本写法
compiler.hooks.someHook.tap(...)
//如果希望在entry配置完毕后执行某个功能
compiler.hooks.entryOption.tap(...)
//如果希望在生成的资源输出到output指定目录之前执行某个功能
compiler.hooks.emit.tap(...)
};
8.6.2 Compiler和Compliation 对象和钩子
对象
- compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。
- compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用
钩子:总体分成两大类:Compiler和Compliation
- Compiler暴露了和webpack整个生命周期相关的钩子
- Compilation暴露了与模块和依赖有关的粒度更小的事件钩子,官方文档中的说法是模块会经历加载(loaded),封存(sealed),优化(optimized),分块(chunked),哈希(hashed)和重新创建(restored)这几个典型步骤,从上面的示例可以看到,compilation是Compiler生命周期中的一个步骤,使用compilation相关钩子的通用写法为:
compiler.hooks.compilation.tap('SomePlugin',function(compilation, callback){
compilation.hooks.someOtherHook.tap('SomeOtherPlugin',function(){
....
})
});
8.6.3 钩子的类型
同步钩子
- syncHook: 不关心返回值
- syncBailHook: 有一个返回值不为null就跳过剩下的逻辑
- SyncWaterfallHook: 下一个任务要拿到上一个任务的返回值
- SyncLoopHook: 监听函数返回true表示继续循环,返回undefine表示结束循环
异步钩子
- AsyncParallelHook: 异步并发执行,仍是单线程
- AsyncParallelBailHook: 异步并发执行,有一个失败了,其他的都不用走了
- AsyncSeriesHook: 异步串行执行
- AsyncSeriesBailHook: 异步串行执行,有一个返回值不为null就跳过剩下的逻辑
- AsyncSeriesWaterfallHook: 异步串行执行,下一个任务要拿到上一个任务的返回值
8.7 常见plugin
-
clean-webpack-plugin: 在构建之前删除上一次build的文件夹
-
copy-webpack-plugin: 复制文件或文件夹到生成后的目录
-
extract-text-webpack | mini-css-extract-plugin: 将所有入口的chunk(entry chunks)中引用的 *.css,移动到独立分离的 CSS 文件
-
html-webpack-plugin: 将build后生成的资源以标签的形式嵌入到HTML模板内
-
hot-module-replacement: 模块热更新
8.8 常见loader
-
babel-loader: 语法,源码转换以便能够运行在当前和旧版本的浏览器或其他环境中
-
css-loader: 配合style-loader可以解析在js中引入的css文件,并以<style>便签将css-loader内部样式注入到我们的HTML页面
-
file-loader: 可以解析js中require的文件,输出到输出目录并返回 public URL
-
html-loader: 可以对HTML模板中指定哪个标签属性组合(tag-attribute combination)元素应该被此 loader 处理
-
less-loader: 依赖less,可以将less编译成css
-
postcss-loader: 配合一些plugin如cssnano,autoprefixer可以对css进行压缩,优化,自动补足前缀等
-
scss-loader: 配合node-scss,可以将scss编译成css
-
style-loader: 配合css-loader可以解析在js中引入的css文件,并以<style>便签将css-loader内部样式注入到我们的HTML页面
-
url-loader: url-loader 功能类似于 file-loader,但是在文件大小(单位 byte)低于指定的限制时,可以返回一个 DataURL(base64)
8.9 常见打包优化
- 使用dll
- 移除prefetch, preload,关闭sourceMap
- webpack-bundle-analyzer打包分析,将大的模块可能的移至CDN。打包时间分析使用speed-measure-webpack-plugin
- 开启gzip,服务器需要支持
- 使用多线程:thread-loader或HappyPack
- webpack4内置的terser启动多线程压缩
- 对项目进行拆分
九、常见性能优化
超过50ms的任务就可认为是长任务,需要进行优化。
9.1 构建工具带来的
- 前置css,后置js,防止js加载,运行阻塞页面渲染
- 将小图达成base64,减少资源请求。[file-loader, url-loader,...]
- 压缩精简html,css和js,减小打包体积。 [uglifyjs, OptimizeCssAssetsPlugin, ...]
- Gzip压缩打包后的文件,该功能需要服务器支持才能正常显示页面
- css预处理器,开启css编程之路
9.2 浏览器相关
- 使用CDN分发网络,请求资源更快
- 减少HTTP请求次数,减少DNS查询次数(尽量减少主机名),避免重定向
- DNS预获取 link标签 ref='dns-prefetch' herf=''
- 使AJAX可缓存:get请求可在客户端缓存;post请求不能再客户端缓存,但是服务端可以缓存数据(redis,memorycache等),提高请求速度。
- 减少DOM数量
- 避免重排和重绘: 减少DOM操作,动画优先使用 opacity, transform 属性;
- 合并DOM的读写操作,如使用 document.createDocumentFragment();
- 使用特殊的函数,优化条件渲染:window.requestAnimationFrame(), window.requestIdleCallback()
- 时间分片函数,使用requestAnimationFrame和createDocumentFragment
9.3 js相关
- 使用 JavaScript Cache API,我们可以使用 service worker。
- 延迟不必要的 JS 首屏加载 defer , aysc, 动态添加script节点
- 删除未使用的 JavaScript 和 合并重复的代码 减少编译时间(JIT)
- 避免内存泄漏 意外的全局变量;没有销毁的计时器;已经删除的 DOM 还是被引用,(删除DOM后将变量设值为 null 可以避免这个问题)
- 避免使用全局变量 & 优先使用局部变量,作用域链查找更快
- 使用 web workers 处理需要大量执行时间的代码(子线程)
- 合理使用事件代理。合并类似的操作,节约内存空间,减少 DOM 操作
- 使用高级函数等,例如addEvent的兼容惰性加载函数; map的性能高于forEach
9.4 css相关
- 避免使用css表达式
- 使用css sprite 雪碧图,减少图片请求
- 在不影响画质的情况下,使用合理的图片格式和压缩图片,优先使用JPG格式,如果能用css3实现动画,则尽量不使用GIF。如果能使用canvas或SVG实现,则尽量不使用图片
9.5 Vue相关
- 在vue2.0中不在data上使用嵌套多层的对象,或使用Object.freeze冻结对象。vue3中使用了lazy reactive不用担心这个。
- 异步加载路由,减少体积
- 通过使用require.context。自动生成路由
// ../components/test目录下的vue文件
let _req = require.context('../components/test', true, /\.vue$/)
let routes = []
_req.keys().map(name => {
const nameArr = name.split('.')
// 模块的export.defalut
const comp = _req(name).default
routes.push({
path: `/test${nameArr[1]}`,
component: comp,
title: comp.name
})
});
export default routes
- 通过$store.registerModule 动态注册状态树,减小打包体积。(混入beforeCreate,异步加载store的模块)
Vue.mixin({
beforeCreate: function() {
if (this.$options[config.dynamicVuex]) {
let name = config.moduleName || this.$options.name
console.log('name = ', name)
import(`./store/module/${name}.store.js`).then(module => { // or require.ensure
this.$store.registerModule(name, module.default)
})
}
}
})
9.6 React相关
- 更合理的传递state和props:
- 在构造函数里使用bind;
- 尽量不使用内联的对象;
- 不传递不必要的属性
- 合理使用shouldComponentUpdate生命钩子和继承PureComponent组件
十、算法和应用
10.1 随机洗牌算法
题目:随机打乱数组里的元素,每个元素不能在原来的位置。
let arr1 = [1,2,3,4,5,6,7,8,9,10]
function shuffle(array) {
for (let i = array.length - 1; i >= 0; i--) {
let randomIndex = Math.floor(Math.random() * (i + 1));
[array[i], array[randomIndex]] = [array[randomIndex], array[i]]
}
return array;
}
shuffle(arr1)
Fisher–Yates shuffle洗牌算法:从后往前遍历,取当前的数和前面的一个随机下标的数交换位置。
10.2 两个鸡蛋与100层楼
题目:两个软硬程度一样但未知的鸡蛋,它们有可能都在一楼就摔碎,也可能从一百层楼摔下来没事。有座100层的建筑,要你用这两个鸡蛋确定哪一层是鸡蛋可以安全落下的最高位置。可以摔碎两个鸡蛋。在有限层数和蛋数的情况下,求即使最坏情况下需要的最少判断次数。
这是一道动态规划的题目,首先假设f[n][m]表示从m层楼仍n个鸡蛋,找到的安全位置的最少判断次数。如果第一个鸡蛋第一次从[1, i]中任选第j层扔下,如果碎了,就必须从[1, j - 1]挨着试, 也就是dp[1][j - 1];如果不碎的话,那么还要在[j, i]层继续仍,即dp[2][i - j]。最坏情况下则取max(dp[1][j - 1], dp[2][i - j])次。
根据方程写出解法:
function egg(floor) {
// dp[1][i] = i, dp[2][i] = i 最差的情况预处理数据
let dp = Array.from(new Array(3),() => Array.from({length: floor + 1}, (v,k) => k))
for (let i = 1; i <= floor; i++ ) {
for (let j = 1; j < i; j++ ) {
dp[2][i] = Math.min ( dp[2][i], 1 + Math.max(dp[1][j-1], dp[2][i-j]) );
}
}
return dp[2][floor]
}
如果给的是n个鸡蛋,用下面的解法:
function floorEgg(egg, floor) {
if (egg < 1 || floor < 1) return 0
// 初始化数组,值为最坏的次数
let dp = Array.from(new Array(egg + 1).keys(),x => Array.from({length: floor + 1}, (v,k) => x && k))
// 构建dp数组
for(let i = 2; i <= egg; i++){
for(let j = 1; j <= floor; j++){
for(let k = 1; k < j; k++){
dp[i][j] = Math.min(dp[i][j], 1 + Math.max(dp[i-1][k-1], dp[i][j-k]));
}
}
}
return dp[egg][floor]
}
十一、概念
11.1 MVVM
View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会立即反应到View 上。
ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。
11.2 组件化思想
简单的说组件就是:将一段UI样式和其对应的功能作为独立的整体去看待,无论这个整体放在哪里去使用,它都具有一样的功能和样式,从而实现复用,这种整体化的思想就是组件化。
组件化设计就是为了增加复用性,灵活性,提高系统设计,从而提高开发效率。
11.3 虚拟DOM
使用Javascript来操纵DOM,操作效率往往很低,由于DOM被表示为树结构,每次DOM中的某些内容都会发生变化,因此对DOM的更改非常快,但更改后的元素,并且它的子项必须经过Reflow / Layout阶段,然后浏览器必须重新绘制更改,这很慢的。
因此,回流/重绘的次数越多,您的应用程序就越卡顿。但是,Javascript运行速度很快,虚拟DOM是放在JS 和 HTML中间的一个层。它可以通过新旧DOM的对比,来获取对比之后的差异对象,然后有针对性的把差异部分真正地渲染到页面上,从而减少实际DOM操作,最终达到性能优化的目的。
11.4 SPA和多页面应用
- 单页面应用: 仅仅在web页面初始化时加载相应的HTML、JavaScript、CSS,一旦页面加载完成了,SPA不会因为用户的操作而进行页面的重新加载或跳转,而是利用 JavaScript 动态的变换HTML的内容,从而实现UI与用户的交互。
- 多页面应用: 多页面跳转刷新所有资源,每个公共资源(js、css等)需选择性重新加载,常用于 app 或 客户端
11.5 CDN
CDN的全称是Content Delivery Network,即内容分发网络。基本原理是在用户和服务器之间增加Cache层,主要是通过接管DNS实现,将用户的请求引导到Cache上获得源服务器的数据,从而降低网络的访问时间。CDN的关键技术主要有负载均衡,内容存储和分发技术。
- 负载均衡:使用整体性的网络负载均衡技术,通过内容路由器中的重定向(DNS)机制,在多个远程POP上均衡用户的请求,以使用户请求得到最近内容源的响应。
- 内容分发:借助于建立索引、缓存、流分裂、组播(Multicast)等技术,将内容发布或投递到距离用户最近的远程服务点(POP)处。
- 内容存储:在功能上包括对各种内容格式的支持,对部分缓存的支持;在性能上包括支持的容量、多文件吞吐率、可靠性、稳定性,都是存储需要考虑的问题。
11.6 函数式编程
函数式编程是种编程方式,它将电脑运算视为函数的计算。在函数编程中,函数是第一等公民,且该函数应该是一个纯函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。列如含有:
- log
- http请求
- 可变数据如new Date()
- DOM操作
纯函数带来的好处就是:更好的进行单元测试和调试,一对一的数据关系可以便于缓存。函数式编程还有其他特性:
- 闭包和高阶函数
- 惰性计算
- 递归
函数式编程有两个最基本的运算:合成compose和柯里化curry。
结束语
以上我的面试题汇总了,我比较菜,如果大家发现有错误的,欢迎指正!开始答应给大家的链接,现在马上分享,不要眨眼哦。
我的个人面经
note.youdao.com/ynoteshare1…
做一个爱分享,爱学习的前端,我是安迪five,oh yeah!