const getUserAgent = function (ua) {
if (ua) {
return ua.toLowerCase();
}
return typeof window !== 'undefined' && navigator.userAgent ? navigator.userAgent.toLowerCase() : '';
};
const isWindow = function (userAgent) {
return !(/mobile/i.test(getUserAgent(userAgent)));
};
const isMac = function (userAgent) {
return /mac os/i.test(getUserAgent(userAgent));
};
const waterMark = (userId, realName) => {
if (!userId && !realName) return;
const _isWindow = isWindow(navigator?.userAgent);
const _isMac = isMac(navigator?.userAgent);
const getWater = (txt) => {
let _size = _size = [220, 80, 80, 40];
let canvas = document.createElement('canvas');
let _opacity = _isWindow && !_isMac ? 0.16 : 0.08; // 兼容windows上水印特别浅
canvas.width = _size[0];
canvas.height = _size[1];
let ctx = canvas.getContext('2d');
ctx.font = '14px monospace';
ctx.textAlign = 'center';
ctx.fillStyle = `rgba(0,0,0,${_opacity})`;
ctx.translate(_size[2], _size[3]);
ctx.rotate(-Math.PI / 8);
ctx.fillText(txt, 0, 0);
return canvas.toDataURL('image/png');
}
let _txt = realName ? `${userId} ${realName}` : userId;
let _water = getWater(_txt);
let _div = document.createElement('div');
_div.setAttribute('style', `
pointer-events: none;
width: 100vw;
height: 100vh;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 10001;
background: url(${_water}) repeat;
`);
document.body.prepend(_div);
}
export default waterMark;
Category: Javascript
JS 常用工具
1 字符串 URL截取
URL 截取特定 参数值
link.split(‘?’)[1]?.match(/(^|&)xCubeShareCode=([^&]*)/)?.[2];
原生App与javascript交互
jsBridge在支付,钱包,媒体拓展,图片处理,活动页面,用户地理位置网络状态都能得到原生强有力支持;
前端和Native对对方的细节知道的越少越好,减少耦合度,暴露的接口尽量控制在5个以内;
实现方式(交互形式)
Native 调用 JS
使用前端暴露在window下的一个方法或者一个对象的方法;
window._handlerFromApp(message)
window.JSBridge._handlerFromApp(message)
message: {
cbId : "cb_(:id)_(:timeStamp)", //回调函数的id
status: 0, //状态数据 (0:失败, 1:成功)
msg : "ok", //反馈的消息
data : {
//... //一些处理后的数据
}
}
JS调用Native
以下只介绍前两个方法,第三个和第二个比较类似
- A. Native暴露一个含有通信方法的类给web调用
- B. Native拦截iframe请求
- C. Native拦截prompt弹出框
A 一个包含调用方法的类
iOS : 可使用javascriptCore
Android: 直接使用WebView的addJavascriptInterface方法
将一个js对象绑定到一个Native类,在类中实现相应的函数,当js需要调用Native的方法时,只需要直接在js中通过绑定的对象调用相应的函数
确定对象名称: (:AppName)JSBridge
Native提供的对象含有的方法:
invoke(funcName, data)
listen(funcName, data)
invoke
:用于web页面调用Native私有方法的通用方法
参数: funcName
, data
funcName
:对应为Native内部私有方法的方法名或映射data
:web传递给Native的必要数据
// data 数据
{
cbId : "cb_(:id)_(:timeStamp)", //回调函数的id
msg : {} //提供给使用方法执行的一些参数
}
/**
//1.拿wx参考为例
wx.previewImg({
current: 'http://xxx_1.png',
urls : [
'http: //xxx_0.png',
'http: //xxx_1.png',
'http: //xxx_2.png',
'http: //xxx_3.png',
]
});
//2.因为wx对jsbridge进行了一次封装,jssdk, 而我们在未封装时应该如下使用
JSBridge.invoke('imagePreview', {
cbId : "cb_(:id)_(:timeStamp)",
msg : {
current: 'http://xxx_1.png',
urls : [
'http: //xxx_0.png',
'http: //xxx_1.png',
'http: //xxx_2.png',
'http: //xxx_3.png',
]
}
});
那么当调用之后,Native执行完成对应的私有方法后,执行一次我们提供的回调接口,以下是javascript的语法,请Native开发工程师对应修改
JSBridge.handlerFromApp({
cbId : "cb_(:id)_(:timeStamp)", //web传给Native的cbId
status: 1, //状态数据 (0:失败, 1:成功)
msg : "预览成功",
data : {}
});
listen
是一个用于web页面监听Native方法实现的通用方法
使用环境: 不属于web页面上的操作。当用户直接操作Native上的功能来影响或发送数据给web,或者操作的功能需要用到web页面上的数据,我们需要告知Native我们希望能收到回调;
例子: 微信监听分享操作
- 分享的内容是web上的内容(标题,描述,图片);
- 获取分享操作是否完成和分享操作的数据收集;
- 分享按钮是原生APP提供;
数据结构和操作与invoke
相似,对应Native开发哥们接收到listen操作后需要存储一个映射,在被监听的操作实现上判断是不是需要执行web端提供的回调接口;
B iframe的魔法
由于Native App可以监听webview的请求,所以web端通过创建一个隐藏的iframe,请求商定后的统一协议来发送数据给Native App;
需要Native开发兄弟在webview开启时候为页面注入jsbridge.js代码并执行(防止被前端浏览器直接查看源代码了解app的代码逻辑)
页面前期准备
1.app打开webview
2.loadUrl(页面url)
3.监听webview开始,并执行一段js代码将包内的jsbridge.js文件引入页面中;
比较
A:android曝安全漏洞,但相对来说实现简单,调用方式容易,且传递参数,无需前端搭建jsbridge,只需要封装易用的sdk,App不需要读取本地静态js文件;
B: iframe规定协议,规范统一,需要前端实现jsbridge和封装sdk, iframe通过url的方式,数据统一为字符串格式,数据量受限制,两端要转义字符;
C: prompt在一些安卓设备受系统劫持,监听prompt兼容性需要测试,也是字符串形式,数据量不受限,需要转义字符;
/**
* 用法:
* import jsBridge from 'fileName.js'
*
* 1、给 APP 端发送数据
* jsBridge.callHandler(eventName, data, callback(reponseData))
* 参数说明:
* eventName (string): 必传, 与 APP 端约定的事件名
* data (object): 非必传, 发送给 APP 端的数据
* callback (function): 通信完成后,前端的回调,reponseData,是APP端返回的数据
*
* 2、接收 APP 端的数据
* jsBridge.registerHandler(eventName, callback(data, responseCallback))
* 参数说明:
* eventName (string): 必传,与 APP 端约定的事件名
* callback (function): data: 是接收到的数据,responseCallback,通信完成后,传给 APP 端的回调
*/
const isAndroid = navigator.userAgent.indexOf('Android') > -1 || navigator.userAgent.indexOf('Adr') > -1;
const isiOS = !!navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/);
//这是必须要写的,用来创建一些设置
function setupWebViewJavascriptBridge(callback) {
if (isAndroid) {
if (window.WebViewJavascriptBridge) {
callback(WebViewJavascriptBridge)
} else {
document.addEventListener(
'WebViewJavascriptBridgeReady',
function () {
callback(WebViewJavascriptBridge)
},
false
);
}
}
if (isiOS) {
if (window.WebViewJavascriptBridge) {
return callback(WebViewJavascriptBridge);
}
if (window.WVJBCallbacks) {
return window.WVJBCallbacks.push(callback);
}
window.WVJBCallbacks = [callback];
let WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function () {
document.documentElement.removeChild(WVJBIframe)
}, 0);
}
}
setupWebViewJavascriptBridge(function (bridge) {
if (isAndroid) {
//安卓端,接收数据时,需要先进行初始化
bridge.init(function (message, responseCallback) {
const data = {
'Javascript Responds': 'Wee!'
};
responseCallback(data);
})
}
})
export default {
// 给APP发送数据
callHandler(name, data, callback) {
setupWebViewJavascriptBridge(function (bridge) {
bridge.callHandler(name, data, callback)
})
},
// 接收APP端的数据
registerHandler(name, callback) {
setupWebViewJavascriptBridge(function (bridge) {
bridge.registerHandler(name, function (data, responseCallback) {
callback(data, responseCallback)
})
})
}
}
ES6 模块特性 – 静态分析
ES6 模块特性
ES6 模块跟 CommonJS 模块的不同,主要有以下两个方面:
- ES6 模块输出的是值的引用,输出接口动态绑定,而 CommonJS 输出的是值的拷贝
- ES6 模块编译时执行,而 CommonJS 模块总是在运行时加载
CommonJS 输出值的拷贝
CommonJS 模块输出的是值的拷贝(原始值的拷贝),也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
// a.js
var b = require('./b');
console.log(b.foo);
setTimeout(() => {
console.log(b.foo);
console.log(require('./b').foo);
}, 1000);
// b.js
let foo = 1;
setTimeout(() => {
foo = 2;
}, 500);
module.exports = {
foo: foo,
// foo: () => {
// return foo;
// },
};
// 执行:node a.js
// 执行结果:
// 1
// 1
// 1
上面代码说明,b 模块加载以后,它的内部 foo 变化就影响不到输出的 exports.foo 了, 所以如果你想要在 CommonJS 中动态获取模块中的值,那么就需要借助于函数延时执行的特性。
// b.js
module.exports = {
foo: () => {
return foo;
},
};
// 1
// 2
// 2
总结一下
- CommonJS 模块重复引入的模块并不会重复执行,再次获取模块直接获得暴露的 module.exports 对象
- 如果你要处处获取到模块内的最新值的话,也可以你每次更新数据的时候每次都要去更新 module.exports 上的值
- 如果你暴露的 module.exports 的属性是个对象,那就不存在这个问题了
// a.js
var b = require('./b');
console.log(b.foo);
setTimeout(() => {
console.log(b.foo);
console.log(require('./b').foo);
}, 1000);
// 每次更新数据的时候每次都要去更新 module.exports 上的值
// b.js
module.exports.foo = 1; // 同 exports.foo = 1
setTimeout(() => {
module.exports.foo = 2;
}, 500);
// 执行:node a.js
// 执行结果:
// 1
// 2
// 2
ES6 输出值的引用
然而在 ES6 模块中就不再是生成输出对象的拷贝,而是动态关联模块中的值。
// a.js
import { foo } from './b';
console.log(foo);
setTimeout(() => {
console.log(foo);
import('./b').then(({ foo }) => {
console.log(foo);
});
}, 1000);
// b.js
export let foo = 1;
setTimeout(() => {
foo = 2;
}, 500);
// 执行:babel-node a.js
// 执行结果:
// 1
// 2
// 2
ES6 静态编译,CommonJS 运行时加载
ES6 模块编译时执行会导致有以下两个特点:
- import 命令会被 JavaScript 引擎静态分析,优先于模块内的其他内容执行。
- export 命令会有变量声明提前的效果。
import 优先执行:
// a.js
console.log('a.js')
import { foo } from './b';
// b.js
export let foo = 1;
console.log('b.js 先执行');
// 执行结果:
// b.js 先执行
// a.js
由于 import 是静态执行,所以 import 具有提升效果即 import 命令在模块中的位置并不影响程序的输出。
export 变量声明提升:
正常的引入模块是没办法看出变量声明提升的特性,需要通过循环依赖加载才能看出。
// a.js
import { foo } from './b';
console.log('a.js');
export const bar = 1;
export const bar2 = () => {
console.log('bar2');
}
export function bar3() {
console.log('bar3');
}
// b.js
export let foo = 1;
import * as a from './a';
console.log(a);
// 执行结果:
// { bar: undefined, bar2: undefined, bar3: [Function: bar3] }
// a.js
从上面的例子可以很直观地看出,a 模块引用了 b 模块,b 模块也引用了 a 模块,export 声明的变量也是优于模块其它内容的执行的,但是具体对变量赋值需要等到执行到相应代码的时候。(当然函数声明和表达式声明不一样,这一点跟 JS 函数性质一样,这里就不过多解释)
ES6 模块和 CommonJS 模块相同点
模块不会重复执行
这个很好理解,无论是 ES6 模块还是 CommonJS 模块,当你重复引入某个相同的模块时,模块只会执行一次。
// a.js
import './b';
import './b';
// b.js
console.log('只会执行一次');
// 执行结果:
// 只会执行一次
CommonJS 模块循环依赖
// a.js
console.log('a starting');
exports.done = false;
const b = require('./b');
console.log('in a, b.done =', b.done);
exports.done = true;
console.log('a done');
// b.js
console.log('b starting');
exports.done = false;
const a = require('./a');
console.log('in b, a.done =', a.done);
exports.done = true;
console.log('b done');
// node a.js
// 执行结果:
// a starting
// b starting
// in b, a.done = false
// b done
// in a, b.done = true
// a done
结合之前讲的特性很好理解,当你从 b 中想引入 a 模块的时候,因为 node 之前已经加载过 a 模块了,所以它不会再去重复执行 a 模块,而是直接去生成当前 a 模块吐出的 module.exports 对象,因为 a 模块引入 b 模块先于给 done 重新赋值,所以当前 a 模块中输出的 module.exports 中 done 的值仍为 false。而当 a 模块中输出 b 模块的 done 值的时候 b 模块已经执行完毕,所以 b 模块中的 done 值为 true
ES6 模块循环依赖
// a.js
console.log('a starting')
import {foo} from './b';
console.log('in b, foo:', foo);
export const bar = 2;
console.log('a done');
// b.js
console.log('b starting');
import {bar} from './a';
export const foo = 'foo';
console.log('in a, bar:', bar);
setTimeout(() => {
console.log('in a, setTimeout bar:', bar);
})
console.log('b done');
// babel-node a.js
// 执行结果:
// b starting
// in a, bar: undefined
// b done
// a starting
// in b, foo: foo
// a done
// in a, setTimeout bar: 2
动态 import()
ES6 模块在编译时就会静态分析,优先于模块内的其他内容执行,所以导致了我们无法写出像下面这样的代码:
if(some condition) {
import a from './a';
}else {
import b from './b';
}
// or
import a from (str + 'b');
import() 允许你在运行时动态地引入 ES6 模块,想到这,你可能也想起了 require.ensure 这个语法,但是它们的用途却截然不同的。
- require.ensure 的出现是 webpack 的产物,它是因为浏览器需要一种异步的机制可以用来异步加载模块,从而减少初始的加载文件的体积,所以如果在服务端的话 require.ensure 就无用武之地了,因为服务端不存在异步加载模块的情况,模块同步进行加载就可以满足使用场景了。 CommonJS 模块可以在运行时确认模块加载。
- 而 import() 则不同,它主要是为了解决 ES6 模块无法在运行时确定模块的引用关系,所以需要引入 import()
它的用法
- 动态的 import() 提供一个基于 Promise 的 API
- 动态的import() 可以在脚本的任何地方使用
- import() 接受字符串文字,你可以根据你的需要构造说明符
async function main() {
const myModule = await import('./myModule.js');
const {export1, export2} = await import('./myModule.js');
const [module1, module2, module3] =
await Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
]);
}
Promise 解析
Promise 构造函数接受一个function
Promise.resolve Promise.reject Promise.all Promise.race 静态方法
- 1. Promise接受一个函数handle作为参数,handle包括resolve和reject两个是函数的参数
- 2.Promise 相当于一个状态机,有三种状态:pending,fulfilled,reject,初始状态为 pending
- 3.调用 resolve,状态由pending => fulfilled
- 4.调用reject,会由pending => rejected
- 5.改变之后不会变化
then 方法
- 1.接受两个参数,onFulfilled和onRejected可选的函数
- 2.不是函数必须被忽略
- 3.onFullfilled: A.当 promise 状态变为成功时必须被调用,其第一个参数为 promise 成功状态传入的值( resolve 执行时传入的值); B.在 promise 状态改变前其不可被调用 C.其调用次数不可超过一次
- 4.onRejected:作用和onFullfilled类似,只不过是promise失败调用
- 5.then方法可以链式调用 A.每次返回一个新的Promise B.执行规则和错误捕获:then的返回值如果是非Promise直接作为下一个新Promise参数,如果是Promise会等Promise执行
Primse Class
class MyPromise {
constructor (handle) {
// 判断handle函数与否
if (typeof handle!=='function') {
throw new Error('MyPromise must accept a function as a parameter')
}
// 添加状态
this._status = 'PENDING'
// 添加状态
this._value = undefined
// 执行handle
try {
handle(this._resolve.bind(this), this._reject.bind(this))
} catch (err) {
this._reject(err)
}
}
// 添加resovle时执行的函数
_resolve (val) {
if (this._status !== 'PENDING') return
// this._status = 'FULFILLED'
// this._value = val
const run = () => {
this._status = FULFILLED
this._value = val
let cb;
while (cb = this._fulfilledQueues.shift()) {
cb(val)
}
}
// 为了支持同步的Promise,这里采用异步调用
setTimeout(() => run(), 0)
}
// 添加reject时执行的函数
_reject (err) {
if (this._status !== 'PENDING') return
this._status = 'REJECTED'
this._value = err
}
}
_fulfilledQueues = []
_rejectedQueues = []
// 添加then方法
then (onFulfilled, onRejected) {
const { _value, _status } = this
// 返回一个新的Promise对象
return new MyPromise((onFulfilledNext, onRejectedNext) => {
// 封装一个成功时执行的函数
let fulfilled = value => {
try {
if (typeof onFulfilled!=='function') {
onFulfilledNext(value)
} else {
let res = onFulfilled(value);
if (res instanceof MyPromise) {
// 如果当前回调函数返回MyPromise对象,必须等待其状态改变后在执行下一个回调
res.then(onFulfilledNext, onRejectedNext)
} else {
//否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
onFulfilledNext(res)
}
}
} catch (err) {
// 如果函数执行出错,新的Promise对象的状态为失败
onRejectedNext(err)
}
}
// 封装一个失败时执行的函数
let rejected = error => {
try {
if (typeof onRejected!=='function') {
onRejectedNext(error)
} else {
let res = onRejected(error);
if (res instanceof MyPromise) {
// 如果当前回调函数返回MyPromise对象,必须等待其状态改变后在执行下一个回调
res.then(onFulfilledNext, onRejectedNext)
} else {
//否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
onFulfilledNext(res)
}
}
} catch (err) {
// 如果函数执行出错,新的Promise对象的状态为失败
onRejectedNext(err)
}
}
switch (_status) {
// 当状态为pending时,将then方法回调函数加入执行队列等待执行
case 'PENDING':
this._fulfilledQueues.push(fulfilled)
this._rejectedQueues.push(rejected)
break
// 当状态已经改变时,立即执行对应的回调函数
case 'FULFILLED':
fulfilled(_value)
break
case 'REJECTED':
rejected(_value)
break
}
})
}
Sentry 错误监控
git https://github.com/getsentry/sentry
- 集成gitlab 一键创建issue
- 配置邮件通知
- 配置规则,添加邮件发送条件
- 配置版本号,为开发和线上配置不同的邮件发送规则
- sourcemap,直接查看报错js代码片段
//最简单的方式是主动触发:
try {
doSomething(a[0])
} catch(e) {
Raven.captureException(e)
}
//window.onerror捕捉异常
window.onerror = function (e) {
Raven.captureException(e)
}
//在vue里可以使用Vue.config.errorHandler 钩子来捕捉
Vue.config.errorHandler = (err, vm, info) => {
Raven.captureException(err)
}
//对于接口报错,可以在全局拦截里实现
request.interceptors.response.use(null, error => {
axiosHelper.error(error)
Raven.captureException(error)
return Promise.reject(error)
})
使用过程中可能遇到的问题
1.采集到的信息不全,没有我们想要的用户信息,如用户guid和phone
解决方案:文档里有提供方法setUserContext(),顾名思义该方法是设置全局上下文,因此我们可以在拿到用户信息后执行一次该方法。
request.interceptors.response.use(null, error => {
axiosHelper.error(error)
Raven.setUserContext({
phone: token.Phone || '',
guid: token.CustomerGuid || ''
})
Raven.captureException(error)
return Promise.reject(error)
})
2.当异常信息过多时,在监控后台没有有效的筛选条件,导致我要看指定的异常信息什么困难
解决方案:查询文档发现有新增标签的方法,标签可以帮助我们来筛选异常信息。
Raven.setTagsContext({
phone: token.Phone || '未登录'
})
3.由于线上的代码都是压缩过的,所以报错时很难定位到具体的哪一行代码出错。
解决方案: 上传sourcemap,sentry会自动匹配源码 https://docs.sentry.io/
搭建自己的sentry服务
由于官方提供的免费服务有一定次数的限制,达到一定限制后想要再使用就需要收费了,但是sentry是开源项目所以我们可以在本地搭建自己的服务,官方页提供了具体的操作步骤。Sentry的搭建
- 通过Docker容器安装 官方教程链接
axios 请求 获取 及错误
https://github.com/axios/axios
axios.get('/user/12345')
.then(function (response) {
console.log(response.data);
console.log(response.status);
console.log(response.statusText);
console.log(response.headers);
console.log(response.config);
});
// Handling Errors
axios.get('/user/12345')
.catch(function (error) {
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx 不包括2xx code 码
console.log(error.response.data);
console.log(error.response.status);
console.log(error.response.headers);
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
console.log(error.request);
} else {
// Something happened in setting up the request that triggered an Error
console.log('Error', error.message);
}
console.log(error.config);
});
axios 可以配置全局
axios.defaults.baseURL = 'https://api.example.com';
axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
// Custom instance defaults 可以制定多个 实例 配置
// Set config defaults when creating the instance
const instance = axios.create({
baseURL: 'https://api.example.com'
});
// Alter defaults after instance has been created
instance.defaults.headers.common['Authorization'] = AUTH_TOKEN;
//配置优先级
//单个请求的配置 > 实例 > 全局
// Create an instance using the config defaults provided by the library
// At this point the timeout config value is `0` as is the default for the library
const instance = axios.create();
// Override timeout default for the library
// Now all requests using this instance will wait 2.5 seconds before timing out
instance.defaults.timeout = 2500;
// Override timeout for this request as it's known to take a long time
instance.get('/longRequest', {
timeout: 5000
});
请求和相应拦截配置
// Add a request interceptor
axios.interceptors.request.use(function (config) {
// Do something before request is sent
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
// Add a response interceptor
axios.interceptors.response.use(function (response) {
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
return response;
}, function (error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
return Promise.reject(error);
});
Cancellation 可以设置 取消请求 中断请求https://github.com/axios/axios#cancellation
从多线程到Event Loop全面梳理
CPU、进程、线程之间的关系
从上文我们已经简单了解了CPU、进程、线程,简单汇总一下。
- 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
- 线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
- 不同进程之间也可以通信,不过代价较大
- 单线程与多线程,都是指在一个进程内的单和多
浏览器是多进程的
我们已经知道了CPU、进程、线程之间的关系,对于计算机来说,每一个应用程序都是一个进程, 而每一个应用程序都会分别有很多的功能模块,这些功能模块实际上是通过子进程来实现的。 对于这种子进程的扩展方式,我们可以称这个应用程序是多进程的。
总结一下:
- 浏览器是多进程的
- 每一个Tab页,就是一个独立的进程
浏览器包含了哪些进程
- 主进程
- 协调控制其他子进程(创建、销毁)
- 浏览器界面显示,用户交互,前进、后退、收藏
- 将渲染进程得到的内存中的Bitmap,绘制到用户界面上
- 处理不可见操作,网络请求,文件访问等
- 第三方插件进程
- 每种类型的插件对应一个进程,仅当使用该插件时才创建
- GPU进程
- 用于3D绘制等
- 渲染进程,就是我们说的浏览器内核
- 负责页面渲染,脚本执行,事件处理等
- 每个tab页一个渲染进程
那么浏览器中包含了这么多的进程,那么对于普通的前端操作来说,最重要的是什么呢?答案是渲染进程,也就是我们常说的浏览器内核
浏览器内核(渲染进程)
而对于渲染进程来说,它当然也是多线程的了,接下来我们来看一下渲染进程包含哪些线程。
- GUI渲染线程
- 负责渲染页面,布局和绘制
- 页面需要重绘和回流时,该线程就会执行
- 与js引擎线程互斥,防止渲染结果不可预期
- JS引擎线程
- 负责处理解析和执行javascript脚本程序
- 只有一个JS引擎线程(单线程)
- 与GUI渲染线程互斥,防止渲染结果不可预期
- 事件触发线程
- 用来控制事件循环(鼠标点击、setTimeout、ajax等)
- 当事件满足触发条件时,将事件放入到JS引擎所在的执行队列中
- 定时触发器线程
- setInterval与setTimeout所在的线程
- 定时任务并不是由JS引擎计时的,是由定时触发线程来计时的
- 计时完毕后,通知事件触发线程
- 异步http请求线程
- 浏览器有一个单独的线程用于处理AJAX请求
- 当请求完成时,若有回调函数,通知事件触发线程
当我们了解了渲染进程包含的这些线程后,我们思考两个问题:
- 为什么 javascript 是单线程的
- 为什么 GUI 渲染线程为什么与 JS 引擎线程互斥
为什么 javascript 是单线程的
首先是历史原因,在创建 javascript 这门语言时,多进程多线程的架构并不流行,硬件支持并不好。
其次是因为多线程的复杂性,多线程操作需要加锁,编码的复杂性会增高。
而且,如果同时操作 DOM ,在多线程不加锁的情况下,最终会导致 DOM 渲染的结果不可预期。
为什么 GUI 渲染线程与 JS 引擎线程互斥
这是由于 JS 是可以操作 DOM 的,如果同时修改元素属性并同时渲染界面(即 JS线程和UI线程同时运行), 那么渲染线程前后获得的元素就可能不一致了。
因此,为了防止渲染出现不可预期的结果,浏览器设定 GUI渲染线程和JS引擎线程为互斥关系, 当JS引擎线程执行时GUI渲染线程会被挂起,GUI更新则会被保存在一个队列中等待JS引擎线程空闲时立即被执行。
从 Event Loop 看 JS 的运行机制
到了这里,终于要进入我们的主题,什么是 Event Loop
先理解一些概念:
- JS 分为同步任务和异步任务
- 同步任务都在JS引擎线程上执行,形成一个执行栈
- 事件触发线程管理一个任务队列,异步任务触发条件达成,将回调事件放到任务队列中
- 执行栈中所有同步任务执行完毕,此时JS引擎线程空闲,系统会读取任务队列,将可运行的异步任务回调事件添加到执行栈中,开始执行
在前端开发中我们会通过setTimeout/setInterval来指定定时任务,会通过XHR/fetch发送网络请求, 接下来简述一下setTimeout/setInterval和XHR/fetch到底做了什么事
我们知道,不管是setTimeout/setInterval和XHR/fetch代码,在这些代码执行时, 本身是同步任务,而其中的回调函数才是异步任务。
当代码执行到setTimeout/setInterval时,实际上是JS引擎线程通知定时触发器线程,间隔一个时间后,会触发一个回调事件, 而定时触发器线程在接收到这个消息后,会在等待的时间后,将回调事件放入到由事件触发线程所管理的事件队列中。
当代码执行到XHR/fetch时,实际上是JS引擎线程通知异步http请求线程,发送一个网络请求,并制定请求完成后的回调事件, 而异步http请求线程在接收到这个消息后,会在请求成功后,将回调事件放入到由事件触发线程所管理的事件队列中。
当我们的同步任务执行完,JS引擎线程会询问事件触发线程,在事件队列中是否有待执行的回调函数,如果有就会加入到执行栈中交给JS引擎线程执行
再用代码来解释一下:
let timerCallback = function() {
console.log('wait one second');
};
let httpCallback = function() {
console.log('get server data success');
}
// 同步任务
console.log('hello');
// 同步任务
// 通知定时器线程 1s 后将 timerCallback 交由事件触发线程处理
// 1s 后 事件触发线程将 timerCallback 加入到事件队列中
setTimeout(timerCallback,1000);
// 同步任务
// 通知异步http请求线程发送网络请求,请求成功后将 httpCallback 交由事件触发线程处理
// 请求成功后事件触发线程将 httpCallback 加入到事件队列中
$.get('www.xxxx.com',httpCallback);
// 同步任务
console.log('world');
//...
// 所有同步任务执行完后
// 询问事件触发线程在事件事件队列中是否有需要执行的回调函数
// 如果没有,一直询问,直到有为止
// 如果有,将回调事件加入执行栈中,开始执行回调代码
宏任务、微任务
当我们基本了解了什么是执行栈,什么是事件队列之后,我们深入了解一下事件循环中宏任务、微任务
什么是宏任务
我们可以将每次执行栈执行的代码当做是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行), 每一个宏任务会从头到尾执行完毕,不会执行其他。
我们前文提到过JS引擎线程和GUI渲染线程是互斥的关系,浏览器为了能够使宏任务和DOM任务有序的进行,会在一个宏任务执行结果后,在下一个宏任务执行前,GUI渲染线程开始工作,对页面进行渲染。
什么是微任务
我们已经知道宏任务结束后,会执行渲染,然后执行下一个宏任务, 而微任务可以理解成在当前宏任务执行后立即执行的任务。
也就是说,当宏任务执行完,会在渲染前,将执行期间所产生的所有微任务都执行完。
Promise.then,Object.observe, MutationObserver, process.nextTick(node.js)等,属于微任务。
// 宏任务-->渲染-->宏任务-->渲染-->渲染...
主代码块,setTimeout,setInterval, I/O , UI 交互,postMessage, MessageChannel, setImmediate(node js)等,都属于宏任务
setTimeout(() => {
console.log(1)
Promise.resolve(3).then(data => console.log(data))
}, 0)
setTimeout(() => {
console.log(2)
}, 0)
// print : 1 3 2
上面代码共包含两个 setTimeout ,也就是说除主代码块外,共有两个宏任务, 其中第一个宏任务执行中,输出 1 ,并且创建了微任务队列,所以在下一个宏任务队列执行前, 先执行微任务,在微任务执行中,输出 3 ,微任务执行后,执行下一次宏任务,执行中输出 2
总结
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
- 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
js 常用方法技巧
根据数字快速生成js
new Array(10) 数组length 是10 不可迭代
new Array(10).keys返回的是可迭代器
[cc lang=”js”][…new Array(10).keys()]
Array.from({length:100},(item, index)=> index+1)
Object.keys(Array.apply(null, {length:100})).map(function(item){ return +item;});
[/cc]
es 6 中的 weak set weak map
只要Set实例中的引用存在,垃圾回收机制就不能释放该对象的内存空间,于是之前提到的Set类型可以被看做是一个强引用的Set集合。
[cc lang=”js”]
let set = new Set();
let key = {};
set.add(key);
console.log(set.size); // 1
// 移除原始引用
key = null;
console.log(set.size); // 1
// 重新取回原始引用
key = […set][0];
[/cc]
例如,在页面中通过JS记录了一些DOM,但是这些DOM可能被另一端脚本移除,而你又不希望自己的代码保留这些DOM元素的最后一个引用(这个情景被称为内存泄漏)。
为了解决这个问题,ES6引入了Weak Set集合。Weak Set集合只存储对象的弱引用,并且不可以存储原始值;
Weak Set只有add、has、delete三个方法。
两类Set的主要区别
最大区别是Weak Set保存的是对象的弱引用,可惜我们没有办法用代码来验证,例如下面的代码
[cc lang=”js”]
let weakSet = new WeakSet(),
set = new Set(),
key = {};
set.add(key);
weakSet.add(key);
console.log(set.size); // 1
console.log(weakSet.size); // undefined
key = null;
console.log(set.size); // 1
console.log(weakSet.size); // undefined
[/cc]
这是因为WeakSet没有size属性。所以说,我们可以看到WeakSet和Set的差别还有下面这几点:
WeakSet中,add、has、delete三个方法传入非对象参数都会报错
WeakSet不可迭代,不能被用于for-of
WeakSet不暴露任何迭代器(例如keys、values方法),所以无法通过程序本身来检测其中的内容
不支持forEach方法
不支持size属性
ES6中的Map集合
Map集合支持的方法
set
get
has(key)
delete(key)
clear()
forEach()
Map集合初始化方式
[cc lang=”js”]let map = new Map([[“name”, “NowhereToRun”], [“age”, “24”]]);[/cc]
Weak Map集合
键名必须是对象,否则会报错;
集合中保存的是对象的弱引用,如果在弱引用之外不存在其他的强引用,会被垃圾回收;但是只有集合的键名遵从这个规则,键名对应的键值如果是一个对象,则保存的是对象的强引用,不会触发垃圾回收
支持的方法:
set
get
has
delete
[cc lang=”js”]let map = new WeakMap();
let element = document.querySelector(“.element”);
map.set(element, “original”);
let value = map.get(element);
console.log(value); // original
// 移除element元素
element.parentNode.removeChild(element);
element = null;[/cc]