2020前端面试-面试题分享 (下) | 掘金技术征文

1,184 阅读23分钟

前言


   上篇和大家分享了浏览器,html, css和js部分的,下篇就和大家来见见剩余的部分。我的主要技术栈是Vue,所以问的Vue的题目会多一点,相对的其他就少一点了,并且认识和学习的可能不少很到位,希望大家多多指教。

   从3月底裸辞到现在,我个人感觉受疫情的影响,很多公司招人都越来越严格了。如果单纯只会一个Vue,那远远是不够的,除了Vue的原理外,至少我们还应该对他的构建工具webpack有所了解。前段时间,我向字节面试官问,他对我这样的普通的2年程序员有什么建议?

   我对他的回答总结两点。一是深度:不仅要明白是如何实现的,还是知道为什么可以这样实现,并且能做到举一反三。例如我们知道transform可以居中,并且避免回流和重绘,那么transform的原理有所什么?二是广度:了解一些前端外的知识,例如服务端,运维相关的。总之,就是我们要学的东西还很多😭。

   最近,我也终于拿到了一份15k以上的offer。不枉费我辛辛苦苦巩固基础,总结了一个多月来的面试题。机会总是会有的,只是可能它在路上抛锚了,来得慢点。与大家共勉之。



五、Vue

5.1 Vue的生命周期

5.1.1 单个组件的生命周期

按创建,激活到更新,停止, 最后销毁的顺序。

  1. beforeCreated:
  2. created
  3. beforeMounted
  4. mounted
  5. activated
  6. beforeUpdated
  7. updated
  8. deactivated
  9. beforeDestory
  10. 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基本原理

  1. 首先在编译阶段,v-model被当成普通指令解析到el.directives,然后在解析v-model的时候,会根据节点的不同请求去执行不同的逻辑。
    • 如果节点是select、checkbox, radio,则监听的是change事件
    • 如果节点是input,textarea,则监听一般是input事件,在.lazy下的情况下是change事件。
    • 如果节点是组件,则是使用自定义的回调函数
  2. 在运行的时候,通过相应事件的监听函数去更改数据。

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)
  1. 如果oldVnode不存在,即是新添加的节点,则创建vnode的DOM
  2. 如果不是真实的节点且是相同类型的节点,则进入结点diff,即patchVnode函数。否则会用新的节点替换老的。这里的相同类型指的是具有相同的key值和一些其他条件,例如标签相同等等
  3. 如果oldVnode === vnode,则认为没有变化 如果oldVnode的isAsyncPlaceholder属性为true时,跳过检查异步组件,return;
  4. 如果oldVnode跟vnode都是静态节点(实例不会发生变化),且具有相同的key,并且当vnode是克隆节点或是v-once指令控制的节点时,则把oldVnode.elm和oldVnode.child都复制到vnode上;
  5. 如果vnode不是文本节点或注释节点
    • 如果vnode和oldVnode都有子节点并且两者的子节点不一致时,就调用updateChildren更新子节点
    • 如果只有vnode有子节点,则调用addVnodes创建子节点
    • 如果只有oldVnode有子节点,则调用removeVnodes把这些子节点都删除
    • 如果vnode文本为undefined,则清空vnode.elm文本;
  6. 如果vnode是文本节点但是vnode.text != oldVnode.text时只需要更新vnode.elm的文本内容就可以。
  7. 在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 原型上添加两个属性router和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

数据流 来自官网的图:

vuex.png

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 单个组件的生命周期

按照创建,更新,销毁的顺序

  1. componentwillMount(React17废弃)
  2. componentDidMount
  3. componentWillReceiveProps(React17替换成getDerivedStateFromProps)
  4. shouldComponentUpdate
  5. componentWillUpdate(React17替换成getSnapshotBeforeUpdate)
  6. componentDidUpdate
  7. 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');
  }};

总体流程如下:

  1. 将state放入enqueueSetState队列中,并调用enqueueUpdate处理要更新的Component
  2. 如果组件当前正处于update中(isBatchingUpdates),则先将Component存入dirtyComponent中。否则调用batchedUpdates处理。
  3. batchedUpdates发起一次transaction.perform()事务
  4. 事务会更新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

  1. 新的核心算法 Fiber
  2. render可以返回数组,字符串
  3. 错误处理机制
  4. Portals组件 渲染外部的dom节点. createPortal API
  5. 更好 更快的服务端渲染 rendertoNodeStream 返回node的流
  6. 体积更小 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 个步骤:

  1. 调用 store.dispatch(action)。
  2. Redux store 调用传入的 reducer 函数。
  3. 根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
  4. Redux store 保存了根 reducer 返回的完整 state 树。

官网地址:www.redux.org.cn/docs/basics…


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
    }
  }
}

代码地址:github.com/zty1205/rea…

七、Node

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。 Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。

内置模块:

  • events: 事件模块
  • buffer: 二进制数据处理模块
  • utils: 工具类模块
  • fs: 文件模块
  • path: 路径工具
  • http: web模块
  • ...

7.1 eventloop

7.1.1 基本流程

  1. timers:执行满足条件的setTimeout、setInterval回调。 [uv__run_timers函数]
  2. I/O callbacks:是否有已完成的I/O操作的回调函数,来自上一轮的poll残留。[uv__run_pending函数]
  3. idle,prepare:node内部特定的阶段,在I/O轮询开始前做一些特定的回调,可忽略 [uv__run_idle, uv__run_prepare函数]
  4. poll:轮询,等待还没完成的I/O事件,会因timers和超时时间等结束等待。[uv__io_poll(loop, timeout)函数]
  5. check:执行setImmediate的回调。[uv__run_check函数]
  6. close callbacks:关闭所有的closing handles,一些onclose事件,例如socket.on("close",func)。[ uv__run_closing_handles(loop)函数]
  7. 重复以上步骤。

源码路径 github.com/nodejs/node…

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 基本流程

  1. 解析shell和config中的配置项,用于激活webpack的加载项和插件
  2. webpack初始化工作,包括构建compiler对象,初始化compiler的上下文,loader和file的输入输出环境
  3. 解析入口js文件,通过对应的工厂方法创建模块,使用acron生成AST树并且遍历AST,处理require的module,如果依赖中包含依赖则遍历build module,在遍历过程中会根据文件类型和loader配置找出合适的loader用来对文件进行转换
  4. 调用seal方法,封装,逐次对每一个module,chunk进行整理,生成编辑后的代码

8.4 模块打包

  1. 通过fs将模块读取成字符串,然后用warp包裹一下,使之成为一个字符串形式的的函数然后调用 vm.runInNewContext 这样类型的方法,这个字符串会变成一个函数。
  2. 这些模块的函数会被存放在数组里,然后进行解析执行。module和export都是传入的对象,webpack会实现require函数,去加载其他模块。
  3. 如果是异步模块,则会通过jsonp的形式去加载该模块打包好生成的chunk。 异步加载模块可以使用import和require.ensure函数,函数将会返回一个promise。
  4. 上面方法都是公共的,可以抽离成模板的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
}

代码地址:github.com/zty1205/rea…


8.5 热更新

  1. client 和 server 建立一个 websocket 通信
  2. 当有文件发生变动(如fs.watchFile)的时候,webpack编译文件,并通过 websocket 向client发送一条更新消息
  3. client 根据收到的hash值,通过ajax获取一个 manifest 描述文件
  4. client 根据manifest 获取新的JS模块的代码
  5. 当取到新的JS代码之后,会更新 modules tree,(installedModules)调用之前通过 module.hot.accept 注册好的回调,可能是loader提供的,也可能是你自己写的

manifest: 描述资源文件对应关系如下,打包后的文件拥有了hash值,所以需要进行映射。

{
  "a.js": "a.41231243.js"
}

8.6 plugin

8.6.1 如何开发一个plugin

  1. 一个 JavaScript 命名函数。
  2. 在插件函数的 prototype 上定义一个 apply 方法。
  3. 指定一个绑定到 webpack 自身的事件钩子。
  4. 处理 webpack 内部实例的特定数据。
  5. 功能完成后调用 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(...)
};

官网地址:www.webpackjs.com/api/plugins…

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启动多线程压缩
  • 对项目进行拆分

推荐文章:blog.csdn.net/lunahaijiao…


九、常见性能优化

超过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)处。
  • 内容存储:在功能上包括对各种内容格式的支持,对部分缓存的支持;在性能上包括支持的容量、多文件吞吐率、可靠性、稳定性,都是存储需要考虑的问题。

答案来自:baike.baidu.com/item/CDN


11.6 函数式编程

函数式编程是种编程方式,它将电脑运算视为函数的计算。在函数编程中,函数是第一等公民,且该函数应该是一个纯函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。列如含有:

  • log
  • http请求
  • 可变数据如new Date()
  • DOM操作

纯函数带来的好处就是:更好的进行单元测试和调试,一对一的数据关系可以便于缓存。函数式编程还有其他特性:

  • 闭包和高阶函数
  • 惰性计算
  • 递归

函数式编程有两个最基本的运算:合成compose和柯里化curry。



结束语


   以上我的面试题汇总了,我比较菜,如果大家发现有错误的,欢迎指正!开始答应给大家的链接,现在马上分享,不要眨眼哦。

我的个人面经
note.youdao.com/ynoteshare1…

   做一个爱分享,爱学习的前端,我是安迪five,oh yeah!