1、set/map区别,map/object区别
2、深拷贝:核心就是递归实现
function deepCopy(obj){
if(typeof obj !== 'object' || obj === null){
return obj;
}
let copy = {};
for(let key in obj){
if(obj.hasOwnProperty(key)){
copy[key]=deepCopy(obj[key]);
}
}
return copy;
}
3、promise实现 .then可以被调用多次
// promise的三种状态
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
class MyPromise {
constructor(executor) {
this._status = PENDING;
this._resolveQueue = [];
this._rejectQueue = [];
let _resolve = val => {
if (this._status !== PENDING) {
return;
} else {
this._status = FULFILLED;
// 一个then方法可以被一个promise执行多次
while (this._resolveQueue.length) {
// 取出then后面执行的函数内容,执行函数
const callback = this._resolveQueue.shift();
callback(val);
}
}
};
let _reject = val => {
if (this._status !== PENDING) {
return;
} else {
this._status = REJECTED;
while (this._rejectQueue.length) {
const callback = this._rejectQueue.shift();
callback(val);
}
}
};
executor(_resolve, _reject);
}
then(resolveFunc, rejectFunc){
this._resolveQueue.push(resolveFunc);
this._rejectQueue.push(rejectFunc);
}
}
4、设计模式
观察者模式:只有两个角色:观察者+被观察者;
// 被观察者-发布者
class Notifier{
constructor(){
this.observerList = [];// 观察者列表
}
add(obj){
this.observerList.push(obj);
}
remove(obj){
// splice不改变原数组
let index = this.observerList.findIndex(item=>item === obj);
this.observerList.splice(index, 1);
}
notify(){
this.observerList.forEach(obj=>obj.update());
}
}
// 观察者
class Observer{
constructor(name){
this.name = name;
}
update(){
console.log(this.name, '收到通知');
}
}
let notifier = new Notifier()
let observer1 = new Observer("张三");
let observer2 = new Observer("李四");
notifier.add(observer1);
notifier.add(observer2);
notifier.remove(observer1);
notifier.notify();
发布订阅模式:有一个中间角色;
// 发布-订阅模式
class Observer{
caches = {};
//发布
emit(eventName, data){
if(this.caches[eventName]){
this.caches[eventName].forEach(fn=>fn(data));
}
}
//订阅
on(eventName, fn){
this.caches[eventName] = this.caches[eventName] || [];
this.caches[eventName].push(fn);
}
// 取消订阅 => 若fn不传, 直接取消该事件所有订阅信息
off (eventName, fn) {
if (this.caches[eventName]) {
const newCaches = fn
? this.caches[eventName].filter(e => e !== fn)
: [];
this.caches[eventName] = newCaches;
}
}
}
5、前端工程化
前端工程化是前端开发过程中不可或缺的一环,熟悉构建工具如 webpack、gulp、grunt、Rollup 和 Parcel 等,能够使用模块化编程方案如 CommonJS 和 ES6 Modules,实现代码的自动化构建、测试和打包。同时也需要了解 CI/CD 持续集成、持续部署等相关知识。
如何实现前端工程化?
负责内容:
- 静态资源(.js/.css文件以及各种格式的图片)和动态资源的处理
- js实现前端业务逻辑
- html模版文件的产出
- 中间层Web服务,一般由nodejs实现
- 前端单元测试
- 前端项目部署
- 使用webpack实现项目构建
目的:将源代码转化为浏览器可以执行的代码,前端构建产出的资源文件只有三种html\css\js,那么需要完成编译的内容有:
- 无法被浏览器直接识别的js代码,包括es6/7/8/9/10等符合ecmascript规范的js代码;
- 无法被浏览器直接识别的css代码
- 无法被浏览器直接识别的html代码
除了针对语言本身外,前端构建还应考虑到web性能优化,包括
- 依赖打包,同步依赖的文件打包在一起,减少http请求数量
- 资源嵌入
- 文件压缩,
- 为文件加入hash
- 将开发环境下的域名和静态资源文件路径修改为生产环境下的域名和路径。
- 文件名称改变??
除了webpack,还有其他的工具。
loader和plugin的区别:
- loader本质是一个函数,在函数中对接收到的内容进行转换,返回转换后的内容。
- Plugin: 插件,基于事件流框架tapable,可以扩展webpack功能,在webpack运行的周期中会广播出许多事件,plugin可以监听这些事件,在合适时机通过webpack提供的api改变输出结果。
webpack构建流程:
- 初始化参数: 从配置文件和shell文件中读取与合并参数,得出最终参数。
- 开始编译:从上一步得到的compiler对象,加载所有配置的插件,执行对象的run方法开始编译。
- 确定入口:根据配置中的entry找出所有入口文件。
- 编译模块:入口文件出发,调用所有loader对模块进行翻译,再找出该模块依赖模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤处理
- 完成模块编译:经过上面翻译完所有模块,得到每个模块被翻译内容以及他们之间依赖关系。
- 输出资源:根据入口和模块之间依赖关系,组装成一个个包含多个模块的chunk,再把每个chunk转换成一个单独文件加入到输入列表
- 输出完成:确定好输出内容,根据配置确定输出的路径和文件,把文件内容写入到文件系统。
Hot Module ReplaceMent工作流程
HMR的核心就是客户端从服务端拉去更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上 WDS 与浏览器之间维护了一个 Websocket,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp 请求获取该chunk的增量更新。
后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin 来完成,提供了相关 API 以供开发者针对自身场景进行处理,像react-hot-loader 和 vue-loader 都是借助这些 API 实现 HMR。
zhuanlan.zhihu.com/p/30669007(…
- 使用babel完成javascript编译
js本身是可以直接在浏览器中执行的,为什么需要babel再编译一次呢?
js只是对ECMAScript标准实现的一个子集,浏览器对ECMAScript新规范的支持比较滞后,最新版本的chrome没有完全支持ECMAScript的所有规范,为了让es规范能衔接浏览器,babel编辑javascript语法的作用就显示出来了。
Babel作用:将浏览器未实现的ECMAScript规范语法转化为可运行的低版本语法,
- css预编译
工作原理:提供便捷的语法和特性供开发者编写源代码,随后经过专门的编译工具将源码转换为css语法。
- 模块化开发
模块化开发可以解决的问题
- 避免命名冲突
- 便于依赖管理
- 性能优化利于
- 提高可维护性、代码可复用性
在ES6之前,前端模块化开发主要有三种规范:commonjs\amd\cmd;ES6 Module规范推出,这种事语言层面的规范,与应用场景无关。浏览器不完全支持这种规范,要实现ES6 Module,需要使用构建工具进行编译。
- 组件化开发
模块化:文件层面上对代码和资源拆分、组件化是设计层面对ui拆分。
-
开发环境的本地服务器与mock服务
-
规范化约束
-
项目部署流程化
将构建产出的代码包部署到测试服务器,将测试完成的代码发布到生产环境。
- 未来
nodejs中间层+浏览器是目前实现‘大前端’基本模式。
6、前后端分离开发模式
前端html页面通过ajax调用后端RESTful API接口并使用json数据进行交互。
springboot
nodejs
module最终返回module.exports={xx:xx, yy:yy}
- 基本模块-http
http服务器:开发http服务,从头处理tcp连接,解析http已经由nodejs自带的http模块完成,不需要直接和http协议沟通,只需要操作http模块提供的request和response对象。
Request对象: 调用request对象的属性和方法就可以拿到所有http请求的信息。
Response对象:操作response对象的方法,就可以把http响应返回给浏览器。
- 框架koa
每一个http请求,koa将调用传入的异步函数处理
async(ctx, next)
ctx 是由koa传入的封装了request和response的变量,next是处理下一步的函数
koa-router
const router = require('koa-router')();
router.get('/hello', async(ctx,next)=>{
ctx.response.body = `<h1>hello</h1>';
});
模版引擎nunjucks,mvc框架,
- Mysql: 使用sequlize:只要api返回promise,就可以调用await
- Mocha: 测试
- Websocket: 可以实时通信
- restful api: 写在api层
7、closure
产生闭包的核心:
- 预扫描内部函数
- 把内部函数引用的外部变量保存到堆中
闭包概念:
调用一个外部函数返回一个内部函数,即使外部函数已经执行结束,内部函数引用外部函数的变量依旧存在于内存中,把这部分变量集合称为闭包。
8、垃圾回收算法
-
通过GC Root标记空间中的活动对象和非活动对象
-
回收非活动对象所占据的内存
-
内存整理
- 频繁回收对象后,内存中就会存在大量不连续空间-称为内存碎片
-
新生代垃圾回收Minor GC(Scavenger算法)
- 标记对象区域中的垃圾
- 对象区域中的存活区移动到空闲区
- 翻转
-
老生代垃圾回收Major GC
- mark-sweep算法直接清理
- mark-compact向一端移动,清理掉这端之外的另一端
9、内存泄漏
- 污染全局:函数体内的对象没有被var\let\const这些关键字声明,v8就会使用this.xx来替换xx
- 闭包
- 'detached节点': DOM树和javascrpit代码都不引用某个dom节点,这个节点才会被作为垃圾进行回收。
emit事件:eventEmitter.emit('start',data);
监听到:eventEmitter.on('start',number=>{console.log(${number} );})
10、dom树
DOM树(Document Object Model Tree)是指网页文档中所有元素的层次结构, dom中每一个html标签都是一个对象,都可以通过javascript来访问并修改页面。
- dom树是如何生成的
渲染引擎内部的html解析器负责将html字节流转换为dom结构。网络进程接收到数据往里面放,渲染进程动态接受字节流,将其解析为dom。
这个步骤分为3步
- 分词器将字节流转换为token,分为tag token和文本token
- 后续将token解析为dom节点,并将dom节点添加到dom树中。
token解析为dom节点的过程就是tag出栈解析为dom,形成dom树。
-
js如何影响dom生成: 执行到js标签时,暂停整个dom解析,执行js代码。执行js前需要先下载js,因为js下载阻塞dom解析,又很耗时间,所以chrome做了很多优化,最主要的如预解析,预解析线程提前下载这些文件。
- 引入js但不阻塞dom的策略
- 使用CDN来加速javascript文件的加载
- 压缩js文件的体积
- js文件中没有操作dom的相关代码,将js脚本设置为异步加载,用async/defer标记, defer标记的代码在domcontentLoaded事件之后执行
11、浏览器缓存机制
根据http报文的缓存标识进行的,http报文分为两种:
- http请求报文:请求行+http头+请求报文主体
- http响应报文:状态行+http头+响应报文主体
这些报文的标识有哪些?juejin.cn/post/684490…
- Cache-control
private: 只有客户端可以缓存
12、用canvas写汉字
汉字笔画顺序和坐标点,移动到起始点开始逐步绘制每一笔。
// 获取canvas元素
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// 设置线条宽度和颜色
ctx.lineWidth = 5;
ctx.strokeStyle = 'black';
// 汉字"人"的笔画数据
const strokes = [
[[100, 100], [100, 300]],
[[100, 200], [300, 200]],
[[300, 200], [300, 400]]
];
// 一笔一画绘制汉字
function drawCharacter() {
for (let i = 0; i < strokes.length; i++) {
const stroke = strokes[i];
// 移动到起始点
ctx.moveTo(stroke[0][0], stroke[0][1]);
// 逐步绘制每一笔
for (let j = 1; j < stroke.length; j++) {
ctx.lineTo(stroke[j][0], stroke[j][1]);
}
// 绘制路径
ctx.stroke();
}
}
// 调用绘制函数
drawCharacter();
13、高频算法题
链表
//链表节点定义
function ListNode(val){
this.val = val;
this.next = null;
}
/**
* 1、判断链表是否有环
*/
//快慢指针
var hasCycle = (head) =>{
if(head === null || head.next === null){
return false;
}
let slow = head;
let fast = head.next;
while(slow !== fast){
if(fast === null || fast.next === null ){
return false;
}
slow = slow.next;
fast = fast.next.next;
}
return true;
}
//遍历过的标记
var hasCycle = (head) => {
while(head){
if(head.tag){
return true;
}
head.tag = true;
head = head.next;
}
return false;
}
/**
* 2、反转链表
*/
var reverseList = (head) => {
//定义两个指针
let prev = head;let cur = null;
while(prev){
let temp = prev.next;
prev.next = cur;
cur = prev;
prev = temp;
}
return cur;
}
/**
* 3、移除链表元素
*/
var removeElements = (head, val) => {
//虚拟头结点
const fakeheadNode = new ListNode(0);
fakeheadNode.next = head;
let temp = fakeheadNode;
while(temp.next !== null){
if(temp.next.val === val){
temp.next = temp.next.next;
}else{
temp = temp.next;
}
}
return fakeheadNode.next;
}
//递归
var removeElements = (head, val) =>{
if(head === null){
return head;
}
head.next = removeElements(head.next, val);
return head.val === val ? head.next : head;
}
/**
*
合并两个有序链表
*/
var mergeTwoLists = function(l1, l2) {
const prehead = new ListNode(-1);
let prev = prehead;
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
prev.next = l1;
l1 = l1.next;
} else {
prev.next = l2;
l2 = l2.next;
}
prev = prev.next;
}
// 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
prev.next = l1 === null ? l2 : l1;
return prehead.next;
};
/**
*
相交链表
*/
//
const getIntersectionNode = (headA, headB) => {
if(headA === null || headB === null){
return null;
}
let pA = headA, pB = headB;
while(pA !== pB){
pA = pA === null ? headB : pA.next;
pB = pB === null ? headA : pB.next;
}
return pA;
}
//删除链表中节点
var deleteNode = function(node) {
node.val = node.next.val;
node.next = node.next.next;
};
//删除链表中倒数第n个结点
function removeNthFromEnd(head, n){
let dmy = new ListNode(0, head);
//初始化快慢指针都在头节点
let slow = dmy;
let fast = dmy;
//快指针向后移动n个位置,slow和fast相隔n
while(n-- > 0) fast = fast.next;
//fast和slow同时移动,直到fast指向为空
while(fast !== null && fast.next !== null){
slow = slow.next;
fast = fast.next;
}
//slow下一个节点就是要删除的节点
slow.next = slow.next.next;
return dmy.next;
}
/**
* 删除链表中重复元素,留下该元素
*/
//对于有序的链表
var deleteDuplicates = (head) => {
if(!head){
return head;
}
let cur = head;
while(cur.next){
if(cur.val === cur.next.val){
cur.next = cur.next.next;
}else{
cur = cur.next;
}
}
return head;
}
/**
* 删除链表中重复的元素,不留该元素
*/
var deleteListDuplicatedNode = (head) => {
if(!head){
return head;
}
//哑结点,最后返回哑结点的next
const dummyHead = new ListNode(0, head);
let cur = dummyHead;
while(cur.next && cur.next.next){
if(cur.next.val === cur.next.next.val){
const x = cur.next.val;
// cur.next 以及所有后面拥有相同元素值的链表节点全部删除
while(cur.next && cur.next.val === x){
cur.next = cur.next.next;
}
}else{
cur = cur.next;
}
}
return dummyHead.next;
}
/** 两两交换链表中结点 */
var swapPairs = head => {
if(head === null || head.next === null){
return head;
}
//新链表头节点
const newHead = head.next;
head.next = swapPairs(newHead.next);
//新结点next为HEAd
newHead.next = head;
return newHead;
}
//节点所有父节点路径
function getNodeRoute(tree, targetId, nodePathArray){
for(let i = 0;i < tree.length; i++){
if(tree[i].children){
const endRecursiveLoop = getNodeRoute(tree[i].children, targetId, nodePathArray);
if(endRecursiveLoop){
nodePathArray.push(tree[i]);
return true;
}
}
if(tree[i].uuid === targetId){
nodePathArray.push(tree[i]);
return true;
}
}
}
数组
1、合并两个有序数组
第一种方法:js方法
function(nums1, m, nums2,n){
nums1.splice(m, nums1.length - m, ...nums2);
nums1.sort((a,b)=>a-b)
}
第二种方法:双指针
var merge = function(nums1, m, nums2, n) {
let p1 = 0, p2 = 0;
const sorted = new Array(m + n).fill(0);
var cur;
while (p1 < m || p2 < n) {
if (p1 === m) {
cur = nums2[p2++];
} else if (p2 === n) {
cur = nums1[p1++];
} else if (nums1[p1] < nums2[p2]) {
cur = nums1[p1++];
} else {
cur = nums2[p2++];
}
sorted[p1 + p2 - 1] = cur;
}
for (let i = 0; i != m + n; ++i) {
nums1[i] = sorted[i];
}
};
2、两数之和
第一种方法:哈希表:数组中每个值都存到哈希表中,遇到目标值,返回哈希值
var twoSum = function(nums, target) {
let map = new Map();
for(let i = 0, len = nums.length; i < len; i++){
if(map.has(target - nums[i])){
return [map.get(target - nums[i]), i];
}else{
map.set(nums[i], i);
}
}
return [];
};
3、三数之和
const threeSum = function(nums) {
if(!nums || nums.length < 3) return []
let result = [], second, last
// 排序
nums.sort((a, b) => a - b)
for (let i = 0; i < nums.length ; i++) {
if(nums[i] > 0) break
// 去重
if(i > 0 && nums[i] === nums[i-1]) continue
second = i + 1
last = nums.length - 1
while(second < last){
const sum = nums[i] + nums[second] + nums[last]
if(!sum){
// sum 为 0
result.push([nums[i], nums[second], nums[last]])
// 去重
while (second<last && nums[second] === nums[second+1]) second++
while (second<last && nums[last] === nums[last-1]) last--
second ++
last --
}
else if (sum < 0) second ++
else if (sum > 0) last --
}
}
return result
};
4、 四数之和
/**
* @param {number[]} nums
* @param {number} target
* @return {number[][]}
*/
var fourSum = function(nums, target) {
//分解为1个数和三数之和
let res = [];
let N = nums.length;
// 对数组排序
nums.sort((a,b)=> a-b);
for(let i = 0; i< N; i++){
//去重
if(nums[i] === nums[i-1]){
continue;
}
for(let j = i + 1;j < N; j++){
//必须添加j > i + 1条件
if(nums[j] === nums[j-1] && j > i+1){
continue;
}
let left = j+1;
let right = N -1;
while(left < right){
const sum = nums[i] + nums[j] + nums[left] + nums[right];
if(sum === target){
res.push([nums[i], nums[j], nums[left], nums[right]]);
while(left < right && nums[left] === nums[left + 1]){
left ++;
}
while(left < right && nums[right ] === nums[right - 1]){
right --;
}
left ++;
right--;
}
else if(sum < target){
left ++;
}else{
right --;
}
}
}
}
return res;
};
5、N数之和
/**
* @param {number[]} nums
* @param {number} target
* @return {number[][]}
*/
var nSum = function(nums, target) {
const helper = (index, N, temp) => {
// 如果下标越界了或者 N < 3 就没有必要在接着走下去了
if (index === len || N < 3) {
return
}
for (let i = index; i < len; i++) {
// 剔除重复的元素
if (i > index && nums[i] === nums[i - 1]) {
continue
}
// 如果 N > 3 的话就接着递归
// 并且在递归结束之后也不走下边的逻辑
// 注意这里不能用 return
// 否则循环便不能跑完整
if (N > 3) {
helper(i + 1, N - 1, [nums[i], ...temp])
continue
}
// 当走到这里的时候,相当于在求「三数之和」了
// temp 数组在这里只是把前面递归加入的数组算进来
let left = i + 1
let right = len - 1
while (left < right) {
let sum = nums[i] + nums[left] + nums[right] + temp.reduce((prev, curr) => prev + curr)
if (sum === target) {
res.push([...temp, nums[i], nums[left], nums[right]])
while (left < right && nums[left] === nums[left + 1]) {
left++
}
while (left < right && nums[right] === nums[right - 1]) {
right--
}
left++
right--
} else if (sum < target) {
left++
} else {
right--
}
}
}
}
let res = []
let len = nums.length
nums.sort((a, b) => a - b)
helper(0, 4, [])
return res
};
6、数组扁平化、去重排序
首先reduce
reduce(callbackFn, initialValue)
// 扁平化
function flatArr(arr){
return arr.reduce((acc, cur)=>{
acc = acc.concat(Array.isArray(cur) ? flatArr(cur):cur);
return acc;
},[]);
}
7、编写函数计算多个数组的交集
const getIntersection = (...arrs) => {
return Array.from(new Set(arrs.reduce((total, arr) => {
return arr.filter(item => total.includes(item));
})));
}
8、最长公共前缀(纵向比较)
/**
* @param {string[]} strs
* @return {string}
*/
var longestCommonPrefix = function(strs) {
//纵向比较
//纵向比较前缀字符串长度
const prefixStrLen = strs[0].length;
for(let i = 0; i< prefixStrLen; i++){
//第一个字符串每一个位置上的值
let i_str = strs[0][i];
// 和后续字符串同一个位置上的值比较
for(let j = 1; j < strs.length; j++){
if(strs[j][i] !== i_str || i === strs[j].length){
return strs[0].substring(0, i);
}
}
}
return strs[0];
};
9、判断是否为回文字符串?
双指针从前向后从后向前
// 使用jsapi
function isPlalindrome(input) {
if (typeof input !== 'string') return false;
return input.split('').reverse().join('') === input;
}
// 双指针
function isPlalindrome(input) {
if (typeof input !== 'string') return false;
let i = 0, j = input.length - 1
while(i < j) {
if(input[i]) !== input[j]) return false
// 双指针移动
i ++
j --
}
return true
}
10、无重复字符的最长子串
滑动窗口算法
// 滑动窗口,维护数组,遍历字符串,字符出现过就删除在则删除滑动窗口数组里相同字符及相同字符前的字符,没出现过就push进数组
var lengthOfLongestSubstring = function(s){
let arr = [], max =0;
for(let i =0;i< s.length; i++){
let index = arr.indexOf(s[i]);
if(index !== -1){
// 当前字符的位置是index, 字符以及字符前的数据长度为index+1
arr.splice(0, index + 1);
}
arr.push(s[i]);
max = Math.max(arr.length, max);
}
return max;
}
11、最长回文子串
* @param {string} s
* @return {string}
*/
var longestPalindrome = function(s) {
const initialize2DArray = (w, h, val = null) => Array(h).fill().map(() => Array(w).fill(val))
let len = s.length
if (len < 2) {
return s
}
//记录最长子串长度和开始位置
let maxLen = 1
let begin = 0
let dp = initialize2DArray(len, len, null)
//初始每个字符都是回文串
for (let i = 0; i < len; i++) {
dp[i][i] = true
}
for (let j = 1; j < len; j++) {
for (let i = 0; i < j; i++) {
if (s[i] != s[j]) {
dp[i][j] = false
} else {
// 长度不大于2,就是回文
if (j - i < 3) {
dp[i][j] = true
} else {
// 两端值相等,其子串是回文,则其也是回文
dp[i][j] = dp[i + 1][j - 1]
}
}
if (dp[i][j] && j - i + 1 > maxLen) {
maxLen = j - i + 1
begin = i
}
}
}
return s.substring(begin, begin + maxLen)
};
12、最短路径和
12.1 广度优先搜索
12.2 深度优先搜索
二叉树的遍历
前序遍历(栈)
var preorderTraversal = function(root) {
const resArr = [];
if(root === null){
return resArr;
}
const stack = [];
stack.push(root);
while(stack.length !== 0){
let node = stack.pop();
resArr.push(node.val);
// 先进后出
if(node.right !==null){
stack.push(node.right);
}
if(node.left !== null){
stack.push(node.left);
}
}
return resArr;
};
中序遍历(栈)
var inorderTraversal = function(root) {
const res = [];
const stk = [];
while (root || stk.length) {
while (root) {
stk.push(root);
root = root.left;
}
root = stk.pop();
res.push(root.val);
root = root.right;
}
return res;
};
13、单词逆序
var reverseWords = function(s)
{
const reverseStr = [];
const length = s.length;
let i = 0;
while (i < length)
{
let start = i;
while (i < length && s[i] != ' ')
{
i++;
}
for (let p = start; p < i; p++)
{
reverseStr.push(s[start + i - 1 - p]);
}
while (i < length && s[i] == ' ')
{
i++;
reverseStr.push(' ');
}
}
return reverseStr.join('');
};
14、括号匹配
栈
思路: 遍历字符串,遇到右括号,出栈,否则入栈
// 遍历字符串,遇到右括号,出栈,否则入栈
const isBracketsValid = function(s)=>{
const len = s.length;
// 两两配对,如果长度不是2的整数倍,那么就不是
if(len % 2 === 1){
return false;
}
// 定义存储右括号为key,左括号为value的map数组
const groupOfBrackets = new Map([[')','('],[']','['],['}', '{']])
// 定义栈
const stack = [];
for(let char of s){
// 当前字符是否是右括号,即groupOfBrackets的key元素
if(groupOfBrackets.has(char)){
// 是但是只有这一个元素
if(!stack.length || stack[stack.length -1] !== groupOfBrackets.get(char)){
return false;
}
//否则出栈
stack.pop();
}
else{
stack.push(char);
}
}
//最后栈为空返回true,否则返回false
return !stack.length;
}
14、版本号排序
// 方法三,通过 点 将版本号分割为数组,如果版本号长度不同,则填入 0,分别比较每一位的大小,可以准确判断每一位的大小
let compareVersion = function (v1, v2) {
// console.log({v1, v2})
if (typeof v1 === "undefined" || typeof v2 === "undefined") {
console.error('请指定要对比的两个版本号', {v1, v2})
return
}
v1 = v1.split('.')
v2 = v2.split('.')
const maxLength = Math.max(v1.length, v2.length)
// 补全
while (v1.length < maxLength) {
v1.push('0')
}
while (v2.length < maxLength) {
v2.push('0')
}
for (let i = 0; i < maxLength; i++) {
let num1 = parseInt(v1[i])
let num2 = parseInt(v2[i])
if (num1 > num2) {
return 1
} else if (num1 < num2) {
return -1
}
}
return 0
}
compareVersion('0.5.1','0.302.1') // -1 , 说明前者小于后者
14、虚拟dom\react diff
15、事件循环
16、MVVM框架
rem视频
pc屏幕尺寸750px/pc根元素字体大小100 = 当前屏幕尺寸/当前字体大小
当前字体大小 = 当前屏幕尺寸/(pc屏幕尺寸750px/pc根元素字体大小100 )
17、移动端适配
18、首屏渲染
zhuanlan.zhihu.com/p/573218063
19、vue/react
20、事件循环
21、跨域设置
设置access-control-allow-origin为我们项目的域名
22、js/css在打包后的html文件中的位置即
script标签的影响
script标签的加载、解析和运行都会影响dom的解析和渲染,是因为js可以操作dom,为了防止渲染过程出现不可预期的结果,浏览器让gui和js线程互斥。当解析器遇到script标签时立即解析并执行脚本,但如果等js加载执行完毕再渲染太久,遇到script标签时,触发一次Paint,将script标签之前元素渲染出来。
不触发Paint的情况
- head中的sript
- inline的script
<script>
console.log(Date.now());
for (let i=0; i<5000000000; i++)
{
let a = i;
}
console.log(Date.now());
</script>
所以script尽量放在body标签结束之前,这样不会阻塞页面整体内容的DOM解析和渲染,这里说的是不是内联的script标签,内联的script放哪里都一样。
link标签
- 因此
<link>标签并不阻塞DOM的解析,但会阻塞DOM的渲染。
<link>标签并不会像带scr属性的<script>标签一样会触发页面paint。浏览器并行解析生成DOM Tree 和 CSSOM Tree,当两者都解析完毕,才会生成render tree,页面才会渲染。所以应尽量减小引入样式文件的大小,提高首屏展示速度。
- link标签会阻塞js执行: js运行时,有可能会请求样式信息,如果此时还没有加载和解析样式,js就有可能会得到错误的回复,产生很多问题。因此浏览器在
<link>标签的加载和解析过程中,会禁止脚本运行。
23、防抖/节流
24、position定位
25、箭头函数/普通函数区别
26、js引擎执行js代码的步骤
babel的工作原理:先将es6源码转换为AST,再将es6语法的ast转化为es5的AST,最后利用ES的AST生成js源代码。
27、BFC
28、浏览器缓存
29、js文件的加载是同步还是异步?
30、浏览器输入地址发生了什么?
简易流程:blog.51cto.com/u_15152252/…
zhuanlan.zhihu.com/p/133906695
tcp为什么三次握手?
发送方可以确定自己发送接收正常但接收方不能确定自己发送是否正常。
tcp为什么四次挥手?
服务端在收到客户端断开连接Fin报文后,并不会立即关闭连接,而是先发送一个ACK包先告诉客户端收到关闭连接的请求,只有当服务器的所有报文发送完毕之后,才发送FIN报文断开连接,因此需要四次挥手
31、null和undefined的区别
www.ruanyifeng.com/blog/2014/0…
32、es module 和common js
33、react key的作用
34、 判断数据类型
- Typeof
- Instanceof :后面一定是大写
- 对象原型链判断方法:Object.prototype.toString.call()
- constructor:null/undefined没有
35、react 生命周期
函数组件useEffect模拟生命周期
| 类组件 | 函数组件 |
|---|---|
| constructor在react组件挂载之前被调用,用来初始化状态、绑定方法 | |
| getDevicedStateFromProps每次调用render方法之前调用,包括初始化/后续更新 | |
| render组件渲染时 | |
| componentDidMountreact组件插入树后被立即调用,发送请求,启动监听的好时机 | useEffect(()=>{console.log('第一次渲染时调用'),[]} |
| shouldcomponentupdate组件准备更新 | |
| getSnapShotBeforeUpdaterender之后,将对组件进行挂载时调用 | |
| componentDidUpdate更新发生后被调用 | useEffect(()=>{console.log('任意属性改变')}或者某些属性的变化useEffect(()=>{console.log('n,m变了'),[n,m]} |
| componentWillUnmount组件要被销毁时调用 | 组件卸载时需要清除effect创建的诸如定时器等资源useEffect(()=>{const timer = setTimeout(()=>{....},1000)return ()=> {console.oog('组件销毁')clearTimerout(timer)}}) |
36、 useEffect和useLayoutEffect区别
基于上面的数据结构,对于use(Layout)Effect来说,React做的事情就是
- render阶段:函数组件开始渲染的时候,创建出对应的hook链表挂载到workInProgress的memoizedState上,并创建effect链表,但是基于上次和本次依赖项的比较结果, 创建的effect是有差异的。这一点暂且可以理解为:依赖项有变化,effect可以被处理,否则不会被处理。
- commit阶段:异步调度useEffect,layout阶段同步处理useLayoutEffect的effect。等到commit阶段完成,更新应用到页面上之后,开始处理useEffect产生的effect。
第二点提到了一个重点,就是useEffect和useLayoutEffect的执行时机不一样,前者被异步调度,当页面渲染完成后再去执行,不会阻塞页面渲染。 后者是在commit阶段新的DOM准备完成,但还未渲染到屏幕之前,同步执行。
作者:Axizs 链接:juejin.cn/post/692168… 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
37、xss/csrf
Xss: 攻击者对客户端网页注入恶意脚本,包括js,html和flash;包括存到服务器的存储型和存储到web服务器通过url注入的反射型
xss防御:
- 服务器对输入脚本进行过滤或者转码
- 充分利用csp
- 使用httponly:对于很多盗取cookie的xss攻击,可以通过设置set-cookie属性值为httponly。使用httponly标记的cookie只能使用在http请求过程中,无法通过js来读取这段cookie.
csrf:通过一些诱导点击获取用户的登录状态或者信息
- 利用cookie的samesite属性
- 验证请求的来源站点:从origin/referer中获取请求来源
- Csrf token: 浏览器向服务器发起请求的时候,服务器生成该字符串,将该字符串植入到返回页面。浏览器端带上该token,第三方站点肯定无法获取
38、三栏布局的写法
39、flex属性
www.ruanyifeng.com/blog/2015/0…
40、怎么理解jsx
jsx是React.createElement(component, props, …children) 函数的语法糖
41、React 组件是怎么渲染为 DOM 元素到页面上的?
ReactDOM用于渲染组件并构造DOM树,然后插入到页面上的某个挂载点上。
42、React 中 setState 调用以后会经历哪些流程?
43、useState是怎么实现的
重点是useState维护着一个数组,按顺序存放着声明的所有数据。因为数组是按顺序 排列,所以每次useState内部调用render时都要重置index,每个组件都有一个自己的_state和index
/**
* 单个数据
*/
let _state;
const myUseState = initialValue => {
_state = _state === undefined ? initialValue : _state;
const setState = newValue =>{
_state = newValue;
render();
}
return [_state, setState];
}
/**
* 多个数据
*/
let _state = [];
let index = 0;
const myUseState = initialValue =>{
const curIndex = index;
_state[curIndex] = _state[curIndex] === undefined ? initialValue : _state[curIndex];
const setState = newValue => {
_state[curIndex] = newValue;
render();
}
index +=1;
return [_state[curIndex], setState];
}
44、前端性能优化
45、数据管理,react context的原理, redux原理
Context是怎么实现的呢?
createContext返回的是一个对象,对象属性包括
- _currentValue: 保存context的值的地方,不建议直接改
- Provider: contextProvider的jsx
- Consumer: contextConsumer的jsx
react渲染:jsx编译成render function,执行后就是vdom,vdom经历reconcile过程转为fiber结构,转完之后一次性commit,更改dom.
Provider的处理:修改了_currentValue的值,在修改context._currentValue之前有push,当前context值入栈处理完fiber节点后再pop出栈,然后恢复context
Consumer: 读取了context._currentValue返回
redux原理
React Components 需要获取一些数据, 然后它就告知 Store 需要获取数据,这就是就是 Action Creactor , Store 接收到之后去 Reducer 查一下, Reducer 会告诉 Store 应该给这个组件什么数据
46、react fiber
解决了什么问题:
js线程和渲染线程互斥,组件较大会导致js线程一直执行阻塞渲染
原理:
- 每个任务增加了优先级
- 异步任务,调用requestIdleCallback api在浏览器空闲时执行
- Dom diff树变成了链表,一个dom对应两个fiber(一个链表)
如何解决存在的问题:
fiber把渲染进程拆分成多个子任务,每次只做一小部分任务,做完看是否有剩余时间,有执行下一个任务;无的话挂起当前任务,把时间控制权交给主线程,主线程不忙再执行。
其中每个任务更新单元为 React Element 对应的 Fiber节点
window.requestIdleCallback()方法将在浏览器的空闲时段内调用的函数排队。
找下一个目标,fiber节点的属性
// 指向父级Fiber节点
this.return = null
// 指向子Fiber节点
this.child = null
// 指向右边第一个兄弟Fiber节点
this.sibling = null
47、react hooks解决了哪些问题,有什么限制
解决了哪些问题
- 自定义hook能更好封装功能:如useEffect相当于componentDidMount\comoponentDidUpdate\componentWillUnmount
- 每调用useHook一次都会生成一份独立的状态
有什么限制
- 不要在
React的循环、条件或嵌套函数中使用; Hooks只能在函数组件中使用,不支持类组件。
为什么有这些限制
48、webpack可以做的优化
49、http相关
Http: 用于在Web浏览器和网站服务器之间传递信息,明文方式发送内容,发送的内容包括(html文件,图片文件以及查询结果等超文本)不提供任何方式加密
Https: http + ssl/tls
https加密过程
- 首先客户端通过URL访问服务器建立SSL连接
- 服务端收到客户端请求后,会将网站支持的证书信息(证书中包含公钥)传送一份给客户端
- 客户端的服务器开始协商SSL连接的安全等级,也就是信息加密的等级
- 客户端的浏览器根据双方同意的安全等级,建立会话密钥,然后利用网站的公钥将会话密钥加密,并传送给网站
- 服务器利用自己的私钥解密出会话密钥
- 服务器利用会话密钥加密与客户端之间的通信
http和https的区别
- Http协议的传输数据是明文的,https使用ssl/tsl对其进行加密
- 使用连接方式不同,默认端口不同,http是80,https是443
- https需要设计加密以及多次握手,性能不如http
- https需要ssl,要钱
SSL的实现这些功能主要依赖于三种手段:
- 对称加密:采用协商的密钥对数据加密
- 非对称加密:实现身份认证和密钥协商
- 摘要算法:验证信息的完整性
- 数字签名:身份验证
http不同版本比较
HTTP1.0:
- 浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接
HTTP1.1:
- 引入了持久连接,即TCP连接默认不关闭,可以被多个请求复用
- 在同一个TCP连接里面,客户端可以同时发送多个请求
- 虽然允许复用TCP连接,但是同一个TCP连接里面,所有的数据通信是按次序进行的,服务器只有处理完一个请求,才会接着处理下一个请求。如果前面的处理特别慢,后面就会有许多请求排队等着
- 新增了一些请求方法
- 新增了一些请求头和响应头
HTTP2.0:
- 采用二进制格式而非文本格式
- 完全多路复用,而非有序并阻塞的、只需一个连接即可实现并行
- 使用报头压缩,降低开销
- 服务器推送
50、HTTP的请求头你知道的有哪些。和缓存相关的有哪些?强缓存和协商缓存同时存在哪个优先级高?
http请求头
和缓存相关的字段
If-nont-match, if-modified-since
优先级
强缓存高于协商缓存,强缓存可以直接读取本地缓存,而协商缓存需要和服务器通信
51、keyof/typeof
keyof:返回新的类型
比如我们有个类型
interface Person{
name: string;
age: number;
location: string;
}
使用keyof字段
type someNewType = keyof Person
这个someNewType就是一个联合字面量类型('name' | 'age' | 'location')
Typeof: 返回当前数据类型
const bmw = { name: "BMW", power: "1000hp" }
typeof bmw
输出{name: string, power: string}
52、webpack构建流程
webpack运行流程:串行,将各个插件的工作流程串联起来
从启动到结束会依次经历三个流程
- 初始化流程:从配置文件和
Shell语句中读取与合并参数,并初始化需要使用的插件和配置插件等执行环境所需要的参数 - 编辑构建:从entry出发,针对每个Module串行调用Loader翻译文件,并找出Module依赖的Module,递归的进行编译
- 输出:编译后的Module组成Chunk,Chunk转换为文件,输出到文件系统。
loader,解决了什么问题
在遇到import或者require加载模块的时候,webpack只支持对js 和 json 文件打包
像css、sass、png等这些类型的文件的时候,webpack则无能为力,这时候就需要配置对应的loader进行文件内容的解析
Plugin,解决了什么问题
暂时无法在飞书文档外展示此内容
loader作用在打包文件之前
plugin作用于整个编译周期
webpack热更新
const webpack = require('webpack');
module.exports = {
devServer: {
hot : true
}
}
定义
修改保存css/js,不刷新页面更新到页面
实现原理
由于socket服务器在HMR Runtime 和 HMR Server之间建立 websocket链接,当文件发生改动的时候,服务端会向浏览器推送一条消息,消息包含文件改动后生成的hash值,如下图的h属性,作为下一次热更细的标识
关于webpack热模块更新的总结如下:
- 通过
webpack-dev-server创建两个服务器:提供静态资源的服务(express)和Socket服务 - express server 负责直接提供静态资源的服务(打包后的资源直接被浏览器请求和解析)
- socket server 是一个 websocket 的长连接,双方可以通信
- 当 socket server 监听到对应的模块发生变化时,会生成两个文件.json(manifest文件)和.js文件(update chunk)
- 通过长连接,socket server 可以直接将这两个文件主动发送给客户端(浏览器)
- 浏览器拿到两个新的文件后,通过HMR runtime机制,加载这两个文件,并且针对修改的模块进行更新
53、http不同状态码
54、react render方法原理
触发render
setState, useState hook,重新渲染
原理:
在render中,我们会编写jsx,jsx通过babel编译后就会转化成我们熟悉的js格式,如下:
在render过程中,React 将新调用的 render函数返回的树与旧版本的树进行比较,这一步是决定如何更新 DOM 的必要步骤,然后进行 diff 比较,更新 DOM树
55、如何避免不必要的Render,提高组件的渲染效率?
如父组件props改变会导致子组件重新渲染,那么方法有
- shouldComponentUpdate: 在该生命周期内比较state和props,确定是否重新渲染
- pureComponent
- React.memo:缓存组件的渲染避免不必要的更新
56、react 是怎么把jsx->dom的
babel编译把jsx转换成这种React.createElement的形式,在这个过程中
- 遇到首字母小写,原生dom
- 遇到首字母大写,自定义组件
最终都是通过ReactDom.render()方法进行挂载
57、react性能优化手段
- 避免使用内联函数:导致每次render创建一个函数实例,而是组件内部创建然后绑定
<div onClick={()=>}></div>
- 使用React Fragments 避免额外标记
export default class NestedRoutingComponent extends React.Component {render() {return (<>
<h1>This is the Header Component</h1>
<h2>Welcome To Demo Page</h2>
</>)}}
- 使用 Immutable
Immutable通过is方法则可以完成对比,而无需像一样通过深度比较的方式比较
- 懒加载组件
在react中使用到了Suspense和 lazy组件实现代码拆分功能,基本使用如下:
- 事件绑定方式
- 服务端渲染
服务端渲染,需要起一个node服务,可以使用express、koa等,调用react的renderToString方法,将根组件渲染成字符串,再输出到响应中
58、react服务端渲染是什么?怎么做?
是什么
服务器完成html拼接,发送给浏览器,为其绑定事件和状态
怎么做
- 服务器生成html
- 发送html给浏览器
- 浏览器接受到内容显示
- 浏览器加载js文件
- js代码接管并执行页面的操作
整体的原理为:
node server 接收客户端请求,得到当前的请求url 路径,然后在已有的路由表内查找到对应的组件,拿到需要请求的数据,将数据作为 props、context或者store 形式传入组件
然后基于 react 内置的服务端渲染方法 renderToString()把组件渲染为 html字符串在把最终的 html进行输出前需要将数据注入到浏览器端
浏览器开始进行渲染和节点对比,然后执行完成组件内事件绑定和一些交互,浏览器重用了服务端输出的 html 节点,整个流程结束
59、setState执行机制
必须通过setState方法来告知react组件state已经发生了改变
关于state方法的定义是从React.Component中继承,定义的源码如下:
Component.prototype.setState = function(partialState, callback) {invariant(typeof partialState === 'object' ||typeof partialState === 'function' ||
partialState == null,'setState(...): takes an object of state variables to update or a ' +'function which returns an object of state variables.',);this.updater.enqueueSetState(this, partialState, callback, 'setState');};
从上面可以看到setState第一个参数可以是一个对象,或者是一个函数,而第二个参数是一个回调函数,用于可以实时的获取到更新之后的数据
更新类型
-
在组件生命周期或React合成事件中,setState是异步
-
在setTimeout或者原生dom事件中,setState是同步
60、react有哪些特性
-
JSX 语法
-
单向数据绑定
-
虚拟 DOM
-
声明式编程
-
Component
61、js隐式转换
- 比较运算符、if、while需要
- 算术运算符
自动转换为boolean值
可以得出个小结:
- undefined
- null
- false
- +0
- -0
- NaN
- ""
除了上面几种会被转化成false,其他都换被转化成true
自动转换为字符串
'5'+function(){} 输出为 '5function()'
自动转换为数值
//null + 1 -> 1
//undefined + 1-> NaN
62、函数作用域
- 全局作用域
- 函数作用域:只能在函数内部访问
- 块级作用域: es6引入的let const
{// 块级作用域中的变量let greeting = 'Hello World!';var lang = 'English';
console.log(greeting); // Prints 'Hello World!'}// 变量 'English'
console.log(lang);// 报错:Uncaught ReferenceError: greeting is not defined
console.log(greeting);
作用域链
要是用一个变量,依次向外层作用域查找
var sex = '男';
function person() {
var name = '张三';
function student() {
var age = 18;
console.log(name); // 张三
console.log(sex); // 男
}
student();
console.log(age); // Uncaught ReferenceError: age is not defined
}
person();
student函数内部属于最内层作用域,找不到name,向上一层作用域person函数内部找,找到了输出“张三”student内部输出sex时找不到,向上一层作用域person函数找,还找不到继续向上一层找,即全局作用域,找到了输出“男”- 在
person函数内部输出age时找不到,向上一层作用域找,即全局作用域,还是找不到则报错
63、原型和原型链
原型
原型对象都有一个constructor指向该函数
暂时无法在飞书文档外展示此内容
原型链
_proto_指向各个构造函数原型对象
64、继承
- 原型链继承
不同的孩子节点实例共享同一个内存空间,其中一个实例改变另一个也会变化
Child.prototype = new Parent();
- 构造函数继承
无法继承原型属性/方法
function Child(){
Parent1.call(this);
this.type = 'child'
}
- 组合继承:合并原型链继承和构造函数继承,会导致实例化执行两次导致多构造一次的性能开销。
- 寄生式继承:object.create
- 寄生组合式继承
65、ts泛型
//ts泛型:不预定义类型,在使用时再指定
/**
* 函数声明
* @param {*} tuple
* @returns
*/
function swap<T,U>(tuple: [T, U]):[U, T]{
return [tuple[1], tuple[0]];
}
swap(['iam', 5]);
/**
* 接口声明
*/
interface Item<T>{
(para: T): T
}
//在使用的时候可以这样
const item: Item<number> = para=> para
/**
* 类声明
*/
66、reactkey如果通过下标指定key有什么问题,这个问题出现在diff算法哪个节点
element diff节点,下标指定,key不发生变化就不操作dom更新
67、http缓存
http缓存:保存资源副本并在下次请求时直接使用副本的技术。
68、ts和js的区别
69、简单请求和非简单请求
非简单请求:浏览器会发起一个options预检请求
70、webpack和其他打包工具区别
71、 type和interface区别
Type: 可以声明类型别名、联合类型和元组类型
// 基本类型别名
type Name = string
// 联合类型
interface Dog {
wong();
}
interface Cat {
miao();
}
type Pet = Dog | Cat
// 具体定义数组每个位置的类型
type PetList = [Dog, Pet]
Interface: 可以合并类型
interface User {
name: string
age: number
}
interface User {
sex: string
}
/*
User 接口为 {
name: string
age: number
sex: string
}
*/
72、call/bind/apply实现
73、借助webpack优化前端性能
js\css\html文件压缩
Tree Shaking
Tree Shaking 是一个术语,在计算机中表示消除死代码,依赖于ES Module的静态语法分析(不执行任何的代码,可以明确知道模块的依赖关系)
在webpack实现Trss shaking有两种不同的方案:
- usedExports:通过标记某些函数是否被使用,之后通过Terser来进行优化的
- sideEffects:跳过整个模块/文件,直接查看该文件是否有副作用
两种不同的配置方案, 有不同的效果
#usedExports
配置方法也很简单,只需要将usedExports设为true
module.exports = {...
optimization:{
usedExports
}}
1 2 3 4 5 6
使用之后,没被用上的代码在webpack打包中会加入unused harmony export mul注释,用来告知 Terser 在优化时,可以删除掉这段代码
如下面sum函数没被用到,webpack打包会添加注释,terser在优化时,则将该函数去掉
代码分离:将代码分离到不同的bundle中,之后我们可以按需加载,或者并行加载这些文件
splitChunks主要属性有如下:
- Chunks,对同步代码还是异步代码进行处理
- minSize: 拆分包的大小, 至少为minSize,如何包的大小不超过minSize,这个包不会拆分
- maxSize: 将大于maxSize的包,拆分为不小于minSize的包
- minChunks:被引入的次数,默认是1
74、js new一个对象
function MyNew() {
let Constructor = Array.prototype.shift.call(arguments); // 1:取出构造函数
let obj = {} // 2:执行会创建一个新对象
obj.__proto__ = Constructor.prototype // 3:该对象的原型等于构造函数prototype
var result = Constructor.apply(obj, arguments) // 4: 执行函数中的代码
return typeof result === 'object' ? result : obj // 5: 返回的值必须为对象
}
75、实现并发限制的调度器
题目描述
JS实现一个带并发限制的异步调度器Scheduler,保证同时运行的任务最多有两个。完善代码中Scheduler类,使得以下程序能正确输出
class Scheduler {
add(promiseCreator) { ... }
// ...
}
const timeout = (time) => new Promise(resolve => {
setTimeout(resolve, time)
})
const scheduler = new Scheduler()
const addTask = (time, order) => {
scheduler.add(() => timeout(time))
.then(() => console.log(order))
}
addTask(1000, '1')
addTask(500, '2')
addTask(300, '3')
addTask(400, '4')
// output: 2 3 1 4// 一开始,1、2两个任务进入队列// 500ms时,2完成,输出2,任务3进队// 800ms时,3完成,输出3,任务4进队// 1000ms时,1完成,输出1// 1200ms时,4完成,输出4
76、 react事件机制
react中,所有事件都是合成的