JavaScript学习笔记(四)单线程和异步

2020-03-15  本文已影响0人  机智的akunda

1. 异步

JavaScript只在一个线程上运行,如果所有的任务都是同步(synchronous)任务,当有一个中间环节的任务需要等待一段时间才能执行完成时(例如ajax请求、静态资源加载等),会造成页面卡死。因此,这类需要等待的任务要通过一定的方式放在异步(asynchronous)任务队列中,并设置回调函数来处理异步任务执行完成之后进行何种操作(回调函数在主线程执行)。

JavaScript引擎采用事件循环(Event Loop)机制来判断异步任务是否执行完毕以回到主线程继续执行其回调函数,只要同步任务执行完成,就去检查异步任务是不是执行完成,并不断重复这个过程直到异步队列清空。

在ES6之前,异步操作最常用的就是回调函数的形式。看下面这个简单的加载脚本的例子:

function loadScript(src) {
    let script = document.createElement('script')
    document.head.appendChild(script)
    script.src = src
}
function test() {
    console.log('test')
}

loadScript(/* url */)
test()

虽然是先调用了loadScript方法加载某个脚本,但并不会立刻执行脚本的加载而是将其放在异步任务队列,然后继续执行test方法,因此控制台会先打印出"test";此时同步任务执行完成,这是如果异步任务(即加载脚本)也执行完成了,再去执行脚本内容。

如果想先执行脚本内容,再执行test,就需要将后续执行的任务(也就是test方法)设置为脚本加载完成这个事件的回调函数:

function loadScript(src, callback) {
    let script = document.createElement('script')
    document.head.appendChild(script)
    script.src = src
    script.onload = function() {
        callback()
    }
}
function test() {
    console.log('test')
}

loadScript(/* url */, test)

异步任务队列中的脚本加载任务执行完成后,通过onload事件触发监听函数,监听函数回到主线程继续执行,并调用传入的test方法。这样整个流程就变为了我们所需要的先加载脚本后调用test方法。

这种方式虽然简单,但当执行多个异步任务时,需在回调函数中嵌套回调函数,使得函数调用语句的结构复杂,难以阅读和维护,也就是所谓的“回调地狱”。

2. Promise对象

ES6针对这一问题,将Promise纳入了标准。还是看上面加载脚本的例子,这次我们按顺序依次加载多个脚本:

function loadScript() {
    return new Promise((resolve, reject) => {
        let script = document.createElement('script')
        document.head.appendChild(script)
        script.onload = () => {
            resolve(src)
        }
        script.onerror = (error) => {
            reject(error)
        }
    })
}

loadScript('./1.js')
    .then(() => {
        return loadScript('./2.js')
    }, (error) => {
        console.log(error)
    })

Promise构造函数接收一个函数作为参数,这个函数的两个参数resolvereject也是两个函数(JavaScript提供),用来改变Promise实例的状态(status)和结果(result):

单看一个脚本的加载过程,和之前loadScript(/* url *, test/)的执行过程并没有什么差异,那为什么要在函数内部创建一个看起来比较复杂的Promise实例并将其作为返回值返回呢?

关键就在于还需要继续加载第二个脚本。由于函数返回的是一个Promise对象,因此就可以链式地使用Promise原型上的方法,比如上面例子中的then方法。then方法定义在Promise.prototype上,其作用是根据调用它的Promise对象(也就是loadScript函数调用之后返回的Promise对象)的状态执行不同的回调函数。

then方法接收两个函数作为参数:

如果还要继续调用then方法进行后续的异步任务操作,当前对象的then方法的第一个函数参数中还要返回一个Promise对象,比如上面例子中的return loadScript('./2.js')

需要注意的一点是,promise对象的改变是单向且唯一的,只能从挂起变为成功或失败,或者从成功变为成功或失败,而一旦状态变为了失败,其状态就被冻结,对then方法的调用就无效了。

这时新的问题又出现了,如果在每一步的then方法中都定义一个处理错误的函数会比较麻烦,那么有没有更简洁的处理方法呢?显然是有的:

loadScript('./1.js')
    .then(() => {
        return loadScript('./2.js')
    })
    .then(() => {
        return loadScript('./3.js')
    })
    .catch((error) => {
        console.log(error)
    })

Promise.prototype上还定义了一个catch方法用来捕获promise实例的状态变为失败时的错误信息。要注意的是,这个catch方法不是try ... catch语句的catch,因此它不能捕获throw抛出的错误。

通过Promise,可以极大的简化多个按顺序执行的异步任务的函数调用语句结构,并且链式调用的形式也很容易理清程序执行的流程,可读性和维护性都优于通过回调函数执行异步任务的形式。

3. 定时器

异步任务涉及两个常用的定时器setTimeoutsetInterval,作用是向异步任务队列添加定时任务。二者用法相似,区别就是前者只执行一次,而后者重复执行多次。

需要注意的是,由于是向异步任务队列添加任务,因此即便指定的延迟时间为0,也是在同步任务执行完成后再执行。这一特性常用来改变函数执行的流程。

3.1 setTimeout

setTimeout用来指定一段语句或函数在多少毫秒后执行,并返回一个整数值作为定时器的标识(用于清除定时器)。
setTimeout的一个重要的应用就是节流(throttle)和防抖(debounce)。

const div = document.getElementById('div')
let timer = null
div.addEventListener('drag', event => {
    if (timer) {
    // 如果定时器存在,说明上一次的定时任务尚未执行,直接返回
        return
    }
    // 如果定时器不存在,说明上一次的定时任务执行完成,需要设置下一次的定时任务
    timer = setTimeout(() => {
        console.log(event.offsetX, event.offsetY)
        // 当前定时任务被触发后,清除定时器
        clearTimeout(timer)
    }, 100)
})
// 这样就实现了无论拖拽速率多快,都是每隔100ms获取一次元素位置
const input = document.getElementById('input')
let timer = null
input.addEventListener('keyup', () => {
    if (timer) {
    // 如果timer有值,说明上一次的定时任务还没执行,这就代表在指定的时间延迟之内,用户又输入了文字
    // 这就需要清除上一次的定时任务
        clearTimeout(timer)
    }
    
    // 然后再为当前的keyup事件设置定时任务
    timer = setTimeout(() => {
        // 模拟触发change事件
        console.log(input.value)
        
        // 定时任务执行后清除定时器
        clearTimeout(timer)
    }, 500)
})
//这样就实现了不管输入文字的速率如何,都是以指定的间隔触发一次change事件

注意节流和防抖的区别:

3.2 setInterval

用法与setTimeout类似,即setInterval(func, delay),以指定的延迟时间delay重复执行func函数。

上一篇 下一篇

猜你喜欢

热点阅读