前端面试-进阶

733 阅读34分钟

Vue

描述 Vue 组件生命周期(有父子组件的情况)

面试高频指数:⭐⭐⭐⭐⭐

生命周期必须会!

Vue 组件生命周期,就是 Vue 实例从创建到销毁的过程。

创建阶段

1. beforeCreate
此阶段为实例初始化之后,此时 data 和 methods 都拿不到,不能获得 DOM 节点。(没有data,没有 $el)

使用场景:可以在这加个 loading 事件

2. created
在这个阶段 vue 实例已经创建,可以获取 data 和 methods,仍然不能获取 DOM 元素。(有 data,没有 $el)

使用场景:可以初始化某些属性值,异步操作可以放在这里,初始化完成时的事件也可以写在这里,比如在这结束 loading 事件

挂载阶段

1. beforeMount
已经编译好了最终模板, 但是还没有将最终的模板渲染到界面上(有 data,有 $el),相关的 render 函数首次被调用

组件实例将要挂载到挂载点, 页面未显示,开发中很少使用

2. mounted
mounted 可能是平时我们使用最多的函数了,在这个阶段,组件模板已经渲染到指定的节点上,页面显示,数据和 DOM 都可以获取到。

使用场景:通常是初始化页面完成后再对数据和 DOM 做一些操作,异步请求也可以写在这里

更新阶段

1. beforeUpdate
数据已经更新了, 但是界面还没有更新

2. updated
数据已经更新了, 界面也更新了

销毁阶段

1. beforeDestroy
在实例销毁前调用,实例依然可以使用,还可以获取到数据

使用场景:一般在这一步做一些重置的操作,比如清除掉组件中的定时器和监听的 dom 事件

2. destroyed
在实例销毁之后调用,调用后,所有的事件监听器会被移除,所有的子实例已经被销毁

keep-alive

在 keep-alive 中,vue 新增了两个钩子函数

activated:因为使用了 keep-alive 的组件会被缓存,所以 created, mounted 这种的钩子函数只会执行一次, 如果我们的子组件需要在每次加载的时候进行某些操作,可以使用 activated 钩子触发。

deactivated:组件被移除时使用。

vue父子组件生命周期的执行顺序

加载渲染过程
父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted

子组件更新过程
父beforeUpdate->子beforeUpdate->子updated->父updated

父组件更新过程
父beforeUpdate->父updated

销毁过程
父beforeDestroy->子beforeDestroy->子destroyed->父destroyed

image.png

参考文档:zhuanlan.zhihu.com/p/163223557

Ajax 请求应该放在哪个生命周期

面试高频指数:⭐⭐⭐⭐⭐

对于一般情况,created 和 mounted 都是可以的;

服务端渲染不支持 mounted 方法,所以在服务端渲染的情况下统一放在 created 中。

涉及到要访问 dom 的操作,我们只能使用 mounted

何时需要使用 beforeDestroy?

面试高频指数:⭐⭐⭐

解除自定义事件 event.$off
清除定时器
解绑自定义的 DOM 事件,如 window scroll 等

Vue 组件如何通讯

面试高频指数:⭐⭐⭐⭐⭐

方法一、props/$emit
在父组件中通过 v-bind 传递数据,在子组件中通过 props 接收数据
在父组件中通过 v-on 传递方法,在子组件的自定义方法中通过 this.$emit('自定义接收名称'); 触发传递过来的方法

方法二、$emit/$on
这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,巧妙而轻量地实现了任何组件间的通信,包括父子、兄弟、跨级。

  1. 定义一个空的Vue实例
var Event = new Vue();
  1. 传递数据
Event.$emit(事件名,数据);
  1. 获取数据
Event.$on(事件名,data => {});

方法三、vuex
vuex 是 Vue 配套的公共数据管理工具,我们可以将共享的数据保存到 vuex 中, 方便整个程序中的任何组件都可以获取和修改 vuex 中保存的公共数据

  • 创建 Vuex 对象
const store = new Vuex.Store({
    // 这里的state就相当于组件中的data, 就是专门用于保存共享数据的
    state: {
        msg: "共享数据"
    }
});
  • 在祖先组件中添加 store 的 key 保存Vuex对象
    只要祖先组件中保存了 Vuex 对象 , 那么祖先组件和所有的后代组件就可以使用 Vuex 中保存的共享数据了
store: store,
  • 在使用Vuex中保存的共享数据的时候, 必须通过如下的格式来使用
<p>{{this.$store.state.msg}}</p>

方法四:作用域插槽
作用域插槽就是带数据的插槽, 就是让父组件在填充子组件插槽内容时也能使用子组件的数据
应用场景在于: 子组件提供数据, 父组件决定如何渲染

  • slot中通过 v-bind:数据名称="数据名称" 方式暴露数据
  • 在父组件中通过 <template v-slot="作用域名称"> 接收数据
  • 在父组件的 <template></template> 中通过 作用域名称.数据名称 方式使用数据
// son.vue
<slot :son="abc">
</slot>

// father.vue
<son>
    <template v-slot="slotData">
        {{ sloData.son }}
    </template>
</son>

Vue 响应式原理

面试高频指数:⭐⭐⭐⭐⭐

  • 在数据初始化的时候,对象内部通过 defineReactive 方法,使用 Object.defineProperty() 对属性进行劫持(这时候只会劫持已经存在的属性)。如果数据是数组类型, Vue2 中是通过重写数组方法来实现劫持的。多层对象是通过递归来实现劫持的。

  • 在初始化流程中的编译阶段,当 render 函数被调用的时候,会读取 Vue 实例中和视图相关的响应式数据,此时会触发 getter 函数进行依赖收集(将观察者 Watcher 对象存放到当前的订阅者 Dep 的 subs 中)。

  • 当数据发生变化或者视图导致的数据发生变化时,会触发数据劫持的 setter 函数,setter 会触发依赖收集中的 Dep.notify() 方法进行发布订阅,告知需要重新渲染视图,Watcher 就会通过 update 方法来更新视图。

Object.defineProperty 的一些缺点(Vue3.0 启用 Proxy),但是 Proxy 兼容性不好,且无法 polyfill
深度监听,需要递归到底,一次性计算量大
无法监听新增/删除属性(Vue.set Vue.delete)

image.png

监听 data 变化的核心 API 是什么?

面试高频指数:⭐⭐

Object.defineProperty

缺点:

深度监听,需要递归到底,一次性计算量大
无法监听新增/删除属性(所以需要 vue.set vue.delete 实现新增/删除属性)
无法监听原生数组,需要特殊处理

vue 如何监听数组变化

面试高频指数:⭐⭐

Object.defineProperty 不能监听数组变化
重新定义原型,重写 push pop 等方法,实现监听
Proxy 可以原生支持监听数组变化

参考文档:www.jianshu.com/p/9c5b30bfb…

双向数据绑定 v-model 的实现原理

面试高频指数:⭐⭐⭐⭐⭐

v-model 其实是语法糖。
基于 v-bind 和 v-on 封装的语法糖,$event.target 获取事件源,实现了双向数据

<input type="text" :value="custom" @input="custom = $event.target.value" />

参考文档:segmentfault.com/a/119000001…

如何自己实现 v-model?

面试高频指数:⭐⭐⭐

image.png

v-show 和 v-if 的区别

面试高频指数:⭐⭐⭐⭐

v-show 是通过 CSS 的 display 属性控制来显示和隐藏
v-if 是组件真正的渲染和销毁,而不是显示和隐藏

v-if 有更高的初始渲染开销,v-show 有更高的切换开销
频繁切换显示状态用 v-show,否则用 v-if

为何 v-for 中要用 key

面试高频指数:⭐⭐⭐⭐⭐

版本一:

  1. 因为 Vue 组件高度复用增加 key 可以标识组件的唯一性,为了更好地区别各个组件。key 的作用主要是为了高效的更新虚拟 DOM。

  2. key 主要用来做 dom diff 算法用的,diff 算法是同级比较,比较当前标签上的 key 还有它当前的标签名,如果 key 和标签名都一样时只是做了一个移动的操作,不会重新创建元素和删除元素。

  3. 没有 key 的时候默认使用的是 “就地复用” 策略。如果数据项的顺序被改变,Vue 不是移动 Dom 元素来匹配数据项的改变,而是简单复用原来位置的每个元素。如果删除第一个元素,在进行比较时发现标签一样值不一样时,就会复用之前的位置,将新值直接放到该位置,以此类推,最后多出一个就会把最后一个删除掉。

  4. 尽量不要使用索引值 index 作 key 值,一定要用唯一标识的值,如 id 等。因为若用数组索引 index 为 key,当向数组中指定位置插入一个新元素后,因为这时候会重新更新 index 索引,对应着后面的虚拟 DOM 的 key 值全部更新了,这个时候还是会做不必要的更新,就像没有加 key 一样,因此 index 虽然能够解决 key 不冲突的问题,但是并不能解决复用的情况。如果是静态数据,用索引号 index 做 key 值是没有问题的。

  5. 标签名一样,key 一样这时候就会就地复用,如果标签名不一样,key 一样不会复用。

就地复用策略:当在进行列表渲染的时候,vue会直接对已有的标签进行复用,不会整个的将所以的标签全部删除和创建,只会重新渲染数据,然后再创建新的元素直到数据渲染完为止。

版本二:
key 属性可以用来提升 v-for 渲染 DOM 的效率。key 属性必须是唯一不变的值(唯一标识),避免数据混乱的情况的出现。

加了 key 之后,vue 可以识别每组节点。如果节点之间内容一致,只是顺序发生变化,那么就没有必要进行增加删除操作了,而是直接进行顺序的更改即可。大大提升效率。

版本三:
必须要用 key, 而且不能用 index 和 random
key 是 vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确,更快速
在 diff 算法中用 tag 和 key 来判断,是否是相同节点

可以减少渲染次数,提高渲染性能

image.png

参考文档:
blog.csdn.net/qq_42072086… segmentfault.com/a/119000003…

为什么 v-for 和 v-if 不建议一起使用

面试高频指数:⭐⭐⭐⭐

v-for 比 v-if 优先,如果每一次都需要遍历整个数组,将会影响速度,尤其是当之需要渲染很小一部分的时候。
如果连用的话会把 v-if 给每个元素都添加一下, 会造成性能问题。
一般时候把 v-if 放在外层,如果不符合就不去执行了。

Vue 常见性能优化

面试高频指数:⭐⭐⭐⭐⭐

合理使用 v-show 和 v-if
合理使用 computed
v-for 时要加 key,以及避免和 v-if 同时使用
data 层级不要太深(因为深度监听一次性监听到底)
自定义事件、DOM 事件、定时器及时销毁
合理使用路由懒加载,异步组件
合理使用 keep-alive
图片懒加载
浏览器缓存
开启 gzip 压缩
使用 CDN
使用 SSR

描述组件渲染和更新的过程

面试高频指数:⭐⭐⭐⭐

初次渲染过程

  • 解析模板为 render 函数
  • 触发响应式,监听 data 属性 getter setter
  • 执行 render 函数,生成 vnode
  • patch(elem,vnode)

更新过程

  • 修改 data,触发 setter (此前在 getter 中 已被 监听)
  • 重新执行 render 函数,生成 newVnode
  • patch(vnode, newVnode)

异步渲染

  • 汇总 data 的修改,一次性更新视图
  • 减少 DOM 操作次数,提高性能

请阐述一下你对虚拟 DOM 和 Dom-Diff 的理解?

面试高频指数:⭐⭐⭐⭐⭐

虚拟 DOM
它是一个 object 对象模型用来模拟真实的 dom
作用是高效地渲染页面,数据驱动视图,减少不必要的 dom 操作,提高渲染效率

DIFF 算法
diff 算法就是用来比较 vdom 结构的,是 vdom 中最核心、最关键的部分

原理:
在数据发生变化,vue 是先根据真实 DOM 生成一颗 VDOM ,当 VDOM 某个节点的数据改变后会生成一个新的 Vnode ,然后 Vnode 和 oldVnode 作对比,发现有不一样的地方就直接修改在真实的 DOM 上,然后使 oldVnode 的值为 Vnode ,来实现更新节点。

原理简述:
(1)先去同级比较,然后再去比较子节点

(2)先去判断一方有子节点一方没有子节点的情况

(3)比较都有子节点的情况

(4)递归比较子节点

优化时间复杂度到 O(n)

  • 只比较同一层级,不跨级比较
  • tag 不同, 则直接删掉重建,不再深度比较
  • tag 和 key,两者都相同,则认为是相同节点,不再深度比较

请用 vnode 描述一个 DOM 结构

面试高频指数:⭐

image.png

const VNode = [
    {
      tag: "div",
      attr: { class: "netease", id: "1", style: "font-size:14px;text-align:center;" },
      children: [
        {
          tag: "span",
          attr: { class: "welcome", style: "color:#cf1132;" },
          children: ["欢迎广大学子加入"],
        },
        {
          tag: "a",
          attr: { href: "http://game.163.com", target: "_blank" },
          children: ["网易游戏"],
        },
      ],
    },
    {
      tag: "p",
      attr: { title: "文本描述", id: "2", style: "text-align:center;" },
      children: ["我和你一样,我们都是游戏热爱者。"],
    },
];

将 vdom 翻译成真实的 dom

function createElm(vnode) {
    let { tag, attr, children } = vnode;
    if (typeof tag === "string") {
      vnode.el = document.createElement(tag);
      for (let key in attr) {
        vnode.el.setAttribute(key, attr[key]);
      }
      children.forEach((child) => {
        if (typeof child === "object") {
          vnode.el.appendChild(createElm(child));
        } else {
          vnode.el.innerText = child;
        }
      });
    }
    return vnode.el;
}

let parentEl = document.createElement("div");
VNode.forEach((i) => {
    parentEl.appendChild(createElm(i));
});
console.log(parentEl.innerHTML);

diff 算法的时间复杂度

面试高频指数:⭐

O(n)

在O(n^3)基础上做了一些调整

参考文档:www.cnblogs.com/queenya/p/1…

谈谈你对 MVVM 的理解

面试高频指数:⭐⭐⭐⭐⭐

传统组件,只是静态渲染,更新还要依靠于操作 DOM,Vue 数据驱动视图

当前端发展起来后,这时前端开发就暴露出了三个痛点问题:

  • 开发者在代码中大量调用相同的 DOM API, 处理繁琐 ,操作冗余,使得代码难以维护。
  • 大量的 DOM 操作使页面渲染性能降低,加载速度变慢,影响用户体验。
  • 当 Model 频繁发生变化,开发者需要主动更新到 View ;当用户的操作导致 View 发生变化,开发者同样需要将变化的数据同步到 Model 中,这样的工作不仅繁琐,而且很难维护复杂多变的数据状态。

MVVM 由 Model、View、ViewModel 三部分构成

Model 代表数据模型,也可以在 Model 中定义数据和业务逻辑;
View 代表 UI 组件,它负责将数据模型转化成 UI 展现出来;
ViewModel 是一个同步 View 和 Model 的对象;

MVVM模式:不需要手动的操作 dom ,主要是实现数据双向绑定

image.png

Vue 为何是异步渲染,$nextTick 有何用

面试高频指数:⭐⭐⭐

1、异步渲染,$nextTick待 DOM 渲染完再回调;
2、页面渲染时会将 data 的修改做整合,多次 data 修改只做一次渲染,减少 DOM 操作次数,提高性能。

$nextTick 原理

面试高频指数:⭐⭐⭐⭐

Vue 是异步渲染的,data 改变之后,DOM 不会立即渲染
$nextTick 是将回调函数延迟在下一次 dom 更新后调用

原理:
把回调函数放入 callbacks 等待执行
将执行函数放到微任务或者宏任务中
事件循环到了微任务或者宏任务,执行函数依次执行 callbacks 中的回调

nextTick 和 $nextTick 区别

面试高频指数:⭐

nextTick(callback):当数据发生变化,DOM 更新后执行回调。
$nextTick(callback):当 dom 发生变化,DOM 更新后执行的回调。

这两个方法没有太大的不同。区别在于:nextTick(callback)是全局的方法;而 $nextTick(callback) 是回调的 this 自动绑定到调用它的实例上;所以用的更多的是$nextTick(callback)!

为何组件 data 必须是一个函数?

面试高频指数:⭐

防止组件重用的时候导致数据相互影响。根本上 .vue 文件编译出来是一个类,这个组件是一个 class,我们在使用这个组件的时候相当于对 class 实现实例化,在实例化的时候执行 data,data 都在闭包中,不会相互影响

var MyComponent = function() {
  this.data = this.data()
}
MyComponent.prototype.data = function() {
  return {
    a: 1,
    b: 2,
  }
}

computed 有何特点

面试高频指数:⭐⭐

有缓存,data 不变不会重新计算;提高性能。

computed 和 watch 有什么区别

  1. computed 是计算一个新的属性,并将该属性挂载到 vm(Vue 实例)上,而 watch 是监听已经存在且已挂载到 vm 上的数据,所以用 watch 同样可以监听 computed 计算属性的变化(其它还有 dataprops
  2. computed 本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问 computed 属性,才会计算新的值,而 watch 则是当数据发生变化便会调用执行函数
  3. 从使用场景上说,computed 适用一个数据被多个数据影响,而 watch 适用一个数据影响多个数据;

computed 怎么实现的

computed 本身是通过代理的方式代理到组件实例上的,所以读取计算属性的时候,执行的是一个内部的 getter,而不是用户定义的方法。

computed 内部实现了一个惰性的 watcher,在实例化的时候不会去求值,其内部通过 dirty 属性标记计算属性是否需要重新求值。当 computed 依赖的任一状态(不一定是 return 中的)发生变化,都会通知这个惰性watcher,让它把 dirty 属性设置为 true。所以,当再次读取这个计算属性的时候,就会重新去求值。

惰性watcher/计算属性在创建时是不会去求值的,是在使用的时候去求值的。

$set 实现原理

function set(target, key, val) {
    if (isUndef(target) || isPrimitive(target)) {
      warn(
        'Cannot set reactive property on undefined, null, or primitive value: ' +
          target
      );
    }
}
复制代码

首先set方法会进行判断,传入的target是否是null、undefined或是原始类型(string, number, boolean, symbol)。如果是就抛出警告。

if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key);
    target.splice(key, 1, val);
    return val;
}

第一行判断target是否是一个数组,并且key值是否是合法key。下面是检查Index的方法。

function isValidArrayIndex(val) {
  var n = parseFloat(String(val));
  return n >= 0 && Math.floor(n) === n && isFinite(val);
}

接着再回到上面的源码,第二行将target.length设置为target.lengthkey最大的值。这是为了防止某些情况下会报错,比如: 设置的key值,大于数组的长度。

new Vue({
    el: '#root',
    data: {
        list: [1, 2]
    }
})
Vue.set(vm.list, 10, 'error');

第三行是一个splice方法,将key位置的值替换为val。注意:当调用splice的时候就会重新渲染新的试图。因为这是一个特殊的splice方法,Vue将其改写了,看下面源码:

var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);

var methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse',
];

methodsToPatch.forEach(function(method) {
  // 缓存原始数组的方法
  var original = arrayProto[method];
  def(arrayMethods, method, function mutator(...args) {
    const result = original.apply(this, args);
    ...省略部分源码
    // 发送更新通知
    ob.dep.notify();
    return result;
  });
});

这是Vue定义的7个对象原型上的方法。

if (key in target && !(key in Object.prototype)) {
  target[key] = val;
  return val;
}
复制代码

上面代码的意思是,如果target对象上已经存在key,且这个key不是Object原型对象上的属性。这说明key这个属性已经是响应式的了,那么就则直接将val赋值给这个属性。

var ob = target.__ob__;
if (target._isVue || (ob && ob.vmCount)) {
  warn(
    'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
  );
  return val;
}

__ob__指的是Observer对象。vmCount用来表示当前对象是否是根级属性_isVue用来表示是否是Vue实例。上面说过target不能是根级属性或者Vue实例

if (!ob) {
  target[key] = val;
  return val;
}
defineReactive$$1(ob.value, key, val);
ob.dep.notify();
return val;

第1行到第4行的意思是,如果target上不存在Observer对象(这说明target只是一个普通的对象,不是一个响应式数据),则直接赋值给key属性。

第5行,ob.value其实就是target,只不过它是Vue实例上$data里的已经被追踪依赖的对象。然后在这个被observed的对象上增加key属性。让key属性也转为getter/setter

第6行,让dep通知所有watcher重新渲染组件。

完整源码

function set(target, key, val) {
    if (isUndef(target) || isPrimitive(target)) {
      warn(
        'Cannot set reactive property on undefined, null, or primitive value: ' +
          target
      );
    }
    if (Array.isArray(target) && isValidArrayIndex(key)) {
      target.length = Math.max(target.length, key);
      target.splice(key, 1, val);
      return val;
    }
    if (key in target && !(key in Object.prototype)) {
      target[key] = val;
      return val;
    }
    var ob = target.__ob__;
    if (target._isVue || (ob && ob.vmCount)) {
      warn(
        'Avoid adding reactive properties to a Vue instance or its root $data ' +
          'at runtime - declare it upfront in the data option.'
      );
      return val;
    }
    if (!ob) {
      target[key] = val;
      return val;
    }
    defineReactive$$1(ob.value, key, val);
    ob.dep.notify();
    return val;
 }

如何将组件所有 props 传递给子组件?

面试高频指数:⭐

$props  <user v-bind="$props">

多个组件有相同逻辑,如何抽离?

面试高频指数:⭐⭐

用 mixin。
定义一个 js文件将export default 中的共有内容写到里面,然后在组件中import,放到 mixin数组中

mixin 的一些缺点:
变量来源不明,不利于阅读。我们希望变量和方法是可查找的,但是 mixin 引入的变量和方法是不可寻找,多mixin 可能造成命名冲突
mixin 和组件可能出现多对多的关系(一个组件引用多个 mixin, 一个 mixin 被多个组件引用),复杂度较高。多对多是最复杂的关系,很容易剪不断理还乱
在 vue3 提出的 Composition API 旨在解决这些问题

minin 是用来干嘛的?有什么弊端?

面试高频指数:⭐

多个组件有相同的逻辑,抽离出来

mixin 的问题
变量来源不明确,不利于阅读
多 mixin 可能会造成命名冲突
minin 和组件可能出现多对多的关系,复杂度较高

何时使用异步组件?

面试高频指数:⭐⭐

加载大组件
路由异步加载

何时需要使用 keep-alive?

面试高频指数:⭐⭐

缓存组件,不需要重复渲染,如多个静态 tab 页的切换
优化性能

什么是作用域插槽?

面试高频指数:⭐

父组件通过 slot 获取子组件中的的值:子组件中通过自定义属性绑定数据,父组件通过 template的 v-slot 属性来接收数据

vuex 中 action 和 mutation有何区别?

面试高频指数:⭐

action 中处理异步,mutation 不可以
mutation 做原子操作
action 可以整合多个 mutation

Vue-Router 原理

面试高频指数:⭐

hash
hash 变化会触发网页跳转,即浏览器的前进后退。
hash 变化不会刷新页面,SPA 必须的特点
hash 永远不会提交到 server 端(前端自生自灭)
window.hashchange

history
用 url 规范的路由,但跳转时不刷新页面
history.pushState
window.onpopstate

两者选择
to B 的系统推荐用 hash,简单易用,对 url 不敏感
to C 的系统,可以考虑选择 H5 history,但需要服务端支持

能选择简单的,就别用复杂的,要考虑成本和收益

vue-router 常用的路由模式

面试高频指数:⭐⭐

hash 默认:有 #,也就是路由的hash,后面是路由
H5 history(需要服务端支持):没有 #,需要服务端再次,无特殊需求可选择 hash模式

如何配置 vue-router 异步加载?

面试高频指数:⭐⭐

异步加载性能会优化很多,配置:component: () => import(......)

React

性能优化

面试高频指数:⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐ 满天星

性能优化,永远是面试的重点,性能优化对于 React 更加重要

  • 渲染列表时加 key
  • 在页面中使用了 setTimout()、addEventListener() 、自定义事件等,要及时在 componentWillUnmount() 中销毁
  • 合理使用异步组件
  • 减少函数 bind this 的次数
  • 使用 React-loadable 动态加载组件
  • shouldComponentUpdate (简称SCU ) 按需使用、React.PureComponent、React.memo
  • state 层级不用太深
  • 合理使用不可变值 ImmutableJS
  • 图片懒加载
  • 使用 SSR

shouldComponentUpdate 的用途

面试高频指数:⭐⭐⭐⭐⭐

默认情况下,在 React 中父组件有更新,子组件则无条件也更新。
shouldComponentUpdate 允许我们手动地判断是否要进行组件更新,根据组件的应用场景设置函数的合理返回值能够帮我们避免不必要的更新。默认返回 true,返回 true 则代表需要更新,返回 false 则不更新

注意:SCU 必须配合 “不可变值” 使用

shouldComponentUpdate (nextProps, nextState) {
    return true // 可以渲染,执行 render(),默认返回 true
    return false // 不能渲染,不执行 render()
}

描述 react 组件生命周期

面试高频指数:⭐⭐⭐⭐⭐

挂载时(4个钩子)

1. constructor()
加载时调用一次,可以实现:初始化 state, 为事件处理函数绑定实例。

2. static getDerivedStateFromProps(props, state)
在初始挂载及后续更新时都会被调用,让组件在 props 变化时更新 state, 每次接收新的 props 之后都会返回一个对象作为新的 state ,如果返回 null,则不更新任何内容。

3. render()
类组件中唯一必须实现的方法,创建虚拟 dom 树,更新 dom 树都在此进行。

4. componentDidMount()
组件挂载之后调用,只调用一次。一般在这里请求数据。

更新时(5个钩子)

1. static getDerivedStateFromProps(props, state)
在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。它应返回一个对象来更新 state,如果返回 null 则不更新任何内容。

2. shouldComponentUpdate(nextProps, nextState)
当 props 或 state 发生变化时,在渲染前调用,return true 就会更新 dom,return false 能阻止更新。 仅作为性能优化的方式而存在。

3. render()
render() 函数应该为纯函数,这意味着在不修改组件 state 的情况下,每次调用时都返回相同的结果,并且它不会直接与浏览器交互。保持 render() 为纯函数,可以使组件更容易思考。

4. getSnapshotBeforeUpdate(prevProps, prevState)
在最近一次的渲染提交到 DOM 节点之前调用,返回一个值,作为 componentDidUpdate 的第三个参数。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。

5. componentDidUpdate(prevProps, prevState, snapshot)
会在更新后会被立即调用,首次渲染不会执行此方法。 当组件更新后,可以在此处对 DOM 进行操作、 进行网络请求 。

卸载时(1个钩子)

1. componentWillUnmount()
在组件卸载及销毁之前直接调用, 在此方法中执行必要的清理操作,例如,清除 timer,取消网络请求等等。

React 发起 ajax 应该在哪个生命周期

componentDidMount

React 组件如何通讯

面试高频指数:⭐⭐⭐⭐⭐

1、父组件向子组件通信:使用 props
父组件通过向子组件传递 props,子组件得到 props 后进行相应的处理。

2、子组件向父组件通信:使用 props + 回调
父组件将一个函数作为 props 传递给子组件,子组件调用该回调函数,便可以向父组件通信。

3、兄弟组件通信: 找到这两个兄弟节点共同的父节点, 结合上面两种方式由父节点转发信息进行通信

4、跨级组件间通信:使用 context
公共信息传递给每个组件,如果组件层级过多,用 props 传递就会繁琐,用 redux 小题大做。Context 通过组件树提供了一个传递数据的方法,从而避免了在每一个层级手动的传递 props 属性。

5、发布订阅模式:发布者发布事件,订阅者监听事件并做出反应, 我们可以通过引入 event 模块进行通信

6、全局状态管理工具:借助 Redux 全局状态管理工具进行通信, 这种工具会维护一个全局状态中心 Store, 并根据不同的事件产生新的状态

组件渲染和更新的过程

渲染过程

  • JSX 即 createElement 函数
  • 执行生成 vnode
  • patch(elem, vnode) 和 patch(vnode,newVnode)

更新过程

  • setState(newState) -> dirtyComponents (可能有子组件)
  • render() 生成 newVnode
  • patch(vnode, newVnode)

渲染列表,为何使用 key

面试高频指数:⭐⭐⭐⭐

同 Vue。必须用 key,且不能是 index 和 redom
diff 算法中通过 tag 和 key 来判断,算法是 sameNode
减少渲染次数,提升渲染性能

JSX 本质是什么

面试高频指数:⭐⭐⭐⭐⭐

是 JavaScript 调用函数 React.createElement() 的语法糖。
React.createElement() 是 h 函数接收 3 个 参数:

  • type: 标识节点的类型。可以是标记名字符串(例如 ’div’ 或 ’span’)、React组件类型(类或函数)或 React 片段类型。
  • config: 组件的所有属性以键值对的形式存储在 config 中,以对象的形式传递。
  • childen: 组件的嵌套内容,也就是子元素,子节点,也是以对象的形式进行传递。

React.createElement() 返回 vnode,vnode 可以通过 patch 渲染

context 是什么,有何用途?

面试高频指数:⭐⭐⭐⭐⭐

context 是用来跨级组件间通信。context 相当于一个大容器,我们可以把要通信的内容放在这个容器中,这样一来,不管嵌套有多深,都可以随意取用。

如何使用:
1.React.createContext 函数用于生成 Context 对象。可以在创建时给 Context 设置默认值:

const ThemeContext = React.createContext('light');

2.Context 对象中有一个 Provider(提供者) 组件,Provider 组件接受一个 value 属性用以将数据传递给消费组件。

<ThemeContext.Provider value="dark">
    <Toolbar />
</ThemeContext.Provider>

3.获取 Context 提供的值可以通过 contextType 或者 Consumer(消费者) 组件中获取。contextType 只能用于类组件,并且只能挂载一个 Context:

class ThemedButton extends React.Component {
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

若想给组件挂载多个 Context, 或者在函数组件内使用 Context 可以使用 Consumer 组件:

<MyContext.Consumer>
  {value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>

描述 redux 单向数据流

面试高频指数:⭐⭐⭐⭐⭐

  • 首先,用户发出 Action。

    store.dispatch(action);
    
  • 然后,Store 自动调用 Reducer,并且传入两个参数:当前 State 和收到的 Action。 Reducer 会返回新的 State 。

    let nextState = todoApp(previousState, action);
    
  • State 一旦有变化,Store 就会调用监听函数。

    // 设置监听函数
    store.subscribe(listener);
    
  • listener 可以通过 store.getState() 得到当前状态,可以触发重新渲染 View。

    function listerner() {
      let newState = store.getState();
      component.setState(newState);   
    }
    

image.png

redux 如何进行异步请求

面试高频指数:⭐

使用异步 action,如 redux-thunk、redux-promise、redux-saga

怎么理解 state 是不可变的

面试高频指数:⭐⭐

state 不可变数据
因为 react 的生命周期中每次调用 shouldComponentUpdate() 会将 state 现有的数据跟将要改变的数据进行比较, 更新发生变化的数据,最大限度减少不必要的更新,达到性能的优化。所以不建议直接更改 state 里面的数据,而是通过 setState 去改变,并且在使用 setState 之前不能影响旧的 state 数据。
在 state 中直接改变引用类型数据, 视图无法更新

setState 是同步还是异步?

面试高频指数:⭐⭐⭐⭐⭐

可能是异步更新
setState 本身的执行过程是同步的,只是因为在 react 的合成事件与钩子函数中执行顺序在更新之前,所以不能直接拿到更新后的值,形成了所谓的异步;

能不能同步,什么时候是同步的?
可以同步,在原生 DOM 事件与 setTimeout 中是同步的

可能会被合并
在合成事件与钩子函数中会对多次 setState 进行更新优化,只执行最后一次;在 setState 中传入对象,会被合并,传入函数则不会被合并;
在原生 DOM 事件与 setTimeout 内不会进行批量更新优化;

setState 更新机制、batchUpdate 机制和 transaction 事务机制

面试高频指数:⭐⭐⭐

setState 更新机制、batchUpdate 机制
setState 接收一个新的状态
该接收到的新状态不会被立即执行么,而是存入到 pendingStates(等待队列)中
判断isBatchingUpdates(是否是批量更新模式)

  1. isBatchingUpdates: true 将接收到的新状态保存到 dirtyComponents(脏组件)中
  2. isBatchingUpdates: false 遍历所有的 dirtyComponents, 并且调用其 updateComponent 方法更新pendingStates 中的 state 或者 props。执行完之后,会将 isBatchingUpdates: true。

setState 无所谓异步还是同步,主要看是否能命中 batchUpdate 机制,判断 isBatchingUpdates

image.png

transaction 事务机制
React 的更新是基于 Transaction(事务)的,Transacation 就是给目标执行的函数包裹一下,加上前置和后置的 hook (有点类似 koa 的 middleware),在开始执行之前先执行 initialize hook,结束之后再执行 close hook,这样搭配上 isBatchingUpdates 这样的布尔标志位就可以实现一整个函数调用栈内的多次 setState 全部入 pending 队列,结束后统一 apply 了。
但是 setTimeout 这样的方法执行是脱离了事务的,react 管控不到,所以就没法 batch 了。

受控组件和非受控组件的区别

面试高频指数:⭐⭐⭐⭐

受控组件是 React 控制的组件,并且是表单数据真实的唯一来源。
如受控 input 组件,它的值来自于属性 value,通过事件 onChange 更新 state 从而更新 value 值。

非受控组件是由 DOM 处理表单数据的地方,而不是在 React 组件中。
如非受控 input 组件,通过 defaultValue 设置默认值,通过 ref 获取 DOM 节点中的数据。

在某些需求,受控组件是无法实现的,如文件上传获取文件名,这个时候就需要使用非受控组件。

优先使用受控组件,符合 React 设计原则。必须操作 DOM 时,再使用非受控组件。

函数组件与 class 组件的区别

面试高频指数:⭐⭐

  1. 函数组件的代码量较少,相比类组件更加简洁。
  2. 函数组件是纯函数,输入 props,输出 JSX
  3. 函数组件中没有 this,没有实例,没有 state,也没有生命周期,不能扩展其他方法,这就决定了函数组件都是展示性组件,接收 props,渲染 DOM,而不关注其他逻辑。
  4. 因为函数组件不需要考虑组件状态和组件生命周期方法中的各种比较校验,所以有很大的性能提升空间。

何时使用异步组件

面试高频指数:⭐⭐

加载大组件,路由懒加载,依赖 lazy 和 Suspense 实现

import React, { Suspense } from 'react';

const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </div>
  );
}

Portal 传送门的使用场景

面试高频指数:⭐

使用 Portals 渲染到 body 上,fixed 元素要放在 body 上,有更好的浏览器兼容。
常见使用场景:

  1. 父组件 overflow: hidden , 但是子组件又想展示;
  2. 父组件的 z-index 太小;
  3. fixed 需要放在 body 第一层;
  4. 子组件需要渲染到父组件外
  5. 常见于全局组件上的应用,比如弹窗,消息提示等等组件。
import ReactDOM from 'react-dom'
render () {
    return ReactDOM.creatPortal(
        <div>{ this.props.children }</div>,
        document.body
    )
}

如何对组件公共逻辑抽离

面试高频指数:⭐⭐

  • mixin,已被 React 废弃
  • 高阶组件 HOC
  • Render Props

Render Props 和 HOC React Hooks 的区别

面试高频指数:⭐⭐⭐⭐

HOC:模式简单,但会增加组件层级
Render Props:代码简洁,学习成本高

为何要合成事件机制?

面试高频指数:⭐⭐

  • 更好的兼容性和跨平台
  • 挂载到 document,减少内存消耗,避免频繁解绑
  • 方便事件的统一管理(如事务机制)

react-router 如何配置懒加载

面试高频指数:⭐⭐

import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import React, { Suspense, lazy } from "react";

const Home = lazy(() => import("./routes/Home"));
const About = lazy(() => import("./routes/About"));

const App = () => {
  <Router>
    <Suspense fallback={<div>Loading...</div>}>
      <Switch>
        <Route exact path="/" component={Home}></Route>
        <Route path="/about" component={About}></Route>
      </Switch>
    </Suspense>
  </Router>;
};

React 事件和 DOM 事件的区别

面试高频指数:⭐⭐

  • 所有事件挂载到 document 的
  • event 不是原生的,是 syntheticEvent 合成事件

React 和 Vue 的区别

面试高频指数:⭐⭐⭐⭐⭐

相同点

  • 都支持组件化
  • 都是数据驱动视图
  • 都使用 vdom 操作 dom
  • 都支持服务器端渲染

区别

  • React 属于单向数据流 —— MVC 模式,Vue 则属于双向 —— MVVM 模式。
  • React 采用 JSX 语法,Vue 采用的则是 html 模板语法
  • React 兼容性比 Vue 好,Vue 不兼容 IE8
  • React 函数式编程,Vue 声明式编程
  • 一般来说,大型项目更倾向于 React,小型则用 Vue

Dva 和 Umi 是干什么用的?有哪些 api

dva 是 React 应用框架,将 React-Router + Redux + Redux-saga 三个 React 工具库包装在一起,简化了 API,让开发 React 应用更加方便和快捷。

api:
history: 指定给路由用的 history,默认是 hashHistory
app.use(hooks) 配置hooks注册插件
app.model(model) 注册model

Umi 是插件化的企业级前端应用框架

  • 插件化 umi 的整个生命周期都是插件化的,甚至其内部实现就是由大量插件组成,比如: pwa、按需加载、一键切换 preact、一键兼容 ie9 等等,都是由插件实现。
  • 可扩展,Umi 实现了完整的生命周期,并使其插件化,Umi 内部功能也全由插件完成。此外还支持插件和插件集,以满足功能和垂直域的分层需求。
  • 开箱即用,Umi 内置了路由、构建、部署、测试等,仅需一个依赖即可上手开发。并且还提供针对 React 的集成插件集,内涵丰富的功能,可满足日常 80% 的开发需求。
  • 约定式路由 类似 next.js 的约定式路由,无需再维护一份冗余的路由配置,支持权限、动态路由、嵌套路由等等。

@umijs/plugin-request 基于 umi-request 和 ahooks 的 useRequest 提供了一套统一的网络请求和错误处理方案。 @umijs/plugin-layout 将布局通过 umi 插件的方式内置,只需通过简单的配置即可拥有 Ant Design 的 Layout,包括导航以及侧边栏。 @umijs/plugin-antd 整合 antd 组件库。 @umijs/plugin-dva 整合 dva 数据流。

Dva 和 redux 的区别

dva 封装了 redux,减少很多重复代码比如 action reducers 常量等
与 redux 数据流向类似,比 redux 更为简洁,省去定义常量和 action, dva 支持异步,redux 如果想要支持异步得弄中间件,redux-saga 或者 chunk 这些。

dva 是为了方便处理数据流而封装的框架,里面不仅仅有 redux 的功能,集成的还要 redux 的中间件 redux-saga,还集成的有路由的功能。而 redux 只是一个数据管理的库,如果要做项目还需要搭载其他的库。

Dva 的应用场景

像用户收货地址这种数据用 dva 很方便,而且不同页面根据信息不同展示的也不同,收货地址一改变,整个应用所有相关页面、相关价格、相关运费全跟着改变

对 redux 的理解

redux 是为了解决 react 组件间通信和组件间状态共享而提出的一种解决方案,主要包括3个部分,(store + action + reducer)

store:用来存储当前 react 状态机(state)的对象。connect 后,store 的改变就会驱动 react 的生命周期循环,从而驱动页面状态的改变

action: 用于接受 state 的改变命令,是改变 state 的唯一途径和入口。一般使用时在当前组件里面调用相关的action 方法,通常把和后端的通信(ajax)函数放在这里

reducer: action 的处理器,用于修改 store 中 state 的值,返回一个新的 state 值

主要解决什么问题:
1、组件间通信

由于 connect 后,各 connect 组件是共享 store 的,所以各组件可以通过 store 来进行数据通信,当然这里必须遵守 redux 的一些规范,比如遵守 view -> aciton -> reducer的改变 state 的路径

2、通过对象驱动组件进入生命周期

对于一个 react 组件来说,只能对自己的 state 改变驱动自己的生命周期,或者通过外部传入的 props 进行驱动。通过 redux,可以通过 store 中改变的 state,来驱动组件进行 update

3、方便进行数据管理和切片

redux 通过对 store 的管理和控制,可以很方便的实现页面状态的管理和切片。通过切片的操作,可以轻松的实现redo 之类的操作

项目设计

基于 React 设计一个 todolist(组件结构,redux state 数据结构)

基于 Vue 设计一个购物车(组件结构,vuex state 数据结构)

Webpack

前端代码为何要进行构建和打包

  • 体积更小(Tree-Shaking、压缩、合并),加载更快
  • 编译高级语言和语法(TS,ES6+,模块化,scss)
  • 兼容性和错误检查(Ployfill、postcss、eslint)
  • 统一、高效的开发环境
  • 统一的构建流程和产出标准
  • 集成公司构建规范(提测、上线等)

谈谈你对 webpack 的认识

webpack 是一个模块打包工具,可以使用 webpack 管理模块依赖,并编译输出模块所需的静态文件。它能够很好地管理与打包 web 开发中所用到的 HTML、JavaScript、css,以及各种静态文件(图片、字体等),让开发更加高效。对于不同类型的资源,webpack 有对应的模块加载器。webpack 模块打包器会分析模块间的依赖关系,最后生成优化且合并后的静态资源

什么是 webpack

webpack 是一个打包工具,webpack 可以将项目中使用的脚本开发语言 typescript、样式开发语言 less 或者 sass 编译成浏览器可以识别的 JavaScript 和 css 文件

在使用 webpack 时,你都做些什么

用来压缩合并 CSS 和 JavaScript 代码,压缩图片,对小图生成 base64 编码,对大图进行压缩,使用 babel 把 ES6+ 编译成 ES5,热重载,局部刷新等。在 output 配置出口文件,在 entry 配置入口文件。使用各种 loder 对各种资源做处理,并解析成浏览器可运行的代码。

说说 webpack 打包的流程

  1. 通过 entry 配置入口文件
  2. 通过 output 指定输出的文件
  3. 使用各种 loader 处理 css、JavaScript、image 等资源,并将它们编译与打包成浏览器可以解析的内容等。

webpack 的核心原理是什么

  1. 一切皆模块
    正如 JavaScript 文件可以是一个 “模块” 一样,其他的 (如 css、image 或 HTML)文件也可以视为模块。这意味着我们可以将事务分割成更小的、易于管理的片段,从而达到重复利用的目的
  2. 按需加载 传统的模块打包工具(module bundler)最终将所有的模块编译并生成一个庞大的 bundle.js 文件。但是在真实的 app 中,bundle.js 文件的大小在 10MB 到 15 MB 之间,这可能导致应用一直处于加载状态。因此,webpack 使用许多特性来分割代码,然后生成多个 bundle js 文件,而且异步加载代码用于实现按需加载

dev server 热更新的实现原理

  • 使用express启动本地服务,当浏览器访问资源时对此做响应。

  • 服务端和客户端使用websocket实现长连接

  • webpack监听源文件的变化,即当开发者保存文件时触发webpack的重新编译。

    • 每次编译都会生成hash值已改动模块的json文件已改动模块代码的js文件
    • 编译完成后通过socket向客户端推送当前编译的hash戳
  • 客户端的websocket监听到有文件改动推送过来的hash戳,会和上一次对比

    • 一致则走缓存
    • 不一致则通过ajaxjsonp向服务端获取最新资源
  • 使用内存文件系统去替换有修改的内容实现局部刷新

webpack 基本配置

  • 拆分配置和 merge
  • 启动本地服务
  • 处理 ES6

module chunk bundle 分别是什么意思,有何区别

  • module - 各个源码文件,webpack 中一切皆模块
  • chunk - 多模块合并成的,如 entry inport() splitChunk
  • bundle - 最终的输出文件

ES module 和 Commonjs 区别是什么

CommonJS

  1. 对于基本数据类型,属于复制。即会被模块缓存。同时,在另一个模块可以对该模块输出的变量重新赋值。
  2. 对于复杂数据类型,属于浅拷贝。由于两个模块引用的对象指向同一个内存空间,因此对该模块的值做修改时会影响另一个模块。
  3. 当使用require命令加载某个模块时,就会运行整个模块的代码。
  4. 当使用require命令加载同一个模块时,不会再执行该模块,而是取到缓存之中的值。也就是说,CommonJS模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
  5. 循环加载时,属于加载时执行。即脚本代码在require的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。

ES6模块

  1. ES6模块中的值属于【动态只读引用】。
  2. 对于只读来说,即不允许修改引入变量的值,import的变量是只读的,不论是基本数据类型还是复杂数据类型。当模块遇到import命令时,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
  3. 对于动态来说,原始值发生变化,import加载的值也会发生变化。不论是基本数据类型还是复杂数据类型。
  4. 循环加载时,ES6模块是动态引用。只要两个模块之间存在某个引用,代码就能够执行。

loader 和 plugin 的区别

  • loader 模块转换器,如 less -> css
  • plugin 扩展插件,如 HtmlWebpackPlugin 它们是两个完全不同的东西。loader 负责处理源文件,如 css、jsx 文件,一次处理一个文件。而 plugins 并不直接操作单个文件,它直接对整个构建过程起作用。

当然loader也是变相的扩展了webpack ,但是它只专注于转化文件这一个领域。而plugin的功能更加的丰富,而不仅局限于资源的加载。在 webpack 的构建流程中,plugin 用于处理更多其他的一些构建任务。可以这么理解,模块代码转换的工作由 loader 来处理,除此之外的其他任何工作都可以交由 plugin 来完成。

常见的 loader 和 plugin 有哪些

loader

  • babel-loader:将下一代的 JavaScript 语法规范转换成现代浏览器能够支持的语法规范。
  • postcss-loader:实现浏览器兼容
  • css-loader、style-loader:这两个建议配合使用,用来解析 CSS 文件依赖
  • lessloader:解析 less 文件
  • file-loader:生成的文件名就是文件内容的 MD5 散列值,并会保留所引用资源的原始扩展名
  • url-loader:功能类似于 file-loader,但当文件内容大小低于指定的限制时,可以返回一个 DataURL。

plugin

  • HtmlWebpackPlugin:依据一个 HTML 模板,生成 HTML 文件,并将打包后的资源文件自动引入
  • commonsChunkPlugin:抽取公共模块,减少包占用的内存空间,例如 vue 的源码、jQuery 的源码等。
  • extract-text-webpack-plugin:将样式抽取成单独的文件
  • HotModuleReplacementPlugin:开启全局的模块热替换(HMR)
  • UglifyJsPlugin:压缩 js 代码

babel 和 webpack 的区别

  • babel - JS 新语法编译工具,不关心模块化
  • webpack - 打包构建工具,是多个 loader plugin 的集合

webpack 如何实现懒加载

  • import()
  • 结合 Vue React 异步组件
  • 结合 Vue-router React-router 异步加载路由

为何 proxy 不能被 Polyfill

如 Class 可以用 function 模拟
如 Promise 可以用 callback 来模拟
但 Proxy 的功能用 Object.definePrototype 无法模拟

webpack 常见性能优化

优化构建速度
可用于生产环境

  • 优化 babel-loader
  • IgnorePlugin
  • noParse
  • happyPack
  • ParallelUglifyPlugin

不可用于生产环境

  • 自动刷新
  • 热更新
  • DllPlugin

优化产出代码

  • 小图片 base64 编码
  • bundle 加 hash
  • 懒加载
  • 提取公共代码
  • 使用 CDN 加速
  • IgnorePlugin
  • 使用 production
  • Scope Hosting

babel

babel-runtime 和 babel-polyfill 的区别

  • babel-ployfill 会污染全局
  • babel-runtime 会污染全局
  • 产出第三方 lib 要用 babel-runtime

Typescript

interface 和 type 有什么区别

相同点

都可以描述一个对象或者函数
interface

interface User {
  name: string
  age: number
}

interface SetUser {
  (name: string, age: number): void;
}

type

type User = {
  name: string
  age: number
};

type SetUser = (name: string, age: number): void;

都允许拓展
interface 可以 extends, 但 type 是不允许 extends 和 implement 的,但是 type 缺可以通过交叉类型 实现 interface 的 extend 行为,并且两者并不是相互独立的,也就是说 interface 可以 extends type, type 也可以 与 interface 类型 交叉 。

虽然效果差不多,但是两者语法不同

interface extends interface

interface Name { 
  name: string; 
}
interface User extends Name { 
  age: number; 
}

type 与 type 交叉

type Name = { 
  name: string; 
}
type User = Name & { age: number  };

interface extends type

type Name = { 
  name: string; 
}
interface User extends Name { 
  age: number; 
}

type 与 interface 交叉

interface Name { 
  name: string; 
}
type User = Name & { 
  age: number; 
}

不同点

type 可以而 interface 不行
type 可以声明基本类型别名,联合类型,元组等类型

// 基本类型别名
type Name = string

// 联合类型
interface Dog {
    wong();
}
interface Cat {
    miao();
}

type Pet = Dog | Cat

// 具体定义数组每个位置的类型
type PetList = [Dog, Pet]

type 语句中还可以使用 typeof 获取实例的 类型进行赋值

// 当你想获取一个变量的类型时,使用 typeof
let div = document.createElement('div');
type B = typeof div

其他骚操作

type StringOrNumber = string | number;  
type Text = string | { text: string };  
type NameLookup = Dictionary<string, Person>;  
type Callback<T> = (data: T) => void;  
type Pair<T> = [T, T];  
type Coordinates = Pair<number>;  
type Tree<T> = T | { left: Tree<T>, right: Tree<T> };

interface 可以而 type 不行

interface 能够声明合并

interface User {
  name: string
  age: number
}

interface User {
  sex: string
}

/*
User 接口为 {
  name: string
  age: number
  sex: string 
}
*/

interface是接口,type是类型,本身就是两个概念。只是碰巧表现上比较相似。
希望定义一个变量类型,就用type,如果希望是能够继承并约束的,就用interface。
如果你不知道该用哪个,说明你只是想定义一个类型而非接口,所以应该用type。

官方解释:能用 interface 的地方就用 interface,否则用 type
原因是因为更贴合 JavaScript 对象的工作方式,再清晰一些,如果我们是定义一个 object,那么最好是使用 interface 去做类型声明,什么时候用 type 呢,当定义一个 function 的时候,用 type 会更好一些

泛型是什么

泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型

class Person<T>{} // 一个尖括号跟在类名后面
function Person<T>(arg: T): T {return arg;}  // 一个尖括号跟在函数名后面
interface Person<T> {}  // 一个尖括号跟在接口名后面

例如我们需要实现一个函数 createArray,它可以创建一个指定长度的数组,同时将每一项都填充一个默认值。 我们预期的是,数组中每一项都应该是输入的 value 的类型,这时候,泛型就派上用场了。

function createArray<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray<string>(3, 'x'); // ['x', 'x', 'x']

泛型约束是什么

在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法:

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);
    return arg;
}

// index.ts(2,19): error TS2339: Property 'length' does not exist on type 'T'.

上例中,泛型 T 不一定包含属性 length,所以编译的时候报错了。

这时,我们可以对泛型进行约束,只允许这个函数传入那些包含 length 属性的变量。这就是泛型约束:

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);
    return arg;
}

抽象类和抽象方法是什么

抽象类作为其他派生类的基类使用,它们一般不会直接被实例化,不同于接口,抽象类可以包含成员的实现细节。abstract关键字是用于定义抽象类和在抽象类内部定义抽象方法。

abstract class Animal {
  abstract makeSound(): void; // 抽象方法必须在派生类中实现
  move(): void {
    console.log('roaming the earch ...');
  }
}

抽象类中的抽象方法不包含具体实现并且必须在派生类中实现。和接口方法相似,两者都定义签名但不包含方法体,然而,抽象方法必须包含 abstract 关键字并且可以包含访问修饰符

abstract class Department {
  // 初始化name成员,参数属性
  constructor(public name: string){
  }
  
  printName(): void{
    console.log('Department name: ' + this.name);
  }
  
  abstract printMeeting(): void; // 必须在派生类中实现
}

class AccountingDepartment extends Department {
  constructor() {
    super('Accounting and Auditing'); // 在派生类的构造函数中必须调用super()
  }
  
  printMeeting(): void{
    console.log('The Accounting Department meets each Monday at 10am.');
  }
  
  generateReports(): void{
    console.log('Generating accounting reports...')
  }
}

let department: Department; // 允许创建一个对抽象类型的引用
department = new Department(); // 错误:不能创建一个抽象类的实例
department = new AccountingDepartment(); // 允许对一个抽象子类进行实例化和赋值
department.printName();
department.printMeeting();
department.generateReports(); // 错误:方法在声明的抽象类种不存在

小程序

小程序的实现原理

小程序的渲染层和逻辑层分别由两个线程管理:渲染层的界面使用 WebView 进行渲染;逻辑层采用 JSCore 运行 JavaScript 代码。一个小程序存在多个界面,所以渲染层存在多个 WebView。这两个线程间的通信经由小程序 Native 侧中转,逻辑层发送网络请求也经由 Native 侧转发

CI/CD

基本 CI/CD 工作流程

一旦你将提交推送到远程仓库的分支上,那么你为该项目设置的CI/CD管道将会被触发。GitLab CI/CD 通过这样做:

  • 运行自动化脚本(串行或并行) 代码Review并获得批准
  • 构建并测试你的应用
  • 就像在你本机中看到的那样,使用Review Apps预览每个合并请求的更改
  • 代码Review并获得批准
  • 合并feature分支到默认分支,同时自动将此次更改部署到生产环境
  • 如果出现问题,可以轻松回滚
  • 通过GitLab UI所有的步骤都是可视化的

image.png

其他面试题

实习中收获了什么

首先学到了很多技术和业务知识,开拓了眼界,也明确了我今后的职业发展方向,学会了团队沟通和协作,也收获了很多人脉,当然也收获了人生中一段难忘的回忆

为什么想要找实习

因为我当时的经验可能不足,我想通过面试和企业的工作磨练自己,帮助我积累更多的经验和技术,也帮助我明确自己的目标

实习中遇到过什么问题怎么解决的

字节

  • 千川图标统一管理方案
    背景:我们团队的图标没有一个统一的管理,大家各自为战,导致千川项目中 SVG 个数膨胀到五百多个,很多有可能是重复的,引用方式也千奇百怪,有的是直接引用的,有的是封组件引的,有的是 cdn 调的。
    问题:
    1.没有一个比较系统的,开发体验友好的,统一的 SVG 封装方式
    2.每次新增图标均需要手动导入
    3.遇到页面中需要展示多个图标的场景,多个请求会增加服务端负载,拖慢页面加载速度。
    4.缺乏灵活性,比如图标 hover 时高亮。
    5.可控性差,SVG 本身可能存在默认样式,由于优先级问题可能存在修改样式不生效。
    解决:
    1.自动导入:利用 require.context 原理以及 webpack esMoudle 实现;新增图标无需手动操作即可自动导入,更加灵活,提高可扩展性。
    2.利用 SVG 的 symbol 元素,将每个 svg 文件内容中的 path 内嵌在一个个 symbol 中,通过 id 进行标识(symbolId),最终合成一个 svg 嵌入到 html 中,这样的好处是不会向服务端发送请求,并且可以在项目任何地方使用。 示例 <svg><use :xlink:href="symbolId" /></svg>
    3.将 Svg Icon 封装为组件,提高可复用性,可维护性。
    4.通过 svgo 插件对 svg 文件进行优化压缩处理,去除不必要的冗余、干扰项。

知乎

相关面试技巧

可以不太深入,但必须知道
熟悉基本用法,了解使用场景
最好能和自己的项目经验结合起来