动画封装、同步异步

一、封装动画库

  1. // Effect 运动方式
  2. const Effect = {
  3. Linear: function (curTime, begin, change, duration) {
  4. return change / duration * curTime + begin;
  5. }
  6. };
  7. function animate({ele, target = {}, duration = 2000, after}) {
  8. // 1. 参数校验
  9. if (!ele || ele.nodeType !== 1) {
  10. throw TypeError('ele is not a DOM ELEMENT')
  11. }
  12. // 2. 准备动画需要的参数
  13. const {css} = window.utils;
  14. // 2.1 根据 target 中传入的属性,计算这些属性的起始位置以及这些属性的运动距离
  15. let begin = {};
  16. let change = {};
  17. // 遍历 target 对象
  18. for (let k in target) {
  19. if (target.hasOwnProperty(k)) {
  20. begin[k] = css(ele, k); // 用 css 方法获取元素的起始位置
  21. change[k] = target[k] - begin[k]; // 获取元素运动的路程
  22. }
  23. }
  24. // 2.2 记录当前时间
  25. let time = 0;
  26. // 2.3 设置 interval
  27. let interval = 16;
  28. // 3. 创建动画定时器
  29. ele.timer && clearInterval(ele.timer); // 每次开始新的动画时停止上一次的动画
  30. ele.timer = setInterval(() => {
  31. time += interval;
  32. if (time > duration) {
  33. css(ele, target);
  34. clearInterval(ele.timer);
  35. // 调用钩子函数
  36. if (typeof after === 'function') {
  37. after.call(ele); // 让 after 执行,并且把 after 中的 this 修改成 ele
  38. }
  39. return;
  40. }
  41. let curState = {};
  42. // 计算当前元素的位置
  43. for (let k in target) {
  44. if (target.hasOwnProperty(k)) {
  45. curState[k] = Effect.Linear(time, begin[k], change[k], duration)
  46. }
  47. }
  48. // 把当前元素的相应的属性设置为当前状态
  49. css(ele, curState);
  50. }, interval)
  51. }

调用:

  1. animate({
  2. ele: box,
  3. target: {
  4. left: 800,
  5. top: 500
  6. },
  7. duration: 2000,
  8. after: function () {
  9. alert('终于跑完了')
  10. }
  11. });

二、定时器

  • 定时器:设定一个定时器,并且设置了等待时间,等时间到了,浏览器会把定时器的回调函数执行了
  • 定时器是挂载在 window 上的方法
  • 浏览器中常用的定时器

    1. window.setTimeout(callback, timeInterval) 到时间后执行一次,也称为超时调用

  • callback: 到达时间后需要执行的方法(设定定时器时传入的第一个参数只是函数定义,并不是函数执行,真正执行是在时间到了以后,浏览器去执行这个函数)

  • timerInterval: 时间间隔
  1. let n = 0;
  2. setTimeout(() => {
  3. n++;
  4. console.log(n);
  5. }, 1000);

2. window.setInterval(callback, timeInterval) 每隔一定时间就会执行一次,也称为间歇调用

  • callback: 到达时间后需要执行的函数
  • timerInterval: 时间间隔

3. setTimeout和setInterval方法都会返回一个定时器ID,是一个数字;

4. 清除定时器

  • clearTimeout 和 clearInterval:这两个方法可以清除指定的定时器;
  • 一旦定时器被清除,不管这个定时器是否发挥过作用(回调函数是否执行过),里面的回调函数都会停止执行;

思考题:

  1. let t = 1000;
  2. setInterval(() => {
  3. console.log('123')
  4. }, t);
  5. setTimeout(() => {
  6. t = 5000
  7. }, 50);
  8. // ? setInterval 定时器执行间隔是多少?为什么?

三、同步和异步

同步和异步

JS 中在执行的时候把 js 代码分成了同步执行任务和异步执行任务

同步执行:

当前执行任务是按照顺序依次处理,当前这件事没有彻底做完,下一件事是执行不了的,

异步执行:当前这件事需要过一会儿再做或者执行时机不确定(定时器是过一会儿才会执行,js 中的事件是执行时机不确定),此时浏览器不是傻傻的等这件事做完,而是继续执行后面的任务,当后面的任务完成后,再去把没做完的事情做完。

JS 中的常见的异步执行:
  1. 所有的事件函数都是异步执行的 box.onclick = function …..
  2. 所有的定时器的回调函数都是异步执行 setTimeout(callback, time)
  3. AJAX 中的异步情形
  4. 回调函数也是异步编程
  • 异步编程:编写异步执行的程序称为异步编程,js 的异步处理机制是 js 这门语言最大的特殊。
  1. let n = 0;
  2. setTimeout(() => {
  3. n++;
  4. console.log('abc')
  5. }, 3000);
  6. console.log(n); // ? 这里输出 n 的值是多少?是

浏览器是如何实现异步机制的呢?

js 是单线程的,它一次只能干一件事。而能实现异步,是依赖于浏览器的任务队列实现的。浏览器中有两个任务队列:主任务队列和等待任务队列;

  • 主任务队列:主任务队列放的都是当前需要同步执行的同步任务;
  • 等待任务队列:把不需要立刻执行的异步任务都放到等待任务队列中

当主任务队列中的任务执行结束后,就去执行等待任务队列中的任务,看看哪些任务该执行了,如果某个任务该执行了,那么浏览器就会执行它。如果有多个都达到执行条件了,谁先到的就先执行谁。

注意:如果主任务队列中的代码执行不完,那么等待任务队列中的任务是不会执行的;

  1. setTimeout(() => console.log('结束啦'), 1000);
  2. while (true) {
  3. // while 循环是个同步任务
  4. }
  • 死循环就相当于主任务队列没有执行完,所以即便定时器到了时间也没办法执行
  1. // => 思考题
  2. setTimeout(() => {
  3. console.log(1);
  4. }, 2000);
  5. console.log(2);
  6. setTimeout(() => {
  7. console.log(3)
  8. }, 3000);
  9. console.log(4);
  10. setTimeout(() => {
  11. console.log(10)
  12. }, 0);
  13. for(let i = 0; i < 9999999; i++) {}
  14. console.log(5);
  15. setTimeout(() => {
  16. console.log(6)
  17. }, 1400);
  18. console.log(7);
  19. setTimeout(() => {
  20. console.log(8)
  21. }, 1500);
  22. console.log(9);
  23. // ? 输出顺序是啥?
  • 定时器的事件写0也不是同步任务,只要是定时器就是异步任务,也需要等到所有的同步任务都执行完成再执行等待任务队列中的任务。因此即使写0,浏览器也存在等待和反应时间,这个时间依赖于浏览器版本以及操作系统和硬件,当设置的最小时间小于这个时间时,定时器使用的就是自身最小的反应和等待时间。
  • 当主任务队列自上而下的执行的时候,如果遇到一个异步操作,浏览器不会等着这个异步的操作,而是先把这个异步任务添加到等待任务队列中排队
  • 当主任务队列中的任务执行完成后,才会去等待任务队列中(如果主任务队列执行不完,不管等待任务队列中的任务时候到达时间,都不会执行);
  • 等待任务队列中谁先到达条件了(如果有多个都到达条件了,谁先到的就先处理谁),就把这个任务放到主任队列中执行,执行完再去等待任务找…

四、回调函数

回调函数是什么?

把一个函数 A 当做实参传递给另一个函数 B,在函数 B 执行的时候,在需要的地方就会把 A 执行,我们把这种机制称为“回调函数机制”;

回调函数的特点:

  1. 根据需求回调函数可以被执行 N 次;
  2. 不仅可以把回调函数执行,还可以给传递的回调函数传递实参,这样在回调函数中设置形参(或者使用 ARG)接收即可
  3. 还可以改变回调函数中的 this 指向
  4. 可以在宿主函数(它在哪里执行,它的宿主函数就是谁)中接收回调函数执行的返回结果
  • 传递一个匿名函数:
  1. let fn = (callback) => {
  2. // callback
  3. // callback && callback.call(obj, 100, 200);
  4. // typeof callback === 'function' ? callback() : null;
  5. let res = callback(10, 20);
  6. console.log(res);
  7. };
  8. fn(function (n, m) {
  9. console.log(this, n, m);
  10. return n + m;
  11. });
  12. // 此时 function (n, m) {....} 就是回调函数 它是被当成实参传递给fn函数的。fn就是其宿主函数
  • 传递一个函数名:
  1. function goo(n, m) {
  2. return n + m;
  3. }
  4. fn(goo); // 此时 goo 就是回调函数

常见的内置的回调函数:

  1. 定时器
  2. 数组 sort 方法
  3. 数组 forEach
  4. 数组 map 方法

实现一个 forEach 方法

  1. let forEach = (iteratorable, callback) => {
  2. for (let i = 0; i < iteratorable.length; i++) {
  3. callback(iteratorable[i], i);
  4. }
  5. };
  6. let ary = [1, 2, 3];
  7. forEach(ary, function (item, index) {
  8. console.log(item, index)
  9. });

五、Promise

  • Promise: 是 ES6 新增的类(new Promise),目的是为了【管理】JS 中的异步编程的;
  1. let p = new Promise();
  2. p.then();
  • promise 对象有三个状态,
    pending (准备:初始化成功,开始执行异步任务)
    fulfilled (成功:异步处理成功)
    rejected (失败:异步处理异常)

创建一个 Promise 实例

  1. new Promise(() => {
  2. // new Promise 时,创建 Promise 的一个实例时,会立即会把当前函数体中执行
  3. setTimeout(() => {
  4. console.log(3);
  5. }, 1000);
  6. console.log(1); // 先输出 1
  7. }).then();
  8. console.log(2); // 再输出 2
  • Promise 是同步的,是用来管理异步的;

如何管理异步呢:

  1. new Promise((resolve, reject) => {
  2. // Promise 的回调函数有两个形参,这两个形参代表着两个函数(将来浏览器调用 Promise 回调时传进来的)
  3. // resolve:当异步操作执行成功,我们执行 resolve 方法
  4. // reject:当异步操作执行失败,我们执行 reject 方法
  5. setTimeout(function () {
  6. resolve('Oh yeah!');
  7. }, 3000);
  8. }).then((res) => {
  9. // => 第一个传递的函数是 resolve(现在可以这么说)
  10. console.log('成功 ', res)
  11. }, (err) => {
  12. // => 第二个传递的函数是 reject
  13. console.log('完犊子 ', err);
  14. });

六、异步的 ajax

  1. let val = null;
  2. let xhr = new XMLHttpRequest();
  3. xhr.open('GET', 'js/1.js', true);
  4. xhr.onreadystatechange = function () {
  5. if (xhr.readyState === 4 && xhr.status === 200) {
  6. val = xhr.responseText;
  7. }
  8. };
  9. xhr.send();
  10. console.log(val); // 如果使用异步的 AJAX 请求,不等 AJAX 彻底完成,就把 VAL 输出,此时的结果是 null;
  11. // 这是为什么?因为 console.log(val) 是同步任务;此时,AJAX 是异步任务,异步任务的执行晚于 console.log(val) 同步任务,就是说在打印 val 时,ajax 还没执行完,所以 val 的值还是 null
  12. // ? 这怎么办?
  13. // 1. 使用回调函数
  14. function ajax(callback) {
  15. let xhr = new XMLHttpRequest();
  16. xhr.open('GET', 'js/1.js', true);
  17. xhr.onreadystatechange = function () {
  18. if (xhr.readyState === 4 && xhr.status === 200) {
  19. callback(xhr.responseText);
  20. }
  21. };
  22. xhr.send(null);
  23. }
  24. function getData(data) {
  25. console.log(data);
  26. }
  27. ajax(getData);
  28. // 2. 使用 Promise 解决
  29. let p = new Promise((resolve, reject) => {
  30. let xhr = new XMLHttpRequest();
  31. xhr.open('GET', 'js/1.js', true);
  32. xhr.onreadystatechange = function () {
  33. if (xhr.readyState === 4 && xhr.status === 200) {
  34. // 成功
  35. let val = xhr.responseText;
  36. resolve(val);
  37. }
  38. if (xhr.status === 4 && xhr.status !== 200) {
  39. // 失败
  40. reject('完蛋');
  41. }
  42. };
  43. xhr.send();
  44. });
  45. p.then((data) => {
  46. console.log(data)
  47. }, (err) => {
  48. console.log(err)
  49. });