js:promise;dom;事件;圆形绘制;渐变;

Promise

houdunren.com (opens new window)open in new window

JavaScript  中存在很多异步操作,Promise  将异步操作队列化,按照期望的顺序执行,返回符合预期的结果。可以通过链式调用多个  Promise  达到我们的目的。

Promise 在各种开源库中已经实现,现在标准化后被浏览器默认支持。

promise 是一个拥有  then  方法的对象或函数

#open in new window问题探讨

下面通过多个示例来感受一下不使用  promise  时,处理相应问题的不易,及生成了不便阅读的代码。

#open in new window定时嵌套

下面是一个定时器执行结束后,执行另一个定时器,这种嵌套造成代码不易阅读

<style>
  div {
    width: 100px;
    height: 100px;
    background: yellowgreen;
    position: absolute;
  }
</style>

<body>
  <div></div>
</body>

<script>
  function interval(callback, delay = 100) {
    let id = setInterval(() => callback(id), delay);
  }

  const div = document.querySelector("div");
  interval(timeId => {
    const left = parseInt(window.getComputedStyle(div).left);
    div.style.left = left + 10 + "px";
    if (left > 200) {
      clearInterval(timeId);
      interval(timeId => {
        const width = parseInt(window.getComputedStyle(div).width);
        div.style.width = width - 1 + "px";
        if (width <= 0) clearInterval(timeId);
      }, 10);
    }
  }, 100);
</script>

#open in new window图片加载

下面是图片后设置图片边框,也需要使用回调函数处理,代码嵌套较复杂

function loadImage(file, resolve, reject) {
  const image = new Image();
  image.src = file;
  image.onload = () => {
    resolve(image);
  };
  image.onerror = () => {
    reject(new Error("load fail"));
  };
  document.body.appendChild(image);
}

loadImage(
  "images/houdunren.png",
  (image) => {
    image.style.border = "solid 5px red";
  },
  (error) => {
    console.log(error);
  }
);

#open in new window加载文件

下面是异步加载外部JS文件,需要使用回调函数执行,并设置的错误处理的回调函数

function load(file, resolve, reject) {
  const script = document.createElement("script");
  script.src = file;
  script.onload = resolve;
  script.onerror = reject;
  document.body.appendChild(script);
}
load(
  "js/hd.js",
  (script) => {
    console.log(`${script.path[0].src} 加载成功`);
    hd();
  },
  (error) => {
    console.log(`${error.srcElement.src} 加载失败`);
  }
);

实例中用到的  hd.js  与  houdunren.js  内容如下

# hd.js
function hd() {
  console.log("hd function run");
}

# houdunren.js
function houdunren() {
  console.log("houdunren function run");
  hd();
}

如果要加载多个脚本时需要嵌套使用,下面houdunren.js  依赖  hd.js,需要先加载hd.js  后加载houdunren.js

不断的回调函数操作将产生回调地狱,使代码很难维护

load(
  "js/hd.js",
  (script) => {
    load(
      "js/houdunren.js",
      (script) => {
        houdunren();
      },
      (error) => {
        console.log(`${error.srcElement.src} 加载失败`);
      }
    );
  },
  (error) => {
    console.log(`${error.srcElement.src} 加载失败`);
  }
);

#open in new window异步请求

使用传统的异步请求也会产生回调嵌套的问题,下在是获取大军的成绩,需要经过以下两步

  1. 根据用户名取得 大军 的编号
  2. 根据编号获取成绩

示例中用到的 php 文件请在  版本库  (opens new window)open in new window中查看

启动 PHP 服务器命令  php -S localhost:8080

function ajax(url, resolve, reject) {
  let xhr = new XMLHttpRequest();
  xhr.open("GET", url);
  xhr.send();
  xhr.onload = function () {
    if (this.status == 200) {
      resolve(JSON.parse(this.response));
    } else {
      reject(this);
    }
  };
}
ajax("http://localhost:8888/php/user.php?name=大军", (user) => {
  ajax(
    `http://localhost:8888/php/houdunren.php?id=${user["id"]}`,
    (response) => {
      console.log(response[0]);
    }
  );
});

#open in new window肯德基

下面是模拟肯德基吃饭的事情,使用  promise  操作异步的方式每个阶段会很清楚

let kfc = new Promise((resolve, reject) => {
  console.log("肯德基厨房开始做饭");
  resolve("我是肯德基,你的餐已经做好了");
});
let dad = kfc.then((msg) => {
  console.log(`收到肯德基消息: ${msg}`);
  return {
    then(resolve) {
      setTimeout(() => {
        resolve("孩子,我吃了两秒了,不辣,你可以吃了");
      }, 2000);
    },
  };
});
let son = dad.then((msg) => {
  return new Promise((resolve, reject) => {
    console.log(`收到爸爸消息: ${msg}`);
    setTimeout(() => {
      resolve("妈妈,我和大军爸爸吃完饭了");
    }, 2000);
  });
});
let ma = son.then((msg) => {
  console.log(`收到孩子消息: ${msg},事情结束`);
});

而使用以往的回调方式,就会让人苦不堪言

function notice(msg, then) {
  then(msg);
}
function meal() {
  notice("肯德基厨房开始做饭", (msg) => {
    console.log(msg);
    notice("我是肯德基,你的餐已经做好", (msg) => {
      console.log(`收到肯德基消息: ${msg}`);
      setTimeout(() => {
        notice("孩子,我吃了两秒了,不辣,你可以吃了", (msg) => {
          console.log(`收到爸爸消息: ${msg}`);
          setTimeout(() => {
            notice("妈妈,我和大军爸爸吃完饭了", (msg) => {
              console.log(`收到孩子消息: ${msg},事情结束`);
            });
          }, 2000);
        });
      }, 2000);
    });
  });
}
meal();

#open in new window异步状态

Promise 可以理解为承诺,就像我们去 KFC 点餐服务员给我们一引取餐票,这就是承诺。如果餐做好了叫我们这就是成功,如果没有办法给我们做出食物这就是拒绝。

  • 一个  promise  必须有一个  then  方法用于处理状态改变

#open in new window状态说明

Promise 包含pendingfulfilledrejected三种状态

  • pending  指初始等待状态,初始化  promise  时的状态
  • resolve  指已经解决,将  promise  状态设置为fulfilled
  • reject  指拒绝处理,将  promise  状态设置为rejected
  • promise  是生产者,通过  resolve  与  reject  函数告之结果
  • promise  非常适合需要一定执行时间的异步任务
  • 状态一旦改变将不可更改

promise 是队列状态,就像体育中的接力赛,或多米诺骨牌游戏,状态一直向后传递,当然其中的任何一个 promise 也可以改变状态。

image

promise 没有使用  resolve  或  reject  更改状态时,状态为  pending

console.log(
  new Promise((resolve, reject) => {
  });
); //Promise {<pending>}

当更改状态后

console.log(
  new Promise((resolve, reject) => {
    resolve("fulfilled");
  })
); //Promise {<resolved>: "fulfilled"}

console.log(
  new Promise((resolve, reject) => {
    reject("rejected");
  })
); //Promise {<rejected>: "rejected"}

promise  创建时即立即执行即同步任务,then  会放在异步微任务中执行,需要等同步任务执行后才执行。

let promise = new Promise((resolve, reject) => {
  resolve("fulfilled");
  console.log("后盾人");
});
promise.then((msg) => {
  console.log(msg);
});
console.log("houdunren.com");

promise  操作都是在其他代码后执行,下面会先输出  houdunren.com  再弹出  success

  • promise  的 then、catch、finally 的方法都是异步任务
  • 程序需要将主任务执行完成才会执行异步队列任务
const promise = new Promise((resolve) => resolve("success"));
promise.then(alert);
alert("houdunren.com");
promise.then(() => {
  alert("后盾人");
});

下例在三秒后将  Promise  状态设置为  fulfilled ,然后执行  then  方法

new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("fulfilled");
  }, 3000);
}).then(
  (msg) => {
    console.log(msg);
  },
  (error) => {
    console.log(error);
  }
);

状态被改变后就不能再修改了,下面先通过resolve  改变为成功状态,表示promise  状态已经完成,就不能使用  reject  更改状态了

new Promise((resolve, reject) => {
  resolve("操作成功");
  reject(new Error("请求失败"));
}).then(
  (msg) => {
    console.log(msg);
  },
  (error) => {
    console.log(error);
  }
);

#open in new window动态改变

下例中 p2 返回了 p1 所以此时 p2 的状态已经无意义了,后面的 then 是对 p1 状态的处理。

const p1 = new Promise((resolve, reject) => {
  // resolve("fulfilled");
  reject("rejected");
});
const p2 = new Promise((resolve) => {
  resolve(p1);
}).then(
  (value) => {
    console.log(value);
  },
  (reason) => {
    console.log(reason);
  }
);

如果  resolve  参数是一个  promise ,将会改变promise状态。

下例中  p1  的状态将被改变为  p2  的状态

const p1 = new Promise((resolve, reject) => {
  resolve(
    //p2
    new Promise((s, e) => {
      s("成功");
    })
  );
}).then((msg) => {
  console.log(msg);
});

当 promise 做为参数传递时,需要等待 promise 执行完才可以继承,下面的 p2 需要等待 p1 执行完成。

  • 因为p2  的resolve  返回了  p1  的 promise,所以此时p2  的then  方法已经是p1  的了
  • 正因为以上原因  then  的第一个函数输出了  p1  的  resolve  的参数
const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("操作成功");
  }, 2000);
});
const p2 = new Promise((resolve, reject) => {
  resolve(p1);
}).then(
  (msg) => {
    console.log(msg);
  },
  (error) => {
    console.log(error);
  }
);

#open in new windowthen

一个 promise 需要提供一个 then 方法访问 promise 结果,then  用于定义当  promise  状态发生改变时的处理,即promise处理异步操作,then  用于结果。

promise  就像  kfc  中的厨房,then  就是我们用户,如果餐做好了即  fulfilled ,做不了拒绝即rejected  状态。那么 then 就要对不同状态处理。

  • then 方法必须返回 promise,用户返回或系统自动返回
  • 第一个函数在resolved  状态时执行,即执行resolve时执行then第一个函数处理成功状态
  • 第二个函数在rejected状态时执行,即执行reject  时执行第二个函数处理失败状态,该函数是可选的
  • 两个函数都接收  promise  传出的值做为参数
  • 也可以使用catch  来处理失败的状态
  • 如果  then  返回  promise ,下一个then  会在当前promise  状态改变后执行

#open in new window语法说明

then 的语法如下,onFulfilled 函数处理  fulfilled  状态, onRejected 函数处理  rejected  状态

  • onFulfilled 或 onRejected 不是函数将被忽略
  • 两个函数只会被调用一次
  • onFulfilled 在 promise 执行成功时调用
  • onRejected 在 promise 执行拒绝时调用
promise.then(onFulfilled, onRejected);

#open in new window基础知识

then  会在  promise  执行完成后执行,then  第一个函数在  resolve成功状态执行

const promise = new Promise((resolve, reject) => {
  resolve("success");
}).then(
  (value) => {
    console.log(`解决:${value}`);
  },
  (reason) => {
    console.log(`拒绝:${reason}`);
  }
);

then  中第二个参数在失败状态执行

const promise = new Promise((resolve, reject) => {
  reject("is error");
});
promise.then(
  (msg) => {
    console.log(`成功:${msg}`);
  },
  (error) => {
    console.log(`失败:${error}`);
  }
);

如果只关心成功则不需要传递  then  的第二个参数

const promise = new Promise((resolve, reject) => {
  resolve("success");
});
promise.then((msg) => {
  console.log(`成功:${msg}`);
});

如果只关心失败时状态,then  的第一个参数传递  null

const promise = new Promise((resolve, reject) => {
  reject("is error");
});
promise.then(null, (error) => {
  console.log(`失败:${error}`);
});

promise 传向 then 的传递值,如果 then 没有可处理函数,会一直向后传递

let p1 = new Promise((resolve, reject) => {
  reject("rejected");
})
  .then()
  .then(null, (f) => console.log(f));

如果 onFulfilled 不是函数且 promise 执行成功, p2 执行成功并返回相同值

let promise = new Promise((resolve, reject) => {
  resolve("resolve");
});
let p2 = promise.then();
p2.then().then((resolve) => {
  console.log(resolve);
});

如果 onRejected 不是函数且 promise 拒绝执行,p2 拒绝执行并返回相同值

let promise = new Promise((resolve, reject) => {
  reject("reject");
});
let p2 = promise.then(() => {});
p2.then(null, null).then(null, (reject) => {
  console.log(reject);
});

#open in new window链式调用

每次的  then  都是一个全新的  promise,默认 then 返回的 promise 状态是 fulfilled

let promise = new Promise((resolve, reject) => {
  resolve("fulfilled");
})
  .then((resolve) => {
    console.log(resolve);
  })
  .then((resolve) => {
    console.log(resolve);
  });

每次的  then  都是一个全新的  promise,不要认为上一个 promise 状态会影响以后 then 返回的状态

let p1 = new Promise(resolve => {
  resolve();
});
let p2 = p1.then(() => {
  console.log("后盾人");
});
p2.then(() => {
  console.log("houdunren.com");
});
console.log(p1); // Promise {<resolved>}
console.log(p2); // Promise {<pending>}

# 再试试把上面两行放在 setTimeout里
setTimeout(() => {
  console.log(p1); // Promise {<resolved>}
  console.log(p2); // Promise {<resolved>}
});

then  是对上个 promise 的rejected  的处理,每个  then  会是一个新的 promise,默认传递  fulfilled状态

new Promise((resolve, reject) => {
  reject();
})
.then(
  resolve => console.log("fulfilled"),
  reject => console.log("rejected")
)
.then(
  resolve => console.log("fulfilled"),
  reject => console.log("rejected")
)
.then(
  resolve => console.log("fulfilled"),
  reject => console.log("rejected")
);

# 执行结果如下
  ejected
  fulfilled
  fulfilled

如果内部返回  promise  时将使用该  promise

let p1 = new Promise((resolve) => {
  resolve();
});
let p2 = p1.then(() => {
  return new Promise((r) => {
    r("houdunren.com");
  });
});
p2.then((v) => {
  console.log(v); //houdunren.com
});

如果  then  返回promise  时,后面的then  就是对返回的  promise  的处理,需要等待该 promise 变更状态后执行。

let promise = new Promise((resolve) => resolve());
let p1 = promise
  .then(() => {
    return new Promise((resolve) => {
      setTimeout(() => {
        console.log(`p1`);
        resolve();
      }, 2000);
    });
  })
  .then(() => {
    return new Promise((a, b) => {
      console.log(`p2`);
    });
  });

如果then返回  promise  时,返回的promise  后面的then  就是处理这个promise  的

如果不  return  情况就不是这样了,即外层的  then  的promise  和内部的promise  是独立的两个 promise

new Promise((resolve, reject) => {
  resolve();
})
  .then((v) => {
    return new Promise((resolve, reject) => {
      resolve("第二个promise");
    }).then((value) => {
      console.log(value);
      return value;
    });
  })
  .then((value) => {
    console.log(value);
  });

这是对上面代码的优化,把内部的  then  提取出来

new Promise((resolve, reject) => {
  resolve();
})
  .then((v) => {
    return new Promise((resolve, reject) => {
      resolve("第二个promise");
    });
  })
  .then((value) => {
    console.log(value);
    return value;
  })
  .then((value) => {
    console.log(value);
  });

#open in new window其它类型

Promise 解决过程是一个抽象的操作,其需输入一个  promise  和一个值,我们表示为  [[Resolve]](promise, x),如果  x  有  then  方法且看上去像一个 Promise ,解决程序即尝试使  promise  接受  x  的状态;否则其用  x  的值来执行  promise 。

#open in new window循环调用

如果  then  返回与  promise  相同将禁止执行

let promise = new Promise((resolve) => {
  resolve();
});
let p2 = promise.then(() => {
  return p2;
}); // TypeError: Chaining cycle detected for promise

#open in new windowpromise

如果返加值是  promise  对象,则需要更新状态后,才可以继承执行后面的promise

new Promise((resolve, reject) => {
  resolve(
    new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve("解决状态");
      }, 2000);
    })
  );
})
  .then(
    (v) => {
      console.log(`fulfilled: ${v}`);
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          reject("失败状态");
        }, 2000);
      });
    },
    (v) => {
      console.log(`rejected: ${v}`);
    }
  )
  .catch((error) => console.log(`rejected: ${error}`));

#open in new windowThenables

包含  then  方法的对象就是一个  promise ,系统将传递 resolvePromise 与 rejectPromise 做为函数参数

下例中使用  resolve  或在then  方法中返回了具有  then方法的对象

  • 该对象即为  promise  要先执行,并在方法内部更改状态
  • 如果不更改状态,后面的  then promise 都为等待状态
new Promise((resolve, reject) => {
  resolve({
    then(resolve, reject) {
      resolve("解决状态");
    },
  });
})
  .then((v) => {
    console.log(`fulfilled: ${v}`);
    return {
      then(resolve, reject) {
        setTimeout(() => {
          reject("失败状态");
        }, 2000);
      },
    };
  })
  .then(null, (error) => {
    console.log(`rejected: ${error}`);
  });

包含  then  方法的对象可以当作 promise 来使用

class User {
  constructor(id) {
    this.id = id;
  }
  then(resolve, reject) {
    resolve(ajax(`http://localhost:8888/php/houdunren.php?id=${this.id}`));
  }
}
new Promise((resolve, reject) => {
  resolve(ajax(`http://localhost:8888/php/user.php?name=大军`));
})
  .then((user) => {
    return new User(user.id);
  })
  .then((lessons) => {
    console.log(lessons);
  });

当然也可以是类

new Promise((resolve, reject) => {
  resolve(
    class {
      static then(resolve, reject) {
        setTimeout(() => {
          resolve("解决状态");
        }, 2000);
      }
    }
  );
}).then(
  (v) => {
    console.log(`fulfilled: ${v}`);
  },
  (v) => {
    console.log(`rejected: ${v}`);
  }
);

如果对象中的 then 不是函数,则将对象做为值传递

new Promise((resolve, reject) => {
  resolve();
})
  .then(() => {
    return {
      then: "后盾人",
    };
  })
  .then((v) => {
    console.log(v); //{then: "后盾人"}
  });

#open in new windowcatch

下面使用未定义的变量同样会触发失败状态

let promise = new Promise((resolve, reject) => {
  hd;
}).then(
  (value) => console.log(value),
  (reason) => console.log(reason)
);

如果 onFulfilled 或 onRejected 抛出异常,则 p2 拒绝执行并返回拒因

let promise = new Promise((resolve, reject) => {
  throw new Error("fail");
});
let p2 = promise.then();
p2.then().then(null, (resolve) => {
  console.log(resolve + ",后盾人");
});

catch 用于失败状态的处理函数,等同于  then(null,reject){}

  • 建议使用  catch  处理错误
  • 将  catch  放在最后面用于统一处理前面发生的错误
const promise = new Promise((resolve, reject) => {
  reject(new Error("Notice: Promise Exception"));
}).catch((msg) => {
  console.error(msg);
});

catch  可以捕获之前所有  promise  的错误,所以建议将  catch  放在最后。下例中  catch  也可以捕获到了第一个  then  返回 的  promise  的错误。

new Promise((resolve, reject) => {
  resolve();
})
  .then(() => {
    return new Promise((resolve, reject) => {
      reject(".then ");
    });
  })
  .then(() => {})
  .catch((msg) => {
    console.log(msg);
  });

错误是冒泡的操作的,下面没有任何一个then  定义第二个函数,将一直冒泡到  catch  处理错误

new Promise((resolve, reject) => {
  reject(new Error("请求失败"));
})
  .then((msg) => {})
  .then((msg) => {})
  .catch((error) => {
    console.log(error);
  });

catch  也可以捕获对  then  抛出的错误处理

new Promise((resolve, reject) => {
  resolve();
})
  .then((msg) => {
    throw new Error("这是then 抛出的错误");
  })
  .catch(() => {
    console.log("33");
  });

catch  也可以捕获其他错误,下面在  then  中使用了未定义的变量,将会把错误抛出到  catch

new Promise((resolve, reject) => {
  resolve("success");
})
  .then((msg) => {
    console.log(a);
  })
  .catch((reason) => {
    console.log(reason);
  });

#open in new window使用建议

建议将错误要交给catch处理而不是在then中完成,不建议使用下面的方式管理错误

new Promise((resolve, reject) => {
  reject(new Error("请求失败"));
}).then(
  (msg) => {
    console.log(msg);
  },
  (error) => {
    console.log(error);
  }
);

#open in new window处理机制

在  promise  中抛出的错误也会被catch  捕获

const promise = new Promise((resolve, reject) => {
  throw new Error("fail");
}).catch((msg) => {
  console.log(msg.toString() + "后盾人");
});

可以将上面的理解为如下代码,可以理解为内部自动执行  try...catch

const promise = new Promise((resolve, reject) => {
  try {
    throw new Error("fail");
  } catch (error) {
    reject(error);
  }
}).catch((msg) => {
  console.log(msg.toString());
});

但像下面的在异步中  throw  将不会触发  catch,而使用系统错误处理

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    throw new Error("fail");
  }, 2000);
}).catch((msg) => {
  console.log(msg + "后盾人");
});

下面在then  方法中使用了没有定义的hd函数,也会抛除到  catch  执行,可以理解为内部自动执行  try...catch

const promise = new Promise((resolve, reject) => {
  resolve();
})
  .then(() => {
    hd();
  })
  .catch((msg) => {
    console.log(msg.toString());
  });

在  catch  中发生的错误也会抛给最近的错误处理

const promise = new Promise((resolve, reject) => {
  reject();
})
  .catch((msg) => {
    hd();
  })
  .then(null, (error) => {
    console.log(error);
  });

#open in new window定制错误

可以根据不同的错误类型进行定制操作,下面将参数错误与 404 错误分别进行了处理

class ParamError extends Error {
  constructor(msg) {
    super(msg);
    this.name = "ParamError";
  }
}
class HttpError extends Error {
  constructor(msg) {
    super(msg);
    this.name = "HttpError";
  }
}
function ajax(url) {
  return new Promise((resolve, reject) => {
    if (!/^http/.test(url)) {
      throw new ParamError("请求地址格式错误");
    }
    let xhr = new XMLHttpRequest();
    xhr.open("GET", url);
    xhr.send();
    xhr.onload = function () {
      if (this.status == 200) {
        resolve(JSON.parse(this.response));
      } else if (this.status == 404) {
        // throw new HttpError("用户不存在");
        reject(new HttpError("用户不存在"));
      } else {
        reject("加载失败");
      }
    };
    xhr.onerror = function () {
      reject(this);
    };
  });
}

ajax(`http://localhost:8888/php/user.php?name=后盾人`)
  .then((value) => {
    console.log(value);
  })
  .catch((error) => {
    if (error instanceof ParamError) {
      console.log(error.message);
    }
    if (error instanceof HttpError) {
      alert(error.message);
    }
    console.log(error);
  });

#open in new window事件处理

unhandledrejection事件用于捕获到未处理的 Promise 错误,下面的 then 产生了错误,但没有catch  处理,这时就会触发事件。该事件有可能在以后被废除,处理方式是对没有处理的错误直接终止。

window.addEventListener("unhandledrejection", function (event) {
  console.log(event.promise); // 产生错误的promise对象
  console.log(event.reason); // Promise的reason
});

new Promise((resolve, reject) => {
  resolve("success");
}).then((msg) => {
  throw new Error("fail");
});

#open in new windowfinally

无论状态是resolve  或  reject  都会执行此动作,finally  与状态无关。

const promise = new Promise((resolve, reject) => {
  reject("hdcms");
})
  .then((msg) => {
    console.log("resolve");
  })
  .catch((msg) => {
    console.log("reject");
  })
  .finally(() => {
    console.log("resolve/reject状态都会执行");
  });

下面使用  finally  处理加载状态,当请求完成时移除加载图标。请在后台 php 文件中添加  sleep(2);  设置延迟响应

<body>
  <style>
    div {
      width: 100px;
      height: 100px;
      background: red;
      color: white;
      display: none;
    }
  </style>
    <div>loading...</div>
</body>
<script>
function ajax(url) {
  return new Promise((resolve, reject) => {
    document.querySelector("div").style.display = "block";
    let xhr = new XMLHttpRequest();
    xhr.open("GET", url);
    xhr.send();
    xhr.onload = function() {
      if (this.status == 200) {
        resolve(JSON.parse(this.response));
      } else {
        reject(this);
      }
    };
  });
}

ajax("http://localhost:8888/php/user.php?name=大军")
  .then(user => {
    console.log(user);
  })
  .catch(error => {
    console.log(error);
  })
  .finally(() => {
    document.querySelector("div").style.display = "none";
  })
</script>

#open in new window实例操作

#open in new window异步请求

下面是将  ajax  修改为  promise  后,代码结构清晰了很多

function ajax(url) {
  return new Promise((resolve, reject) => {
    let xhr = new XMLHttpRequest();
    xhr.open("GET", url);
    xhr.send();
    xhr.onload = function () {
      if (this.status == 200) {
        resolve(JSON.parse(this.response));
      } else {
        reject(this);
      }
    };
  });
}

ajax("http://localhost:8888/php/user.php?name=大军")
  .then((user) =>
    ajax(`http://localhost:8888/php/houdunren.php?id=${user["id"]}`)
  )
  .then((lesson) => {
    console.log(lesson);
  });

#open in new window图片加载

下面是异步加载图片示例

function loadImage(file) {
  return new Promise((resolve, reject) => {
    const image = new Image();
    image.src = file;
    image.onload = () => {
      resolve(image);
    };
    image.onerror = reject;
    document.body.appendChild(image);
  });
}

loadImage("images/houdunren.png").then((image) => {
  image.style.border = "solid 20px black";
  console.log("宽度:" + window.getComputedStyle(image).width);
});

#open in new window定时器

下面是封装的timeout  函数,使用定时器操作更加方便

function timeout(times) {
  return new Promise((resolve) => {
    setTimeout(resolve, times);
  });
}

timeout(3000)
  .then(() => {
    console.log("3秒后执行");
    return timeout(1000);
  })
  .then(() => {
    console.log("执行上一步的promise后1秒执行");
  });

封闭  setInterval  定时器并实现动画效果

<body>
  <style>
    div {
      width: 100px;
      height: 100px;
      background: yellowgreen;
      position: absolute;
    }
  </style>
  <div></div>
</body>
<script>
  function interval(delay = 1000, callback) {
    return new Promise(resolve => {
      let id = setInterval(() => {
        callback(id, resolve);
      }, delay);
    });
  }
  interval(100, (id, resolve) => {
    const div = document.querySelector("div");
    let left = parseInt(window.getComputedStyle(div).left);
    div.style.left = left + 10 + "px";
    if (left >= 200) {
      clearInterval(id);
      resolve(div);
    }
  }).then(div => {
    interval(50, (id, resolve) => {
      let width = parseInt(window.getComputedStyle(div).width);
      div.style.width = width - 10 + "px";
      if (width <= 20) {
        clearInterval(id);
      }
    });
  });
</script>

#open in new window链式操作

  • 第个  then  都是一个 promise
  • 如果  then  返回 promse,只当promise  结束后,才会继承执行下一个  then

#open in new window语法介绍

下面是对同一个  promise  的多个  then ,每个then  都得到了同一个 promise 结果,这不是链式操作,实际使用意义不大。

image

const promise = new Promise((resolve, reject) => {
  resolve("后盾人");
});
promise.then((hd) => {
  hd += "-hdcms";
  console.log(hd); //后盾人-hdcms
});
promise.then((hd) => {
  hd += "-houdunren";
  console.log(hd); //后盾人-houdunren
});

第一个  then  也是一个 promise,当没接受到结果是状态为  pending

const promise = new Promise((resolve, reject) => {
  resolve("后盾人");
});

console.log(
  promise.then((hd) => {
    hd += "-hdcms";
    console.log(hd);
  })
); //Promise {<pending>}

promise  中的  then  方法可以链接执行,then  方法的返回值会传递到下一个then  方法。

  • then  会返回一个promise ,所以如果有多个then  时会连续执行
  • then  返回的值会做为当前promise  的结果

下面是链式操作的  then,即始没有  return  也是会执行,因为每个then  会返回promise

new Promise((resolve, reject) => {
  resolve("后盾人");
})
  .then((hd) => {
    hd += "-hdcms";
    console.log(hd); //后盾人-hdcms
    return hd;
  })
  .then((hd) => {
    hd += "-houdunren";
    console.log(hd); //后盾人-hdcms-houdunren
  });

then  方法可以返回一个promise  对象,等promise  执行结束后,才会继承执行后面的  then。后面的then  方法就是对新返回的promise  状态的处理

new Promise((resolve, reject) => {
  resolve("第一个promise");
})
  .then((msg) => {
    console.log(msg);
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve("第二个promise");
      }, 3000);
    });
  })
  .then((msg) => {
    console.log(msg);
  });

#open in new window链式加载

使用promise  链式操作重构前面章节中的文件加载,使用代码会变得更清晰

function load(file) {
  return new Promise((resolve, reject) => {
    const script = document.createElement("script");
    script.src = file;
    script.onload = () => resolve(script);
    script.onerror = () => reject();
    document.body.appendChild(script);
  });
}

load("js/hd.js")
  .then(() => load("js/houdunren.js"))
  .then(() => houdunren());

#open in new window操作元素

下面使用  promise  对元素事件进行处理

<body>
  <div>
    <h2>第九章 闭包与作用域</h2>
    <button>收藏课程</button>
  </div>
</body>

<script>
new Promise(resolve => {
  document.querySelector("button").addEventListener("click", e => {
    resolve();
  });
})
.then(() => {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log("执行收藏任务");
      resolve();
    }, 2000);
  });
})
.then(() => {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log("更新积分");
      resolve();
    }, 2000);
  });
})
.then(() => {
  console.log("收藏成功!奖励10积分");
})
.catch(error => console.log(errro));

#open in new window异步请求

下面是使用链式操作获取学生成绩

function ajax(url) {
  return new Promise((resolve, reject) => {
    let xhr = new XMLHttpRequest();
    xhr.open("GET", url);
    xhr.send();
    xhr.onload = function () {
      if (this.status == 200) {
        resolve(JSON.parse(this.response));
      } else {
        reject(this);
      }
    };
  });
}
ajax("http://localhost:8888/php/user.php?name=大军")
  .then((user) => {
    return ajax(`http://localhost:8888/php/houdunren.php?id=${user["id"]}`);
  })
  .then((lesson) => {
    console.log(lesson);
  });

#open in new window扩展接口

#open in new windowresolve

使用  promise.resolve  方法可以快速的返回一个 promise 对象

根据值返加  promise

Promise.resolve("后盾人").then((value) => {
  console.log(value); //后盾人
});

下面将请求结果缓存,如果再次请求时直接返回带值的  promise

  • 为了演示使用了定时器,你也可以在后台设置延迟响应
function query(name) {
  const cache = query.cache || (query.cache = new Map());
  if (cache.has(name)) {
    console.log("走缓存了");
    return Promise.resolve(cache.get(name));
  }
  return ajax(`http://localhost:8888/php/user.php?name=${name}`).then(
    (response) => {
      cache.set(name, response);
      console.log("没走缓存");
      return response;
    }
  );
}
query("大军").then((response) => {
  console.log(response);
});
setTimeout(() => {
  query("大军").then((response) => {
    console.log(response);
  });
}, 1000);

如果是  thenable  对象,会将对象包装成 promise 处理,这与其他 promise 处理方式一样的

const hd = {
  then(resolve, reject) {
    resolve("后盾人");
  },
};
Promise.resolve(hd).then((value) => {
  console.log(value);
});

#open in new windowreject

和  Promise.resolve  类似,reject  生成一个失败的promise

Promise.reject("fail").catch((error) => console.log(error));

下面使用  Project.reject  设置状态

new Promise((resolve) => {
  resolve("后盾人");
})
  .then((v) => {
    if (v != "houdunren.com") return Promise.reject(new Error("fail"));
  })
  .catch((error) => {
    console.log(error);
  });

#open in new windowall

使用Promise.all  方法可以同时执行多个并行异步操作,比如页面加载时同进获取课程列表与推荐课程。

  • 任何一个  Promise  执行失败就会调用  catch方法
  • 适用于一次发送多个异步操作
  • 参数必须是可迭代类型,如 Array/Set
  • 成功后返回  promise  结果的有序数组

下例中当  hdcms、houdunren  两个 Promise 状态都为  fulfilled  时,hd 状态才为fulfilled

const hdcms = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("第一个Promise");
  }, 1000);
});
const houdunren = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("第二个异步");
  }, 1000);
});
const hd = Promise.all([hdcms, houdunren])
  .then((results) => {
    console.log(results);
  })
  .catch((msg) => {
    console.log(msg);
  });

根据用户名获取用户,有任何一个用户获取不到时  promise.all  状态失败,执行  catch  方法

function ajax(url) {
  return new Promise((resolve, reject) => {
    let xhr = new XMLHttpRequest();
    xhr.open("GET", url);
    xhr.send();
    xhr.onload = function () {
      if (this.status == 200) {
        resolve(JSON.parse(this.response));
      } else {
        reject(this);
      }
    };
  });
}

const api = "http://localhost:8888/php";
const promises = ["大军", "后盾人"].map((name) => {
  return ajax(`${api}/user.php?name=${name}`);
});

Promise.all(promises)
  .then((response) => {
    console.log(response);
  })
  .catch((error) => {
    console.log(error);
  });

可以将其他非promise  数据添加到  all  中,它将被处理成  Promise.resolve

...
const promises = [
  ajax(`${api}/user.php?name=大军`),
  ajax(`${api}/user.php?name=后盾人`),
  { id: 3, name: "hdcms", email: "admin@hdcms.com" }
];
...

如果某一个promise没有 catch 处理,将使用promise.all  的 catch 处理

let p1 = new Promise((resolve, reject) => {
  resolve("fulfilled");
});
let p2 = new Promise((resolve, reject) => {
  reject("rejected");
});
Promise.all([p1, p2]).catch((reason) => {
  console.log(reason);
});

#open in new windowallSettled

allSettled  用于处理多个promise ,只关注执行完成,不关注是否全部执行成功,allSettled  状态只会是fulfilled

下面的 p2 返回状态为  rejected ,但promise.allSettled  不关心,它始终将状态设置为  fulfilled 。

const p1 = new Promise((resolve, reject) => {
  resolve("resolved");
});
const p2 = new Promise((resolve, reject) => {
  reject("rejected");
});
Promise.allSettled([p1, p2]).then((msg) => {
  console.log(msg);
});

下面是获取用户信息,但不关注某个用户是否获取不成功

const api = "http://localhost:8888/php";
const promises = [
  ajax(`${api}/user.php?name=大军`),
  ajax(`${api}/user.php?name=后盾人`),
];
Promise.allSettled(promises).then((response) => {
  console.log(response);
});

#open in new windowrace

使用Promise.race()  处理容错异步,和race单词一样哪个 Promise 快用哪个,哪个先返回用哪个。

  • 以最快返回的 promise 为准
  • 如果最快返加的状态为rejected  那整个promiserejected执行 cache
  • 如果参数不是 promise,内部将自动转为 promise

下面将第一次请求的异步时间调整为两秒,这时第二个先返回就用第二人。

const hdcms = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("第一个Promise");
  }, 2000);
});
const houdunren = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("第二个异步");
  }, 1000);
});
Promise.race([hdcms, houdunren])
  .then((results) => {
    console.log(results);
  })
  .catch((msg) => {
    console.log(msg);
  });

获取用户资料,如果两秒内没有结果  promise.race  状态失败,执行catch  方法

const api = "http://localhost:8888/php";
const promises = [
  ajax(`${api}/user.php?name=大军`),
  new Promise((a, b) => setTimeout(() => b(new Error("request fail")), 2000)),
];
Promise.race(promises)
  .then((response) => {
    console.log(response);
  })
  .catch((error) => {
    console.log(error);
  });

#open in new window任务队列

#open in new window实现原理

如果  then  返回promise  时,后面的then  就是对返回的  promise  的处理

let promise = Promise.resolve();
let p1 = promise.then(() => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`p1`);
      resolve();
    }, 1000);
  });
});
p1.then(() => {
  return new Promise((a, b) => {
    setTimeout(() => {
      console.log(`p2`);
    }, 1000);
  });
});

下面使用  map  构建的队列,有以下几点需要说明

  • then  内部返回的  promise  更改外部的  promise  变量
  • 为了让任务继承,执行完任务需要将  promise  状态修改为  fulfilled
function queue(nums) {
  let promise = Promise.resolve();
  nums.map((n) => {
    promise = promise.then((v) => {
      return new Promise((resolve) => {
        console.log(n);
        resolve();
      });
    });
  });
}

queue([1, 2, 3, 4, 5]);

下面再来通过  reduce  来实现队列

function queue(nums) {
  return nums.reduce((promise, n) => {
    return promise.then(() => {
      return new Promise((resolve) => {
        console.log(n);
        resolve();
      });
    });
  }, Promise.resolve());
}

queue([1, 2, 3, 4, 5]);

#open in new window队列请求

下面是异步加载用户并渲染到视图中的队列实例

  • 请在后台添加延迟脚本,以观察队列执行过程
  • 也可以在任何promise  中添加定时器观察
class User {
  //加载用户
  ajax(user) {
    let url = `http://localhost:8888/php/user.php?name=${user}`;
    return new Promise((resolve) => {
      let xhr = new XMLHttpRequest();
      xhr.open("GET", url);
      xhr.send();
      xhr.onload = function () {
        if (this.status == 200) {
          resolve(JSON.parse(this.response));
        } else {
          reject(this);
        }
      };
    });
  }
  //启动
  render(users) {
    users.reduce((promise, user) => {
      return promise
        .then(() => {
          return this.ajax(user);
        })
        .then((user) => {
          return this.view(user);
        });
    }, Promise.resolve());
  }
  //宣染视图
  view(user) {
    return new Promise((resolve) => {
      let h1 = document.createElement("h1");
      h1.innerHTML = user.name;
      document.body.appendChild(h1);
      resolve();
    });
  }
}
new User().render(["大军", "后盾人"]);

#open in new window高可用封装

上例中处理是在队列中完成,不方便业务定制,下面将 Promise 处理在剥离到外部

后台请求处理类

export default function (url) {
  return new Promise((resolve, reject) => {
    let xhr = new XMLHttpRequest();
    xhr.open("GET", url);
    xhr.send();
    xhr.onload = function () {
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(this);
      }
    };
  });
}

队列处理类

export default function (promises) {
  promises.reduce((promise, next) => promise.then(next), Promise.resolve());
}

后台脚本

<?php
$users = [
    1 => "小明",
    2 => "李四",
    3 => "张三"
];
sleep(1);
echo $users[$_GET['id']];

使用队列

<script type="module">
  import queue from './queue.js' import axios from './axios.js' queue( [1, 2,
  3].map(v => () => axios(`user.php?id=${v}`).then(user => console.log(user)) )
  )
</script>

#open in new windowasync/await

使用  async/await  是 promise 的语法糖,可以让编写 promise 更清晰易懂,也是推荐编写 promise 的方式。

  • async/await  本质还是 promise,只是更简洁的语法糖书写
  • async/await  使用更清晰的 promise 来替换 promise.then/catch 的方式

#open in new windowasync

下面在  hd  函数前加上 async,函数将返回 promise,我们就可以像使用标准 Promise 一样使用了。

async function hd() {
  return "houdunren.com";
}
console.log(hd());
hd().then((value) => {
  console.log(value);
});

如果有多个 await 需要排队执行完成,我们可以很方便的处理多个异步队列

async function hd(message) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(message);
    }, 2000);
  });
}
async function run() {
  let h1 = await hd("后盾人");
  console.log(h1);
  let h2 = await hd("houdunren.com");
  console.log(h2);
}
run();

#open in new windowawait

使用  await  关键词后会等待 promise 完

  • await  后面一般是 promise,如果不是直接返回
  • await  必须放在 async 定义的函数中使用
  • await  用于替代  then  使编码更优雅

下例会在 await 这行暂停执行,直到等待 promise 返回结果后才继执行。

async function hd() {
  const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("houdunren.com");
    }, 2000);
  });
  let result = await promise;
  console.log(result);
}
hd();

一般 await 后面是外部其它的 promise 对象

async function hd() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("fulfilled");
    }, 2000);
  });
}
async function run() {
  let value = await hd();
  console.log("houdunren.com");
  console.log(value);
}
run();

下面是请求后台获取用户课程成绩的示例

async function user() {
  let user = await ajax(`http://localhost:8888/php/user.php?name=大军`);
  let lessons = await ajax(
    `http://localhost:8888/php/houdunren.php?id=${user.id}`
  );
  console.log(lessons);
}

也可以将操作放在立即执行函数中完成

(async () => {
  let user = await ajax(`http://localhost:8888/php/user.php?name=大军`);
  let lessons = await ajax(
    `http://localhost:8888/php/houdunren.php?id=${user.id}`
  );
  console.log(lessons);
})();

下面是使用 async 设置定时器,并间隔时间来输出内容

async function sleep(ms = 2000) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}
async function run() {
  for (const value of ["后盾人", "大军"]) {
    await sleep();
    console.log(value);
  }
}
run();

#open in new window加载进度

下面是请求后台加载用户并通过进度条展示的效果

<body>
  <style>
    div {
      height: 50px;
      width: 0px;
      background: green;
    }
  </style>
  <div id="loading"></div>
</body>
<script src="js/ajax.js"></script>
<script>
  async function query(name) {
    return ajax(`http://localhost:8888/php/user.php?name=${name}`);
  }
  (async () => {
    let users = ["后盾人", "大军", "李四", "王五", "赵六"];
    for (let i = 0; i < users.length; i++) {
      await query(users[i]);
      let progress = (i + 1) / users.length;
      loading.style.width = progress * 100 + "%";
    }
  })();
</script>

#open in new window类中使用

和 promise 一样,await 也可以操作thenables  对象

class User {
  constructor(name) {
    this.name = name;
  }
  then(resolve, reject) {
    let user = ajax(`http://localhost:8888/php/user.php?name=${this.name}`);
    resolve(user);
  }
}
async function get() {
  let user = await new User("大军");
  console.log(user);
}
get();

类方法也可以通过  async  与  await  来操作 promise

class User {
  constructor() {}
  async get(name) {
    let user = await ajax(`http://localhost:8888/php/user.php?name=${name}`);
    user.name += "-houdunren.com";
    return user;
  }
}
new User().get("大军").then((resolve) => {
  console.log(resolve);
});

#open in new window其他声明

函数声明

async function get(name) {
  return await ajax(`http://localhost:8888/php/user.php?name=${name}`);
}
get("后盾人").then((user) => {
  console.log(user);
});

函数表达式

let get = async function (name) {
  return await ajax(`http://localhost:8888/php/user.php?name=${name}`);
};
get("后盾人").then((user) => {
  console.log(user);
});

对象方法声明

let hd = {
  async get(name) {
    return await ajax(`http://localhost:8888/php/user.php?name=${name}`);
  },
};

hd.get("后盾人").then((user) => {
  console.log(user);
});

立即执行函数

(async () => {
  let user = await ajax(`http://localhost:8888/php/user.php?name=大军`);
  let lessons = await ajax(
    `http://localhost:8888/php/houdunren.php?id=${user.id}`
  );
  console.log(lessons);
})();

类方法中的使用

class User {
  async get(name) {
    return await ajax(`http://localhost:8888/php/user.php?name=${name}`);
  }
}
let user = new User().get("后盾人").then((user) => {
  console.log(user);
});

#open in new window错误处理

async 内部发生的错误,会将必变 promise 对象为 rejected 状态,所以可以使用catch  来处理

async function hd() {
  console.log(houdunren);
}
hd().catch((error) => {
  throw new Error(error);
});

下面是异步请求数据不存在时的错误处理

async function get(name) {
  return await ajax(`http://localhost:8888/php/user.php?name=${name}`);
}

get("大军小哥").catch((error) => {
  alert("用户不存在");
});

如果promise  被拒绝将抛出异常,可以使用  try...catch  处理错误

async function get(name) {
  try {
    let user = await ajax(`http://localhost:8888/php/user.php?name=${name}`);
    console.log(user);
  } catch (error) {
    alert("用户不存在");
  }
}
get("大军老师");

多个 await 时当前面的出现失败,后面的将不可以执行

async function hd() {
  await Promise.reject("fail");
  await Promise.resolve("success").then((value) => {
    console.log(value);
  });
}
hd();

如果对前一个错误进行了处理,后面的 await 可以继续执行

async function hd() {
  await Promise.reject("fail").catch((e) => console.log(e));
  await Promise.resolve("success").then((value) => {
    console.log(value);
  });
}
hd();

也可以使用  try...catch  特性忽略不必要的错误

async function hd() {
  try {
    await Promise.reject("fail");
  } catch (error) {}
  await Promise.resolve("success").then((value) => {
    console.log(value);
  });
}
hd();

也可以将多个 await 放在 try...catch 中统一处理错误

async function hd(name) {
  const host = "http://localhost:8888/php";
  try {
    const user = await ajax(`${host}/user.php?name=${name}`);
    const lessons = await ajax(`${host}/user.php?id=${user.id}`);
    console.log(lessons);
  } catch (error) {
    console.log("用户不存在");
  }
}
hd("后盾人教程");

#open in new window并发执行

有时需要多个 await 同时执行,有以下几种方法处理,下面多个 await 将产生等待

async function p1() {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("houdunren");
      resolve();
    }, 2000);
  });
}
async function p2() {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("hdcms");
      resolve();
    }, 2000);
  });
}
async function hd() {
  await p1();
  await p2();
}
hd();

使用  Promise.all()  处理多个 promise 并行执行

async function hd() {
  await Promise.all([p1(), p2()]);
}
hd();

让 promise 先执行后再使用 await 处理结果

async function hd() {
  let h1 = p1();
  let h2 = p2();
  await h1;
  await h2;
}
hd();

任务管理

houdunren.com (opens new window)open in new window@ 大军大叔

JavaScript 语言的一大特点就是单线程,也就是说同一个时间只能处理一个任务。为了协调事件、用户交互、脚本、UI 渲染和网络处理等行为,防止主线程的不阻塞,(事件循环)Event Loop 的方案应用而生。

JavaScript 处理任务是在等待任务、执行任务 、休眠等待新任务中不断循环中,也称这种机制为事件循环。

  • 主线程中的任务执行完后,才执行任务队列中的任务
  • 有新任务到来时会将其放入队列,采取先进先执行的策略执行队列中的任务
  • 比如多个  setTimeout  同时到时间了,就要依次执行

任务包括 script(整体代码)、 setTimeout、setInterval、DOM 渲染、DOM 事件、Promise、XMLHTTPREQUEST 等

#open in new window原理分析

下面通过一个例子来详细分析宏任务与微任务

console.log("后盾人");
setTimeout(function() {
  console.log("定时器");
}, 0);
Promise.resolve()
  .then(function() {
    console.log("promise1");
  })
  .then(function() {
    console.log("promise2");
  });
console.log("houdunren.com");

#输出结果为
后盾人
houdunren.com
promise1
promise2
定时器

  1. 先执最前面的宏任务 script,然后输出
script start

  1. 然后执行到 setTimeout 异步宏任务,并将其放入宏任务队列,等待执行
  2. 之后执行到 Promise.then 微任务,并将其放入微任务队列,等待执行
  3. 然后执行到主代码输出
script end

  1. 主线程所有任务处理完成
  2. 通过事件循环遍历微任务队列,将刚才放入的 Promise.then 微任务读取到主线程执行,然后输出
promise1;
  1. 之后又执行 promse.then 产生新的微任务,并放入微任务队列
  2. 主线程任务执行完毕
  3. 现次事件循环遍历微任务队列,读取到 promise2 微任务放入主线程执行,然后输出
promise2;
  1. 主线程任务执行完毕
  2. 此时微任务队列已经无任务,然后从宏任务队列中读取到 setTimeout 任务并加入主线程,然后输出
setTimeout;

image

#open in new window脚本加载

引擎在执行任务时不会进行 DOM 渲染,所以如果把script  定义在前面,要先执行完任务后再渲染 DOM,建议将script  放在 BODY 结束标签前。

#open in new window定时器

定时器会放入异步任务队列,也需要等待同步任务执行完成后执行。

下面设置了 6 毫秒执行,如果主线程代码执行 10 毫秒,定时器要等主线程执行完才执行。

HTML 标准规定最小时间不能低于 4 毫秒,有些异步操作如 DOM 操作最低是 16 毫秒,总之把时间设置大些对性能更好。

setTimeout(func, 6);

下面的代码会先输出  houdunren.com  之后输出  后盾人

setTimeout(() => {
  console.log("后盾人");
}, 0);
console.log("houdunren.com");

这是对定时器的说明,其他的异步操作如事件、XMLHTTPREQUEST 等逻辑是一样的

#open in new window微任务

微任务一般由用户代码产生,微任务较宏任务执行优先级更高,Promise.then  是典型的微任务,实例化 Promise 时执行的代码是同步的,便 then 注册的回调函数是异步微任务的。

任务的执行顺序是同步任务、微任务、宏任务所以下面执行结果是  1、2、3、4

setTimeout(() => console.log(4));

new Promise((resolve) => {
  resolve();
  console.log(1);
}).then((_) => {
  console.log(3);
});

console.log(2);

我们再来看下面稍复杂的任务代码

setTimeout(() => {
  console.log("定时器");
  setTimeout(() => {
    console.log("timeout timeout");
  }, 0);
  new Promise((resolve) => {
    console.log("settimeout Promise");
    resolve();
  }).then(() => {
    console.log("settimeout then");
  });
}, 0);
new Promise((resolve) => {
  console.log("Promise");
  resolve();
}).then(() => {
  console.log("then");
});
console.log("后盾人");

以上代码执行结果为

Promise
后盾人
then
定时器
settimeout Promise
settimeout then
timeout timeout

#open in new window实例操作

#open in new window进度条

下面的定时器虽然都定时了一秒钟,但也是按先进行出原则,依次执行

let i = 0;
setTimeout(() => {
  console.log(++i);
}, 1000);

setTimeout(() => {
  console.log(++i);
}, 1000);

下面是一个进度条的示例,将每个数字放在一个任务中执行

<body>
  <style>
    body {
      padding: 30px;
    }
    #hd {
      height: 30px;
      background: yellowgreen;
      width: 0;
      text-align: center;
      font-weight: bold;
    }
  </style>
  <div id="hd"></div>
</body>

<script>
  function view() {
    let i = 0;
    (function handle() {
      hd.innerHTML = i + "%";
      hd.style.width = i + "%";
      if (i++ < 100) {
        setTimeout(handle, 20);
      }
    })();
  }
  view();
  console.log("定时器开始了...");
</script>

#open in new window任务分解

一个比较耗时的任务可能造成游览器卡死现象,所以可以将任务拆分为多小小异步小任务执行。下面是一个数字统计的函数,我们会发现运行时间特别长

console.time("runtime");
function hd(num) {
  let count = 0;
  for (let i = 0; i <= num; i++) {
    count += i;
  }
  console.log(count);
  console.timeEnd("runtime");
}
let num = 987654321;
hd(num);
console.log("houdunren.com"); //需要等待上面执行完才会执行

现在把任务分解成小块放入任务队列,游览器就不会出现卡死的现象了,也不会影响后续代码的执行

console.time("runtime");
let count = 0;
let num = 987654321;
function hd() {
  for (let i = 0; i < 100000000; i++) {
    if (num <= 0) break;
    count += num--;
  }
  if (num > 0) {
    console.log(num);
    setTimeout(hd);
  } else {
    console.log(num);
    console.log(count);
  }
}
hd();
console.log("houdunren.com"); //立刻显示出来

交给微任务处理是更好的选择

async function hd(num) {
  let res = await Promise.resolve().then((_) => {
    let count = 0;
    for (let i = 0; i < num; i++) {
      count += num--;
    }
    return count;
  });
  console.log(res);
}
hd(987654321);
console.log("后盾人");

起步构建

houdunren.com (opens new window)open in new window@ 大军大叔

本章来自己开发一个 Promise 实现,提升异步编程的能力。

首先声明定义类并声明 Promise 状态与值,有以下几个细节需要注意。

  • executor 为执行者
  • 当执行者出现异常时触发拒绝状态
  • 使用静态属性保存状态值
  • 状态只能改变一次,所以在 resolve 与 reject 添加条件判断
  • 因为  resolverejected方法在 executor 中调用,作用域也是 executor 作用域,这会造成 this 指向 window,现在我们使用的是 class 定义,this 为 undefined。
class HD {
  static PENDING = "pending";
  static FULFILLED = "fulfilled";
  static REJECTED = "rejected";
  constructor(executor) {
    this.status = HD.PENDING;
    this.value = null;
    try {
      executor(this.resolve.bind(this), this.reject.bind(this));
    } catch (error) {
      this.reject(error);
    }
  }
  resolve(value) {
    if (this.status == HD.PENDING) {
      this.status = HD.FULFILLED;
      this.value = value;
    }
  }
  reject(value) {
    if (this.status == HD.PENDING) {
      this.status = HD.REJECTED;
      this.value = value;
    }
  }
}

下面测试一下状态改变

<script src="HD.js"></script>
<script>
  let p = new HD((resolve, reject) => {
    resolve("后盾人");
  });
  console.log(p);
</script>

#open in new windowTHEN

现在添加 then 方法来处理状态的改变,有以下几点说明

  1. then 可以有两个参数,即成功和错误时的回调函数
  2. then 的函数参数都不是必须的,所以需要设置默认值为函数,用于处理当没有传递时情况
  3. 当执行 then 传递的函数发生异常时,统一交给 onRejected 来处理错误

#open in new window基础构建

then(onFulfilled, onRejected) {
  if (typeof onFulfilled != "function") {
    onFulfilled = value => value;
  }
  if (typeof onRejected != "function") {
    onRejected = value => value;
  }
  if (this.status == HD.FULFILLED) {
    try {
      onFulfilled(this.value);
    } catch (error) {
      onRejected(error);
    }
  }
  if (this.status == HD.REJECTED) {
    try {
      onRejected(this.value);
    } catch (error) {
      onRejected(error);
    }
  }
}

下面来测试 then 方法的,结果正常输出后盾人

let p = new HD((resolve, reject) => {
  resolve("后盾人");
}).then(
  (value) => {
    console.log(value);
  },
  (reason) => {
    console.log(reason);
  }
);
console.log("houdunren.com");

#open in new window异步任务

但上面的代码产生的 Promise 并不是异步的,使用 setTimeout 来将 onFulfilled 与 onRejected 做为异步宏任务执行

then(onFulfilled, onRejected) {
  if (typeof onFulfilled != "function") {
    onFulfilled = value => value;
  }
  if (typeof onRejected != "function") {
    onRejected = value => value;
  }
  if (this.status == HD.FULFILLED) {
    setTimeout(() => {
      try {
        onFulfilled(this.value);
      } catch (error) {
        onRejected(error);
      }
    });
  }
  if (this.status == HD.REJECTED) {
    setTimeout(() => {
      try {
        onRejected(this.value);
      } catch (error) {
        onRejected(error);
      }
    });
  }
}

现在再执行代码,已经有异步效果了,先输出了houdunren.com

let p = new HD((resolve, reject) => {
  resolve("后盾人");
}).then(
  (value) => {
    console.log(value);
  },
  (reason) => {
    console.log(reason);
  }
);
console.log("houdunren.com");

#open in new windowPENDING 状态

目前 then 方法无法处理 promise 为 pending 时的状态

...
let p = new HD((resolve, reject) => {
  setTimeout(() => {
    resolve("后盾人");
  });
})
...

为了处理以下情况,需要进行几点改动

  1. 在构造函数中添加 callbacks 来保存 pending 状态时处理函数,当状态改变时循环调用
constructor(executor) {
    ...
  this.callbacks = [];
  ...
}

  1. 将 then 方法的回调函数添加到 callbacks 数组中,用于异步执行
then(onFulfilled, onRejected) {
  if (typeof onFulfilled != "function") {
    onFulfilled = value => value;
  }
  if (typeof onRejected != "function") {
    onRejected = value => value;
  }
    if (this.status == HD.PENDING) {
    this.callbacks.push({
      onFulfilled: value => {
        try {
          onFulfilled(value);
        } catch (error) {
          onRejected(error);
        }
      },
      onRejected: value => {
        try {
          onRejected(value);
        } catch (error) {
          onRejected(error);
        }
      }
    });
  }
  ...
}

  1. resovle 与 reject 中添加处理 callback 方法的代码
resolve(value) {
  if (this.status == HD.PENDING) {
    this.status = HD.FULFILLED;
    this.value = value;
    this.callbacks.map(callback => {
      callback.onFulfilled(value);
    });
  }
}
reject(value) {
  if (this.status == HD.PENDING) {
    this.status = HD.REJECTED;
    this.value = value;
    this.callbacks.map(callback => {
      callback.onRejected(value);
    });
  }
}

#open in new windowPENDING 异步

执行以下代码发现并不是异步操作,应该先输出  大叔视频  然后是`后盾人

let p = new HD((resolve, reject) => {
  setTimeout(() => {
    resolve("后盾人");
    console.log("大叔视频");
  });
}).then(
  (value) => {
    console.log(value);
  },
  (reason) => {
    console.log(reason);
  }
);

解决以上问题,只需要将 resolve 与 reject 执行通过 setTimeout 定义为异步任务

resolve(value) {
  if (this.status == HD.PENDING) {
       this.status = HD.FULFILLED;
        this.value = value;
    setTimeout(() => {
      this.callbacks.map(callback => {
        callback.onFulfilled(value);
      });
    });
  }
}
reject(value) {
  if (this.status == HD.PENDING) {
      this.status = HD.REJECTED;
    this.value = value;
    setTimeout(() => {
      this.callbacks.map(callback => {
        callback.onRejected(value);
      });
    });
  }
}

#open in new window链式操作

Promise 中的 then 是链式调用执行的,所以 then 也要返回 Promise 才能实现

  1. then 的 onReject 函数是对前面 Promise 的 rejected 的处理
  2. 但该 Promise 返回状态要为 fulfilled,所以在调用 onRejected 后改变当前 promise 为 fulfilled 状态
then(onFulfilled, onRejected) {
  if (typeof onFulfilled != "function") {
    onFulfilled = value => value;
  }
  if (typeof onRejected != "function") {
    onRejected = value => value;
  }
  return new HD((resolve, reject) => {
    if (this.status == HD.PENDING) {
      this.callbacks.push({
        onFulfilled: value => {
          try {
            let result = onFulfilled(value);
            resolve(result);
          } catch (error) {
            reject(error);
          }
        },
        onRejected: value => {
          try {
            let result = onRejected(value);
            resolve(result);
          } catch (error) {
            reject(error);
          }
        }
      });
    }
    if (this.status == HD.FULFILLED) {
      setTimeout(() => {
        try {
          let result = onFulfilled(this.value);
          resolve(result);
        } catch (error) {
          reject(error);
        }
      });
    }
    if (this.status == HD.REJECTED) {
      setTimeout(() => {
        try {
          let result = onRejected(this.value);
          resolve(result);
        } catch (error) {
          reject(error);
        }
      });
    }
  });
}

下面执行测试后,链式操作已经有效了

let p = new HD((resolve, reject) => {
  resolve("后盾人");
  console.log("hdcms.com");
})
  .then(
    (value) => {
      console.log(value);
      return "大叔视频";
    },
    (reason) => {
      console.log(reason);
    }
  )
  .then(
    (value) => {
      console.log(value);
    },
    (reason) => {
      console.log(reason);
    }
  );
console.log("houdunren.com");

#open in new window返回类型

如果 then 返回的是 Promise 呢?所以我们需要判断分别处理返回值为 Promise 与普通值的情况

#open in new window基本实现

下面来实现不同类型不同处理机制

then(onFulfilled, onRejected) {
  if (typeof onFulfilled != "function") {
    onFulfilled = value => value;
  }
  if (typeof onRejected != "function") {
    onRejected = value => value;
  }
  return new HD((resolve, reject) => {
    if (this.status == HD.PENDING) {
      this.callbacks.push({
        onFulfilled: value => {
          try {
            let result = onFulfilled(value);
            if (result instanceof HD) {
              result.then(resolve, reject);
            } else {
              resolve(result);
            }
          } catch (error) {
            reject(error);
          }
        },
        onRejected: value => {
          try {
            let result = onRejected(value);
            if (result instanceof HD) {
              result.then(resolve, reject);
            } else {
              resolve(result);
            }
          } catch (error) {
            reject(error);
          }
        }
      });
    }
    if (this.status == HD.FULFILLED) {
      setTimeout(() => {
        try {
          let result = onFulfilled(this.value);
          if (result instanceof HD) {
            result.then(resolve, reject);
          } else {
            resolve(result);
          }
        } catch (error) {
          reject(error);
        }
      });
    }
    if (this.status == HD.REJECTED) {
      setTimeout(() => {
        try {
          let result = onRejected(this.value);
          if (result instanceof HD) {
            result.then(resolve, reject);
          } else {
            resolve(result);
          }
        } catch (error) {
          reject(error);
        }
      });
    }
  });
}

#open in new window代码复用

现在发现 pendding、fulfilled、rejected 状态的代码非常相似,所以可以提取出方法 Parse 来复用

then(onFulfilled, onRejected) {
  if (typeof onFulfilled != "function") {
    onFulfilled = value => value;
  }
  if (typeof onRejected != "function") {
    onRejected = value => value;
  }
  return new HD((resolve, reject) => {
    if (this.status == HD.PENDING) {
      this.callbacks.push({
        onFulfilled: value => {
          this.parse(onFulfilled(this.value), resolve, reject);
        },
        onRejected: value => {
          this.parse(onRejected(this.value), resolve, reject);
        }
      });
    }
    if (this.status == HD.FULFILLED) {
      setTimeout(() => {
        this.parse(onFulfilled(this.value), resolve, reject);
      });
    }
    if (this.status == HD.REJECTED) {
      setTimeout(() => {
        this.parse(onRejected(this.value), resolve, reject);
      });
    }
  });
}
parse(result, resolve, reject) {
  try {
    if (result instanceof HD) {
      result.then(resolve, reject);
    } else {
      resolve(result);
    }
  } catch (error) {
    reject(error);
  }
}

#open in new window返回约束

then 的返回的 promise 不能是 then 相同的 Promise,下面是原生 Promise 的示例将产生错误

let promise = new Promise((resolve) => {
  setTimeout(() => {
    resolve("后盾人");
  });
});
let p = promise.then((value) => {
  return p;
});

解决上面的问题来完善代码,添加当前 promise 做为 parse 的第一个参数与函数结果比对

then(onFulfilled, onRejected) {
  if (typeof onFulfilled != "function") {
    onFulfilled = value => value;
  }
  if (typeof onRejected != "function") {
    onRejected = value => value;
  }
  let promise = new HD((resolve, reject) => {
    if (this.status == HD.PENDING) {
      this.callbacks.push({
        onFulfilled: value => {
          this.parse(promise, onFulfilled(this.value), resolve, reject);
        },
        onRejected: value => {
          this.parse(promise, onRejected(this.value), resolve, reject);
        }
      });
    }
    if (this.status == HD.FULFILLED) {
      setTimeout(() => {
        this.parse(promise, onFulfilled(this.value), resolve, reject);
      });
    }
    if (this.status == HD.REJECTED) {
      setTimeout(() => {
        this.parse(promise, onRejected(this.value), resolve, reject);
      });
    }
  });
  return promise;
}
parse(promise, result, resolve, reject) {
  if (promise == result) {
    throw new TypeError("Chaining cycle detected for promise");
  }
  try {
    if (result instanceof HD) {
      result.then(resolve, reject);
    } else {
      resolve(result);
    }
  } catch (error) {
    reject(error);
  }
}

现在进行测试也可以得到原生一样效果了

let p = new HD((resolve, reject) => {
  resolve("后盾人");
});
p = p.then((value) => {
  return p;
});

#open in new windowRESOLVE

下面来实现 Promise 的 resolve 方法

static resolve(value) {
  return new HD((resolve, reject) => {
    if (value instanceof HD) {
      value.then(resolve, reject);
    } else {
      resolve(value);
    }
  });
}

使用普通值的测试

HD.resolve("后盾人").then((value) => {
  console.log(value);
});

使用状态为 fulfilled 的 promise 值测试

HD.resolve(
  new HD((resolve) => {
    resolve("houdunren.com");
  })
).then((value) => {
  console.log(value);
});

使用状态为 rejected 的 Promise 测试

HD.resolve(
  new HD((_, reject) => {
    reject("reacted");
  })
).then(
  (value) => {
    console.log(value);
  },
  (reason) => {
    console.log(reason);
  }
);

#open in new windowREJEDCT

下面定义 Promise 的 rejecte 方法

static reject(reason) {
  return new HD((_, reject) => {
    reject(reason);
  });
}

使用测试

HD.reject("rejected").then(null, (reason) => {
  console.log(reason);
});

#open in new windowALL

下面来实现 Promise 的 all 方法

static all(promises) {
  let resolves = [];
  return new HD((resolve, reject) => {
    promises.forEach((promise, index) => {
      promise.then(
        value => {
          resolves.push(value);
          if (resolves.length == promises.length) {
            resolve(resolves);
          }
        },
        reason => {
          reject(reason);
        }
      );
    });
  });
}

来对所有 Promise 状态为 fulfilled 的测试

let p1 = new HD((resolve, reject) => {
  resolve("后盾人");
});
let p2 = new HD((resolve, reject) => {
  reject("后盾人");
});
let promises = HD.all([p1, p2]).then(
  (promises) => {
    console.log(promises);
  },
  (reason) => {
    console.log(reason);
  }
);

使用我们写的 resolve 进行测试

let p1 = HD.resolve("后盾人");
let p2 = HD.resolve("houdunren.com");
let promises = HD.all([p1, p2]).then(
  (promises) => {
    console.log(promises);
  },
  (reason) => {
    console.log(reason);
  }
);

其中一个 Promise 为 rejected 时的效果

let p1 = HD.resolve("后盾人");
let p2 = HD.reject("rejected");
let promises = HD.all([p1, p2]).then(
  (promises) => {
    console.log(promises);
  },
  (reason) => {
    console.log(reason);
  }
);

#open in new windowRACE

下面实现 Promise 的 race 方法

static race(promises) {
  return new HD((resolve, reject) => {
    promises.map(promise => {
      promise.then(value => {
        resolve(value);
      });
    });
  });
}

我们来进行测试

let p1 = HD.resolve("后盾人");
let p2 = HD.resolve("houdunren.com");
let promises = HD.race([p1, p2]).then(
  (promises) => {
    console.log(promises);
  },
  (reason) => {
    console.log(reason);
  }
);

使用延迟 Promise 后的效果

let p1 = new HD((resolve) => {
  setInterval(() => {
    resolve("后盾人");
  }, 2000);
});
let p2 = new HD((resolve) => {
  setInterval(() => {
    resolve("houdunren.com");
  }, 1000);
});
let promises = HD.race([p1, p2]).then(
  (promises) => {
    console.log(promises);
  },
  (reason) => {
    console.log(reason);
  }
);

基础知识

houdunren.com (opens new window)open in new window@ 大军大叔

操作文档 HTML 的 JS 处理方式为 DOM 即 Document Object Model 文档对象模型。如果对 HTML 很了解使用 DOM 并不复杂。

浏览器在加载页面是会生成 DOM 对象,以供我们使用 JS 控制页面元素。

#open in new window文档渲染

浏览器会将 HTML 文本内容进行渲染,并生成相应的 JS 对象,同时会对不符规则的标签进行处理。

  • 浏览器会将标签规范后渲染页面
  • 目的一让页面可以正确呈现
  • 目的二可以生成统一的 JS 可操作对象

#open in new window标签修复

在 html 中只有内容houdunren.com  而没有任何标签时,通过浏览器的  检查>元素  标签查看会自动修复成以下格式的内容

image

下面 H1 标签结束错误并且属性也没有引号,浏览器在渲染中会进行修复

<body>
  <h1 id=houdunren>后盾人<h1>
</body>

处理后的结果

<html>
  <head></head>
  <body>
    <h1 id="houdunren">后盾人</h1>
  </body>
</html>

#open in new window表格处理

表格 tabel 中不允许有内容,浏览器在渲染过程中会进行处理

<table>
  houdunren.com
  <tr>
    <td>houdunwang.com</td>
  </tr>
</table>

渲染后会添加 tbody 标签并将 table 中的字符移出

houdunren.com
<table>
  <tbody>
      <tr>
      <td>houdunwang.com</td>
    </tr>
  </tbody>
</table>

#open in new window标签移动

所有内容要写在 BODY 标签中,下面的 SCRIPT 标签写在了 BODY 后面,浏览器渲染后也会进行处理

<body></body>
<script>
  console.dir('houdunren.com')
</script>

渲染后处理的结果

<body>
  <script>console.dir('houdunren.com')</script>
</body>

#open in new window操作时机

需要保证浏览器已经渲染了内容才可以读取的节点对象,下例将无法读取到节点对象

<script>
  const node = document.getElementById('houdunwang')
  console.log(node) //null
</script>
<h1 id="houdunwang">houdunren.com</h1>

不过我们可以将脚本通过事件放在页面渲染完执行

<script>
  window.onload = () => {
    const node = document.getElementById('houdunwang')
    console.log(node)
  }
</script>
<h1 id="houdunwang">houdunren.com</h1>

或使用定时器将脚本设置为异步执行

<script>
  setTimeout(() => {
    const node = document.getElementById('houdunwang')
    console.log(node)
  })
</script>
<h1 id="houdunwang">houdunren.com</h1>

也可以放在文档加载后的事件处理函数中

<script>
  window.onload = function () {
    let hd = document.getElementById('hd')
    console.log(hd)
  }
</script>
<div id="hd">houdunren</div>

或将脚本设置在外部文件并使用 defer 属性加载,defer 即会等到 DOM 解析后迟延执行

<script defer="defer" src="3.js"></script>
<div id="houdunwang"></div>

#open in new window节点对象

JS 中操作 DOM 的内容称为节点对象(node),即然是对象就包括操作 NODE 的属性和方法

  • 包括 12 种类型的节点对象
  • 常用了节点为 document、标签元素节点、文本节点、注释节点
  • 节点均继承自 Node 类型,所以拥有相同的属性或方法
  • document 是 DOM 操作的起始节点
<body id="houdunwang">
  <!-- 后盾人 -->
</body>
<script>
    // document节点 noteType为9
  console.log(document.nodeType)

  // 第一个子节点为<!DOCTYPE html>,且nodetype为10
  console.log(document.childNodes.item(0).nodeType)

  // body 是标签节点 nodeType为1
  console.log(document.body.nodeType)

  // body的属性节点 nodeType 为2
  console.log(document.body.attributes[0].nodeType)

    // body的第一个节点为文本节点,nodeType为3
  console.log(document.body.childNodes.item(0).nodeType)

  // body的第二个节点为注释,nodeType类型为8
  console.log(document.body.childNodes[1].nodeType)
</script>

#open in new window原型链

在浏览器渲染过程中会将文档内容生成为不同的对象,我伙来对下例中的 h1 标签进行讨论,其他节点情况相似

  • 不同类型节点由专有的构造函数创建对象
  • 使用 console.dir 可以打印出 DOM 节点对象结构
  • 节点也是对象所以也具有 JS 对象的特征
<h1 id="houdunwang">houdunren.com</h1>
<script>
  function prototype(el) {
    console.dir(el.__proto__)
    el.__proto__ ? prototype(el.__proto__) : ''
  }
  const node = document.getElementById('houdunwang')
  prototype(node)
</script>

最终得到的节点的原型链为

原型说明
Object根对象,提供 hasOwnProperty 等基本对象操作支持
EventTarget提供 addEventListener、removeEventListener 等事件支持方法
Node提供 firstChild、parentNode 等节点操作方法
Element提供 getElementsByTagName、querySelector 等方法
HTMLElement所有元素的基础类,提供 childNodes、nodeType、nodeName、className、nodeName 等方法
HTMLHeadingElementHead 标题元素类

我们将上面的方法优化一下,实现提取节点原型链的数组

<h2 id="h2 value">houdunren.com</h2>
<input type="text" id="inputId" value="后盾人" />
<script>
    function prototype(el) {
        const prototypes = []
        prototypes.push(el.__proto__)
        prototypes.push(...(el.__proto__ ? prototype(el.__proto__) : []))
        return prototypes
    }
    const h2 = document.querySelector('h2')
    const input = document.querySelector('input')

    console.log(prototype(input))
</script>

下面为标题元素增加两个原型方法,改变颜色与隐藏元素

<h2 onclick="this.color('red')">houdunren.com</h2>
<script>
  const h2 = document.querySelector('h2')
  HTMLHeadingElement.prototype = Object.assign(HTMLHeadingElement.prototype, {
    color(color) {
      this.style.color = color
    },
    hide() {
      this.style.display = 'none'
    },
  })
</script>

#open in new window对象特征

即然 DOM 与我们其他 JS 创建的对象特征相仿,所以也可以为 DOM 对象添加属性或方法。

对于系统应用的属性,应该明确含义不应该随意使用,比如 ID 是用于标识元素唯一属性,不能用于其他目地

  • 后面会讲到其他解决方案,来自定义属性,ID 属性可以直接修改但是不建议这么做
let hd = document.getElementById("hd");
hd.id = "houdunren.com";
console.log(hd);

title 用于显示提示文档也不应该用于其他目地

<div id="hd">houdunren.com</div>
<script>
  let hd = document.getElementById('hd')
  hd.title = 'houdunren.com'
  console.log(hd)
</script>

下面是为对象合并属性的示例

<div id="hd">houdunren.com</div>
<script>
  let hd = document.getElementById('hd')

  Object.assign(hd, {
    //设置标签内容
    innerHTML: '大军大叔',
    color: 'red',
    change() {
      this.innerHTML = '后盾人'
      this.style.color = this.color
    },
    onclick() {
      this.change()
    },
  })
</script>

使用对象特性更改样式属性

<div id="hd">houdunren.com</div>
<script>
  let hd = document.getElementById('hd')
  Object.assign(hd.style, {
    color: 'white',
    backgroundColor: 'red',
  })
</script>

#open in new window常用节点

JS 提供了访问常用节点的 api

方法说明
documentdocument 是 DOM 操作的起始节点
document.documentElement文档节点即 html 标签节点
document.bodybody 标签节点
document.headhead 标签节点
document.links超链接集合
document.anchors所有锚点集合
document.formsform 表单集合
document.images图片集合

#open in new windowDOCUMENT

document 是 window 对象的属性,是由 HTMLDocument 类实现的实例。

  • document 包含 DocumentType(唯一)或 html 元素(唯一)或 comment 等元素

原型链中也包含 Node,所以可以使用有关节点操作的方法如 nodeType/NodeName 等

console.dir(document.nodeType);
console.dir(document.nodeName);

有关使用 Document 操作 cookie 与本地储存将会在相应章节中介绍

使用 title 获取和设置文档标题

//获取文档标题
console.log(document.title);

//设置文档标签
document.title = "后盾人-houdunren.com";

获取当前 URL

console.log(document.URL);

获取域名

document.domain;

获取来源地址

console.log(document.referrer);

系统针对特定标签提供了快速选择的方式

#open in new windowID

下面是直接使用 ID 获取元素(这是非标准操作,对浏览器有挑剔)

<div id="app">后盾人</div>
<script>
  // 直接通过 ID 获取元素(非标准操作)
  console.dir(app)
</script>

下面展示的是获取所有 a 标签

<div name="app">
  <a href="">houdunren.com</a>
  <a href="">houdunwang.com</a>
</div>
<script>
  const nodes = document.links
  console.dir(nodes)
</script>

#open in new windowanchors

下例是获取锚点集合后能过 锚点 name 属性获取元素

<div>
  <a href="" name="n1">houdunren.com</a>
  <a href="" name="n2">houdunwang.com</a>
</div>
<script>
  // 通过锚点获取元素
  console.dir(document.anchors.n2)
</script>

#open in new windowimages

下面是获取所有图片节点

<img src="" alt="" />
<img src="" alt="" />
<img src="" alt="" />
<script>
  // 获取所有图片节点
  console.dir(document.images)
</script>

#open in new window节点属性

不同类型的节点拥有不同属性,下面是节点属性的说明与示例

#open in new windownodeType

nodeType 指以数值返回节点类型

nodeType说明
1元素节点
2属性节点
3文本节点
8注释节点
9document 对象

下面是节点 nodeType 的示例

<div id="app">
  <div class="houdunren" data="hd">houdunren.com</div>
  <div class="houdunwang">houdunwang.com</div>
  <div class="xiangjun"><!-- 大军大叔 --></div>
</div>
<script>
  const node = document.querySelector(`#app`)
  console.log(node.nodeType) //1
  console.log(node.firstChild.nodeType) //3
  console.log(node.attributes.id.nodeType) //2

  const xj = document.querySelector('.xiangjun')
  console.log(xj.childNodes[0].nodeType) //8
</script>

下面是根据指定的 nodeType 递归获取节点元素的示例

  • 可获取文本、注释、标签等节点元素

#open in new windowPrototype

当然也可以使用对象的原型进行检测

  • section 、main、aslide 标签的原型对象为 HTMLElement
  • 其他非系统标签的原型对象为 HTMLUnknownElement
let h1 = document.querySelector("h1");
let p = document.querySelector("p");
console.log(h1 instanceof HTMLHeadingElement); //true
console.log(p instanceof HTMLHeadingElement); //false
console.log(p instanceof Element); //true

下例是通过构建函数获取节点的示例

#open in new windownodeName

nodeName 指定节点的名称

  • 获取值为大写形式
nodeTypenodeName
1元素名称如 DIV
2属性名称
3#text
8#comment

下面来操作 nodeName

<div id="app">
  <div class="houdunren" data="hd">houdunren.com</div>
  <div class="houdunwang">houdunwang.com</div>
  <div class="xiangjun"><!-- 大军大叔 --></div>
  <span> 后盾人</span>
</div>
<script>
  const div = document.querySelector(`#app`)
  const span = document.querySelector('span')

  // 标签节点为大写的标签名DIV
  console.log(div.nodeName)
  console.log(span.nodeName)

  // 文本节点为 #text
  console.log(div.firstChild.nodeName)

  //属性节点为属性名
  console.log(div.attributes.id.nodeName)

  // 注释节点为#comment
  const xj = document.querySelector('.xiangjun')
  console.log(xj.childNodes[0].nodeName)
</script>

#open in new windowtagName

nodeName 可以获取不限于元素的节点名,tagName 仅能用于获取标签节点的名称

  • tagName 存在于 Element 类的原型中
  • 文本、注释节点值为 undefined
  • 获取的值为大写的标签名
<div id="app">
  <div class="houdunren" data="hd">houdunren.com</div>
  <div class="houdunwang">houdunwang.com</div>
  <div class="xiangjun"><!-- 大军大叔 --></div>
  <span> 后盾人</span>
</div>
<script>
  const div = document.querySelector(`#app`)
  const span = document.querySelector('span')

  // 标签节点为大写的标签名 如DIV、SPAN
  console.log(div.tagName)
  console.log(span.tagName)

  // 文本节点为undefined
  console.log(div.firstChild.tagName)

  //属性节点为undefined
  console.log(div.attributes.id.tagName)

  // 注释节点为 undefined
  const xj = document.querySelector('.xiangjun')
  console.log(xj.childNodes[0].tagName)
</script>

#open in new windownodeValue

使用 nodeValue 或 data 函数获取节点值,也可以使用节点的 data 属性获取节点内容

nodeTypenodeValue
1null
2属性值
3文本内容
8注释内容

下面来看 nodeValue 的示例

<div id="app">
  <div class="houdunren">houdunren.com</div>
  <div class="houdunwang">houdunwang.com</div>
  <div class="xiangjun"><!-- 大军大叔 --></div>
</div>
<script>
  const node = document.querySelector(`#app`)
  //标签的 nodeValue 值为 null
  console.log(node.nodeValue)

  //属性的 nodeVale 值为属性值
  console.log(node.attributes.id.nodeValue)

  //文本的 nodeValue 值为文本内容
  const houdunwang = document.querySelector('.houdunwang')
  console.log(houdunwang.firstChild.nodeValue)

  //注释的 nodeValue 值为注释内容
  const xj = document.querySelector('.xiangjun')
  console.log(xj.childNodes[0].nodeValue)
</script>

使用 data 属性可以获取文本与注释内容

<div id="app">
  houdunren.com
  <!-- 后盾人 注释内容-->
</div>

<script>
  const app = document.querySelector('#app')
  console.log(app.childNodes[0].data)
  console.log(app.childNodes[1].data)
</script>

#open in new window树状节点

下面获取标签树状结构即多级标签结构,来加深一下节点的使用

<div id="app">
  <ul>
    <li><span></span><span></span></li>
    <li><span></span><span></span></li>
    <li><span></span><span></span></li>
  </ul>
</div>

<script>
function tree(el) {
  return Array.from(el.childNodes)
    .filter(node =>node.tagName)
    .map(node => ({
      name: node.nodeName,
      children: tree(node),
    }))
}
console.log(tree(document.getElementById('app')))

上例结果如下

Array(2)
0: {name: 'HEAD', children: Array(4)}
1: {name: 'BODY', children: Array(2)}

#open in new window节点集合

Nodelist 与 HTMLCollection 都是包含多个节点标签的集合,大部分功能也是相同的。

  • getElementsBy...等方法返回的是 HTMLCollection
  • querySelectorAll 返回的是 NodeList
  • NodeList 节点列表是动态的,即内容添加后会动态更新
<div></div>
<div></div>
<script>
  //结果为NodeList
  console.log(document.querySelectorAll('div'))

  //结果为HTMLCollection
  console.log(document.getElementsByTagName('div'))
</script>

#open in new windowlength

Nodelist 与 HTMLCollection 包含 length 属性,记录了节点元素的数量

<div name="app">
  <div id="houdunren">houdunren.com</div>
  <div name="houdunwang">houdunwang.com</div>
</div>
<script>
  const nodes = document.getElementsByTagName('div')
  for (let i = 0; i < nodes.length; i++) {
    console.log(nodes[i])
  }
</script>

#open in new windowitem

Nodelist 与 HTMLCollection 提供了 item()方法来根据索引获取元素

<div name="app">
  <div id="houdunren">houdunren.com</div>
  <div name="houdunwang">houdunwang.com</div>
</div>

<script>
  const nodes = document.getElementsByTagName('div')
  console.dir(nodes.item(0))
</script>

使用数组索引获取更方便

<div name="app">
  <div id="houdunren">houdunren.com</div>
  <div name="houdunwang">houdunwang.com</div>
</div>

<script>
  const nodes = document.getElementsByTagName('div')
  console.dir(nodes[0])
</script>

#open in new windownamedItem

HTMLCollection 具有 namedItem 方法可以按 name 或 id 属性来获取元素

<div name="app">
  <div id="houdunren">houdunren.com</div>
  <div name="houdunwang">houdunwang.com</div>
</div>

<script>
  const nodes = document.getElementsByTagName('div')
  console.dir(nodes.namedItem('houdunwang'))
   console.dir(nodes.namedItem('houdunren'))
</script>

也可以使用数组或属性方式获取

<div name="app">
  <div id="houdunren">houdunren.com</div>
  <div name="houdunwang">houdunwang.com</div>
</div>

<script>
  const nodes = document.getElementsByTagName('div')
  console.dir(nodes['houdunwang']);
  console.dir(nodes.houdunren)
</script>

数字索引时使用 item 方法,字符串索引时使用 namedItem 或 items 方法

<h1 id="hd">houdunren.com</h1>
<h1 name="xj">大军大叔</h1>
<script>
  let items = document.getElementsByTagName('h1')
  console.log(items[0])
  console.log(items['xj'])
</script>

#open in new window动态与静态

通过 getElementsByTagname 等 getElementsBy... 函数获取的 Nodelist 与 HTMLCollection 集合是动态的,即有元素添加或移动操作将实时反映最新状态。

  • 使用 getElement...返回的都是动态的集合
  • 使用 querySelectorAll 返回的是静态集合

#open in new window动态特性

下例中通过按钮动态添加元素后,获取的元素集合是动态的,而不是上次获取的固定快照。

<h1>houdunren.com</h1>
<h1>houdunwang.com</h1>
<button id="add">添加元素</button>

<script>
  let elements = document.getElementsByTagName('h1')
  console.log(elements)
  let button = document.querySelector('#add')
  button.addEventListener('click', () => {
    document.querySelector('body').insertAdjacentHTML('beforeend', '<h1>大军大叔</h1>')
    console.log(elements)
  })
</script>

document.querySelectorAll 获取的集合是静态的

<h1>houdunren.com</h1>
<h1>houdunwang.com</h1>
<button id="add">添加元素</button>

<script>
  let elements = document.querySelectorAll('h1')
  console.log(elements.length)
  let button = document.querySelector('#add')
  button.addEventListener('click', () => {
    document.querySelector('body').insertAdjacentHTML('beforeend', '<h1>大军大叔</h1>')
    console.log(elements.length)
  })
</script>

#open in new window使用静态

如果需要保存静态集合,则需要对集合进行复制

<div id="houdunren">houdunren.com</div>
<div name="houdunwang">houdunwang.com</div>
<script>
  const nodes = document.getElementsByTagName('div')
  const clone = Array.prototype.slice.call(nodes)
  console.log(nodes.length);//2
  document.body.appendChild(document.createElement('div'))
  console.log(nodes.length);//3
  console.log(clone.length);//2
</script>

#open in new window遍历节点

#open in new windowforOf

Nodelist 与 HTMLCollection 是类数组的可迭代对象所以可以使用 for...of 进行遍历

<div id="houdunren">houdunren.com</div>
<div name="houdunwang">houdunwang.com</div>
<script>
  const nodes = document.getElementsByTagName('div')
  for (const item of nodes) {
    console.log(item)
  }
</script>

#open in new windowforEach

Nodelist 节点列表也可以使用 forEach 来进行遍历,但 HTMLCollection 则不可以

<div id="houdunren">houdunren.com</div>
<div name="houdunwang">houdunwang.com</div>
<script>
  const nodes = document.querySelectorAll('div')
  nodes.forEach((node, key) => {
    console.log(node)
  })
</script>

#open in new windowcall/apply

节点集合对象原型中不存在 map 方法,但可以借用 Array 的原型 map 方法实现遍历

<div id="houdunren">houdunren.com</div>
<div name="houdunwang">houdunwang.com</div>

<script>
  const nodes = document.querySelectorAll('div')
  Array.prototype.map.call(nodes, (node, index) => {
    console.log(node, index)
  })
</script>

当然也可以使用以下方式操作

[].filter.call(nodes, (node) => {
  console.log(node);
});

#open in new windowArray.from

Array.from 用于将类数组转为组件,并提供第二个迭代函数。所以可以借用 Array.from 实现遍历

<div id="houdunren">houdunren.com</div>
<div name="houdunwang">houdunwang.com</div>

<script>
  const nodes = document.getElementsByTagName('div')
  Array.from(nodes, (node, index) => {
    console.log(node, index)
  })
</script>

#open in new window展开语法

下面使用点语法转换节点为数组

<h1>houdunren.com</h1>
<h1>houdunwang.com</h1>
<script>
  let elements = document.getElementsByTagName('h1')
  console.log(elements)
  ;[...elements].map((item) => {
    item.addEventListener('click', function () {
      this.style.textTransform = 'uppercase'
    })
  })
</script>

#open in new window节点关系

节点是父子级嵌套与前后兄弟关系,使用 DOM 提供的 API 可以获取这种关系的元素。

  • 文本和注释也是节点,所以也在匹配结果中

#open in new window基础知识

节点是根据 HTML 内容产生的,所以也存在父子、兄弟、祖先、后代等节点关系,下例中的代码就会产生这种多重关系

  • h1 与 ul 是兄弟关系
  • span 与 li 是父子关系
  • ul 与 span 是后代关系
  • span 与 ul 是祖先关系
<h1>后盾人</h1>
<ul>
  <li>
    <span>houdunren</span>
    <strong>houdunwang</strong>
  </li>
</ul>

下面是通过节点关系获取相应元素的方法

节点属性说明
childNodes获取所有子节点
parentNode获取父节点
firstChild第一个子节点
lastChild最后一个子节点
nextSibling下一个兄弟节点
previousSibling上一个兄弟节点

子节点集合与首、尾节点获取

  • 文本也是 node 所以也会在匹配当中
<div id="app">
  <div class="houdunren" data="hd">houdunren.com</div>
  <div class="houdunwang">houdunwang.com</div>
  <div class="xiangjun">大军大叔</div>
</div>
<script>
  const node = document.querySelector(`#app`)
  console.log(node.childNodes) //所有子节点
  console.log(node.firstChild) //第一个子节点是文本节点
  console.log(node.lastChild) //最后一个子节点也是文本节点
</script>

下面通过示例操作节点关联

  • 文本也是 node 所以也会在匹配当中
<div id="app">
  <div class="houdunren" data="hd">houdunren.com</div>
  <div class="houdunwang">houdunwang.com</div>
  <div class="xiangjun">大军大叔</div>
</div>
<script>
  const node = app.querySelector(`.houdunwang`)
  console.log(node.parentNode) //div#app
  console.log(node.childNodes) //文本节点
  console.log(node.nextSibling) //下一个兄弟节点是文本节点
  console.log(node.previousSibling) //上一个节点也是文本节点
</script>

document 是顶级节点 html 标签的父节点是 document

<script>console.log(document.documentElement.parentNode === document)</script>

#open in new window父节点集合

下例是查找元素的所有父节点

<div id="houdunren">houdunren.com</div>

<script>
  function parentNodes(node) {
    let nodes = []
    while ((node = node.parentNode)) nodes.push(node)
    return nodes
  }
  const el = document.getElementById('houdunren')
  const nodes = parentNodes(el)
  console.log(nodes)
</script>

使用递归获取所有父级节点

<div>
  <ul>
    <li><span></span></li>
  </ul>
</div>
<script>
  const span = document.querySelector('span')

  function parentNodes(node) {
    const nodes = new Array(node.parentNode)
    if (node.parentNode) nodes.push(...parentNodes(node.parentNode))
    return nodes
  }

  const nodes = parentNodes(document.querySelector('span'))
  console.log(nodes)
</script>

#open in new window后代节点集合

获取所有的后代元素 SPAN 的内容

<div id="app">
  <span>houdunren.com</span>
  <h2>
    <span>houdunwang.com</span>
  </h2>
</div>

<script>
  function getChildNodeByName(el, name) {
    const items = []
    Array.from(el.children).forEach(node => {
      if (node.tagName == name.toUpperCase()) items.push(node)
      items.push(...getChildNodeByName(node, name))
    })

    return items
  }
  const nodes = getChildNodeByName(document, 'span')
  console.log(nodes)
</script>

#open in new window标签关系

使用 childNodes 等获取的节点包括文本与注释,但这不是我们常用的,为此系统也提供了只操作元素的关系方法。

#open in new window基础知识

下面是处理标签关系的常用 API

节点属性说明
parentElement获取父元素
children获取所有子元素
childElementCount子标签元素的数量
firstElementChild第一个子标签
lastElementChild最后一个子标签
previousElementSibling上一个兄弟标签
nextElementSibling下一个兄弟标签
contains返回布尔值,判断传入的节点是否为该节点的后代节点

以下实例展示怎样通过元素关系获取元素

<div id="app">
  <div class="houdunren" data="hd">houdunren.com</div>
  <div class="houdunwang">houdunwang.com</div>
  <div class="xiangjun"><!-- 大军大叔 --></div>
</div>

<script>
  const app = document.querySelector(`#app`)
  console.log(app.children) //所有子元素
  console.log(app.firstElementChild) //第一个子元素 div.houdunren
  console.log(app.lastElementChild) //最后一个子元素 div.xiangjun

  const houdunwang = document.querySelector('.houdunwang')
  console.log(houdunwang.parentElement) //父元素 div#app

  console.log(houdunwang.previousElementSibling) //上一个兄弟元素 div.houdunren
  console.log(houdunwang.nextElementSibling) //下一个兄弟元素 div.xiangjun
</script>

html 标签的父节点是 document,但父标签节点不存在

<script>
  console.log(document.documentElement.parentNode === document) //true
  console.log(document.documentElement.parentElement) //null
</script>

#open in new window按类名获取标签

下例是按 className 来获取标签

<div>
  <ul>
    <li class="hd item">houdunren.com</li>
    <li class="item">后盾人</li>
    <li class="hd">大军</li>
  </ul>
</div>
<script>
  function getTagByClassName(className, tag = document) {
    const items = []
    Array.from(tag.children).map(el => {
      if (el.className.includes(className)) items.push(el)
      items.push(...getTagByClassName(className, el))
    })
    return items
  }

  console.log(getTagByClassName('hd'))
</script>

#open in new window标签获取

系统提供了丰富的选择节点(NODE)的操作方法,下面我们来一一说明。

#open in new windowgetElementById

使用 ID 选择是非常方便的选择具有 ID 值的节点元素,但注意 ID 应该是唯一的

只能通过 document 对象上使用

<div id="houdunren">houdunren.com</div>
<script>
  const node = document.getElementById('houdunren')
  console.dir(node)
</script>

getElementById 只能通过 document 访问,不能通过元素读取拥有 ID 的子元素,下面的操作将产生错误

<div id="app">
  houdunren.com
  <div id="houdunwang">houdunwang.com</div>
</div>
<script>
  const app = document.getElementById('app')
  const node = app.getElementById('houdunwang') //app.getElementById is not a function
  console.log(node)
</script>

下面自定义函数来支持批量按 ID 选择元素

<div id="houdunren">houdunren.com</div>
<div id="app"></div>
<script>
  function getByElementIds(ids) {
    return ids.map((id) => document.getElementById(id))
  }
  let nodes = getByElementIds(['houdunren', 'app'])
  console.dir(nodes)
</script>

拥有 ID 的元素可做为 WINDOW 的属性进行访问

<div id="app">
  houdunren.com
</div>
<script>
  console.log(app.innerHTML)
</script>

如果声明了变量这种访问方式将无效,所以并不建议使用这种方式访问对象

<div id="app">
  houdunren.com
</div>
<script>
  let app = 'houdunwang'
  console.log(app.innerHTML)
</script>

#open in new windowgetElementsByName

使用 getElementByName 获取设置了 name 属性的元素,虽然在 DIV 等元素上同样有效,但一般用来对表单元素进行操作时使用。

  • 返回 NodeList 节点列表对象
  • NodeList 顺序为元素在文档中的顺序
  • 需要在 document 对象上使用
<div name="houdunren">houdunren.com</div>
<input type="text" name="username" />

<script>
  const div = document.getElementsByName('houdunren')
  console.dir(div)
  const input = document.getElementsByName('username')
  console.dir(input)
</script>

#open in new windowgetElementsByTagName

使用 getElementsByTagName 用于按标签名获取元素

  • 返回 HTMLCollection 节点列表对象
  • 是不区分大小的获取
<div name="houdunren">houdunren.com</div>
<div id="app"></div>
<script>
  const divs = document.getElementsByTagName('div')
  console.dir(divs)
</script>

通配符

可以使用通配符 ***** 获取所有元素

<div name="houdunren">houdunren.com</div>
<div id="app"></div>

<script>
  const nodes = document.getElementsByTagName('*')
  console.dir(nodes)
</script>

某个元素也可以使用通配置符 ***** 获取后代元素,下面获取 id 为 houdunren 的所有后代元素

<div id="houdunren">
  <span>houdunren.com</span>
  <span>houdunwang.com</span>
</div>

<script>
  const nodes = document.getElementsByTagName('*').namedItem('houdunren').getElementsByTagName('*')
  console.dir(nodes)
</script>

#open in new windowgetElementsByClassName

getElementsByClassName 用于按 class 样式属性值获取元素集合

  • 设置多个值时顺序无关,指包含这些 class 属性的元素
<div class="houdunren houdunwang xiangjun">houdunren.com</div>
<div class="houdunwang">houdunwang.com</div>

<script>
  const nodes = document.getElementsByClassName('houdunwang')
  console.log(nodes.length) //2

  //查找同时具有 houdunwang 与 houdunren 两个class属性的元素
  const tags = document.body.getElementsByClassName('houdunwang houdunren ')
  console.log(tags.length) //1
</script>

下面我们来自己开发一个与 getElementsByClassName 相同的功能函数

<div class="houdunren houdunwang xiangjun">houdunren.com</div>
<div class="houdunwang">houdunwang.com</div>
<script>
  function getByClassName(names) {
    //将用户参数转为数组,并过滤掉空值
    const classNames = names.split(/\s+/).filter(t => t)

    return Array.from(document.getElementsByTagName('*')).filter(tag => {
      // 提取标签的所有 class 值为数组
      return classNames.every(className => {
        const names = tag.className
          .toUpperCase()
          .split(/\s+/)
          .filter(t => t)

        //检索标签是否存在class
        return names.some(name => name == className.toUpperCase())
      })
    })
  }

  console.log(getByClassName('houdunwang houdunren '))
</script>

#open in new window样式选择器

在 CSS 中可以通过样式选择器修饰元素样式,在 DOM 操作中也可以使用这种方式查找元素。使用过 jQuery 库的朋友,应该对这种选择方式印象深刻。

使用 getElementsByTagName 等方式选择元素不够灵活,建议使用下面的样式选择器操作,更加方便灵活

#open in new windowquerySelectorAll

使用 querySelectorAll 根据 CSS 选择器获取 Nodelist 节点列表

  • 获取的 NodeList 节点列表是静态的,添加或删除元素后不变

获取所有 div 元素

<div class="xiangjun">大军大叔</div>
<div id="app">
  <div class="houdunren houdunwang">houdunren.com</div>
  <div class="houdunwang">houdunwang.com</div>
</div>

<script>
  const app = document.getElementById('app')
  const nodes = app.querySelectorAll('div')
  console.log(nodes.length) //2
</script>

获取 id 为 app 元素的,class 为 houdunren 的后代元素

<div class="xiangjun">大军大叔</div>
<div id="app">
  <div class="houdunren houdunwang">houdunren.com</div>
  <div class="houdunwang">houdunwang.com</div>
</div>
<script>
  const nodes = document.querySelectorAll('#app .houdunren')
  console.log(nodes.length) //2
</script>

根据元素属性值获取元素集合

<div id="app">
  <div class="houdunren houdunwang" data="hd">houdunren.com</div>
  <div class="houdunwang">houdunwang.com</div>
</div>
<script>
  const nodes = document.querySelectorAll(`#app .houdunren[data='hd']`)
  console.log(nodes.length) //2
</script>

再来看一些通过样式选择器查找元素

<div id="app">
  <div class="houdunren">houdunren.com</div>
  <div class="houdunwang">houdunwang.com</div>
  <span>后盾人</span>
</div>

<script>
  //查找紧临兄弟元素
  console.log(document.querySelectorAll('.houdunren+div.houdunwang'))

  //查找最后一个 div 子元素
  console.log(document.querySelector('#app div:last-of-type'))

  //查找第二个 div 元素
  console.log(document.querySelector('#app div:nth-of-type(2)').innerHTML)
</script>

#open in new windowquerySelector

querySelector 使用 CSS 选择器获取一个元素,下面是根据属性获取单个元素

<div id="app">
  <div class="houdunren houdunwang" data="hd">houdunren.com</div>
  <div class="houdunwang">houdunwang.com</div>
</div>
<script>
  const node = app.querySelector(`#app .houdunren[data='hd']`)
  console.log(node)
</script>

#open in new windowmatches

用于检测元素是否是指定的样式选择器匹配,下面过滤掉所有 name 属性的 LI 元素

<div id="app">
  <li>houdunren</li>
  <li>大军大叔</li>
  <li name="houdunwang">houdunwang.com</li>
</div>
<script>
  const nodes = [...document.querySelectorAll('li')].filter(node => {
    return !node.matches(`[name]`)
  })
  console.log(nodes)
</script>

#open in new windowclosest

查找最近的符合选择器的祖先元素(包括自身),下例查找父级拥有  .comment类的元素

<div class="comment">
  <ul class="comment">
    <li>houdunren.com</li>
  </ul>
</div>

<script>
  const li = document.getElementsByTagName('li')[0]
  const node = li.closest(`.comment`)
  //结果为 ul.comment
  console.log(node)
</script>

#open in new window标准属性

元素的标准属性具有相对应的 DOM 对象属性

  • 操作属性区分大小写
  • 多个单词属性命名规则为第一个单词小写,其他单词大写
  • 属性值是多类型并不全是字符串,也可能是对象等
  • 事件处理程序属性值为函数
  • style 属性为 CSSStyleDeclaration 对象
  • DOM 对象不同生成的属性也不同

#open in new window属性别名

有些属性名与 JS 关键词冲突,系统已经起了别名

属性别名
classclassName
forhtmlFor

#open in new window操作属性

元素的标准属性可以直接进行操作,下面是直接设置元素的 className

<div id="app">
  <div class="houdunren" data="hd">houdunren.com</div>
  <div class="houdunwang">houdunwang.com</div>
</div>
<script>
  const app = document.querySelector(`#app`)
  app.className = 'houdunren houdunwang'
</script>

下面设置图像元素的标准属性

<img src="" alt="" />
<script>
  let img = document.images[0]
  img.src = 'https://www.houdurnen.com/avatar.jpg'
  img.alt = '后盾人'
</script>

使用 hidden 隐藏元素

<div id="app">houdunren.com</div>
<script>
  const app = document.querySelector('#app')
  app.addEventListener('click', function () {
    this.hidden = true
  })
</script>

#open in new window多类型

大部分属性值是都是字符串,但并不是全部,下例中需要转换为数值后进行数据运算

<input type="number" name="age" value="88" />

<script>
  let input = document.getElementsByName('age').item(0)
  input.value = parseInt(input.value) + 100
</script>

下面表单 checked 属性值为 Boolean 类型

<label for="hot"> <input id="hot" type="checkbox" name="hot" />热门 </label>
<script>
  const node = document.querySelector(`[name='hot']`)
  node.addEventListener('change', function () {
    console.log(this.checked)
  })
</script>

属性值并都与 HTML 定义的值一样,下面返回的 href 属性值是完整链接

<a href="#houdunren" id="home">后盾人</a>
<script>
  const node = document.querySelector(`#home`)
  console.log(node.href)
</script>

#open in new window元素特征

对于标准的属性可以使用 DOM 属性的方式进行操作,但对于标签的非标准的定制属性则不可以。但 JS 提供了方法来控制标准或非标准的属性

可以理解为元素的属性分两个地方保存,DOM 属性中记录标准属性,特征中记录标准和定制属性

  • 使用特征操作时属性名称不区分大小写
  • 特征值都为字符串类型
方法说明
getAttribute获取属性
setAttribute设置属性
removeAttribute删除属性
hasAttribute属性检测

特征是可迭代对象,下面使用 for...of 来进行遍历操作

<div id="app" content="后盾人" color="red">houdunwang.com</div>
<script>
  const app = document.querySelector('#app')
  for (const { name, value } of app.attributes) {
    console.log(name, value)
  }
</script>

属性值都为字符串,所以数值类型需要进行转换

<input type="number" name="age" value="88" />
<script>
  let input = document.getElementsByName('age').item(0)
  let value = input.getAttribute('value') * 1 + 100
  input.setAttribute('value', value)
</script>

使用 removeAttribute 删除元素的 class 属性,并通过 hasAttribute 进行检测删除结果

<div class="houdunwang">houdunwang.com</div>
<script>
  let houdunwang = document.querySelector('.houdunwang')
  houdunwang.removeAttribute('class')
  console.log(houdunwang.hasAttribute('class')) //false
</script>

特征值与 HTML 定义是一致的,这和属性值是不同的

<a href="#houdunren" id="home">后盾人</a>
<script>
  const node = document.querySelector(`#home`)

  // http://127.0.0.1:5500/test.html#houdunren
  console.log(node.href)

  // #houdunren
  console.log(node.getAttribute('href'))
</script>

#open in new windowattributes

元素提供了 attributes 属性可以只读的获取元素的属性

<div class="houdunwang" data-content="后盾人">houdunwang.com</div>
<script>
  let houdunwang = document.querySelector('.houdunwang')
  console.dir(houdunwang.attributes['class'].nodeValue) //houdunwang
  console.dir(houdunwang.attributes['data-content'].nodeValue) //后盾人
</script>

#open in new window自定义特征

虽然可以随意定义特征并使用 getAttribute 等方法管理,但很容易造成与标签的现在或未来属性重名。建议使用以 data-为前缀的自定义特征处理,针对这种定义方式 JS 也提供了接口方便操作。

  • 元素中以 data-为前缀的属性会添加到属性集中
  • 使用元素的 dataset 可获取属性集中的属性
  • 改变 dataset 的值也会影响到元素上

下面演示使用属性集设置 DIV 标签内容

<div class="houdunwang" data-content="后盾人" data-color="red">houdunwang.com</div>

<script>
  let houdunwang = document.querySelector('.houdunwang')
  let content = houdunwang.dataset.content
  console.log(content) //后盾人
  houdunwang.innerHTML = `<span style="color:${houdunwang.dataset.color}">${content}</span>`
</script>

多个单词的特征使用驼峰命名方式读取

<div class="houdunwang" data-title-color="red">houdunwang.com</div>
<script>
  let houdunwang = document.querySelector('.houdunwang')
  houdunwang.innerHTML = `
    <span style="color:${houdunwang.dataset.titleColor}">${houdunwang.innerHTML}</span>
  `
</script>

改变 dataset 值也会影响到页面元素上

<div class="houdunwang" data-title-color="red">houdunwang.com</div>
<script>
  let houdunwang = document.querySelector('.houdunwang')
  houdunwang.addEventListener('click', function () {
    this.dataset.titleColor = ['red', 'green', 'blue'][Math.floor(Math.random() * 3)]
    this.style.color = this.dataset.titleColor
  })
</script>

#open in new window属性同步

特征和属性是记录元素属性的两个不同场所,大部分更改会进行同步操作。

下面使用属性更改了 className,会自动同步到了特征集中,反之亦然

<div id="app" class="red">houdunren.com</div>
<script>
  const app = document.querySelector('#app')
  app.className = 'houdunwang'
  console.log(app.getAttribute('class')) //houdunwang
  app.setAttribute('class', 'blue')
  console.log(app.className) //blue
</script>

下面对 input 值使用属性设置,但并没有同步到特征

<input type="text" name="package" value="houdunren.com" />
<script>
  const package = document.querySelector(`[name='package']`)
  package.value = 'houdunwang.com'
  console.log(package.getAttribute('value'))//houdunren.com
</script>

但改变 input 的特征 value 会同步到 DOM 对象属性

<input type="text" name="package" value="houdunren.com" />
<script>
  const package = document.querySelector(`[name='package']`)
  package.setAttribute('value', 'houdunwang.com')
  console.log(package.value) //houdunwang.com
</script>

#open in new window创建节点

创建节点的就是构建出 DOM 对象,然后根据需要添加到其他节点中

#open in new windowappend

append 也是用于添加元素,同时他也可以直接添加文本等内容。

<script>
  document.body.append((document.createElement('div').innerText = '大军'))
  document.body.append('houdunren.com')
</script>

#open in new windowcreateTextNode

创建文本对象并添加到元素中

<div id="app"></div>
<script>
  let app = document.querySelector('#app')
  let text = document.createTextNode('houdunren')
  app.append(text)
</script>

#open in new windowcreateElement

使用 createElement 方法可以标签节点对象,创建 span 标签新节点并添加到 div#app

<div id="app"></div>
<script>
  let app = document.querySelector('#app')
  let span = document.createElement('span')
  span.innerHTML = 'houdunren'
  app.append(span)
</script>

使用 PROMISE 结合节点操作来加载外部 JAVASCRIPT 文件

function js(file) {
  return new Promise((resolve, reject) => {
    let js = document.createElement("script");
    js.type = "text/javascript";
    js.src = file;
    js.onload = resolve;
    js.onerror = reject;
    document.head.appendChild(js);
  });
}

js("11.js")
  .then(() => console.log("加载成功"))
  .catch((error) => console.log(`${error.target.src} 加载失败`));

使用同样的逻辑来实现加载 CSS 文件

function css(file) {
  return new Promise((resolve, reject) => {
    let css = document.createElement("link");
    css.rel = "stylesheet";
    css.href = file;
    css.onload = resolve;
    css.onerror = reject;
    document.head.appendChild(css);
  });
}
css("1.css").then(() => {
  console.log("加载成功");
});

#open in new windowcloneNode&importNode

使用 cloneNode 和 document.importNode 用于复制节点对象操作

  • cloneNode 是节点方法
  • cloneNode 参数为 true 时递归复制子节点即深拷贝
  • importNode 是 documet 对象方法

复制 div#app 节点并添加到 body 元素中

<div id="app">houdunren</div>
<script>
  let app = document.querySelector('#app')
  let newApp = app.cloneNode(true)
  document.body.appendChild(newApp)
</script>

document.importNode 方法是部分 IE 浏览器不支持的,也是复制节点对象的方法

  • 第一个参数为节点对象
  • 第二个参数为 true 时递归复制
<div id="app">houdunren</div>
<script>
  let app = document.querySelector('#app')
  let newApp = document.importNode(app, true)
  document.body.appendChild(newApp)
</script>

#open in new window节点内容

#open in new windowinnerHTML

inneHTML 用于向标签中添加 html 内容,同时触发浏览器的解析器重绘 DOM。

下例使用 innerHTML 获取和设置 div 内容

  • innerHTML 中只解析 HTML 标签语法,所以其中的 script 不会做为 JS 处理
<div id="app">
  <div class="houdunren" data="hd">houdunren.com</div>
  <div class="houdunwang">houdunwang.com</div>
</div>
<script>
  let app = document.querySelector('#app')
  console.log(app.innerHTML)

  app.innerHTML = '<h1>后盾人</h1>'
</script>

重绘节点

使用 innertHTML 操作会重绘元素,下面在点击第二次就没有效果了

  • 因为对#app 内容进行了重绘,即删除原内容然后设置新内容
  • 重绘后产生的 button 对象没有事件
  • 重绘后又产生了新 img 对象,所以在控制台中可看到新图片在加载
<div id="app">
  <button>houdunren.com</button>
  <img src="1.jpg" alt="" />
</div>
<script>
  const app = document.querySelector('#app')
  app.querySelector('button').addEventListener('click', function () {
    alert(this.innerHTML)
    this.parentElement.innerHTML += '<hr/>大军大叔'
  })
</script>

#open in new windowouterHTML

outerHTML 与 innerHTML 的区别是包含父标签

  • outerHTML 不会删除原来的旧元素
  • 只是用新内容替换替换旧内容,旧内容(元素)依然存在

下面将 div#app 替换为新内容

<div id="app">
  <div class="houdunren" data="hd">houdunren.com</div>
  <div class="houdunwang">houdunwang.com</div>
</div>
<script>
  let app = document.querySelector('#app')
  console.log(app.outerHTML)

  app.outerHTML = '<h1>后盾人</h1>'
</script>

使用 innerHTML 内容是被删除然后使用新内容

<div id="app">
  houdunren.com
</div>
<script>
  const app = document.querySelector('#app')
  console.log(app)
  app.innerHTML = 'houdunwang.com'
  console.log(app)
</script>

而使用 outerHTML 是保留旧内容,页面中使用新内容

<div id="app">
  houdunren.com
</div>
<script>
  const app = document.querySelector('#app')
  console.log(app)
  app.outerHTML = 'houdunwang.com'
  console.log(app)
</script>

#open in new windowtextContent 与 innerText

textContent 与 innerText 是访问或添加文本内容到元素中

  • textContentb 部分 IE 浏览器版本不支持
  • innerText 部分 FireFox 浏览器版本不支持
  • 获取时忽略所有标签,只获取文本内容
  • 设置时将内容中的标签当文本对待不进行标签解析

获取时忽略内容中的所有标签

<div id="app">
  <h1>houdunren.com</h1>
</div>
<script>
  let app = document.querySelector('#app')
  console.log(app.textContent)
</script>

设置时将标签当文本对待,即转为 HTML 实体内容

<div id="app">
  <div class="houdunren" data="hd">houdunren.com</div>
  <div class="houdunwang">houdunwang.com</div>
</div>
<script>
  let app = document.querySelector('#app')
  app.textContent="<h1>后盾人</h1>"
</script>

#open in new windowouterText

与 innerText 差别是会影响所操作的标签

<h1>houdunren.com</h1>
<script>
  let h1 = document.querySelector('h1')
  h1.outerText = '后盾人'
</script>

#open in new windowinsertAdjacentText

将文本插入到元素指定位置,不会对文本中的标签进行解析,包括以下位置

选项说明
beforebegin元素本身前面
afterend元素本身后面
afterbegin元素内部前面
beforeend元素内部后面

添加文本内容到 div#app 前面

<div id="app">
  <div class="houdunren" data="hd">houdunren.com</div>
  <div class="houdunwang">houdunwang.com</div>
</div>
<script>
  let app = document.querySelector('#app')
  let span = document.createElement('span')
  app.insertAdjacentText('beforebegin', '<h1>后盾人</h1>')
</script>

#open in new window节点管理

现在我们来讨论下节点元素的管理,包括添加、删除、替换等操作

#open in new window推荐方法

方法说明
append节点尾部添加新节点或字符串
prepend节点开始添加新节点或字符串
before节点前面添加新节点或字符串
after节点后面添加新节点或字符串
replaceWith将节点替换为新节点或字符串

在标签内容后面添加新内容

<div id="app">
  houdunren.com
</div>
<script>
  let app = document.querySelector('#app')
  app.append('-houdunwang.com')
</script>

同时添加多个内容,包括字符串与元素标签

<div id="app">
  houdunren.com
</div>
<script>
  let app = document.querySelector('#app')
  let h1 = document.createElement('h1')
  h1.append('后盾人')
  app.append('@', h1)
</script>

将标签替换为新内容

<div id="app">
  houdunren.com
</div>
<script>
  let app = document.querySelector('#app')
  let h1 = document.createElement('h1')
  h1.append('houdunwang.com')
  app.replaceWith(h1)
</script>

添加新元素 h1 到目标元素 div#app 里面

<div id="app"></div>
<script>
  let app = document.querySelector('#app')
  let h1 = document.createElement('h1')
  h1.innerHTML = 'houdunren'
  app.append(h1)
</script>

将 h2 移动到 h1 之前

<h1>houdunren.com@h1</h1>
<h2>houdunwang@h2</h2>
<script>
  let h1 = document.querySelector('h1')
  let h2 = document.querySelector('h2')
  h1.before(h2)
</script>

使用 remove 方法可以删除节点

<div id="app">
  houdunren.com
</div>
<script>
  let app = document.querySelector('#app')
  app.remove()
</script>

#open in new windowinsertAdjacentHTML

将 html 文本插入到元素指定位置,浏览器会对文本进行标签解析,包括以下位置

选项说明
beforebegin元素本身前面
afterend元素本身后面
afterbegin元素内部前面
beforeend元素内部后面

在 div#app 前添加 HTML 文本

<div id="app">
  <div class="houdunren" data="hd">houdunren.com</div>
  <div class="houdunwang">houdunwang.com</div>
</div>
<script>
  let app = document.querySelector('#app')
  let span = document.createElement('span')
  app.insertAdjacentHTML('beforebegin', '<h1>后盾人</h1>')
</script>

#open in new windowinsertAdjacentElement

insertAdjacentElement() 方法将指定元素插入到元素的指定位置,包括以下位置

  • 第一个参数是位置
  • 第二个参数为新元素节点
选项说明
beforebegin元素本身前面
afterend元素本身后面
afterbegin元素内部前面
beforeend元素内部后面

在 div#app 前插入 span 标签

<div id="app">
  <div class="houdunren" data="hd">houdunren.com</div>
  <div class="houdunwang">houdunwang.com</div>
</div>
<script>
  let app = document.querySelector('#app')
  let span = document.createElement('span')
  span.innerHTML = '后盾人'
  app.insertAdjacentElement('beforebegin', span)
</script>

#open in new window古老方法

下面列表过去使用的操作节点的方法,现在不建议使用了。但在阅读老代码时可来此查看语法

方法说明
appendChild添加节点
insertBefore用于插入元素到另一个元素的前面
removeChild删除节点
replaceChild进行节点的替换操作

#open in new windowDocumentFragment

当对节点进行添加、删除等操作时,都会引起页面回流来重新渲染页面,即重新渲染颜色,尺寸,大小、位置等等。所以会带来对性能的影响。

解决以上问题可以使用以下几种方式

  1. 可以将 DOM 写成 html 字符串,然后使用 innerHTML 添加到页面中,但这种操作会比较麻烦,且不方便使用节点操作的相关方法。
  2. 使用 createDocumentFragment 来管理节点时,此时节点都在内存中,而不是 DOM 树中。对节点的操作不会引发页面回流,带来比较好的性能体验。

DocumentFragment 特点

  • createDocumentFragment 父节点为 null
  • 继承自 node 所以可以使用 NODE 的属性和方法
  • createDocumentFragment 创建的是文档碎片,节点类型 nodeType 为 11。因为不在 DOM 树中所以只能通过 JS 进行操作
  • 添加 createDocumentFragment 添加到 DOM 后,就不可以再操作 createDocumentFragment 元素了,这与 DOM 操作是不同的
  • 将文档 DOM 添加到 createDocumentFragment 时,会移除文档中的 DOM 元素
  • createDocumentFragment 创建的节点添加到其他节点上时,会将子节点一并添加
  • createDocumentFragment 是虚拟节点对象,不直接操作 DOM 所以性能更好
  • 在排序/移动等大量 DOM 操作时建议使用 createDocumentFragment

#open in new window表单控制

表单是高频操作的元素,下面来掌握表单项的 DOM 操作

#open in new window表单查找

JS 为表单的操作提供了单独的集合控制

  • 使用 document.forms 获取表单集合
  • 使用 form 的 name 属性获取指定 form 元素
  • 根据表单项的 name 属性使用 form.elements.title 获取表单项,
  • 也可以直接写成 form.name 形式,不需要 form.elements.title
  • 针对 radio/checkbox 获取的表单项是一个集合
<form action="" name="hd">
  <input type="text" name="title" />
</form>
<script>
  const form = document.forms.hd
  console.log(form.elements.title)
</script>

通过表单项可以反向查找 FORM

<form action="" name="hd">
  <input type="text" name="title" />
</form>
<script>
  const form = document.forms.hd
  console.log(form.title.form === form) //true
</script>

#open in new window样式管理

通过 DOM 修改样式可以通过更改元素的 class 属性或通过 style 对象设置行样式来完成。

  • 建议使用 class 控制样式,将任务交给 CSS 处理,更简单高效

#open in new window批量设置

使用 JS 的 className 可以批量设置样式

<div id="app" class="d-flex container">后盾人</div>
<script>
  let app = document.getElementById('app')
  app.className = 'houdunwang'
</script>

也可以通过特征的方式来更改

<div id="app" class="d-flex container">后盾人</div>
<script>
  let app = document.getElementById('app')
  app.setAttribute('class', 'houdunwang')
</script>

#open in new windowclassList

如果对类单独进行控制使用 classList 属性操作

方法说明
node.classList.add添加类名
node.classList.remove删除类名
node.classList.toggle切换类名
node.classList.contains类名检测

在元素的原有 class 上添加新 class

<div id="app" class="d-flex container">后盾人</div>
<script>
  let app = document.getElementById('app')
  app.classList.add('houdunwang')
</script>

使用 classList 也可以移除 class 列表中的部分 class

<div id="app" class="d-flex container">后盾人</div>
<script>
  let app = document.getElementById('app')
  app.classList.remove('container')
</script>

使用 toggle 切换类,即类已经存在时删除,不存在时添加

<div id="app" class="d-flex container">后盾人</div>
<script>
  let app = document.getElementById('app')
  app.addEventListener('click', function () {
    this.classList.toggle('houdunwang')
  })
</script>

使用 contains 检查 class 是否存在

<div id="app" class="d-flex container">后盾人</div>
<script>
  let app = document.getElementById('app')
  console.log(app.classList.contains('container')) //true
  console.log(app.classList.contains('houdunwang')) //false
</script>

#open in new window设置行样式

使用 style 对象可以对样式属性单独设置,使用 cssText 可以批量设置行样式

样式属性设置

使用节点的 style 对象来设置行样式

  • 多个单词的属性使用驼峰进行命名
<div id="app" class="d-flex container">后盾人</div>
<script>
  let app = document.getElementById('app')
  app.style.backgroundColor = 'red'
  app.style.color = 'yellow'
</script>

批量设置行样式

使用 cssText 属性可以批量设置行样式,属性名和写 CSS 一样不需要考虑驼峰命名

<div id="app" class="d-flex container">后盾人</div>
<script>
  let app = document.getElementById('app')
  app.style.cssText = `background-color:red;color:yellow`
</script>

也可以通过 setAttribute 改变 style 特征来批量设置样式

<div id="app" class="d-flex container">后盾人</div>
<script>
  let app = document.getElementById('app')
  app.setAttribute('style', `background-color:red;color:yellow;`)
</script>

#open in new window获取样式

可以通过 style 对象,window.window.getComputedStyle 对象获取样式属性,下面进行说明

style

可以使用 DOM 对象的 style 属性读取行样式

  • style 对象不能获取行样式外定义的样式
<style>
  div {
    color: yellow;
  }
</style>
<div id="app" style="background-color: red; margin: 20px;">后盾人</div>
<script>
  let app = document.getElementById('app')
  console.log(app.style.backgroundColor)
  console.log(app.style.margin)
  console.log(app.style.marginTop)
  console.log(app.style.color)

getComputedStyle

使用 window.getComputedStyle 可获取所有应用在元素上的样式属性

  • 函数第一个参数为元素
  • 第二个参数为伪类
  • 这是计算后的样式属性,所以取得的单位和定义时的可能会有不同
<style>
  div {
    font-size: 35px;
    color: yellow;
  }
</style>
<div id="app" style="background-color: red; margin: 20px;">后盾人</div>
<script>
  let app = document.getElementById('app')
  let fontSize = window.getComputedStyle(app).fontSize
  console.log(fontSize.slice(0, -2))
  console.log(parseInt(fontSize))
</script>

视口与文档

houdunren.com (opens new window)open in new window@ 大军大叔

首先理解视口(窗口)与文档的含义

  • 网页很多都是多屏(通过滚动条显示看不见的内容),所以文档尺寸一般大于视口尺寸
  • 视口尺寸不包括浏览器工具条、菜单、标签、状态栏等
  • 当你打开控制台后,视口尺寸就相应变小了
  • position 使用文档定位,fixed 使用视口定位
  • 文档坐标在页面滚动时不发生改变
  • 视口坐标的操作需要考虑滚动条的位置

image

#open in new window视口与文档尺寸

视口坐标需要知道滚动条位置才可以进行计算,有以下几种方式获取滚动位置

方法说明注意
window.innerWidth视口宽度包括滚动条(不常用)
window.innerHeight视口高度包括滚动条(不常用)
document.documentElement.clientWidth视口宽度
document.documentElement.clientHeight视口高度

#open in new window几何尺寸

元素在页面中拥有多个描述几何数值的尺寸,下面截图进行了形象的描述。

坐标都是从左上角计算,这与 CSS 中的 right/bottom 等不同

image

#open in new window方法列表

下面是获取尺寸的方法或属性

方法说明备注
element.getBoundingClientRect返回元素在视口坐标及元素大小,包括外边距,width/height 与 offsetWidth/offsetHeight 匹配窗口坐标
element.getClientRects行级元素每行尺寸位置组成的数组
element.offsetParent拥有定位属性的父级,或 body/td/th/table对于隐藏元素/body/html 值为 null
element.offsetWidth元素宽度尺寸,包括内边距与边框和滚动条
element.offsetHeight元素高度尺寸,包括内边距与边框和滚动条
element.offsetLeft相对于祖先元素的 X 轴坐标
element.offsetTop相对于祖先元素的 Y 轴坐标
element.clientWidth元素宽度,不包含边框,只包含内容和内边距,行元素尺寸为 0
element.clientHeight元素高度,不包含边框,只包含内容和内边距,行元素尺寸为 0
element.clientLeft内容距离外部的距离,滚动条在左侧时包括滚动条尺寸
element.clientTop内容距离顶部的距离,滚动条在顶部时包括滚动条尺寸
element.scrollWidth元素宽度,内容+内边距+内容溢出的尺寸
element.scrollHeight元素高度,内容+内边距+内容溢出的尺寸
element.scrollLeft水平滚动条左侧已经滚动的宽度
element.scrollTop垂直滚动条顶部已经滚动的高度

#open in new windowgetComputedStyle

为什么有时不能使用 getComputedStyle

  • 尺寸设置 auto 时获取结果不可用
  • 由于滚动条的存在,不同浏览器返回结果不同
  • 当元素没有设置 CSS 尺寸时,获取不到相应的尺寸内容

#open in new windowgetBoundingClientRect

使用 getBoundingClientRect 获取元素相对于文档的几何坐标信息。

  • 如果是标准盒子模型,元素的尺寸等于width/height + padding + border-width的总和。
  • 如果box-sizing: border-box,元素的的尺寸等于  width/height

image

<style>
    * {
        padding: 0;
        margin: 0;
    }
    main {
        padding: 200px;
        position: relative;
    }
    #app {
        width: 200px;
        background: #e34334;
        margin: 100px;
        padding: 50px;
        border: 20px solid #efbc0f;
        color: white;
        text-align: center;
    }
</style>
<main>
    <div id="app">houdunren.com</div>
</main>
<script>
    let app = document.getElementById('app')
    let info = app.getBoundingClientRect()
    console.table(info)
</script>

计算结果的矩形尺寸包括外边距,不包括边框与内边距,上例计算结果如下

尺寸
x300
y300
width340
height162.40000915527344
top300
right640
bottom462.40000915527344
left300

#open in new windowgetClientRects

getClientRects 用于返回多行元素所占的尺寸,下面示例将为每行返回所占的空间尺寸

<style>
    span {
    width: 200px;
    overflow: auto;
  }
</style>

<span>
网页很多都是多屏,所以文档尺寸一般大于视口尺寸,当打开控制台后,视口尺寸相应变小。网页很多都是多屏,所以文档尺寸一般大于视口尺寸,当打开控制台后,视口尺寸相应变小。网页很多都是多屏,所以文档尺寸一般大于视口尺寸,当打开控制台后,视口尺寸相应变小。
</span>
<script>
  let span = document.querySelector('span')
  let info = span.getClientRects()
  console.log(info)
</script>

上例计算结果如下

(index)xywidthheighttoprightbottomleft
0881496.450073242187522.39999961853027381504.450073242187530.3999996185302738
1830.399999618530273436.225006103515622.39999961853027330.399999618530273444.225006103515652.799999237060558
length2

#open in new window坐标点元素

JS 提供了方法获取指定坐标上的元素,如果指定坐标点在视口外,返回值为 NULL

  • 坐标都是从左上角计算,这与 CSS 中的 right/bottom 等不同
  • 窗口坐标类似于 position:fixed
  • 文档坐标类似于 position:absolute
方法说明
element.elementsFromPoint返回指定坐标点所在的元素集合
element.elementFromPoint返回指定坐标点最底层的元素

#open in new window元素集合

返回指定坐标点上的元素集合

<style>
  div {
    width: 200px;
    height: 200px;
  }
</style>
<div></div>
<script>
  const info = document.elementsFromPoint(100, 100)
  console.log(info)
</script>

返回结果为

0: div
1: body
2: html

#open in new window底层元素

返回坐标点上的底层的元素

<style>
  div {
    width: 200px;
    height: 200px;
  }
</style>
<div></div>
<script>
  const info = document.elementFromPoint(100, 100)
  console.log(info)
</script>

返回结果为

div;

#open in new window滚动控制

下面掌握文档或元素的滚动操作

#open in new window方法列表

方法说明说明
element.scrollLeft获取和设置元素 X 轴滚动位置
element.scrollTop获取和设置元素 Y 轴滚动位置
element.scrollBy()按偏移量进行滚动内容参数为对象,{top:垂直偏移量,left:水平偏移量,behavior:'滚动方式'}
element.scroll() 或 element.scrollTo()滚动到指定的具体位置参数为对象,{top:X 轴文档位置,left:Y 轴文档位置,behavior:'滚动方式'}
element.scrollIntoView(bool)定位到顶部或底部参数为 true 元素定位到顶部,为 false 定位到窗口底部

#open in new window文档滚动位置

下例是查看文档滚动的 X/Y 坐标示例,请在控制台查看结果

<div style="width: 3000px; height: 3000px; background: #e34334"></div>
<script>
    window.onscroll = function () {
        console.log(document.documentElement.scrollTop)
        console.log(document.documentElement.scrollLeft)
    }
</script>

也可以使用 window.pageXOffset 对象属性获取

<div style="width: 3000px; height: 3000px; background: #e34334"></div>
<script>
    window.onscroll = function () {
        console.log(window.pageXOffset)
        console.log(window.pageYOffset)
    }
</script>

#open in new window元素滚动位置

下面查看元素内容的滚动属性,请在控制台查看结果

  • 要为元素设置 overflow:auto 以使其产生滚动条
  • 使用 scroll 事件来监听结果

image

<div id="app" style="width: 300px; height: 300px; border: solid 6px #e34334; overflow: auto">
    <div style="width: 1000px; height: 1000px; background: #833ca4"></div>
</div>
<script>
    const app = document.getElementById('app')
    app.addEventListener('scroll', function () {
        console.log(this.scrollLeft)
        console.log(this.scrollTop)
    })
</script>

#open in new window控制滚动

下面介绍的是控制元素滚动的操作方法

#open in new windowscrollBy

使用 scrollBy 滚动文档

  • behavior:smooth 为平滑滚动
<style>
  body {
    height: 3000px;
  }
</style>

<script type="module">
  setInterval(() => {
    document.documentElement.scrollBy({ top: 30, behavior: 'smooth' })
  }, 100)
</script>

#open in new windowscroll

使用 scroll 滚动到指定位置

  • behavior:smooth 为平滑滚动
<style>
  body {
    height: 3000px;
  }
</style>

<script type="module">
  setTimeout(() => {
    document.documentElement.scroll({ top: 30, behavior: 'smooth' })
  }, 1000)
</script>

#open in new windowscrollIntoView

使用元素 scrollIntoView 方法实现滚动操作,参数可以是布尔值或对象

  • 参数为 true 时顶部对齐,相当于{block: "start"}
  • 参数为 false 时底部对齐,相当于{block: "end"}
  • 也可定义 {behavior:'smooth'} 来进行平滑滚动
<style>
    div {
        height: 2000px;
        background: red;
        border-top: solid 50px #efbc0f;
        border-bottom: solid 50px #1bb491;
    }
    span {
        border-radius: 50%;
        color: #fff;
        background: #000;
        width: 50px;
        height: 50px;
        display: block;
        text-align: center;
        line-height: 50px;
        position: fixed;
        top: 50%;
        right: 50px;
        border: solid 2px #ddd;
    }
</style>
<div id="app">hdcms.com</div>
<span>TOP</span>

<script>
    document.querySelector('span').addEventListener('click', () => {
        let app = document.querySelector('#app')
        app.scrollIntoView({ block: 'end', behavior: 'smooth' })
    })
</script>

#open in new window回到顶部

下例是开发中常用的回到顶部示例

<style>
    * {
        padding: 0;
        margin: 0;
    }
    span {
        width: 50px;
        height: 50px;
        background-color: #e34334;
        color: #fff;
        display: flex;
        justify-content: center;
        align-items: center;
        text-align: center;
        position: fixed;
        right: 50px;
        bottom: 50px;
        border-radius: 10px;
        opacity: 0;
        transition: 1s;
        cursor: pointer;
    }
    span.show {
        opacity: 1;
        transform: rotate(360deg);
    }
</style>

<div id="app" style="height: 2000px">
    houdunren.com@大军大叔
</div>

<span id="bt">TOP</span>

<script>
    window.addEventListener('scroll', () => {
            // 判断是否距离页面底部200px
        let state = document.documentElement.offsetHeight - 200 < document.documentElement.scrollTop + document.documentElement.clientHeight

                // 按钮元素
        const span = document.querySelector('span')

        // 根据滚动位置添加或移除类
        span.classList[state ? 'add' : 'remove']('show')
    })

    // 回到顶部按钮事件
    document.querySelector('#bt').addEventListener('click', function () {
        // 平滑回滚到页面顶部
        document.documentElement.scrollIntoView({ block: 'start', behavior: 'smooth' })
    })
</script>

#open in new window漂浮广告

下面是全屏漂浮广告的示例

<main>
    <div id="app" style="width: 200px; height: 200px; background:#E34334">houdunren.com</div>
</main>
<script>
    class Ad {
        constructor(options) {
            this.$el = document.querySelector(options.el)
            this.$options = Object.assign({ timeout: 2, step: 1 }, options)
            //初始移动方向,1向下/向右 -1 向上/向左
            this.x = this.y = 1

            // 设置定位模式
            this.$el.style.position = 'fixed'
            setInterval(this.run.bind(this), this.$options.timeout)
        }
        //定时回调函数
        run() {
            this.$el.style.left = this.left() + 'px'
            this.$el.style.top = this.top() + 'px'
        }
        left() {
            let { x, width } = this.$el.getBoundingClientRect()
            let { clientWidth } = document.documentElement
            if (x > clientWidth - width) this.x = -1
            if (x < 0) this.x = 1

            return x + this.x * this.$options.step
        }
        top() {
            let { y, height } = this.$el.getBoundingClientRect()
            let { clientHeight } = document.documentElement
            if (y > clientHeight - height) this.y = -1
            if (y < 0) this.y = 1

            return y + this.y * this.$options.step
        }
    }

    new Ad({ el: '#app', timeout: 10, step: 1 })
</script>

基础知识

houdunren.com (opens new window)open in new window@ 大军大叔

在文档、浏览器、标签元素等元素在特定状态下触发的行为即为事件,比如用户的单击行为、表单内容的改变行为即为事件,我们可以为不同的事件定义处理程序。JS 使用异步事件驱动的形式管理事件。

#open in new window事件类型

JS 为不同的事件定义的类型,也可以称为事件名称。

#open in new window事件目标

事件目标指产生事件的对象,比如 a 标签被点击那么 a 标签就是事件目标。元素是可以嵌套的,所以在进行一次点击行为时可能会触发多个事件目标。

#open in new window处理程序

事件的目的是要执行一段代码,我们称这类代码为事件处理(监听)程序。当在对象上触发事件时就会执行定义的事件处理程序。

#open in new windowHTML 绑定

可以在 html 元素上设置事件处理程序,浏览器解析后会绑定到 DOM 属性中

<button onclick="alert(`houdunren.com`)">后盾人</button>

往往事件处理程序业务比较复杂,所以绑定方法或函数会很常见

  • 绑定函数或方法时需要加上括号
<button onclick="show()">后盾人</button>
<script>
  function show() {
    alert('houdunren.com')
  }
</script>

当然也可以使用方法做为事件处理程序

<input type="text" onkeyup="HD.show()" />
<script>
  class HD {
    static show() {
      console.log('houdunren')
    }
  }
</script>

可以传递事件源对象与事件对象

<button onclick="show(this,'houdunren','hdcms','大军大叔',event)">后盾人</button>
<script>
    function show(...args) {
        console.log(args)
    }
</script>

#open in new windowDOM 绑定

也可以将事件处理程序绑定到 DOM 属性中

  • 使用 setAttribute 方法设置事件处理程序无效
  • 属性名区分大小写
<div id="app">houdunren.com</div>
<script>
  const app = document.querySelector('#app')
  app.onclick = function () {
    this.style.color = 'red'
  }
</script>

无法为事件类型绑定多个事件处理程序,下面绑定了多个事件处理程序,因为属性是相同的所以只有最后一个有效

<div id="app">houdunren.com</div>
<script>
  const app = document.querySelector('#app')
  app.onclick = function () {
    this.style.color = 'red'
  }
  app.onclick = function () {
    this.style.fontSize = '55px'
  }
</script>

#open in new window事件监听

通过上面的说明我们知道使用 HTML 与 DOM 绑定事件都有缺陷,建议使用新的事件监听绑定方式 addEventListener 操作事件

使用 addEventListener 添加事件处理程序有以下几个特点

  • transtionend / DOMContentLoaded 等事件类型只能使用 addEventListener 处理
  • 同一事件类型设置多个事件处理程序,按设置的顺序先后执行
  • 也可以对未来添加的元素绑定事件
方法说明
addEventListener添加事件处理程序
removeEventListener移除事件处理程序

addEventListener 的参数说明如下

  1. 参数一事件类型
  2. 参数二事件处理程序
  3. 参数三为定制的选项,可传递 object 或 boolean 类型。后面会详细介绍使用区别

#open in new window绑定多个事件

使用 addEventListener 来多个事件处理程序

<div id="app">houdunren.com</div>
<script>
  const app = document.querySelector('#app')
  app.addEventListener('click', function () {
    this.style.color = 'red'
  })
  app.addEventListener('click', function () {
    this.style.fontSize = '55px'
  })
</script>

#open in new window通过对象绑定

如果事件处理程序可以是对象,对象的 handleEvent 方法会做为事件处理程序执行。下面将元素的事件统一交由对象处理

<div id="app">houdunren.com</div>
<script>
  const app = document.querySelector('#app')
  class HD {
    handleEvent(e) {
      this[e.type](e)
    }
    click() {
      console.log('单击事件')
    }
    mouseover() {
      console.log('鼠标移动事件')
    }
  }
  app.addEventListener('click', new HD())
  app.addEventListener('mouseover', new HD())
</script>

#open in new window移除事件

使用 removeEventListener 删除绑定的事件处理程序

  • 事件处理程序单独定义函数或方法,这可以保证事件处理程序是同一个
<div id="app">houdunren.com</div>
<button id="hd">删除事件</button>

<script>
  const app = document.querySelector('#app')
  const hd = document.querySelector('#hd')
  function show() {
    console.log('APP我执行了')
  }
  app.addEventListener('click', show)
  hd.addEventListener('click', function () {
    app.removeEventListener('click', show)
  })
</script>

#open in new window事件选项

addEventListener 的第三个参数为定制的选项,可传递 object 或 boolean 类型

下面是传递对象时的说明

选项可选参数
oncetrue/false只执行一次事件
capturetrue/false事件是在捕获/冒泡哪个阶段执行,true:捕获阶段 false:冒泡阶段
passivetrue/false声明事件里不会调用  preventDefault(),可以减少系统默认行为的等待

下面使用 once:true 来指定事件只执行一次

<button id="app">houdunren.com</button>
<script>
    const app = document.querySelector('#app')
    app.addEventListener(
        'click',
        function () {
            alert('houdunren@大军大叔')
        },
        { once: true }
    )
</script>

设置  { capture: true }  或直接设置第三个参数为 true 用来在捕获阶段执行事件

addEventListener 的第三个参数传递 true/false 和设置 {capture:true/false}是一样

<div id="app" style="background-color: red">
    <button id="bt">houdunren.com</button>
</div>
<script>
    const app = document.querySelector('#app')
    const bt = document.querySelector('#bt')
    app.addEventListener(
        'click',
        function () {
            alert('这是div事件 ')
        },
        { capture: true }
    )

    bt.addEventListener(
        'click',
        function () {
            alert('这是按钮事件 ')
        },
        { capture: true }
    )
</script>

设置  { capture: false }  或直接设置第三个参数为 false 用来在冒泡阶段执行事件

<div id="app" style="background-color: red">
    <button id="bt">houdunren.com</button>
</div>
<script>
    const app = document.querySelector('#app')
    const bt = document.querySelector('#bt')
    app.addEventListener(
        'click',
        function () {
            alert('这是div事件 ')
        },
        { capture: false }
    )

    bt.addEventListener(
        'click',
        function () {
            alert('这是按钮事件 ')
        },
        { capture: false }
    )
</script>

#open in new window事件对象

执行事件处理程序时,会产生当前事件相关信息的对象,即为事件对事。系统会自动做为参数传递给事件处理程序。

  • 大部分浏览器将事件对象保存到 window.event 中
  • 有些浏览器会将事件对象做为事件处理程序的参数传递

事件对象常用属性如下:

属性说明
type事件类型
target事件目标对象,冒泡方式时父级对象可以通过该属性找到在哪个子元素上最终执行事件
currentTarget当前执行事件的对象
timeStamp事件发生时间
x相对窗口的 X 坐标
y相对窗口的 Y 坐标
clientX相对窗口的 X 坐标
clientY相对窗口的 Y 坐标
screenX相对计算机屏幕的 X 坐标
screenY相对计算机屏幕的 Y 坐标
pageX相对于文档的 X 坐标
pageY相对于文档的 Y 坐标
offsetX相对于事件对象的 X 坐标
offsetY相对于事件对象的 Y 坐标
layerX相对于父级定位的 X 坐标
layerY相对于父级定位的 Y 坐标
path冒泡的路径
altKey是否按了 alt 键
shiftKey是否按了 shift 键
metaKey是否按了媒体键
window.pageXOffset文档参考窗口水平滚动的距离
window.pageYOffset文档参考窗口垂直滚动的距离

#open in new window冒泡捕获

#open in new window冒泡行为

标签元素是嵌套的,在一个元素上触发的事件,同时也会向上执行父级元素的事件处理程序,一直到 HTML 标签元素。

  • 大部分事件都会冒泡,但像 focus 事件则不会
  • event.target 可以在事件链中最底层的定义事件的对象
  • event.currentTarget == this 即当前执行事件的对象

以下示例有标签的嵌套,并且父子标签都设置了事件,当在子标签上触发事件事会冒泡执行父级标签的事件

image

<style>
  #app {
    background: #34495e;
    width: 300px;
    padding: 30px;
  }
  #app h2 {
    background-color: #f1c40f;
    margin-right: -100px;
  }
</style>
<div id="app">
  <h2>houdunren.com</h2>
</div>
<script>
  const app = document.querySelector('#app')
  const h2 = document.querySelector('h2')
  app.addEventListener('click', (event) => {
    console.log(`event.currentTarget:${event.currentTarget.nodeName}`)
    console.log(`event.target:${event.target.nodeName}`)
    console.log('app event')
  })
  h2.addEventListener('click', () => {
    console.log(`event.currentTarget:${event.currentTarget.nodeName}`)
    console.log(`event.target:${event.target.nodeName}`)
    console.log(`h2 event`)
  })
</script>

#open in new window阻止冒泡

冒泡过程中的任何事件处理程序中,都可以执行  event.stopPropagation()  方法阻止继续进行冒泡传递

  • event.stopPropagation() 用于阻止冒泡
  • 如果同一类型事件绑定多个事件处理程序 event.stopPropagation() 只阻止当前的事件处理程序
  • event.stopImmediatePropagation() 阻止事件冒泡并且阻止相同事件的其他事件处理程序被调用

下例中为 h2 的事件处理程序添加了阻止冒泡动作,将不会产生冒泡,也就不会执行父级中的事件处理程序了。

<style>
  #app {
    background: #34495e;
    width: 300px;
    padding: 30px;
  }
  #app h2 {
    background-color: #f1c40f;
    margin-right: -100px;
  }
</style>
<div id="app">
  <h2>houdunren.com</h2>
</div>
<script>
  const app = document.querySelector('#app')
  const h2 = document.querySelector('h2')
  app.addEventListener('click', (event) => {
    console.log(`event.currentTarget:${event.currentTarget.nodeName}`)
    console.log(`event.target:${event.target.nodeName}`)
    console.log('app event')
  })
  h2.addEventListener('click', (event) => {
    event.stopPropagation()
    console.log(`event.currentTarget:${event.currentTarget.nodeName}`)
    console.log(`event.target:${event.target.nodeName}`)
    console.log(`h2 event`)
  })
   h2.addEventListener('click', (event) => {
      console.log('h2 的第二个事件处理程序')
   })
</script>

以上代码如果将 event.stopPropagation() 替换为 event.stopPropagation() ,那么 h2 的其他同类型的事件处理程序将不执行,同时阻止冒泡。

#open in new window事件捕获

事件执行顺序为 捕获 > 事件目标 > 冒泡,在向下传递到目标对象的过程即为事件捕获。事件捕获在实际使用中频率不高。

  • 通过设置第三个参数为 true 或{ capture: true } 在捕获阶段执行事件处理程序
const app = document.querySelector("#app");
const h2 = document.querySelector("h2");
app.addEventListener(
  "click",
  (event) => {
    console.log("app event");
  },
  { capture: true }
);
h2.addEventListener("click", (event) => {
  console.log(`h2 event`);
});

#open in new window事件代理

借助冒泡思路,我们可以不为子元素设置事件,而将事件设置在父级。然后通过父级事件对象的 event.target 查找子元素,并对他做出处理。

  • 这在为多个元素添加相同事件时很方便
  • 会使添加事件变得非常容易

下面是为父级 UL 设置事件来控制子元素 LI 的样式切换

<style>
  .hd {
    border: solid 2px #ddd;
    background-color: red;
    color: white;
  }
</style>
<ul>
  <li>houdunren.com</li>
  <li>hdcms.com</li>
</ul>

<script>
  'use strict'
  const ul = document.querySelector('ul')
  ul.addEventListener('click', () => {
    if (event.target.tagName === 'LI') event.target.classList.toggle('hd')
  })
</script>

可以使用事件代理来共享事件处理程序,不用为每个元素单独绑定事件

<ul>
  <li data-action="hidden">houdunren.com</li>
  <li data-action="color" data-color="red">hdcms.com</li>
</ul>
<script>
  class HD {
    constructor(el) {
      el.addEventListener('click', (e) => {
        const action = e.target.dataset.action
        this[action](e)
      })
    }
    hidden() {
      event.target.hidden = true
    }
    color() {
      event.target.style.color = event.target.dataset.color
    }
  }
  new HD(document.querySelector('ul'))
</script>

下面是使用事件代理实现的 TAB 面板效果

image

<div class="tab">
  <dl>
    <dt data-action="toggle">在线教程</dt>
    <dd data-action="hidden">houdunren.com</dd>
  </dl>
  <dl>
    <dt data-action="toggle">开源软件</dt>
    <dd data-action="hidden">hdcms.com</dd>
  </dl>
</div>

<script>
  class HD {
    constructor(el) {
      this.el = el
      el.addEventListener('click', this.handle.bind(this))
    }
    handle() {
      const action = event.target.dataset.action
      if (action) this[action]()
    }
    hidden() {
      event.target.hidden = true
    }
    toggle() {
      this.el.querySelectorAll(`[data-action='hidden']`).forEach((e) => (e.hidden = true))
      event.target.nextElementSibling.hidden = ''
    }
  }
  new HD(document.querySelector('.tab'))
</script>

下面实现通过代理事件行为,在表单提交时禁用提交按钮,并记录提示次数

<form>
  <input type="text" />
  <button type="button" data-submit-disabled data-action="submit,counter">提交表单</button>
</form>
<script>
  class FORM {
    constructor(form) {
      this.form = form
      this.form.counter = 0
      this.form.addEventListener('click', this.handle.bind(this))
    }
    handle() {
      const actions = event.target.dataset.action
      actions.split(',').forEach((action) => {
        if (this[action]) this[action]()
      })
    }
    submit() {
      event.preventDefault()
      this.disableButton(true)
      console.log('提交中')
      setTimeout(() => {
        this.disableButton(false)
        console.log('提交结束')
      }, 1000)
    }
    disableButton(state) {
      this.form.querySelectorAll(`[data-submit-disabled]`).forEach((bt) => {
        bt.disabled = state
      })
    }
    counter() {
      this.form.counter++
      console.log(this.form.counter)
    }
  }
  document.querySelectorAll(`form`).forEach((form) => {
    new FORM(form)
  })
</script>

#open in new window未来元素

下面使用事件代理来对未来元素进行事件绑定

<div id="app">
  <h2>houdunren.com</h2>
</div>

<script>
  function show() {
    console.log(this.textContent)
  }
  const app = document.querySelector('#app')
  const h2 = document.querySelectorAll('h2')
  app.addEventListener('click', () => {
    show.call(event.target)
  })
  let newH2 = document.createElement('h2')
  newH2.textContent = 'hdcms.com'
  app.append(newH2)
</script>

#open in new window默认行为

JS 中有些对象会设置默认事件处理程序,比如 A 链接在点击时会进行跳转。

一般默认处理程序会在用户定义的处理程序后执行,所以我们可以在我们定义的事件处理中取消默认事件处理程序的执行。

  • 使用 onclick 绑定的事件处理程序,return false 可以阻止默认行为
  • 推荐使用event.preventDefault()阻止默认行为

下面阻止超链接的默认行为

<a href="https://www.houdunren.com">后盾人</a>
<script>
  document.querySelector('a').addEventListener('click', () => {
    event.preventDefault()
    alert(event.target.innerText)
  })
</script>

#open in new window窗口文档

下面来学习针对窗口和文档的事件的处理。

#open in new window事件类型

事件名说明
window.onload文档解析及外部资源加载后
DOMContentLoaded文档解析后执行,不需要等待图片/样式文件等外部资源加载,该事件只能通过 addEventListener 设置
window.beforeunload文档刷新或关闭时
window.unload文档卸载时
scroll页面滚动时

#open in new windowonload

window.onload 事件在文档解析后及图片、外部样式文件等资源加载完后执行

<script>
  window.onload = function () {
    alert('houdunren.com')
  }
</script>
<div id="app">houdunren.com</div>

#open in new windowDOMContentLoaded

DOMContentLoaded 事件在文档标签解析后执行,不需要等外部图片、样式文件、JS 文件等资源加载

<script>
  window.addEventListener('DOMContentLoaded', (event) => {
    console.log('houdunren.com')
  })
</script>
<div id="app">houdunren.com</div>

#open in new windowbeforeunload

当浏览器窗口关闭或者刷新时,会触发 beforeunload 事件,可以取消关闭或刷新页面。

  • 返回值为非空字符串时,有些浏览器会做为弹出的提示信息内容
  • 部分浏览器使用 addEventListener 无法绑定事件
window.onbeforeunload = function (e) {
  return "真的要离开吗?";
};

#open in new windowunload

window.unload 事件在文档资源被卸载时执行,在 beforeunload 后执行

  • 不能执行 alert、confirm 等交互指令
  • 发生错误也不会阻止页面关闭或刷新
window.addEventListener("unload", function (e) {
  localStorage.setItem("name", "houdunren");
});

#open in new window鼠标事件

#open in new window事件类型

针对鼠标操作的行为有多种事件类型

  • 鼠标事件会触发在 Z-INDEX 层级最高的那个元素上
事件名说明
click鼠标单击事件,同时触发 mousedown/mouseup
dblclick鼠标双击事件
contextmenu点击右键后显示的所在环境的菜单
mousedown鼠标按下
mouseup鼠标抬起时
mousemove鼠标移动时
mouseover鼠标移动时
mouseout鼠标从元素上离开时
mouseup鼠标抬起时
mouseenter鼠标移入时触发,不产生冒泡行为
mosueleave鼠标移出时触发,不产生冒泡行为
oncopy复制内容时触发
scroll元素滚动时,可以为元素设置 overflow:auto; 产生滚动条来测试

#open in new window事件对象

鼠标事件产生的事件对象包含相对应的属性

属性说明
which执行 mousedown/mouseup 时,显示所按的键 1 左键,2 中键,3 右键
clientX相对窗口 X 坐标
clientY相对窗口 Y 坐标
pageX相对于文档的 X 坐标
pageY相对于文档的 Y 坐标
offsetX目标元素内部的 X 坐标
offsetY目标元素内部的 Y 坐标
altKey是否按了 alt 键
ctrlKey是否按了 ctlr 键
shiftKey是否按了 shift 键
metaKey是否按了媒体键
relatedTargetmouseover 事件时从哪个元素来的,mouseout 事件时指要移动到的元素。当无来源(在自身上移动)或移动到窗口外时值为 null

#open in new window禁止复制

<body>
  houdunren.com
  <script>
    document.addEventListener('copy', () => {
      event.preventDefault()
      alert('禁止复制内容')
    })
  </script>
</body>

#open in new windowrelatedTarget

relatedTarget 是控制鼠标移动事件的来源和目标对象的

  • 如果移动过快会跳转中间对象
<div id="app">houdunren.com</div>
<div id="cms">hdcms.com</div>
<script>
  const app = document.querySelector(`#app`)
  const cms = document.querySelector(`#cms`)
  app.addEventListener('mouseout', () => {
    console.log(event.target)
    console.log(event.relatedTarget)
  })
</script>

#open in new windowmouseenter 与 mouseleave

mouseenter 与 mouseleave 事件从子元素移动到父元素时不触发父元素事件

<style>
  #app {
    background: red;
    padding: 80px;
    width: 500px;
  }
  #cms {
    background: teal;
    padding: 30px;
  }
</style>
<div id="app">
  houdunren.com
  <div id="cms">hdcms.com</div>
</div>

<script>
  const app = document.querySelector(`#app`)
  const cms = document.querySelector(`#cms`)

  app.addEventListener('mouseenter', () => {
    console.log('app')
  })
  cms.addEventListener('mouseenter', () => {
    console.log('cms')
  })
</script>

#open in new window键盘事件

针对键盘输入操作的行为有多种事件类型

事件名说明
Keydown键盘按下时,一直按键不松开时 keydown 事件会重复触发
keyup按键抬起时

#open in new window事件对象

键盘事件产生的事件对象包含相对应的属性

属性说明
keyCode返回键盘的 ASCII 字符数字
code按键码,字符以 Key 开始,数字以 Digit 开始,特殊字符有专属名子。左右 ALT 键字符不同。
不同布局的键盘值会不同
key按键的字符含义表示,大小写不同。不能区分左右 ALT 等。不同语言操作系统下值会不同
altKey是否按了 alt 键
ctrlKey是否按了 ctlr 键
shiftKey是否按了 shift 键
metaKey是否按了媒体键

#open in new window表单事件

下面是可以用在表单上的事件类型

事件类型说明
focus获取焦点事件
blur失去焦点事件
element.focus()让元素强制获取焦点
element.blur()让元素失去焦点
change文本框在内容发生改变并失去焦点时触发,select/checkbox/radio 选项改变时触发事件
inputInput、textarea 或 select 元素的  value  被修改时,会触发  input  事件。而 change 是鼠标离开后或选择一个不同的 option 时触发。
submit提交表单

基础知识

houdunren.com (opens new window)open in new window@ 大军大叔

浏览器天生具发送 HTTP 请求的能力,比如在在址栏输入内容,提交 FORM 表单等。本章来学习通过 JS 程序来管理 HTTP 请求的能力。

使用 JS 脚本发送 HTTP 请求,不会带来页面的刷新,所以用户体验非常好。

#open in new windowXMLHttpRequest

使用 XMLHttpRequest 发送请求,是一种存在很久的方案。现代浏览器支持使用 fetch 的异步请求方式,fetch 基于 promise 异步操作体验更好。

#open in new window基本使用

使用 XMLHttpRequest 发送请求需要执行以下几步

  1. 使用 new XMLHttpRequest 创建 xhr 对象
  2. xhr.open 初始化请求参数
  3. xhr.send 发送网络请求
  4. xhr.onload 监听请求结果
  5. xhr.onerror 请求中断等错误发生时的处理

#open in new window响应类型

通过设置  xhr.responseType  对响应结果进行声明,来对结果自动进行处理。

下面是可以使用的响应类型

类型说明
text响应结果为文本
json响应内容为 JSON,系统会自动将结果转为 JSON 对象
blob二进制数据响应
documentXML DOCUMENT 内容

#open in new window响应结果

xhr.onload 用于处理响应完毕后的处理

使用以下属性来处理结果

  • xhr.status 为 HTTP 状态码 如 404/422/403 等,当为 200 时为正确响应
  • xhr.statusText HTTP 状态码内容,200 时为 ok,404 为 Not Found
  • xhr.response 服务器端响应的内容

#open in new window使用示例

const xhr = new XMLHttpRequest();
xhr.timeout = 5000;
xhr.open("GET", "hd.php");
xhr.send();
xhr.onload = function () {
  if (xhr.status == 200) {
    console.log(xhr.response);
  } else {
    console.log(`${xhr.status}:${xhr.statusText}`);
  }
};
xhr.onerror = function (error) {
  console.log(error);
};

#open in new window发送表单

下面来使用 XMLHttpRequest 发送 POST 请求

#open in new window后台服务

下面创建 hd.php 后台脚本(你可以使用你喜欢的后台脚本进行测试)

<?php
echo $_POST['title'];

然后启动服务器

php -S localhost:8000

现在可以在浏览器中访问  http://localhsot:8080/hd.php  请求 hd.php

#open in new window前端异步请求

<form action="" id="form">
    <input type="text" name="title" />
    <input type="submit" />
</form>

<script>
    const form = document.getElementById('form')
    form.addEventListener('submit', function () {
        //阻止默认提交行为
        event.preventDefault()

        post('hd.php', new FormData(this))
    })
    function post(url, data) {
        const xhr = new XMLHttpRequest()
        xhr.open('POST', url)
        xhr.send(data)
        xhr.onload = () => {
            if (xhr.status == 200) {
                console.log(xhr.response)
            } else {
                console.log(`${xhr.status}:${xhr.statusText}`)
            }
        }
    }
</script>

#open in new window封装请求类

下面结合 Promise 来构建一个 XHR 异步处理类,使异步请求操作的变得更简单。

class HD {
  options = {
    responseType: "json",
  };
  constructor(method = "GET", url, data = null, options) {
    this.method = method;
    this.url = url;
    this.data = this.formatData(data);
    Object.assign(this.options, options);
  }
  formatData(data) {
    if (typeof data != "object" || data == null) data = {};
    let form = new FormData();
    for (const [name, value] of Object.entries(data)) {
      form.append(name, value);
    }

    return form;
  }
  static get(url, options) {
    return new this("GET", url, null, options).xhr();
  }
  static post(url, data, options) {
    return new this("POST", url, data, options).xhr();
  }
  xhr() {
    return new Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest();
      xhr.open(this.method, this.url);
      xhr.responseType = this.options.responseType;
      xhr.send(this.data);
      xhr.onload = function () {
        if (xhr.status != 200) {
          reject({ status: xhr.status, error: xhr.statusText });
        } else {
          resolve(xhr.response);
        }
      };
      xhr.onerror = function (error) {
        reject(error);
      };
    });
  }
}

使用 HD.get 静态方法发送 GET 请求

HD.get("1.php", {
  responseType: "text",
}).then((response) => {
  console.log(response);
});

使用 HD.post 静态方法发送 POST 请求

HD.post("2.php", data, {
  responseType: "json",
}).then((response) => {
  console.log(response);
});

#open in new windowFETCH

FETCH 是 JS 升级后提供的更简便的网络请求的操作方法,内部使用 Promise 来完成异步请求。

  • response.json()接收 JSON 类型数据
  • response.text()接收 TEXT 类型数据
  • response.blog()接收 Blog 二进制数据

#open in new window请示步骤

使用 fetch 方法发送异步请求需要分以下两步操作

#open in new window响应头解析

第一步对服务器返回的响应头进行解析,会接到 Response 类创建的对象实例,里面包含以下属性。

  • status:HTTP 状态码
  • ok:状态码为 200-299 时为 true 的布尔值

#open in new window响应内容解析

第二步对返回的保存在 response.body 中的响应结果进行解析,支持了以下几种方式对结果进行解析。

  • response.json()接收 JSON 类型数据
  • response.text()接收 TEXT 类型数据
  • response.blog()接收 Blog 二进制数据

以上方法不能同时使用,因为使用一个方法后数据已经被处理,其他方法就不可以操作了

#open in new window实例操作

下面来体验使用 fetch 发送请求

#open in new window后台服务

下面创建 hd.php 后台脚本(你可以使用你喜欢的后台脚本进行测试)

<?php
$articles = [
    ['name' => '后盾人'],
    ['name' => 'hdcms.com'],
    ['name' => 'houdunren.com']
];
echo json_encode($articles);

然后启动服务器

php -S localhost:8000

现在可以在浏览器中访问  http://localhsot:8080/hd.php  请求 hd.php

#open in new window发送请求

下面使用 FETCH 发送 GET 请求

fetch(`1.php`)
  .then((response) => {
    return response.json();
  })
  .then((articles) => {
    console.log(articles);
  });

因为 fetch 结果是 promise 所以也可以使用 async/await 操作

async function query() {
  const response = await fetch(`1.php`);
  const articles = await response.json();
  console.log(articles);
}
query();

#open in new windowPOST

发送 POST 请求需要设置请求头 Request header

#open in new window发送请求

  • 发送的 JSON 类型需要设置请求头为  application/json;charset=utf-8
async function post() {
  const response = await fetch(`hd.php`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json;charset=utf-8",
    },
    body: JSON.stringify({ name: "后盾人" }),
  });
  if (response.ok) {
    const articles = await response.json();
    console.log(articles);
  }
}
post();

#open in new window后台响应

因为前台发送的是非表单数据,而是 JSON 字符串所以后台使用  php://input  来接收数据

<?php
echo file_get_contents('php://input');

基础知识

Canvas 是用使用 JS 画布的思想来绘制图形,下面通过一些示例掌握 Canvas 的使用

houdunren.com (opens new window)open in new window@ 大军大叔

image

#open in new window项目模板

以下示例因为使用到了 Typescript,所以我们使用 vite 创建 typescript 项目,并选择使用  vanilla  模板来开发

$ yarn create vite

项目安装执行结果

执行结果
✔ Project name: … aaa
✔ Select a framework: › vanilla
✔ Select a variant: › vanilla-ts

目录结构

├── images                         //图片文件
│   └── p2.jpeg
├── index.html                //项目模板文件
├── package.json            //项目配置文件
├── src
│   ├── main.ts                //项目主文件,我们在这里编码
│   ├── style.css            //公共样式
│   └── vite-env.d.ts    //TS类型声明文件
├── tsconfig.json            //TS配置文件
└── yarn.lock                    //扩展包版本锁定文件

#open in new window矩形绘制

下面来学习使用 strokeRect 方法绘制边框矩形

#open in new window实心矩形

使用 fillRect 方法可以绘制实心矩形,下面是 fillRect 方法的参数说明

参数说明
x矩形左上角的 x 坐标
y矩形左上角的 y 坐标
width矩形的宽度,以像素计
height矩形的高度,以像素计

下面使用纯色填充画布

image

#open in new window空心矩形

使用 strokeRect 方法可以绘制空心矩形,下面是 strokeRect 方法的参数说明

参数说明
x矩形左上角的 x 坐标
y矩形左上角的 y 坐标
width矩形的宽度,以像素计
height矩形的高度,以像素计

下面绘制实线边框的示例代码

image

<canvas id="canvas" width="500" height="500"> 您的浏览器不支持 HTML5 canvas </canvas>
<script>
    const el = document.getElementById('canvas')
    //画布对象
    const ctx = el.getContext('2d')
    //定义填充颜色
    ctx.strokeStyle = '#16a085'
    //线条宽度
    ctx.lineWidth = 30
    //边角类型:bevel斜角 ,round圆角,miter尖角
    ctx.lineJoin = 'round'
    //绘制矩形边框
    ctx.strokeRect(50, 50, 300, 300)
</script>

#open in new window圆形绘制

使用 canvas 可以绘制圆形

#open in new windowarc

下面是绘制圆方法 arc 的参数说明

参数说明
x圆的中心的 x 坐标。
y圆的中心的 y 坐标。
r圆的半径。
sAngle起始角,以弧度计。(弧的圆形的三点钟位置是 0 度)。
eAngle结束角,以弧度计。
counterclockwise可选。规定应该逆时针还是顺时针绘图。False = 顺时针,true = 逆时针。

#open in new window绘制空心圆

image

<div class="app">
    <canvas id="canvas" width="500" height="500"></canvas>
</div>

<script>
    const el = document.querySelector('canvas')
    const ctx = el.getContext('2d')
    //填充画布颜色
    ctx.beginPath()
    ctx.strokeStyle = 'red'
    ctx.lineWidth = 20
    ctx.arc(100, 100, 60, 0, 2 * Math.PI)
    ctx.stroke()
</script>
<div class="app"></div>

#open in new window绘制实心圆

下面来掌握使用 canvas 绘制填充圆,绘制圆使用 arc 函数,具体参数说明参考上例。

image

<div class="app">
    <canvas id="canvas" width="500" height="500"></canvas>
</div>

<script>
    const el = document.querySelector('canvas')
    const ctx = el.getContext('2d')
    //填充画布颜色
    ctx.beginPath()
    ctx.fillStyle = '#f1c40f'
    ctx.lineWidth = 20
    ctx.arc(100, 100, 60, 0, 2 * Math.PI)
    ctx.fill()
</script>
<div class="app"></div>

#open in new window节点绘制

我们可以通过以下方法定义不同节点、线条样式来绘制图形

  • beginPath() 重置绘制路径
  • lineTo() 开始绘制线条
  • moveTo() 把路径移动到画布中的指定点,但不会创建线条(lineTo 方法会绘制线条)
  • closePath() 闭合线条绘制,即当前点连接到线条开始绘制点
  • lineWidth 线条宽度
  • strokeStyle 线条的样式,可以是颜色 、渐变
  • stroke() 根据上面方法定义的节点绘制出线条

#open in new window绘制多边形

下面是根据节点来绘制三角形图形

image

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />

        <style>
            * {
                margin: 0;
                padding: 0;
                box-sizing: border-box;
            }
            body {
                display: flex;
                width: 100vw;
                height: 100vh;
                justify-content: center;
                align-items: center;
            }
            app {
                display: flex;
                flex-direction: column;
            }
        </style>
    </head>
    <body>
        <div class="app">
            <canvas id="canvas" width="400" height="400"></canvas>
        </div>

        <script>
            const el = document.querySelector('canvas')
            const ctx = el.getContext('2d')
            //填充画布颜色
            ctx.fillStyle = '#8e44ad'
            ctx.fillRect(0, 0, el.width, el.height)
            //开始画线
            ctx.beginPath()
            //移动起始点
            ctx.moveTo(200, 0)
            //下一个节点
            ctx.lineTo(400, 200)
            //下一个节点
            ctx.lineTo(0, 200)
            //闭合节点
            ctx.closePath()
            //线宽
            ctx.lineWidth = 10
            //线颜色
            ctx.strokeStyle = '#f1c40f'
            //画线
            ctx.stroke()
        </script>
        <div class="app"></div>

        <script type="module">
            import main from './main.js'
        </script>
    </body>
</html>

#open in new window线性渐变

使用 canvas 的 createLinearGradient() 方法可以创建线性的渐变对象,用于实现线性渐变效果。

#open in new windowcreateLinearGradient

下面是 createLinearGradient 线性渐变的参数

参数描述
x0渐变开始点的 x 坐标
y0渐变开始点的 y 坐标
x1渐变结束点的 x 坐标
y1渐变结束点的 y 坐标

#open in new window渐变边框

下面是绘制激变的边框的效果

image

#open in new window渐变填充

渐变也可以用于填充

image

#open in new window清空区域

下面是将红色画布上清除一块区域,清除后的内容是透明的。

image

<canvas id="app" width="500" height="500"></canvas>
<script>
    const canvas = document.getElementById('app').getContext('2d')

    canvas.fillStyle = 'red'
    canvas.fillRect(0, 0, 500, 500)
        //清除矩形区域
    canvas.clearRect(50, 50, 100, 100)
</script>

#open in new window填充文字

下面掌握使用 canvas 的 fillText 方法绘制填充文字

#open in new windowfillText

下面是 fillText 方法的参数

参数描述
text规定在画布上输出的文本。
x开始绘制文本的 x 坐标位置(相对于画布)。
y开始绘制文本的 y 坐标位置(相对于画布)。
maxWidth可选。允许的最大文本宽度,以像素计。

#open in new windowtextBaseline

textBaseline 用于定义文字基线

参数说明
alphabetic默认。文本基线是普通的字母基线。
top文本基线是 em 方框的顶端。。
hanging文本基线是悬挂基线。
middle文本基线是 em 方框的正中。
ideographic文本基线是表意基线。
bottom文本基线是 em 方框的底端。

#open in new windowtextAlign

textAlign 用于文本的对齐方式的属性

参数说明
left文本左对齐
right文本右对齐
center文本居中对齐
start文本对齐界线开始的地方 (左对齐指本地从左向右,右对齐指本地从右向左)
end文本对齐界线结束的地方 (左对齐指本地从左向右,右对齐指本地从右向左)

#open in new window示例代码

image

<canvas id="canvas" width="500" height="500"></canvas>
<script>
    const el = document.getElementById('canvas')
    //画布对象
    const ctx = el.getContext('2d')
    //填充样式
    ctx.fillStyle = 'red'
    //文字大小与字体设置
    ctx.font = '30px CascadiaMono'
    //定义文字基线
    ctx.textBaseline = 'top'
    //文字居中
    ctx.textAlign = 'center'
    ctx.fillText('houdunren.com@大军老师', 10, 250)
</script>

#open in new window激变文字

image

<canvas id="canvas" width="500" height="500"></canvas>
<script>
    const el = document.getElementById('canvas')
    //画布对象
    const ctx = el.getContext('2d')
    //定义渐变的开始与结束坐标
    const gradient = ctx.createLinearGradient(0  , 0, 500, 500)
    // 定义渐变位置与颜色,参数一为位置是从 0~1 之间,参数二为激变颜色
    gradient.addColorStop(0, '#1abc9c')
    gradient.addColorStop(0.5, '#9b59b6')
    gradient.addColorStop(1, '#f1c40f')
    //渐变填充
    ctx.strokeStyle = gradient
    //文字大小与字体设置
    ctx.font = '30px CascadiaMono'
    ctx.strokeText('houdunren.com@大军老师', 10, 250)
</script>

#open in new window图片填充

下面掌握将图片填充到画布

#open in new window参数说明

参数描述
image规定要使用的图片、画布或视频元素。
repeat默认。该模式在水平和垂直方向重复。
repeat-x该模式只在水平方向重复。
repeat-y该模式只在垂直方向重复。
no-repeat该模式只显示一次(不重复)。

#open in new window示例代码

image

#open in new window图片缩放

下面将图片直接绘制到画布上。

image

#open in new window绘制像素

下面是绘制像素点的示例

image

#open in new window绘制不规则

image

#open in new window黑板实例

下面我们为开发个小黑板功能,可以在上面写字并可以生成截图。

image

以下是使用 typescript 编写,如果你没有 ts 环境,请删除代码中的类型声明。

class Draw {
  constructor(
    public width: number,
    public height: number,
    public el = document.querySelector<HTMLCanvasElement>('#canvas')!,
    public app = el.getContext('2d')!,
    public btns = el.insertAdjacentElement('afterend', document.createElement('div'))!
  ) {
    this.el.width = this.width
    this.el.height = this.height
    this.setBackground()
    this.event()
  }

  //事件绑定
  private event() {
    //bind会返回新函数,addEventListener与removeEventListener要使用相同函数
    const callback = this.drawEventCallback.bind(this)

    this.el.addEventListener('mousedown', () => {
      //重新画线
      this.app.beginPath()
      //鼠标移动事件
      this.el.addEventListener('mousemove', callback)
    })

    //鼠标抬起时移除事件
    this.el.addEventListener('mouseup', () => this.el.removeEventListener('mousemove', callback))
    return this
  }

  //黑板写字的事件回调函数
  private drawEventCallback(event: MouseEvent) {
    this.app.lineTo(event.offsetX, event.offsetY)
    this.app.strokeStyle = 'white'
    this.app.stroke()
  }

  //截图
  public short() {
    const bt = document.createElement('button')
    bt.innerText = '截图'
    this.btns.insertAdjacentElement('beforeend', bt)
    const img = new Image()
    this.el.insertAdjacentElement('afterend', img)

    bt.addEventListener('click', () => {
      //使用canval标签的toDataURL方法,获取图片数据内容
      img.src = this.el.toDataURL('image/jpeg')
      img.style.cssText = 'width:300px;position:absolute;bottom:50px;right:0;border:solid 10px white;left:50%;transform:translateX(-50%);'
    })
    return this
  }

  //清屏
  public clear() {
    const bt = document.createElement('button')
    bt.innerText = '清屏'
    this.btns.insertAdjacentElement('beforeend', bt)
    bt.addEventListener('click', () => {
      this.app.fillStyle = '#000'
      this.app.fillRect(0, 0, this.el.width, this.el.height)
    })
  }

  //初始背景为黑色
  private setBackground() {
    this.app.fillStyle = '#000'
    this.app.fillRect(0, 0, this.el.width, this.el.height)
  }
}

const blackboard = new Draw(800, 300)
blackboard.short()
blackboard.clear()

Last Updated:
Contributors: 刘荣杰