深入理解 Promise,看看你会几道题

1. Promise A+规范的基本概念

Promise是一套专门处理异步场景的规范,它能有效的避免回调地狱的产生,使异步代码更加清晰、简洁、统一

这套规范最早诞生于前端社区,规范名称为Promise A+

该规范出现后,立即得到了很多开发者的响应

Promise A+ 规定:

  1. 所有的异步场景,都可以看作是一个异步任务,每个异步任务,在JS中应该表现为一个对象 ,该对象称之为Promise对象,也叫做任务对象

  2. 每个任务对象,都应该有两个阶段、三个状态

    根据常理,它们之间存在以下逻辑:

    • 任务总是从未决阶段变到已决阶段,无法逆行
    • 任务总是从挂起状态变到完成或失败状态,无法逆行
    • 时间不能倒流,历史不可改写,任务一旦完成或失败,状态就固定下来,永远无法改变
  3. 挂起->完成,称之为resolve挂起->失败称之为reject。任务完成时,可能有一个相关数据;任务失败时,可能有一个失败原因。

  4. 可以针对任务进行后续处理,针对完成状态的后续处理称之为onFulfilled,针对失败的后续处理称之为onRejected

2. 创建Promise

js 复制代码
// 创建一个任务对象,该任务立即进入 pending 状态
const pro = new Promise((resolve, reject) => {
  // 任务的具体执行流程,该函数会立即被执行
  // 调用 resolve(data),可将任务变为 fulfilled 状态, data 为需要传递的相关数据
  // 调用 reject(reason),可将任务变为 rejected 状态,reason 为需要传递的失败原因
});

pro.then(
  (data) => {
    // onFulfilled 函数,当任务完成后,会自动运行该函数,data为任务完成的相关数据
  },
  (reason) => {
    // onRejected 函数,当任务失败后,会自动运行该函数,reason为任务失败的相关原因
  }
);

3. 针对Promise进行后续处理

下面任务的最终状态是什么,相关数据或失败原因是什么,最终输出是什么?

js 复制代码
const pro1 = new Promise((resolve, reject) => {
    console.log('开始任务');
    resolve(1);
    reject(2);
    resolve(3);
    console.log('结束任务');
})
console.log(pro1);

const pro2 = new Promise((resolve, reject) => {
    console.log('开始任务');
    resolve(1);
    resolve(2);
    console.log('结束任务');
})
console.log(pro2);
  • 这里有一点需要注意,promise的状态一旦确定下来之后,是不会发生变化的,但后续的代码还会继续执行。

4.Promise链式调用

  1. then方法必须会返回一个新的promise, 可以理解为 后续处理也是一个任务

  2. 新任务的状态取决于后续处理:

    • 若没有相关的后续处理,新任务的状态和前任务一致,数据为前任务的状态。
    • 若有后续处理但未执行,新任务挂起。
    • 若后续处理执行了,则根据后续处理的情况确定新任务的状态。
      • 后续处理执行无错,新任务的状态未完成,数据为后续处理的返回值。
      • 后续处理执行有错,新任务的状态为失败,数据为异常数据。
      • 后续执行后返回时一个任务对象,新任务的状态和数据与该任务对象一致。

为了更直观的练习链式调用,来看几个题目:

js 复制代码
// 下面代码的输出结果是什么
const pro1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1);
  }, 1000);
});

const pro2 = pro1.then((data) => {
  console.log(data);
  return data + 1;
});

const pro3 = pro2.then((data) => {
  console.log(data);
});

console.log(pro1, pro2, pro3);

setTimeout(() => {
  console.log(pro1, pro2, pro3);
}, 2000);
  • 结果是先输出三个padding状态的promise, 然后再打印 1 2 undefined,为啥?
  • 首先来看第一个起始任务pro1, 等待1秒之后成功,pro1处于padding状态,接着产生了第二个任务pro2,pro2是pro1的后续处理,所以毫无疑问也是padding状态,里面的任务先不会执行,同样的pro3也是padding挂起状态。
  • 接着继续执行pro1里面的代码,1秒钟到了后,pro1变成了fulfilled 数据是1;继续执行pro2里面的代码,打印了pro1传递过来的1,pro2变成了fulfilled data+1 返回一个2;pro3打印pro2传递过来的2,pro3状态变成fufilled, 没有返回结果,所以打印undefined;

再来一道,稍微改一下,把pro2的then改成catch:

js 复制代码
const pro1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1);
  }, 1000);
});

const pro2 = pro1.catch((data) => {
  console.log(data);
  return data + 1;
});

const pro3 = pro2.then((data) => {
  console.log(data);
});

console.log(pro1, pro2, pro3);

setTimeout(() => {
  console.log(pro1, pro2, pro3);
}, 2000);
  • 结果是先输出三个padding状态的promise, 然后再打印 1 1 undefined
  • 起始任务pro1, 等待1秒之后成功,pro1处于padding状态,打印fulfilled 1;接着产生了第二个任务pro2,但是这里pro2并没有对pro1的成功做处理,而是用catch, pro2的跟pro1一样处于padding状态,打印fulfilled 1;pro3针对pro2的成功做了处理,输出完成的数据1,pro3的运行状态取决于pro2的运行结果,运行过程没有报错,padding状态,并没有返回结果,打印fulfilled undefined。
  • 最后打印 3个padding 1 fufilled 1 fufilled 1 fufilled undefined

再来改一下,给pro2抛一个错

js 复制代码
const pro1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1);
  }, 1000);
});

const pro2 = pro1.then((data) => {
  throw 3;
  return data + 1;
});

const pro3 = pro2.then((data) => {
  console.log(data);
});

console.log(pro1, pro2, pro3);

setTimeout(() => {
  console.log(pro1, pro2, pro3);
}, 2000);
  • 默认pro1、pro2、pro3 都是pendding状态,pro1打印fufilled 1;运行pro2的过程中报了一个错,错误对象是3,pro2处于 rejected状态,下面的return不会运行,因为前面报错了;pro3 没有针对pro2的错误进行处理,pro3的状态跟pro2完全一致,rejected 3.
  • 最终输出 3个padding, fufilled 1 reject 3 reject 3

继续下一道题目:

js 复制代码
const pro = new Promise((resolve, reject) => {
    resolve(1)
}).then((res) => {
    console.log(res);
    return 2;
}).catch((err) => {
    return 3;
}).then((res) => {
    console.log(res);
})

cnosole.log(pro)
  • 你可能会以为是 1 3 undefined,为啥不是呢?
  • 首先一步一步来分析,第一个promise处于padding resolve一个1,在第二个promise打印,输出 fulfilled 1; 第三个promise只处理了失败的结果,并且return 3,问题就在这里,你可能以为会把这个3给到第四个 promise,结果确实由于第三个promise没有对第二个promise成功的状态进行处理,所以延续了第二个promise的fulfilled 2的结果;第4个promise的结果取决于上一个promise,第三个promise成功了,并且延续的第二个的promise,所以在第四个promise打印的时候还是2,并不是3,最后第4个promise没有结果可返回,打印了一个undefined。

再稍微改造一下:

js 复制代码
const pro = new Promise((resolve, reject) => {
    resolve()
}).then((res) => {
    console.log(res.toString());
    return 2;
}).catch((err) => {
    return 3;
}).then((res) => {
    console.log(res);
})
console.log(pro)
  • 第一个promise 这里简称pro1, pro1 resolve一个空,所以是fuilled undefined;pro2 将pro1延续的undefined进行toString(), 所以肯定报错,pro2变成了rejected状态;因为pro2报错,pro3的状态取决于pro2的处理,pro3捕获到了异常,并成功return了一个3,运行状态为fulfilled 返回一个3;pro4运行过程中没有错误,运行状态为 fufilled 打印pro3延续的结果3,pro4没有返回值,最后打印undefined

继续下一道:

js 复制代码
new Promise((resolve, reject) => {
  resolve(1)
})
  .then((res) => {
    console.log(res);
    return new Error('2');
  })
  .catch((err) => {
    throw err;
    return 3;
  })
  .then((res) => {
    console.log(res);
  });
  • 首先第一个pro1,resolve了一个1,fulfilled 1;pro2打印1,抛出异常,fulfilled error('2'); pro3捕获到了异常,同时抛出异常,后面的return 3 不会执行,fulfilled error('2');pro4 打印pro3的错误,fulfilled undefined;
js 复制代码
const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject();
  }, 1000);
});
const promise2 = promise1.catch(() => {
  return 2;
});

console.log('promise1', promise1);
console.log('promise2', promise2);

setTimeout(() => {
  console.log('promise1', promise1);
  console.log('promise2', promise2);
}, 2000);
  • 一开始打印的两个promise 都处于padding挂起状态, 1秒后 promise1被拒绝了,rejected undefined;promise2对promise1的错误进行处理,并返回了一个2,promise2成功运行 fulfilled 2;

换一个类型的题目:

js 复制代码
const promise = new Promise((resolve, reject) => {
    console.log(1);
    setTimeout(() => {
        console.log(2);
        resolve();
        console.log(3);
    });
}).then(() => {
    console.log(4);
})

console.log(5);
  • 运行第一个promise,输出1,setTimeout没有传第二个参数,你可能会以为会立即执行,但是因为setTimeout属于宏任务,就算没有传递第二个参数,被分配到宏任务队列也是处于等待执行状态,因为resolve在setTimeout里面,整个promise没有改变状态,依旧还是padding状态;
  • 第二个promise的then需要等待成功之后才会执行,但是因为上一个promise未成功,第二个promise不会进入微任务队列,所以不会执行;
  • 接着打印全局作用域的5,全局作用域的任务执行完了,就会把宏任务队列的setTimeout进行执行,打印2,返回第一个promise的resolve状态,第二个promise进入微任务队列,第一个promise继续打印3,第一个promise执行完了,轮到第二个promise执行,打印4;最终结果1 5 2 3 4
js 复制代码
setTimeout(() => {
    console.log(1);
});

const promise = new Promise((resolve, reject) => {
    console.log(2);
    resolve();
}).then(() => {
    console.log(3);
})
console.log(4);
  • 首先登场的是一个setTimeout,加入到宏队列;其次是一个promise,输出一个2,resolve将状态改为fulfilled;
  • .then开启了第二个promise,进入微任务队列,这个时候不会立即执行微任务队列,因为全局作用域还有代码没执行,全局作用域打印一个4
  • 接着执行微任务队列,打印3,最后执行宏任务,打印1

来个简单一点的:

js 复制代码
const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject();
  }, 1000);
});
const promise2 = promise1.catch(() => {
  return 2;
});

console.log('promise1', promise1);
console.log('promise2', promise2);

setTimeout(() => {
  console.log('promise1', promise1);
  console.log('promise2', promise2);
}, 2000);
  • 有了前面的积累,充满的一肯定一眼就能看出输出啥了,没错,首先是两个padding,2s后打印undefined 2

5. async await 消除回调

  • async 关键字用于修饰函数,一般出现在函数前面,一定返回一个promise;
  • await 关键字表示等待某个Promise完成,它必须用于async函数中;
js 复制代码
async function m(){
    console.log(0);
    const n = await 1;
    console.log(n);
}
m();
console.log(2);
  • 为啥是 0 2 1,脑瓜子是不是嗡嗡的,听我细细道来,别被语法糖迷惑住了,async 后面的一定返回一个promise,m函数里面首先打印一个0;
  • const n = await 1;console.log(n) 相当于 Promise.resolve(1).then((e) => { console.log(n); }) , 给n赋值和输出n这段逻辑本质上是在微队列,打印完0之后不会卡住等待await执行,而是结束掉m函数,执行全局作用域的代码,打印2;最后执行微队列的代码,打印1,有意思吧;

再来一道:

js 复制代码
async function m() {
  console.log(0);
  const n = await 1;
  console.log(n);
}

(async () => {
  await m();
  console.log(2);
})();

console.log(3);
  • 看完不自信了吧,为啥?看了这么多题,还是会错?别急,听我一步一步分析
  • 首先看括号里面的立即执行函数,立即执行函数里面await调用m()函数,打印0,后面的await进入微队列等待,那么立即执行函数里面的await返回的promise也是处于padding状态,下面的代码无法继续执行;
  • 回到全局作用域,打印3,执行栈清空,开始执行微队列,打印1,m()函数执行完,可以执行立即执行函数了,打印2;最后打印 0 3 1 2

继续下一道:

js 复制代码
async function m1() {
  return 1;
}

async function m2() {
  const n = await m1();
  console.log(n);
  return 2;
}

async function m3() {
  const n = m2();
  console.log(n);
  return 3;
}

m3().then((n) => {
  console.log(n);
});

m3();

console.log(4);
  • 嘿,朋友,还好吗?别自闭了,这个确实有点绕,也不知道谁脑洞这么大出的这题,如果你有此困惑,说明还没彻底掌握,坚持住,听我逐一分析。
  • 首先直接看m3.then(), m3返回一个promise, m3里面调用m2,m2里面调用m1,m1里面 return 了一个1,状态为fulfilled 1;
  • m2的await m1()相当于 await 1,这个时候打印n,后续代码进入微队列,状态为padding。
  • m3 const n = m2() 注意这里没有使用 await,打印n,返回3,状态为fulfilled 3;
  • m3执行完了,调用 .then 函数,.then里面的n是m3 返回的 3,所以打印3;
  • 接着下面又调用了一次m3,继续m3调用m2,m2调用m1,m1返回1,状态为fufilled 1;m2 的await 进入微队列,后续代码进入微队列,状态为padding;m3没有等待m2,直接完成,返回3;
  • 最后输出全局作用域的4,微队列的一个个拿出来,先输出 1,接着是3 1,最后打印 4 1 3 1

累了没?那来一道看起来比较简单的开火车题目:

js 复制代码
Promise.resolve(1).then(2).then(Promise.resolve(3)).then(console.log);
  • 你个老六,说好的比较简单,怎么火车上面还有雷,2和3去哪了,被炸了吗?
  • 别慌,事出反常必有妖,且听我慢慢分析,事情还得从 .then(2)开始,注意,这里不是一个函数,而是一个2,这不合常理,你只要记住一点,以后看到then里面传的不是函数,直接忽略这段代码,为什么?因为promise的then发现你传递的不是一个promise,也就是没有注册回调函数,那它的结果和状态和上一个promise是一致的,相当于 .then(null);
  • 继续看 .then(Promise.resolve(3)),.then里面传递的是一个promise对象,依旧不是函数,所以是无效的传递,也相当于 .then(null);
  • 最后一个不用解释了,.then(console.log)console.log是一个函数,打印undefined。
  • 奇怪的知识又增加了吧,记得关注我,我会提供更多查漏补缺的知识,来刷新你的认识。

来一个比较经典的问题,你可能会比较熟悉:

js 复制代码
var a;
var b = new Promise((resolve, reject) => {
  console.log('promise1');
  setTimeout(() => {
    resolve();
  }, 1000);
})
  .then(() => {
    console.log('promise2');
  })
  .then(() => {
    console.log('promise3');
  })
  .then(() => {
    console.log('promise4');
  });

a = new Promise(async (resolve, reject) => {
  console.log(a);
  await b;
  console.log(a);
  console.log('after1');
  await a;
  resolve(true);
  console.log('after2');
});

console.log('end');
  • 这个题有点复杂,但是别乱了阵脚,还是逐一分析。
  • 首先定义一个a,值为undefined,然后定义一个b,你可能会以为直接是一个promise,但在js里面变量的声明和赋值是有先后顺序的,出现b这种,得先把promise算出来,才能赋值给b,在没算出来之前,都是undefined。
  • 来看b的表达式,开了一个火车,从第一个promise开始,resolve放在一个定时器里面,也就是1s之后才能确定后面promise的状态,这里有个小知识,不管promise的火车有多长,.then接的有多少个,返回的都是最后一个 .then ,所以b是一个 padding状态的promise,打印 'promise1'。
  • 继续看下面代码,创建了一个新的promise赋值给a,new Promise 表达式里面首先打印a,因为a还没有成功赋值,所以打印上面的undefined;
  • 接着看await b 等待上面的代码,因此后面await b后面的代码不会执行,进入微队列,因此整个赋值给b的promise执行解释,状态为padding,赋值给a;
  • 执行打印全局作用域的 end ,等待1s后,定时器结束,赋值给b的promis状态为fufilled,执行后面的 .then 函数,开火车,依次打印 'promise2' 'promise3' 'promise4';
  • b的promise赋值完成之后,微任务队列中 await b后面的代码可以执行了,接着输出一个 'after1',又继续打印a,a这里前面执行过,为padding状态的promise;
  • 接着看 await a, 自己等待padding状态的自己之后再执行下面的代码?这事肯定没尽头,所以后面的代码不会再执行了。

继续下一道,我怕再玩下去,你要忍不住到要打我了:

js 复制代码
async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}
async function async2() {
  console.log('async2');
}

console.log('script start');

setTimeout(function () {
  console.log('setTimeout');
}, 0);

async1();

new Promise(function (resolve) {
  console.log('promise1');
  resolve();
}).then(function () {
  console.log('promise2');
});
console.log('script end');
  • 这是一道promise async 混合的题目,但是不要慌,其实都一样。
  • 首先打印一个 'script start', 再看宏任务,有一个setTimeout,等待0秒,排队去,再看微任务,async1()被调用,打印 'async1 start',然后await 异步调用async2(),打印async2,返回一个promise,后续代码进入微队列;
  • 继续回到外层的 new Promise,打印 'promise1',resolve完成,后面的.then进入微队列;
  • 继续回到全局作用域的代码,打印 'script end',主线程执行完了,开始从微任务队列找代码执行,微任务首先打印async1里面等待的 'async1 end', async1这个promise状态完成,fufilled,然后再输出 new Promise 中挂起的 'promise2',微队列清空;
  • 再看宏队列,打印 'setTimeout',结束。

最后一道,绝对颠覆你对finally的认知:

js 复制代码
Promise.resolve('1')
  .then(res => {
    console.log(res)
  })
  .finally(() => {
    console.log('finally')
  })
Promise.resolve('2')
  .finally(() => {
    console.log('finally2')
  	return '我是finally2返回的值'
  })
  .then(res => {
    console.log('finally2后面的then函数', res)
  })

打印结果:

  • 你可能会疑惑,为什么在执行第一个promise的时候,打印完1,没有继续执行finally,而是转而执行第二个promise的finally?
  • 这个问题涉及到 JavaScript 中 Promise 的执行机制,特别是微任务(microtask)的调度顺序。让我为你详细解答,为什么在执行第一个 Promise 的 .then 并打印 '1' 后,没有立即执行它的 .finally,而是转而执行第二个 Promise 的 .finally

为了方便理解,先看看代码:

javascript 复制代码
Promise.resolve('1')
  .then(res => {
    console.log(res)  // 打印 '1'
  })
  .finally(() => {
    console.log('finally')  // 打印 'finally'
  })

Promise.resolve('2')
  .finally(() => {
    console.log('finally2')  // 打印 'finally2'
    return '我是finally2返回的值'
  })
  .then(res => {
    console.log('finally2后面的then函数', res)  // 打印 'finally2后面的then函数 2'
  })

运行结果是:

csharp 复制代码
1
finally2
finally
finally2后面的then函数 2

你好奇的是:为什么在打印 '1' 后,没有紧接着打印 'finally',而是先打印了 'finally2'

这与 JavaScript 的 微任务调度机制 有关。让我们一步步拆解:

1. Promise 的链式调用与微任务队列

  • 在 JavaScript 中,Promise 的 .then.catch.finally 都会返回一个新的 Promise 对象。
  • 当一个 Promise 完成(resolved 或 rejected)时,它注册的回调(如 .then.finally 中的函数)会被加入 微任务队列
  • 微任务队列是先进先出(FIFO)的,任务会按照加入队列的顺序依次执行。

2. 同步代码的执行

当代码运行时,同步部分会立即执行:

  • 第一个 PromisePromise.resolve('1') 创建一个立即 resolved 的 Promise,值为 '1'。它的 .then 回调(console.log(res))被加入微任务队列。
  • 第二个 PromisePromise.resolve('2') 也创建一个立即 resolved 的 Promise,值为 '2'。它的 .finally 回调(console.log('finally2'))也被加入微任务队列。

同步代码执行完后,微任务队列的初始状态是:

  • 队列:[第一个 Promise 的 .then, 第二个 Promise 的 .finally]

3. 微任务的逐步执行

JavaScript 的事件循环会在同步代码执行完后,清空微任务队列。让我们看看具体过程:

  • 执行第一个任务

    • 从队列中取出 第一个 Promise 的 .then,执行 console.log('1'),打印 '1'
    • .then 执行后,返回一个新的 Promise(默认 resolved),并将 .finally(() => { console.log('finally') }) 加入微任务队列。
    • 此时队列变为:[第二个 Promise 的 .finally, 第一个 Promise 的 .finally]
  • 执行第二个任务

    • 队列中下一个任务是 第二个 Promise 的 .finally,执行 console.log('finally2'),打印 'finally2'
    • .finally 执行后,返回一个新的 Promise(值为原始的 '2'),并将 .then(res => { console.log('finally2后面的then函数', res) }) 加入微任务队列。
    • 队列变为:[第一个 Promise 的 .finally, 第二个 Promise 的 .then]
  • 执行第三个任务

    • 执行 第一个 Promise 的 .finally,打印 'finally'
    • 队列变为:[第二个 Promise 的 .then]
  • 执行第四个任务

    • 执行 第二个 Promise 的 .then,打印 'finally2后面的then函数 2'
    • 队列清空,执行结束。

4. 关键点解答

为什么在打印 '1' 后没有立即执行 'finally'?原因在于:

  • 当第一个 Promise 的 .then 执行时,它的 .finally 回调并不是立即执行,而是被加入微任务队列的末尾。
  • 此时,第二个 Promise 的 .finally 已经在队列中(因为它在同步代码执行时就加入了),而且排在 第一个 Promise 的 .finally 前面。
  • 微任务队列严格遵循先进先出的原则,因此在执行完 第一个 Promise 的 .then 后,会先执行队列中已有的 第二个 Promise 的 .finally(打印 'finally2'),然后才轮到 第一个 Promise 的 .finally(打印 'finally')。

为了更直观地展示,以下是队列的演变过程:

  1. 初始状态(同步代码后):

    • 队列:[第一个 Promise 的 .then, 第二个 Promise 的 .finally]
  2. 执行 .then 后 (打印 '1'):

    • 队列:[第二个 Promise 的 .finally, 第一个 Promise 的 .finally]
  3. 执行第二个 .finally 后 (打印 'finally2'):

    • 队列:[第一个 Promise 的 .finally, 第二个 Promise 的 .then]
  4. 执行第一个 .finally 后 (打印 'finally'):

    • 队列:[第二个 Promise 的 .then]
  5. 执行第二个 .then 后 (打印 'finally2后面的then函数 2'):

    • 队列:空

在执行第一个 Promise 的 .then 并打印 '1' 后,它的 .finally 没有立即执行,是因为 .finally 的回调是在 .then 执行后才加入微任务队列的。而第二个 Promise 的 .finally 在同步代码阶段就已加入队列,排在前面。因此,微任务调度机制决定先执行 finally2,然后才执行第一个 Promise 的 finally。这种顺序是由微任务队列的 FIFO 特性决定的,确保了异步任务的执行有条不紊。

总结

  • 优先级:同步代码 > 微任务 > 宏任务;微任务队列必须清空后才会处理下一个宏任务。
  • 状态不可逆:一旦从 Pending 变为其他状态,不能再改变;
  • js代码永远不会卡住,后面有代码就会继续执行后面的;
  • 任何一个 .then() 抛出错误,会直接跳到最近的 .catch()
  • then里面如果传的不是函数,直接忽略这段代码,因为promise的then发现你传递的不是一个promise,也就是没有注册回调函数,那它的结果和状态和上一个promise是一致的,相当于 .then(null);
  • .finally()方法的回调函数不接受任何的参数,不管Promise对象最后的状态如何都会执行;
  • 微任务队列是先进先出(FIFO)的,任务会按照加入队列的顺序依次执行;
  • .then、.catch 和 .finally 会返回一个新的 Promise 对象,事件循环会在同步代码执行完后,清空微任务队列。
相关推荐
inksci30 分钟前
Vue 3 中通过 this. 调用 setup 暴露的函数
前端·javascript·vue.js
未来之窗软件服务1 小时前
monaco-editor 微软开源本地WEB-IDE-自定义自己的开发工具
开发语言·前端·javascript·编辑器·仙盟创梦ide
白白糖1 小时前
二、HTML
前端·html
子燕若水1 小时前
continue dev 的配置
java·服务器·前端
学习HCIA的小白1 小时前
关于浏览器对于HTML实体编码,urlencode,Unicode解析
前端·html
向明天乄2 小时前
Vue3 后台管理系统模板
前端·vue.js
香蕉可乐荷包蛋2 小时前
vue 常见ui库对比(element、ant、antV等)
javascript·vue.js·ui
彩旗工作室3 小时前
Web应用开发指南
前端
孙俊熙3 小时前
react中封装一个预览.doc和.docx文件的组件
前端·react.js·前端框架
wuhen_n4 小时前
CSS元素动画篇:基于当前位置的变换动画(四)
前端·css·html·css3·html5