了解web worker

157 阅读6分钟

单线程

作为前端开发我们应该都知道:JS是单线程的。那为什么JS要被设计成单线程呢?

这主要和js的用途有关,js作为浏览器的脚本语言,主要是实现用户和浏览器交互,以及操作dom的增删改查,这就决定了他只能是单线程,否则就会带来复杂的同步问题,或则像其他后台语言的资源抢占问题或则加锁逻辑。举个例子:如果js被设计了多线程,如果有一个线程要修改一个dom元素,另一个线程要删除这个dom元素,此时浏览器就会不知道怎么处理。所以从诞生起,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

web worker多线程?违背单线程原则了?

这里其实就是对单线程的误解了

浏览器是多进程的,不同类型的标签页就会开启不同的进程,还有GPU进程,插件进程等等。这里我们就不展开讲了,我们就关注其中一个进程:浏览器渲染进程。分析一下在这个进程里面做了什么

这个进程主要是负责HTML,CSS,JS等文件的解析和执行。只有js线程显然是不能完成这么多工作的,所以在整个过程中渲染进程就会开启多个线程协作完成

  • GUI渲染线程
  • JS引擎线程
  • 事件触发线程
  • 定时器触发线程
  • 异步HTTP请求线程

这里的线程也不展开讲了,讲事件循环的时候可以再深入分析一下,我们可以看到实际上的js相关线程就是JS引擎线程,它就是负责处理javascript程序的,那么它就只能是单线程吗?实际上不是的

JavaScript 只在一个线程上运行,不代表 JavaScript 引擎只有一个线程。事实上,JavaScript 引擎有多个线程,单个脚本只能在一个线程上运行(称为主线程),其他线程都是在后台配合

这里就引出了web worker

web worker

为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质。

这样是不是就很强大了,主线程创建一个worker线程,主线程和worker线程单独运行互不干扰,主线程把一些复杂任务,比如计算密集型或高延迟任务丢给worker线程,worker线程运行之后把结果返回给主线程,这样主线程就不会被阻塞或者变慢了。

当然这里会有个疑问。如果了解事件循环的话可能会怀疑,这个和把任务丢给任务队列有啥区别呢,同样把一些任务丢给了其他线程处理,待会儿我们用例子分析一下。

使用

在主线程中生成 Worker 线程很容易:

第一个参数是脚本的网址(必须遵守同源政策),该参数是必需的,且只能加载 JS 脚本,否则报错。第二个参数是配置对象,该对象可选。它的一个作用就是指定 Worker 的名称,用来区分多个 Worker 线程。

var myWorker = new Worker(jsUrl, options)

子线程中就是写入正常的js逻辑,主线程和子线程之间的通信用下面这些api

主线程api,worker标识new Worker生成的实例

  • worker.postMessage: 主线程往worker线程发消息,消息可以是任意类型数据,包括二进制数据
  • worker.terminate: 主线程关闭worker线程
  • worker.onmessage: 指定worker线程发消息时的回调,也可以通过worker.addEventListener('message',cb)的方式
  • worker.onerror: 指定worker线程发生错误时的回调,也可以 worker.addEventListener('error',cb)

worker线程中全局对象是self

  • self.postMessage: worker线程往主线程发消息,消息可以是任意类型数据,包括二进制数据
  • self.close: worker线程关闭自己
  • self.onmessage: 指定主线程发worker线程消息时的回调,也可以self.addEventListener('message',cb)
  • self.onerror: 指定worker线程发生错误时的回调,也可以 self.addEventListener('error',cb)

worker线程限制

  • 同源限制 worker线程执行的脚本文件必须和主线程的脚本文件同源(所以如果你想测试,本地没启服务是玩不了的)

  • DOM操作限制 worker线程在与主线程的window不同的另一个全局上下文中运行,其中无法读取主线程所在网页的DOM对象,也不能获取 documentwindow等对象,但是可以获取navigatorlocation(只读)XMLHttpRequestsetTimeout族等浏览器API。

  • 通信限制 worker线程与主线程不在同一个上下文,不能直接通信,需要通过postMessage方法来通信。

example1:

简单例子先来看看api

index.html

<!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=0.5">
    <title>测试一下</title>
    <style>
        
    </style>
</head>
<body>
    <p>Worker 输出内容:<span id='app'></span></p>
    <p>Worker 输出加工内容:<span id='handleapp'></span></p>
    <input type='text' id='msg'>
    <button onclick='sendMessage()'>发送</button>
    <button onclick='stopWorker()'>stop!</button>
    <script>
        var myWorker = new Worker('./test.js', { name : 'myWorker' });
        myWorker.onmessage = (event) => {
            let res = event.data || {}
            let {data, handleData} = res
            if(data) {
                document.getElementById('app').innerHTML = data
            }
            if(handleData) {
                document.getElementById('handleapp').innerHTML = handleData
            }
        }
    
        function sendMessage () {
            let msg = document.getElementById('msg')
            myWorker.postMessage(msg.value)
        }
    
        function stopWorker() {
            myWorker.terminate()
        }
    </script>
</body>

</html>

test.js

let i = 1

function simpleCount() {
    i++
    self.postMessage({
        data: i
    })
    setTimeout(simpleCount, 1000)
}

simpleCount()

self.onmessage = ev => {
    self.postMessage({
        handleData: 'hello ' + ev.data
    })
}

example2:

再来看看一个例子解决主线程的阻塞问题,这个构造的比较极端,明白就好了

浏览器遇到js就会解析js直到解析完成,此时js是阻塞的,渲染也是阻塞的

<!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=0.5">
    <title>测试一下</title>
    <style>
        
    </style>
</head>
<body>
    <p>Worker 输出内容:<span id='app'></span></p>
    <p>Worker 输出加工内容:<span id='handleapp'></span></p>
    <input type='text' id='msg'>
    <button onclick='sendMessage()'>发送</button>
    <button onclick='stopWorker()'>stop!</button>
    <script>
        let date1 = Date.now()
        for(var i = 0; i < 100000000; i++) {
            let a = JSON.stringify({})
        }
        console.log(Date.now() - date1, Date.now())
        document.getElementById('app').innerHTML = '我计算完了'
    </script>
</body>

</html>

在使用worker之前回想一下之前说的使用任务队列的形式解决阻塞问题,测试的时候会发现不会阻塞js执行和页面渲染了,事实上真的解决问题了吗?

实际上在我们定时器线程判断定时结束后会把我们的任务放进事件触发线程维护的任务队列中,当我们主线程执行完成后会从队列中取出当前可执行任务执行,这里就可以看出,实际上执行过程还是会在主线程,如果主线程当前有其他任务就仍然会发生阻塞,只是这种异步任务一般发生在主线程空闲期我们不会很刻意的见到

<!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=0.5">
    <title>测试一下</title>
    <style>
        
    </style>
</head>
<body>
    <p>Worker 输出内容:<span id='app'></span></p>
    <p>Worker 输出加工内容:<span id='handleapp'></span></p>
    <input type='text' id='msg'>
    <button onclick='sendMessage()'>发送</button>
    <button onclick='stopWorker()'>stop!</button>
    <script>
        setTimeout(() => {
            let date1 = Date.now()
            for(var i = 0; i < 100000000; i++) {
                let a = JSON.stringify({})
            }
            console.log(Date.now() - date1, Date.now())
            document.getElementById('app').innerHTML = '我计算完了'
        }, 100)
    </script>
</body>

</html>

使用web worker,此时的计算就都在worker线程中了

<!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=0.5">
    <title>测试一下</title>
    <style>
        
    </style>
</head>
<body>
    <p>Worker 输出内容:<span id='app'></span></p>
    <p>Worker 输出加工内容:<span id='handleapp'></span></p>
    <input type='text' id='msg'>
    <button onclick='sendMessage()'>发送</button>
    <button onclick='stopWorker()'>stop!</button>
    <script>
        var myWorker = new Worker('./test.js', { name : 'myWorker' });
        myWorker.onmessage = (event) => {
            let res = event.data || {}
            let {data, handleData} = res
            document.getElementById('app').innerHTML = data
        }
    </script>
</body>

</html>
let date1 = Date.now()
for(var i = 0; i < 100000000; i++) {
    let a = JSON.stringify({})
}
console.log(Date.now() - date1, Date.now())
self.postMessage({
    data: '我计算完了'
})