宏任务 和 微任务、观察者以及模块
示例:pandas 是基于NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。
JavaScript 有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。这个模型与其它语言中的模型截然不同,比如 C 和 Java。
接下来的内容解释了这个理论模型。现代 JavaScript 引擎实现并着重优化了以下描述的这些语义。
在JavaScript
中,所有的任务都可以分为
第一个宏任务开始,直接暴露在script元素中的代码属于全局作用域。
console.log(1);
函数setTimeout
本身的执行属于同步操作,但setTimeout
第一个参数对应的函数属于异步任务.
setTimeout( () => console.log(2), 0 );
览器处理到script时会立即创建一个宏任务
let p = new Promise((resolve, reject)=>{
console.log(3);
resolve();
});
在当前宏任务中创建一个微任务
p.then( () => console.log(4) );
同一个script元素内属于一个宏任务,从第二个宏任务开始,在当前宏任务中创建一个微任务,并且用setTimeout
注册一个宏任务。
setTimeout( () => console.log(6), 0 );
由setTimeout
注册的回调操作属于异步任务(宏任务)
setTimeout( add, 0, 2, 3 );
const endTime = Date.now() + 10000;
let showTime = () => {
let datetime = new Date();
console.log( 'macro task 3: ', datetime.toISOString() );
if( datetime > endTime ) {
clearInterval(timer); // 清除timer对应的定时器
}
};
而由setInterval
注册的回调操作属于异步任务(宏任务)
const timer = setInterval( showTime, 1000 );
console.log( timer );
当前任务调用 readFile
函数本身属于同步操作,但是由readFile
所注册的回调操作属于异步操作(宏任务)。
fs.readFile( pathname, 'utf8', handler);
console.log('macro task 1: end');
所以可以产生宏任务的操作:
setTimeout
注册的回调操作(即用setTimout
创建的延迟任务(延迟执行一次))setInterval
注册的回调操作(即用setInterval
创建的定时任务(周期性执行))setImmediate
注册的回调操作(即用setImmediate
创建的延迟任务(延迟执行一次))UI
交互事件 (浏览器)宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合
常见的宏任务有:
script
(可以理解为外层同步代码)
setTimeout/setInterval
UI rendering/UI
事件
postMessage
、MessageChannel
setImmediate
、I/O
(Node.js
)
按照这个流程,它的执行机制是:
const timer = setInterval( showTime, 1000 );
console.log( timer );
let showNames = (...names) => names.forEach( name => console.log(name) );
let showNames = (...names) => {
console.log( `macro task 4: 输出姓名`);
names.forEach( name => console.log(name) );
}
const immediate = setImmediate( showNames, '李某芳', '王某然', '杨某俊', '罗某丹', '秦某娜', '菲尔娜' );
console.log( immediate );
const pathname = path.resolve(__dirname,'readme.txt');
const handler = (error, content) => {
console.log( `macro task 5: 读取文件`);
if( error ){
console.log(error);
return;
一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前
常见的微任务有:
Promise.then
MutaionObserver
Object.observe
(已废弃;Proxy 对象替代)process.nextTick
(Node.js
)console.log( 'macro task 1: begin' );
setImmediate( ()=> console.log( 'macro task 2') );
let p = Promise.resolve('\t1 micro task (fulfilled)');
p.then( result => console.log( result ) )
.catch( reason => console.log( reason ) )
.finally( () => console.log('\t2 micro task') );
let t = Promise.reject('\t3 micro task (rejected)');
t.then( result => console.log(result) )
.catch( reason => console.log( reason ) )
.finally( () => console.log('\t4 micro task') );
let divide = (a,b) => {
console.log('\t5 micro task');
let r = a / b;
return r;
}
process.nextTick( divide, 100, 3 );
console.log( 'macro task 1: end' );
观察器的配置(需要观察什么变动):
const config = { attributes: true, childList: true, subtree: true };
创建一个观察器实例并传入回调函数
const observer = new MutationObserver(callback);
const click = (element, handler) =>{
element.addEventListener('click', handler, false);
}
确定被观察的目标元素
const target = document.querySelector('.target');
const btns = document.querySelectorAll('.buttons>button');
启用观察者
click( btns[0], evt => {
console.log( `宏任务:${evt.target.innerHTML}` );
// 开始观察目标节点
observer.observe( target, config );
});
关闭观察者
click( btns[5], evt => {
console.log( `宏任务:${evt.target.innerHTML}` );
// 停止观察
observer.disconnect();
});
})();
在 node.js
环境中一个 .js
文件就是一个模块。
而由 匿名函数 所传入的 exports
和 module.exports
是同一个对象
console.log( exports === module.exports );
通过动态为 exports 对象 添加属性的方式导出 name 变量
exports.name = '天字一号';
通过为 exports 对象 定义属性的方式来导出 version 变量
Object.defineProperty( exports, 'version', {
value: '壹点零',
enumerable: true,
configurable: false,
writable: false
});
通过动态为 module.exports
对象 添加属性的方式导出 description 变量
module.exports.description = '这是天字一号模块壹点零版';
console.log( '[first] exports: ', exports );
console.log( '[first] module.exports: ', module.exports );
修改匿名函数的 exports 参数值,但是并没有影响 module.exports
属性值。
exports = {
name: '天字二号',
version: '壹点零',
description: '这是天字二号模块的壹点零版'
}
修改 module.exports
属性值
module.exports = {
name: '地字一号',
version: '壹点零',
description: '这是地字一号模块的壹点零版'
}
通过修改 module.exports 属性值的方式将整个pure对象导出
module.exports = pure;
用 require 函数导入模块,如果导入的模块是 node.js 内置模块,则在参数中仅指定模块名称即可
require 是 node 用来加载并执行其它文件导出的模块的方法。
在 NodeJs 中,我们引入的任何一个模块都对应一个 Module 实例,包括入口文件。
const path = require('path');
若导入的模块是自定义模块,则需要显式指定模块路径和名称
const first = require('./first.js');
const second = require('./second.js');
每个文件中的所有源代码都被 module wrapper 包裹到一个函数中,在 node.js 调用 module wrapper 之前会创建一个表示该模块的对象(即通过参数传入的 module 实例)
console.log('exports: ', exports ); // module wrapper 的 第 1 个参数
console.log('require函数: ');
console.log( require.toString() ); // module wrapper 的 第 2 个参数
console.log('module: ', module ); // module wrapper 的 第 3 个参数
console.log('文件路径: ', __filename); // module wrapper 的 第 4 个参数
console.log('文件目录: ', __dirname); // module wrapper 的 第 5 个参数