手写实现
Promise.all
let PromiseAll = function (promiseArr) {
let resultArr = [];
let promiseArrLength = promiseArr.length;
let resolveCount = 0;
return new Promise((resolve, reject) => {
promiseArr.forEach((item, index) => {
item.then(res => {
resolveCount++;
resultArr[index] = res;
if (resolveCount === promiseArrLength) {
resolve(resultArr)
}
}).catch(err => {
reject(err)
});
})
})
}
const p1 = new Promise(res => res(1))
const p2 = new Promise(res => res(1))
const p3 = new Promise(res => res(1))
const su = PromiseAll([p1, p2, p3])
su.then(res => {
console.log(res)
}).catch(res => {
console.log(res)
})
限制请求并发数量
let limitTask2 = function (arrParams, maxTask, cb) {
let resultArr = [];
let arrLength = arrParams.length;
if (!arrLength) return Promise.resolve();
let resolveCount = 0;
let fistTaskList = arrParams.splice(0, maxTask);
return new Promise((resolve, reject) => {
requestAction(fistTaskList)
function requestAction(requestItem){
if(requestItem){
requestItem.forEach((item, index) => {
cb(item).then(res => {
successFun(res, index)
}).catch(err => {
reject(err)
});
})
}else{
let nextTast = arrParams.shift();
if(!nextTast){
return;
}
let otherLength = arrParams.length;
cb(nextTast).then(res => {
successFun(res, arrLength - otherLength - 1);
}).catch(err => {
reject(err)
})
}
}
function successFun(res, index) {
resolveCount++;
resultArr[index] = res;
if (resolveCount === arrLength) {
resolve(resultArr);
return;
}
requestAction();
}
})
}
const cb = (timeout) => new Promise((resolve, reject) => {
setTimeout(() => {
resolve(timeout);
}, timeout)
})
console.time();
const su = limitTask2([1000, 2000, 3000], 2, cb)
su.then(res => {
console.log(res)
console.timeEnd();
}).catch(res => {
console.log(res)
})
防抖
function debounce(func, wait) {
let timeout;
return function() {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(this, arguments);
}, wait);
};
}
// 使用
window.onscroll = debounce(function() {
console.log('debounce');
}, 1000);
节流
function throttle(fn, delay) {
var prevTime = Date.now();
return function() {
var curTime = Date.now();
if (curTime - prevTime > delay) {
fn.apply(this, arguments);
prevTime = curTime;
}
};
}
// 使用
var throtteScroll = throttle(function() {
console.log('throtte');
}, 1000);
window.onscroll = throtteScroll;
Bind Call Apply
Function.prototype.myBind = function(context, ...args){
if(typeof context!== "function"){
throw "err"
}
const fn = this;
return function(){
return fn.call(context, ...args, ...arguments)
}
}
Function.prototype.myBind = function (context) {
if(typeof this !== 'function'){
return new Error("not function");
}
const _this = this;
let args = [...arguments].slice(1);
return function F(){
args = args.concat(...arguments);
if(this instanceof F){
return new _this(...args);
}else{
_this.apply(context, args);
}
}
}
Function.prototype.myCall = function(context, ...args){
context._fn = this;
const result = context.fn(...args);
delete context._fn;
return result;
}
Function.prototype.myApply = function(context, args){
context._fn = this;
const result = context.fn(...args);
delete context._fn;
return result;
}
深克隆
function deepClone(data) {
function judgeType(obj) {
return Object.prototype.toString.call(obj).slice(8,-1);
}
const type = judgeType(data);
let obj;
if (type === 'Array') {
obj = [];
for (let i = 0, len = data.length; i < len; i++ ) {
obj.push(deepClone(data[i]));
}
} else if (type === 'Object') {
obj = new data.constructor ();//可保持继承链,解决该问题:如果obj中的对象是由构造函数生成的,则使用深拷贝后,会丢弃对象的constructor;
for (let key in data) {
obj[key] = deepClone(data[key]);//实现深克隆的关键
}
} else {
return data;
}
return obj;
}
instanceOf
// L 表示左表达式,R 表示右表达式
function instance_of(L, R) {
var O = R.prototype;
L = L.__proto__;
while (true) {
if (L === null) return false;
// 这里重点:当 O 严格等于 L 时,返回 true
if (O === L) return true;
L = L.__proto__;
}
}
New
function myNew() {
// 创建一个实例对象
var obj = new Object();
// 取得外部传入的构造器
var Constructor = Array.prototype.shift.call(arguments);
// 实现继承,实例可以访问构造器的属性
obj.__proto__ = Constructor.prototype;
// 调用构造器,并改变其 this 指向到实例
var ret = Constructor.apply(obj, arguments);
// 如果构造函数返回值是对象则返回这个对象,如果不是对象则返回新的实例对象
return typeof ret === 'object' && ret !== null ? ret : obj;
}
Object.create
Object.create = Object.create || function(obj){
var F = function(){};
F.prototype = obj;
return new F();
}
柯里化
function createCurry(func, args) {
var argity = func.length;
var args = args || [];
return function () {
var _args = [].slice.apply(arguments);
args.push(..._args);
if (args.length < argity) {
return createCurry.call(this, func, args);
}
return func.apply(this, args);
}
}
手写Promise
class MyPromise {
constructor(fn) {
this.resolvedCallbacks = [];
this.rejectedCallbacks = [];
this.state = 'PENDING';
this.value = '';
fn(this.resolve.bind(this), this.reject.bind(this));
}
resolve(value) {
if (this.state === 'PENDING') {
this.state = 'RESOLVED';
this.value = value;
this.resolvedCallbacks.map(cb => cb(value));
}
}
reject(value) {
if (this.state === 'PENDING') {
this.state = 'REJECTED';
this.value = value;
this.rejectedCallbacks.map(cb => cb(value));
}
}
then(onFulfilled, onRejected) {
if (this.state === 'PENDING') {
this.resolvedCallbacks.push(onFulfilled);
this.rejectedCallbacks.push(onRejected);
}
if (this.state === 'RESOLVED') {
onFulfilled(this.value);
}
if (this.state === 'REJECTED') {
onRejected(this.value);
}
}
}
手写jsonp
function jsonp ({url, query}) {
let script = document.createElement("script");
let cb = `jsonpCallBack${new Date().getTime()}${Math.floor(Math.random(5)*100000)}`
let params = {...query, cb}
let arr = []
for (let key in params) {
arr.push(`${key}=${params[key]}`)
}
script.src = `${url}?${arr.join("&")}`
document.body.appendChild(script)
return new Promise((resolve, rej) => {
window[cb] = function (res) {
resolve(res)
document.body.removeChild(script)
delete window[cb]
}
})
}
jsonp({
url:'/getList',
query: {name: 'ys',age: 19}
}).then((res) => {
console.log(res)
})
柯里化
const sum = (a, b, c, d) => a + b + c + d;
function curry2(fn){
return function judge(...args){
console.warn(args,fn);
if(args.length >= fn.length){
return fn(...args)
}else{
return function (...arg){
return judge(...args, ...arg)
}
}
}
}
const currySum = curry2(sum);
console.warn(currySum(1)(2)(3)(4));
数据结构和算法
常用数据结构
- 数组 适合查找 不适合插入
- 链表 适合插入 不适合查找 需要从头找任何元素
- 集和 就是Set 由一组无序且唯一的项组成
- 字典 就是对象 以键和值的方式存储
- 树
- 根节点 内部节点 外部节点(叶子节点)
- 二叉树 每个节点最多有两个子节点
- 二叉搜索树 BST 只允许左侧节点比父小 右侧节点比父大或等于父节点
- 二叉搜索树
- 中序遍历 从最小向最大遍历
- 先序遍历
- 后序遍历
- 图
排序
- 冒泡
- 快排序
- 归并排序
- 分而治之的思想 都拆分成最后为一个的数组
- 拆分成左右两个数组 同时两个数组需要按照顺序排列 并且按照顺序返回
- 选择排序 找出最高放第一个 再剩余中找出最高放第二个
- 插入排序 分为已排好 和未排好 每次从未排好拿出一个放在已经排好里
冒泡
// 先实现一个交换数据的方案
function swap(myArray, p1, p2){
var temp = myArray[p1];
myArray[p1] = myArray[p2];
myArray[p2] = temp;
}
function bubbleSort(myArray){
var len = myArray.length;
var i;
var j;
var stop;
for (i = 0; i < len - 1; i++){
for (j = 0, stop = len - 1 - i; j < stop; j++){
if (myArray[j] > myArray[j + 1]){
swap(myArray, j, j + 1);
}
}
}
return myArray;
}
快排
它的基本思想很简单:先确定一个“支点”(pivot),将所有小于“支点”的值都放在该点的左侧,大于“支点”的值都放在该点的右侧,然后对左右两侧不断重复这个过程,直到所有排序完成。
var quickSort = function(arr) {
if (arr.length <= 1) { return arr; }
var pivotIndex = Math.floor(arr.length / 2);
var pivot = arr.splice(pivotIndex, 1)[0];
var left = [];
var right = [];
for (var i = 0; i < arr.length; i++){
if (arr[i] < pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return quickSort(left).concat([pivot], quickSort(right));
};
选择排序
每次找到最小的放入空数组
选择排序(Selection Sort)与冒泡排序类似,也是依次对相邻的数进行两两比较。不同之处在于,它不是每比较一次就调换位置,而是一轮比较完毕,找到最大值(或最小值)之后,将其放在正确的位置,其他数的位置不变。
function selectionSort(myArray){
var len = myArray.length,
min;
for (i=0; i < len; i++){
// 将当前位置设为最小值
min = i;
// 检查数组其余部分是否更小
for (j=i+1; j < len; j++){
if (myArray[j] < myArray[min]){
min = j;
}
}
// 如果当前位置不是最小值,将其换为最小值
if (i != min){
swap(myArray, i, min);
}
}
return myArray;
}
插入排序
一次排序一个
插入排序(insertion sort)比前面两种排序方法都更有效率。它将数组分成“已排序”和“未排序”两部分,一开始的时候,“已排序”的部分只有一个元素,然后将它后面一个元素从“未排序”部分插入“已排序”部分,从而“已排序”部分增加一个元素,“未排序”部分减少一个元素。以此类推,完成全部排序。
function insertionSort(myArray) {
var len = myArray.length, // 数组的长度
value, // 当前比较的值
i, // 未排序部分的当前位置
j; // 已排序部分的当前位置
for (i=0; i < len; i++) {
// 储存当前位置的值
value = myArray[i];
/*
* 当已排序部分的当前元素大于value,
* 就将当前元素向后移一位,再将前一位与value比较
*/
for (j=i-1; j > -1 && myArray[j] > value; j--) {
myArray[j+1] = myArray[j];
}
myArray[j+1] = value;
}
return myArray;
}
合并排序
它的基本思想是,将两个已经排序的数组合并,要比从头开始排序所有元素来得快。因此,可以将数组拆开,分成n个只有一个元素的数组,然后不断地两两合并,直到全部排序完成。
function merge(left, right){
var result = [],
il = 0,
ir = 0;
while (il < left.length && ir < right.length){
if (left[il] < right[ir]){
result.push(left[il++]);
} else {
result.push(right[ir++]);
}
}
return result.concat(left.slice(il)).concat(right.slice(ir));
}
function mergeSort(myArray){
if (myArray.length < 2) {
return myArray;
}
var middle = Math.floor(myArray.length / 2),
left = myArray.slice(0, middle),
right = myArray.slice(middle);
return merge(mergeSort(left), mergeSort(right));
}
最大连续子数组
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
var maxSubArray = function(array) {
if (array.length == 0)
return 0
var sum = array[0] //保存每组的和
var maxSum = array[0] //连续子数组最大和
for (var i = 1; i < array.length; i++) {
sum = Math.max(sum + array[i], array[i]);
maxSum = Math.max(sum, maxSum)
}
return maxSum
};
手写Promise
// 判断变量否为function
const isFunction = variable => typeof variable === 'function'
// 定义Promise的三种状态常量
const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'
class MyPromise {
constructor(handle) {
if (!isFunction(handle)) {
throw new Error('MyPromise must accept a function as a parameter')
}
// 添加状态
this._status = PENDING
// 添加状态
this._value = undefined
// 添加成功回调函数队列
this._fulfilledQueues = []
// 添加失败回调函数队列
this._rejectedQueues = []
// 执行handle
try {
handle(this._resolve.bind(this), this._reject.bind(this))
} catch (err) {
this._reject(err)
}
}
// 添加resovle时执行的函数
_resolve(val) {
const run = () => {
if (this._status !== PENDING) return
this._status = FULFILLED
// 依次执行成功队列中的函数,并清空队列
const runFulfilled = (value) => {
let cb;
while (cb = this._fulfilledQueues.shift()) {
cb(value)
}
}
// 依次执行失败队列中的函数,并清空队列
const runRejected = (error) => {
let cb;
while (cb = this._rejectedQueues.shift()) {
cb(error)
}
}
/* 如果resolve的参数为Promise对象,则必须等待该Promise对象状态改变后,
当前Promsie的状态才会改变,且状态取决于参数Promsie对象的状态
*/
if (val instanceof MyPromise) {
val.then(value => {
this._value = value
runFulfilled(value)
}, err => {
this._value = err
runRejected(err)
})
} else {
this._value = val
runFulfilled(val)
}
}
// 为了支持同步的Promise,这里采用异步调用
setTimeout(run, 0)
}
// 添加reject时执行的函数
_reject(err) {
if (this._status !== PENDING) return
// 依次执行失败队列中的函数,并清空队列
const run = () => {
this._status = REJECTED
this._value = err
let cb;
while (cb = this._rejectedQueues.shift()) {
cb(err)
}
}
// 为了支持同步的Promise,这里采用异步调用
setTimeout(run, 0)
}
// 添加then方法
then(onFulfilled, onRejected) {
const {_value, _status} = this
// 返回一个新的Promise对象
return new MyPromise((onFulfilledNext, onRejectedNext) => {
// 封装一个成功时执行的函数
let fulfilled = value => {
try {
if (!isFunction(onFulfilled)) {
onFulfilledNext(value)
} else {
let res = onFulfilled(value);
if (res instanceof MyPromise) {
// 如果当前回调函数返回MyPromise对象,必须等待其状态改变后在执行下一个回调
res.then(onFulfilledNext, onRejectedNext)
} else {
//否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
onFulfilledNext(res)
}
}
} catch (err) {
// 如果函数执行出错,新的Promise对象的状态为失败
onRejectedNext(err)
}
}
// 封装一个失败时执行的函数
let rejected = error => {
try {
if (!isFunction(onRejected)) {
onRejectedNext(error)
} else {
let res = onRejected(error);
if (res instanceof MyPromise) {
// 如果当前回调函数返回MyPromise对象,必须等待其状态改变后在执行下一个回调
res.then(onFulfilledNext, onRejectedNext)
} else {
//否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
onFulfilledNext(res)
}
}
} catch (err) {
// 如果函数执行出错,新的Promise对象的状态为失败
onRejectedNext(err)
}
}
switch (_status) {
// 当状态为pending时,将then方法回调函数加入执行队列等待执行
case PENDING:
this._fulfilledQueues.push(fulfilled)
this._rejectedQueues.push(rejected)
break
// 当状态已经改变时,立即执行对应的回调函数
case FULFILLED:
fulfilled(_value)
break
case REJECTED:
rejected(_value)
break
}
})
}
// 添加catch方法
catch(onRejected) {
return this.then(undefined, onRejected)
}
// 添加静态resolve方法
static resolve(value) {
// 如果参数是MyPromise实例,直接返回这个实例
if (value instanceof MyPromise) return value
return new MyPromise(resolve => resolve(value))
}
// 添加静态reject方法
static reject(value) {
return new MyPromise((resolve, reject) => reject(value))
}
// 添加静态all方法
static all(list) {
return new MyPromise((resolve, reject) => {
/**
* 返回值的集合
*/
let values = []
let count = 0
for (let [i, p] of list.entries()) {
// 数组参数如果不是MyPromise实例,先调用MyPromise.resolve
this.resolve(p).then(res => {
values[i] = res
count++
// 所有状态都变成fulfilled时返回的MyPromise状态就变成fulfilled
if (count === list.length) resolve(values)
}, err => {
// 有一个被rejected时返回的MyPromise状态就变成rejected
reject(err)
})
}
})
}
// 添加静态race方法
static race(list) {
return new MyPromise((resolve, reject) => {
for (let p of list) {
// 只要有一个实例率先改变状态,新的MyPromise的状态就跟着改变
this.resolve(p).then(res => {
resolve(res)
}, err => {
reject(err)
})
}
})
}
finally(cb) {
return this.then(
value => MyPromise.resolve(cb()).then(() => value),
reason => MyPromise.resolve(cb()).then(() => {
throw reason
})
);
}
}
Vue
Vue和React和Angular有什么区别
Vue的设计之初借鉴了Angular的模板语法和数据绑定,同时又参考了React的组件化思想和虚拟Dom技术。
MVVM
Modal-View-ViewModal,Modal标识模型层,View标识试图层,ViewModal是View和Modal的桥梁,数据更新到viewModal层并自动渲染到页面中,试图的变化又会更新数据。
Vue如何实现响应式
Vue2: Object.defineProperty 重新定义 data 中所有的属性, Object.defineProperty 可以使数据的获取与设置增加一个拦截的功能,拦截属性的获取,进行依赖收集。拦截属性的更新操作,进行通知。 具体的过程:首先Vue使用 initData 初始化用户传入的参数,然后使用 new Observer 对数据进行观测,如果数据是一个对象类型就会调用 this.walk(value) 对对象进行处理,内部使用 defineeReactive 循环对象属性定义响应式变化,核心就是使用 Object.defineProperty 重新定义数据。
Vue中如何检测数组的变化
- 是用来函数劫持的方式,重写了数组方法,具体呢就是更改了数组的原型,更改成自己的,用户调数组的一些方法的时候,走的就是自己的方法,然后通知视图去更新。
- 数组里每一项可能是对象,那么我就是会对数组的每一项进行观测,(且只有数组里的对象才能进行观测,观测过的也不会进行观测
- vue3:改用 proxy ,可直接监听对象数组的变化。
事件绑定原理
原生 DOM 的绑定:Vue在创建真实DOM时会调用 createElment ,默认会调用 invokeCreateHooks 。会遍历当前平台下相对的属性处理代码,其中就有 updateDOMListeners 方法,内部会传入 add() 方法
组件绑定事件,原生事件,自定义事件;组件绑定之间是通过Vue中自定义的 $on 方法实现的。
如何自定义v-modal
v-model 可以看成是 value+input 方法的语法糖(组件)。原生的 v-model ,会根据标签的不同生成不同的事件与属性。解析一个指令来。
自定义:自己写 model 属性,里面放上 prop 和 event
为什么采用异步渲染
Vue 是组件级更新,如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染,所以为了性能, Vue 会在本轮数据更新后,在异步更新视图。核心思想 nextTick 。
dep.notify() 通知 watcher进行更新, subs[i].update 依次调用 watcher 的 update , queueWatcher 将watcher 去重放入队列, nextTick( flushSchedulerQueue )在下一tick中刷新watcher队列(异步)
nextTick
异步方法,异步渲染最后一步,与JS事件循环联系紧密。主要使用了宏任务微任务(setTimeout、promise那些),定义了一个异步方法,多次调用nextTick会将方法存入队列,通过异步方法清空当前队列。
父子组件生命周期调用顺序
类似于洋葱模型
Vue组件通信
- 父子间通信:父亲提供数据通过属性 props传给儿子;儿子通过 emit 触发自己的事件(发布订阅)
- 利用父子关系 children
- ref 获取组件实例,调用组件的属性、方法
- 跨组件通信 Event Bus
- vuex 状态管理实现通信
Vuex原理
如何从真实DOM到虚拟DOM
- 将模板转换成 ast 树, ast 用对象来描述真实的JS语法(将真实DOM转换成虚拟DOM)
- 优化树
- 将 ast 树生成代码
用Vnode描述一个DOM
虚拟节点就是用一个对象来描述一个真实的DOM元素。首先将 template (真实DOM)先转成 ast , ast 树通过 codegen 生成 render 函数, render 函数里的 _c 方法将它转为虚拟dom
diff算法
- 最小量更新, key 很重要。这个可以是这个节点的唯一标识,告诉 diff 算法,在更改前后它们是同一个DOM节点
- 只有是同一个虚拟节点才会进行精细化比较,否则就是暴力删除旧的,插入新的。
- 只进行同层比较,不会进行跨层比较。
data为什么是一个函数
避免组件中的数据互相影响。同一个组件被复用多次会创建多个实例,如果 data 是一个对象的话,这些实例用的是同一个构造函数。为了保证组件的数据独立,要求每个组件都必须通过 data 函数返回一个对象作为组件的状态。
为什么要使用异步组件
- 节省打包出的结果,异步组件分开打包,采用jsonp的方式进行加载,有效解决文件过大的问题。
- 核心就是包组件定义变成一个函数,依赖 import() 语法,可以实现文件的分割加载。
action 与 mutation 的区别
- mutation 是同步更新, $watch 严格模式下会报错
- action 是异步操作,可以获取数据后调用 mutation 提交最终数据
插槽与作用域插槽的区别
创建组件虚拟节点时,会将组件儿子的虚拟节点保存起来。当初始化组件时,通过插槽属性将儿子进行分类 {a:[vnode],b[vnode]}
渲染组件时会拿对应的 slot 属性的节点进行替换操作。(插槽的作用域为父组件)
作用域插槽 作用域插槽在解析的时候不会作为组件的孩子节点。会解析成函数,当子组件渲染时,会调用此函数进行渲染。
普通插槽渲染的作用域是父组件,作用域插槽的渲染作用域是当前子组件。
vue中相同逻辑如何抽离
其实就是考察 vue.mixin 用法,给组件每个生命周期,函数都混入一些公共逻辑。
谈谈对keep-alive的了解
keep-alive 可以实现组件的缓存,当组件切换时不会对当前组件进行卸载。
常用的2个属性 include/exclude ,2个生命周期 activated , deactivated
React
setStatey异步
调用setState后发生了什么
- React将传入的参数和当前的状态合并
- 走生命周期 按照流程触发render
- 生成新的虚拟DOM,与旧的虚拟DOM进行diff
- 重新渲染UI
React中事件处理逻辑
为了解决跨浏览器兼容性问题,React 会将浏览器原生事件(Browser Native Event)封装为合成事件(SyntheticEvent)传入设置的事件处理器中。这里的合成事件提供了与原生事件相同的接口,不过它们屏蔽了底层浏览器的细节差异,保证了行为的一致性。另外有意思的是,React 并没有直接将事件附着到子元素上,而是以单一事件监听器的方式将所有的事件发送到顶层进行处理。这样 React 在更新 DOM 的时候就不需要考虑如何去处理附着在 DOM 上的事件监听器,最终达到优化性能的目的。
为什么虚拟DOM会提高性能
虚拟dom相当于在js和真实dom中间加了一个缓存,利用dom diff算法避免了没有必要的dom操作,从而提高性能。 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异 把2所记录的差异应用到步骤1所构建的真正的DOM树上,视图就更新了。
Babel
抽象语法树,AST;
词法分析 -> 语法分析 -> 语法树
@babel/core、@babel/preset-env
ES6、7代码输入 -> babylon进行解析 -> 得到AST(抽象语法树)-> plugin用babel-traverse对AST树进行遍历转译 ->得到新的AST树->用babel-generator通过AST树生成ES5代码
React16生命周期的变更
- 弃用的生命周期:componentWillMount、componentWillReceivePorps,componentWillUpdate
- 新增的生命周期:componentDidCatch getDerivedStateFromProps getSnapshotBeforeUpdate
React diff算法
- tree diff
- 两棵树只对同一层次节点进行比较,如果不存在则直接删除,不会进一步比较
- component diff
- element diff 插入 删除 移动
Webpack
loader的作用:
- 实现对不同格式的文件的处理,比如说将scss转换为css,或者typescript转化为js
- 转换这些文件,从而使其能够被添加到依赖图中
Plugins
而plugins并不是直接操作单个文件,它直接对整个构建过程起作用
module,chunk 和 bundle的区别
module,chunk 和 bundle 其实就是同一份逻辑代码在不同转换场景下的取了三个名字
我们直接写出来的是 module,webpack 处理时是 chunk,最后生成浏览器可以直接运行的 bundle。