写点什么

美团前端常见面试题整理

作者:loveX001
  • 2023-02-23
    浙江
  • 本文字数:22448 字

    阅读完需:约 74 分钟

Object.assign()

描述Object.assign()方法用于将所有可枚举Object.propertyIsEnumerable() 返回 true)和自有Object.hasOwnProperty() 返回 true)属性的值从一个或多个源对象复制到目标对象。它将返回修改后的目标对象(请注意这个操作是浅拷贝)。


实现


Object.assign = function(target, ...source) {    if(target == null) {        throw new TypeError('Cannot convert undefined or null to object');    }    let res = Object(target);    source.forEach(function(obj) {        if(obj != null) {            // for...in 只会遍历对象自身的和继承的可枚举的属性(不含 Symbol 属性)            // hasOwnProperty 方法只考虑对象自身的属性            for(let key in obj) {                if(obj.hasOwnProperty(key)) {                    res[key] = obj[key];                }            }        }    });    return res;}
复制代码

setTimeout、Promise、Async/Await 的区别

(1)setTimeout

console.log('script start')    //1. 打印 script startsetTimeout(function(){    console.log('settimeout')    // 4. 打印 settimeout})    // 2. 调用 setTimeout 函数,并定义其完成后执行的回调函数console.log('script end')    //3. 打印 script start// 输出顺序:script start->script end->settimeout
复制代码

(2)Promise

Promise 本身是同步的立即执行函数, 当在 executor 中执行 resolve 或者 reject 的时候, 此时是异步操作, 会先执行 then/catch 等,当主栈完成后,才会去调用 resolve/reject 中存放的方法执行,打印 p 的时候,是打印的返回结果,一个 Promise 实例。


console.log('script start')let promise1 = new Promise(function (resolve) {    console.log('promise1')    resolve()    console.log('promise1 end')}).then(function () {    console.log('promise2')})setTimeout(function(){    console.log('settimeout')})console.log('script end')// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout
复制代码


当 JS 主线程执行到 Promise 对象时:


  • promise1.then() 的回调就是一个 task

  • promise1 是 resolved 或 rejected: 那这个 task 就会放入当前事件循环回合的 microtask queue

  • promise1 是 pending: 这个 task 就会放入 事件循环的未来的某个(可能下一个)回合的 microtask queue 中

  • setTimeout 的回调也是个 task ,它会被放入 macrotask queue 即使是 0ms 的情况

(3)async/await

async function async1(){   console.log('async1 start');    await async2();    console.log('async1 end')}async function async2(){    console.log('async2')}console.log('script start');async1();console.log('script end')// 输出顺序:script start->async1 start->async2->script end->async1 end
复制代码


async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。


例如:


async function func1() {    return 1}console.log(func1())
复制代码



func1 的运行结果其实就是一个 Promise 对象。因此也可以使用 then 来处理后续逻辑。


func1().then(res => {    console.log(res);  // 30})
复制代码


await 的含义为等待,也就是 async 函数需要等待 await 后的函数执行完成并且有了返回结果(Promise 对象)之后,才能继续执行下面的代码。await 通过返回一个 Promise 对象来实现同步的效果。

代码输出问题

function A(){}function B(a){  this.a = a;}function C(a){  if(a){this.a = a;  }}A.prototype.a = 1;B.prototype.a = 1;C.prototype.a = 1;
console.log(new A().a);console.log(new B().a);console.log(new C(2).a);
复制代码


输出结果:1 undefined 2


解析:


  1. console.log(new A().a),new A()为构造函数创建的对象,本身没有 a 属性,所以向它的原型去找,发现原型的 a 属性的属性值为 1,故该输出值为 1;

  2. console.log(new B().a),ew B()为构造函数创建的对象,该构造函数有参数 a,但该对象没有传参,故该输出值为 undefined;

  3. console.log(new C(2).a),new C()为构造函数创建的对象,该构造函数有参数 a,且传的实参为 2,执行函数内部,发现 if 为真,执行 this.a = 2,故属性 a 的值为 2。

如何解决跨越问题

(1)CORS

下面是 MDN 对于 CORS 的定义:


跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器  让运行在一个 origin (domain)上的 Web 应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。


CORS 需要浏览器和服务器同时支持,整个 CORS 过程都是浏览器完成的,无需用户参与。因此实现 CORS 的关键就是服务器,只要服务器实现了 CORS 请求,就可以跨源通信了。


浏览器将 CORS 分为简单请求非简单请求


简单请求不会触发 CORS 预检请求。若该请求满足以下两个条件,就可以看作是简单请求:


1)请求方法是以下三种方法之一:


  • HEAD

  • GET

  • POST


2)HTTP 的头信息不超出以下几种字段:


  • Accept

  • Accept-Language

  • Content-Language

  • Last-Event-ID

  • Content-Type:只限于三个值 application/x-www-form-urlencoded、multipart/form-data、text/plain


若不满足以上条件,就属于非简单请求了。


(1)简单请求过程:


对于简单请求,浏览器会直接发出 CORS 请求,它会在请求的头信息中增加一个 Orign 字段,该字段用来说明本次请求来自哪个源(协议+端口+域名),服务器会根据这个值来决定是否同意这次请求。如果 Orign 指定的域名在许可范围之内,服务器返回的响应就会多出以下信息头:


Access-Control-Allow-Origin: http://api.bob.com  // 和Orign一直Access-Control-Allow-Credentials: true   // 表示是否允许发送CookieAccess-Control-Expose-Headers: FooBar   // 指定返回其他字段的值Content-Type: text/html; charset=utf-8   // 表示文档类型
复制代码


如果 Orign 指定的域名不在许可范围之内,服务器会返回一个正常的 HTTP 回应,浏览器发现没有上面的 Access-Control-Allow-Origin 头部信息,就知道出错了。这个错误无法通过状态码识别,因为返回的状态码可能是 200。


在简单请求中,在服务器内,至少需要设置字段:Access-Control-Allow-Origin


(2)非简单请求过程


非简单请求是对服务器有特殊要求的请求,比如请求方法为 DELETE 或者 PUT 等。非简单请求的 CORS 请求会在正式通信之前进行一次 HTTP 查询请求,称为预检请求


浏览器会询问服务器,当前所在的网页是否在服务器允许访问的范围内,以及可以使用哪些 HTTP 请求方式和头信息字段,只有得到肯定的回复,才会进行正式的 HTTP 请求,否则就会报错。


预检请求使用的请求方法是 OPTIONS,表示这个请求是来询问的。他的头信息中的关键字段是 Orign,表示请求来自哪个源。除此之外,头信息中还包括两个字段:


  • Access-Control-Request-Method:该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法。

  • Access-Control-Request-Headers: 该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段。


服务器在收到浏览器的预检请求之后,会根据头信息的三个字段来进行判断,如果返回的头信息在中有 Access-Control-Allow-Origin 这个字段就是允许跨域请求,如果没有,就是不同意这个预检请求,就会报错。


服务器回应的 CORS 的字段如下:


Access-Control-Allow-Origin: http://api.bob.com  // 允许跨域的源地址Access-Control-Allow-Methods: GET, POST, PUT // 服务器支持的所有跨域请求的方法Access-Control-Allow-Headers: X-Custom-Header  // 服务器支持的所有头信息字段Access-Control-Allow-Credentials: true   // 表示是否允许发送CookieAccess-Control-Max-Age: 1728000  // 用来指定本次预检请求的有效期,单位为秒
复制代码


只要服务器通过了预检请求,在以后每次的 CORS 请求都会自带一个 Origin 头信息字段。服务器的回应,也都会有一个 Access-Control-Allow-Origin 头信息字段。


在非简单请求中,至少需要设置以下字段:


'Access-Control-Allow-Origin'  'Access-Control-Allow-Methods''Access-Control-Allow-Headers'
复制代码
减少 OPTIONS 请求次数:

OPTIONS 请求次数过多就会损耗页面加载的性能,降低用户体验度。所以尽量要减少 OPTIONS 请求次数,可以后端在请求的返回头部添加:Access-Control-Max-Age:number。它表示预检请求的返回结果可以被缓存多久,单位是秒。该字段只对完全一样的 URL 的缓存设置生效,所以设置了缓存时间,在这个时间范围内,再次发送请求就不需要进行预检请求了。

CORS 中 Cookie 相关问题:

在 CORS 请求中,如果想要传递 Cookie,就要满足以下三个条件:


  • 在请求中设置 withCredentials


默认情况下在跨域请求,浏览器是不带 cookie 的。但是我们可以通过设置 withCredentials 来进行传递 cookie.


// 原生 xml 的设置方式var xhr = new XMLHttpRequest();xhr.withCredentials = true;// axios 设置方式axios.defaults.withCredentials = true;
复制代码


  • Access-Control-Allow-Credentials 设置为 true

  • Access-Control-Allow-Origin 设置为非 *

(2)JSONP

jsonp 的原理就是利用<script>标签没有跨域限制,通过<script>标签 src 属性,发送带有 callback 参数的 GET 请求,服务端将接口返回数据拼凑到 callback 函数中,返回给浏览器,浏览器解析执行,从而前端拿到 callback 函数返回的数据。1)原生 JS 实现:


<script>    var script = document.createElement('script');    script.type = 'text/javascript';    // 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数    script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';    document.head.appendChild(script);    // 回调执行函数    function handleCallback(res) {        alert(JSON.stringify(res));    } </script>
复制代码


服务端返回如下(返回时即执行全局函数):


handleCallback({"success": true, "user": "admin"})
复制代码


2)Vue axios 实现:


this.$http = axios;this.$http.jsonp('http://www.domain2.com:8080/login', {    params: {},    jsonp: 'handleCallback'}).then((res) => {    console.log(res); })
复制代码


后端 node.js 代码:


var querystring = require('querystring');var http = require('http');var server = http.createServer();server.on('request', function(req, res) {    var params = querystring.parse(req.url.split('?')[1]);    var fn = params.callback;    // jsonp返回设置    res.writeHead(200, { 'Content-Type': 'text/javascript' });    res.write(fn + '(' + JSON.stringify(params) + ')');    res.end();});server.listen('8080');console.log('Server is running at port 8080...');
复制代码


JSONP 的缺点:


  • 具有局限性, 仅支持 get 方法

  • 不安全,可能会遭受 XSS 攻击

(3)postMessage 跨域

postMessage 是 HTML5 XMLHttpRequest Level 2 中的 API,且是为数不多可以跨域操作的 window 属性之一,它可用于解决以下方面的问题:


  • 页面和其打开的新窗口的数据传递

  • 多窗口之间消息传递

  • 页面与嵌套的 iframe 消息传递

  • 上面三个场景的跨域数据传递


用法:postMessage(data,origin)方法接受两个参数:


  • data: html5 规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用 JSON.stringify()序列化。

  • origin: 协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。


1)a.html:(domain1.com/a.html)


<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe><script>           var iframe = document.getElementById('iframe');    iframe.onload = function() {        var data = {            name: 'aym'        };        // 向domain2传送跨域数据        iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');    };    // 接受domain2返回数据    window.addEventListener('message', function(e) {        alert('data from domain2 ---> ' + e.data);    }, false);</script>
复制代码


2)b.html:(domain2.com/b.html)


<script>    // 接收domain1的数据    window.addEventListener('message', function(e) {        alert('data from domain1 ---> ' + e.data);        var data = JSON.parse(e.data);        if (data) {            data.number = 16;            // 处理后再发回domain1            window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');        }    }, false);</script>
复制代码

(4)nginx 代理跨域

nginx 代理跨域,实质和 CORS 跨域原理一样,通过配置文件设置请求响应头 Access-Control-Allow-Origin…等字段。


1)nginx 配置解决 iconfont 跨域浏览器跨域访问 js、css、img 等常规静态资源被同源策略许可,但 iconfont 字体文件(eot|otf|ttf|woff|svg)例外,此时可在 nginx 的静态资源服务器中加入以下配置。


location / {  add_header Access-Control-Allow-Origin *;}
复制代码


2)nginx 反向代理接口跨域跨域问题:同源策略仅是针对浏览器的安全策略。服务器端调用 HTTP 接口只是使用 HTTP 协议,不需要同源策略,也就不存在跨域问题。实现思路:通过 Nginx 配置一个代理服务器域名与 domain1 相同,端口不同)做跳板机,反向代理访问 domain2 接口,并且可以顺便修改 cookie 中 domain 信息,方便当前域 cookie 写入,实现跨域访问。


nginx 具体配置:


#proxy服务器server {    listen       81;    server_name  www.domain1.com;    location / {        proxy_pass   http://www.domain2.com:8080;  #反向代理        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名        index  index.html index.htm;        # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用        add_header Access-Control-Allow-Origin http://www.domain1.com;  #当前端只跨域不带cookie时,可为*        add_header Access-Control-Allow-Credentials true;    }}
复制代码

(5)nodejs 中间件代理跨域

node 中间件实现跨域代理,原理大致与 nginx 相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置 cookieDomainRewrite 参数修改响应头中 cookie 中域名,实现当前域的 cookie 写入,方便接口登录认证。


1)非 vue 框架的跨域 使用 node + express + http-proxy-middleware 搭建一个 proxy 服务器。


  • 前端代码:


var xhr = new XMLHttpRequest();// 前端开关:浏览器是否读写cookiexhr.withCredentials = true;// 访问http-proxy-middleware代理服务器xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);xhr.send();
复制代码


  • 中间件服务器代码:


var express = require('express');var proxy = require('http-proxy-middleware');var app = express();app.use('/', proxy({    // 代理跨域目标接口    target: 'http://www.domain2.com:8080',    changeOrigin: true,    // 修改响应头信息,实现跨域并允许带cookie    onProxyRes: function(proxyRes, req, res) {        res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');        res.header('Access-Control-Allow-Credentials', 'true');    },    // 修改响应信息中的cookie域名    cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改}));app.listen(3000);console.log('Proxy server is listen at port 3000...');
复制代码


2)vue 框架的跨域


node + vue + webpack + webpack-dev-server 搭建的项目,跨域请求接口,直接修改 webpack.config.js 配置。开发环境下,vue 渲染服务和接口代理服务都是 webpack-dev-server 同一个,所以页面与代理接口之间不再跨域。


webpack.config.js 部分配置:


module.exports = {    entry: {},    module: {},    ...    devServer: {        historyApiFallback: true,        proxy: [{            context: '/login',            target: 'http://www.domain2.com:8080',  // 代理跨域目标接口            changeOrigin: true,            secure: false,  // 当代理某些https服务报错时用            cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改        }],        noInfo: true    }}
复制代码

(6)document.domain + iframe 跨域

此方案仅限主域相同,子域不同的跨域应用场景。实现原理:两个页面都通过 js 强制设置 document.domain 为基础主域,就实现了同域。1)父窗口:(domain.com/a.html)


<iframe id="iframe" src="http://child.domain.com/b.html"></iframe><script>    document.domain = 'domain.com';    var user = 'admin';</script>
复制代码


1)子窗口:(child.domain.com/a.html)


<script>    document.domain = 'domain.com';    // 获取父窗口中变量    console.log('get js data from parent ---> ' + window.parent.user);</script>
复制代码

(7)location.hash + iframe 跨域

实现原理:a 欲与 b 跨域相互通信,通过中间页 c 来实现。 三个页面,不同域之间利用 iframe 的 location.hash 传值,相同域之间直接 js 访问来通信。


具体实现:A 域:a.html -> B 域:b.html -> A 域:c.html,a 与 b 不同域只能通过 hash 值单向通信,b 与 c 也不同域也只能单向通信,但 c 与 a 同域,所以 c 可通过 parent.parent 访问 a 页面所有对象。


1)a.html:(domain1.com/a.html)


<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe><script>    var iframe = document.getElementById('iframe');    // 向b.html传hash值    setTimeout(function() {        iframe.src = iframe.src + '#user=admin';    }, 1000);        // 开放给同域c.html的回调方法    function onCallback(res) {        alert('data from c.html ---> ' + res);    }</script>
复制代码


2)b.html:(.domain2.com/b.html)


<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe><script>    var iframe = document.getElementById('iframe');    // 监听a.html传来的hash值,再传给c.html    window.onhashchange = function () {        iframe.src = iframe.src + location.hash;    };</script>
复制代码


<script>    // 监听b.html传来的hash值    window.onhashchange = function () {        // 再通过操作同域a.html的js回调,将结果传回        window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));    };</script>
复制代码

(8)window.name + iframe 跨域

window.name 属性的独特之处:name 值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。


1)a.html:(domain1.com/a.html)


var proxy = function(url, callback) {    var state = 0;    var iframe = document.createElement('iframe');    // 加载跨域页面    iframe.src = url;    // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name    iframe.onload = function() {        if (state === 1) {            // 第2次onload(同域proxy页)成功后,读取同域window.name中数据            callback(iframe.contentWindow.name);            destoryFrame();        } else if (state === 0) {            // 第1次onload(跨域页)成功后,切换到同域代理页面            iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';            state = 1;        }    };    document.body.appendChild(iframe);    // 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)    function destoryFrame() {        iframe.contentWindow.document.write('');        iframe.contentWindow.close();        document.body.removeChild(iframe);    }};// 请求跨域b页面数据proxy('http://www.domain2.com/b.html', function(data){    alert(data);});
复制代码


2)proxy.html:(domain1.com/proxy.html)


中间代理页,与 a.html 同域,内容为空即可。3)b.html:(domain2.com/b.html)


<script>        window.name = 'This is domain2 data!';</script>
复制代码


通过 iframe 的 src 属性由外域转向本地域,跨域数据即由 iframe 的 window.name 从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

(9)WebSocket 协议跨域

WebSocket protocol 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是 server push 技术的一种很好的实现。


原生 WebSocket API 使用起来不太方便,我们使用 Socket.io,它很好地封装了 webSocket 接口,提供了更简单、灵活的接口,也对不支持 webSocket 的浏览器提供了向下兼容。


1)前端代码:


<div>user input:<input type="text"></div><script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script><script>var socket = io('http://www.domain2.com:8080');// 连接成功处理socket.on('connect', function() {    // 监听服务端消息    socket.on('message', function(msg) {        console.log('data from server: ---> ' + msg);     });    // 监听服务端关闭    socket.on('disconnect', function() {         console.log('Server socket has closed.');     });});document.getElementsByTagName('input')[0].onblur = function() {    socket.send(this.value);};</script>
复制代码


2)Nodejs socket 后台:


var http = require('http');var socket = require('socket.io');// 启http服务var server = http.createServer(function(req, res) {    res.writeHead(200, {        'Content-type': 'text/html'    });    res.end();});server.listen('8080');console.log('Server is running at port 8080...');// 监听socket连接socket.listen(server).on('connection', function(client) {    // 接收信息    client.on('message', function(msg) {        client.send('hello:' + msg);        console.log('data from client: ---> ' + msg);    });    // 断开处理    client.on('disconnect', function() {        console.log('Client socket has closed.');     });});
复制代码

数组扁平化

数组扁平化就是将 [1, [2, [3]]] 这种多层的数组拍平成一层 [1, 2, 3]。使用 Array.prototype.flat 可以直接将多层数组拍平成一层:


[1, [2, [3]]].flat(2)  // [1, 2, 3]
复制代码


现在就是要实现 flat 这种效果。


ES5 实现:递归。


function flatten(arr) {    var result = [];    for (var i = 0, len = arr.length; i < len; i++) {        if (Array.isArray(arr[i])) {            result = result.concat(flatten(arr[i]))        } else {            result.push(arr[i])        }    }    return result;}
复制代码


ES6 实现:


function flatten(arr) {    while (arr.some(item => Array.isArray(item))) {        arr = [].concat(...arr);    }    return arr;}
复制代码

Node 中的 Event Loop 和浏览器中的有什么区别?process.nextTick 执行顺序?

Node 中的 Event Loop 和浏览器中的是完全不相同的东西。


Node 的 Event Loop 分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。


(1)Timers(计时器阶段):初次进入事件循环,会从计时器阶段开始。此阶段会判断是否存在过期的计时器回调(包含 setTimeout 和 setInterval),如果存在则会执行所有过期的计时器回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务,执行完微任务后再进入 Pending callbacks 阶段。


(2)Pending callbacks:执行推迟到下一个循环迭代的 I / O 回调(系统调用相关的回调)。


(3)Idle/Prepare:仅供内部使用。


(4)Poll(轮询阶段)


  • 当回调队列不为空时:会执行回调,若回调中触发了相应的微任务,这里的微任务执行时机和其他地方有所不同,不会等到所有回调执行完毕后才执行,而是针对每一个回调执行完毕后,就执行相应微任务。执行完所有的回调后,变为下面的情况。

  • 当回调队列为空时(没有回调或所有回调执行完毕):但如果存在有计时器(setTimeout、setInterval 和 setImmediate)没有执行,会结束轮询阶段,进入 Check 阶段。否则会阻塞并等待任何正在执行的 I/O 操作完成,并马上执行相应的回调,直到所有回调执行完毕。


(5)Check(查询阶段):会检查是否存在 setImmediate 相关的回调,如果存在则执行所有回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务,执行完微任务后再进入 Close callbacks 阶段。


(6)Close callbacks:执行一些关闭回调,比如 socket.on('close', ...)等。


下面来看一个例子,首先在有些情况下,定时器的执行顺序其实是随机


setTimeout(() => {    console.log('setTimeout')}, 0)setImmediate(() => {    console.log('setImmediate')})
复制代码


对于以上代码来说,setTimeout 可能执行在前,也可能执行在后


  • 首先 setTimeout(fn, 0) === setTimeout(fn, 1),这是由源码决定的

  • 进入事件循环也是需要成本的,如果在准备时候花费了大于 1ms 的时间,那么在 timer 阶段就会直接执行 setTimeout 回调

  • 那么如果准备时间花费小于 1ms,那么就是 setImmediate 回调先执行了


当然在某些情况下,他们的执行顺序一定是固定的,比如以下代码:


const fs = require('fs')fs.readFile(__filename, () => {    setTimeout(() => {        console.log('timeout');    }, 0)    setImmediate(() => {        console.log('immediate')    })})
复制代码


在上述代码中,setImmediate 永远先执行。因为两个代码写在 IO 回调中,IO 回调是在 poll 阶段执行,当回调执行完毕后队列为空,发现存在 setImmediate 回调,所以就直接跳转到 check 阶段去执行回调了。


上面都是 macrotask 的执行情况,对于 microtask 来说,它会在以上每个阶段完成前清空 microtask 队列,


setTimeout(() => {  console.log('timer21')}, 0)Promise.resolve().then(function() {  console.log('promise1')})
复制代码


对于以上代码来说,其实和浏览器中的输出是一样的,microtask 永远执行在 macrotask 前面。


最后来看 Node 中的 process.nextTick,这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。


setTimeout(() => { console.log('timer1') Promise.resolve().then(function() {   console.log('promise1') })}, 0)process.nextTick(() => { console.log('nextTick') process.nextTick(() => {   console.log('nextTick')   process.nextTick(() => {     console.log('nextTick')     process.nextTick(() => {       console.log('nextTick')     })   }) })})
复制代码


对于以上代码,永远都是先把 nextTick 全部打印出来。


参考 前端进阶面试题详细解答

如何提⾼webpack 的打包速度?

(1)优化 Loader

对于 Loader 来说,影响打包效率首当其冲必属 Babel 了。因为 Babel 会将代码转为字符串生成 AST,然后对 AST 继续进行转变最后再生成新的代码,项目越大,转换代码越多,效率就越低。当然了,这是可以优化的。


首先我们优化 Loader 的文件搜索范围


module.exports = {  module: {    rules: [      {        // js 文件才使用 babel        test: /\.js$/,        loader: 'babel-loader',        // 只在 src 文件夹下查找        include: [resolve('src')],        // 不会去查找的路径        exclude: /node_modules/      }    ]  }}
复制代码


对于 Babel 来说,希望只作用在 JS 代码上的,然后 node_modules 中使用的代码都是编译过的,所以完全没有必要再去处理一遍。


当然这样做还不够,还可以将 Babel 编译过的文件缓存起来,下次只需要编译更改过的代码文件即可,这样可以大幅度加快打包时间


loader: 'babel-loader?cacheDirectory=true'
复制代码

(2)HappyPack

受限于 Node 是单线程运行的,所以 Webpack 在打包的过程中也是单线程的,特别是在执行 Loader 的时候,长时间编译的任务很多,这样就会导致等待的情况。


HappyPack 可以将 Loader 的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率了


module: {  loaders: [    {      test: /\.js$/,      include: [resolve('src')],      exclude: /node_modules/,      // id 后面的内容对应下面      loader: 'happypack/loader?id=happybabel'    }  ]},plugins: [  new HappyPack({    id: 'happybabel',    loaders: ['babel-loader?cacheDirectory'],    // 开启 4 个线程    threads: 4  })]
复制代码

(3)DllPlugin

DllPlugin 可以将特定的类库提前打包然后引入。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。DllPlugin 的使用方法如下:


// 单独配置在一个文件中// webpack.dll.conf.jsconst path = require('path')const webpack = require('webpack')module.exports = {  entry: {    // 想统一打包的类库    vendor: ['react']  },  output: {    path: path.join(__dirname, 'dist'),    filename: '[name].dll.js',    library: '[name]-[hash]'  },  plugins: [    new webpack.DllPlugin({      // name 必须和 output.library 一致      name: '[name]-[hash]',      // 该属性需要与 DllReferencePlugin 中一致      context: __dirname,      path: path.join(__dirname, 'dist', '[name]-manifest.json')    })  ]}
复制代码


然后需要执行这个配置文件生成依赖文件,接下来需要使用 DllReferencePlugin 将依赖文件引入项目中


// webpack.conf.jsmodule.exports = {  // ...省略其他配置  plugins: [    new webpack.DllReferencePlugin({      context: __dirname,      // manifest 就是之前打包出来的 json 文件      manifest: require('./dist/vendor-manifest.json'),    })  ]}
复制代码

(4)代码压缩

在 Webpack3 中,一般使用 UglifyJS 来压缩代码,但是这个是单线程运行的,为了加快效率,可以使用 webpack-parallel-uglify-plugin 来并行运行 UglifyJS,从而提高效率。


在 Webpack4 中,不需要以上这些操作了,只需要将 mode 设置为 production 就可以默认开启以上功能。代码压缩也是我们必做的性能优化方案,当然我们不止可以压缩 JS 代码,还可以压缩 HTML、CSS 代码,并且在压缩 JS 代码的过程中,我们还可以通过配置实现比如删除 console.log 这类代码的功能。

(5)其他

可以通过一些小的优化点来加快打包速度


  • resolve.extensions:用来表明文件后缀列表,默认查找顺序是 ['.js', '.json'],如果你的导入文件没有添加后缀就会按照这个顺序查找文件。我们应该尽可能减少后缀列表长度,然后将出现频率高的后缀排在前面

  • resolve.alias:可以通过别名的方式来映射一个路径,能让 Webpack 更快找到路径

  • module.noParse:如果你确定一个文件下没有其他依赖,就可以使用该属性让 Webpack 不扫描该文件,这种方式对于大型的类库很有帮助

如何减少 Webpack 打包体积

(1)按需加载

在开发 SPA 项目的时候,项目中都会存在很多路由页面。如果将这些页面全部打包进一个 JS 文件的话,虽然将多个请求合并了,但是同样也加载了很多并不需要的代码,耗费了更长的时间。那么为了首页能更快地呈现给用户,希望首页能加载的文件体积越小越好,这时候就可以使用按需加载,将每个路由页面单独打包为一个文件。当然不仅仅路由可以按需加载,对于 loadash 这种大型类库同样可以使用这个功能。


按需加载的代码实现这里就不详细展开了,因为鉴于用的框架不同,实现起来都是不一样的。当然了,虽然他们的用法可能不同,但是底层的机制都是一样的。都是当使用的时候再去下载对应文件,返回一个 Promise,当 Promise 成功以后去执行回调。

(2)Scope Hoisting

Scope Hoisting 会分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去。


比如希望打包两个文件:


// test.jsexport const a = 1// index.jsimport { a } from './test.js'
复制代码


对于这种情况,打包出来的代码会类似这样:


[  /* 0 */  function (module, exports, require) {    //...  },  /* 1 */  function (module, exports, require) {    //...  }]
复制代码


但是如果使用 Scope Hoisting ,代码就会尽可能的合并到一个函数中去,也就变成了这样的类似代码:


[  /* 0 */  function (module, exports, require) {    //...  }]
复制代码


这样的打包方式生成的代码明显比之前的少多了。如果在 Webpack4 中你希望开启这个功能,只需要启用 optimization.concatenateModules 就可以了:


module.exports = {  optimization: {    concatenateModules: true  }}
复制代码

(3)Tree Shaking

Tree Shaking 可以实现删除项目中未被引用的代码,比如:


// test.jsexport const a = 1export const b = 2// index.jsimport { a } from './test.js'
复制代码


对于以上情况,test 文件中的变量 b 如果没有在项目中使用到的话,就不会被打包到文件中。


如果使用 Webpack 4 的话,开启生产环境就会自动启动这个优化功能。

常见的 HTTP 请求头和响应头

HTTP Request Header 常见的请求头:


  • Accept:浏览器能够处理的内容类型

  • Accept-Charset:浏览器能够显示的字符集

  • Accept-Encoding:浏览器能够处理的压缩编码

  • Accept-Language:浏览器当前设置的语言

  • Connection:浏览器与服务器之间连接的类型

  • Cookie:当前页面设置的任何 Cookie

  • Host:发出请求的页面所在的域

  • Referer:发出请求的页面的 URL

  • User-Agent:浏览器的用户代理字符串


HTTP Responses Header 常见的响应头:


  • Date:表示消息发送的时间,时间的描述格式由 rfc822 定义

  • server:服务器名称

  • Connection:浏览器与服务器之间连接的类型

  • Cache-Control:控制 HTTP 缓存

  • content-type:表示后面的文档属于什么 MIME 类型


常见的 Content-Type 属性值有以下四种:


(1)application/x-www-form-urlencoded:浏览器的原生 form 表单,如果不设置 enctype 属性,那么最终就会以 application/x-www-form-urlencoded 方式提交数据。该种方式提交的数据放在 body 里面,数据按照 key1=val1&key2=val2 的方式进行编码,key 和 val 都进行了 URL 转码。


(2)multipart/form-data:该种方式也是一个常见的 POST 提交方式,通常表单上传文件时使用该种方式。


(3)application/json:服务器消息主体是序列化后的 JSON 字符串。


(4)text/xml:该种方式主要用来提交 XML 格式的数据。

React 事务机制



左右居中方案

  • 行内元素: text-align: center

  • 定宽块状元素: 左右 margin 值为 auto

  • 不定宽块状元素: table布局,position + transform


/* 方案1 */.wrap {  text-align: center}.center {  display: inline;  /* or */  /* display: inline-block; */}/* 方案2 */.center {  width: 100px;  margin: 0 auto;}/* 方案2 */.wrap {  position: relative;}.center {  position: absulote;  left: 50%;  transform: translateX(-50%);}
复制代码

原函数形参不定长(此时 fn.length 为 0)

function curry(fn) {    // 保存参数,除去第一个函数参数    let args = [].slice.call(arguments, 1);    // 返回一个新函数    let curried = function () {        // 新函数调用时会继续传参        let allArgs = [...args, ...arguments];        return curry(fn, ...allArgs);    };    // 利用toString隐式转换的特性,当最后执行函数时,会隐式转换    curried.toString = function () {        return fn(...args);    };    return curried;}
// 测试function add(...args) { return args.reduce((pre, cur) => pre + cur, 0);}console.log(add(1, 2, 3, 4));let addCurry = curry(add);console.log(addCurry(1)(2)(3) == 6); // trueconsole.log(addCurry(1, 2, 3)(4) == 10); // trueconsole.log(addCurry(2, 6)(1).toString()); // 9console.log(addCurry(2, 6)(1, 8)); // 打印 curried 函数
复制代码

TCP 粘包是怎么回事,如何处理?

默认情况下, TCP 连接会启⽤延迟传送算法 (Nagle 算法), 在数据发送之前缓存他们. 如果短时间有多个数据发送, 会缓冲到⼀起作⼀次发送 (缓冲⼤⼩⻅ socket.bufferSize ), 这样可以减少 IO 消耗提⾼性能.


如果是传输⽂件的话, 那么根本不⽤处理粘包的问题, 来⼀个包拼⼀个包就好了。但是如果是多条消息, 或者是别的⽤途的数据那么就需要处理粘包.


下面看⼀个例⼦, 连续调⽤两次 send 分别发送两段数据 data1 和 data2, 在接收端有以下⼏种常⻅的情况:A. 先接收到 data1, 然后接收到 data2 .B. 先接收到 data1 的部分数据, 然后接收到 data1 余下的部分以及 data2 的全部.C. 先接收到了 data1 的全部数据和 data2 的部分数据, 然后接收到了 data2 的余下的数据.D. ⼀次性接收到了 data1 和 data2 的全部数据.


其中的 BCD 就是我们常⻅的粘包的情况. ⽽对于处理粘包的问题, 常⻅的解决⽅案有:


  • 多次发送之前间隔⼀个等待时间:只需要等上⼀段时间再进⾏下⼀次 send 就好, 适⽤于交互频率特别低的场景. 缺点也很明显, 对于⽐较频繁的场景⽽⾔传输效率实在太低,不过⼏乎不⽤做什么处理.

  • 关闭 Nagle 算法:关闭 Nagle 算法, 在 Node.js 中你可以通过 socket.setNoDelay() ⽅法来关闭 Nagle 算法, 让每⼀次 send 都不缓冲直接发送。该⽅法⽐较适⽤于每次发送的数据都⽐较⼤ (但不是⽂件那么⼤), 并且频率不是特别⾼的场景。如果是每次发送的数据量⽐较⼩, 并且频率特别⾼的, 关闭 Nagle 纯属⾃废武功。另外, 该⽅法不适⽤于⽹络较差的情况, 因为 Nagle 算法是在服务端进⾏的包合并情况, 但是如果短时间内客户端的⽹络情况不好, 或者应⽤层由于某些原因不能及时将 TCP 的数据 recv, 就会造成多个包在客户端缓冲从⽽粘包的情况。 (如果是在稳定的机房内部通信那么这个概率是⽐较⼩可以选择忽略的)

  • 进⾏封包/拆包: 封包/拆包是⽬前业内常⻅的解决⽅案了。即给每个数据包在发送之前, 于其前/后放⼀些有特征的数据, 然后收到数据的时 候根据特征数据分割出来各个数据包。

懒加载与预加载的区别

这两种方式都是提高网页性能的方式,两者主要区别是一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。


  • 懒加载也叫延迟加载,指的是在长网页中延迟加载图片的时机,当用户需要访问时,再去加载,这样可以提高网站的首屏加载速度,提升用户的体验,并且可以减少服务器的压力。它适用于图片很多,页面很长的电商网站的场景。懒加载的实现原理是,将页面上的图片的 src 属性设置为空字符串,将图片的真实路径保存在一个自定义属性中,当页面滚动的时候,进行判断,如果图片进入页面可视区域内,则从自定义属性中取出真实路径赋值给图片的 src 属性,以此来实现图片的延迟加载。

  • 预加载指的是将所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源。 通过预加载能够减少用户的等待时间,提高用户的体验。我了解的预加载的最常用的方式是使用 js 中的 image 对象,通过为 image 对象来设置 scr 属性,来实现图片的预加载。

内存泄露

  • 意外的全局变量: 无法被回收

  • 定时器: 未被正确关闭,导致所引用的外部变量无法被释放

  • 事件监听: 没有正确销毁 (低版本浏览器可能出现)

  • 闭包

  • 第一种情况是我们由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。

  • 第二种情况是我们设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。

  • 第三种情况是我们获取一个 DOM 元素的引用,而后面这个元素被删除,由于我们一直保留了对这个元素的引用,所以它也无法被回收。

  • 第四种情况是不合理的使用闭包,从而导致某些变量一直被留在内存当中。

  • dom 引用: dom 元素被删除时,内存中的引用未被正确清空

  • 控制台console.log打印的东西


可用 chrome 中的 timeline 进行内存标记,可视化查看内存的变化情况,找出异常点。


内存泄露排查方法(opens new window)

nextTick

nextTick 可以让我们在下次 DOM 更新循环结束之后执行延迟回调,用于获得更新后的 DOM


nextTick主要使用了宏任务和微任务。根据执行环境分别尝试采用


  • Promise

  • MutationObserver

  • setImmediate

  • 如果以上都不行则采用setTimeout


定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列

渲染机制

1. 浏览器如何渲染网页


概述:浏览器渲染一共有五步


  1. 处理 HTML 并构建 DOM 树。

  2. 处理 CSS构建 CSSOM 树。

  3. DOMCSSOM 合并成一个渲染树。

  4. 根据渲染树来布局,计算每个节点的位置。

  5. 调用 GPU 绘制,合成图层,显示在屏幕上


第四步和第五步是最耗时的部分,这两步合起来,就是我们通常所说的渲染


具体如下图过程如下图所示




渲染


  • 网页生成的时候,至少会渲染一次

  • 在用户访问的过程中,还会不断重新渲染


重新渲染需要重复之前的第四步(重新生成布局)+第五步(重新绘制)或者只有第五个步(重新绘制)


  • 在构建 CSSOM 树时,会阻塞渲染,直至 CSSOM树构建完成。并且构建 CSSOM 树是一个十分消耗性能的过程,所以应该尽量保证层级扁平,减少过度层叠,越是具体的 CSS 选择器,执行速度越慢

  • HTML 解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件。并且CSS也会影响 JS 的执行,只有当解析完样式表才会执行 JS,所以也可以认为这种情况下,CSS 也会暂停构建 DOM


2. 浏览器渲染五个阶段


2.1 第一步:解析 HTML 标签,构建 DOM 树


在这个阶段,引擎开始解析html,解析出来的结果会成为一棵domdom的目的至少有2


  • 作为下个阶段渲染树状图的输入

  • 成为网页和脚本的交互界面。(最常用的就是getElementById等等)


当解析器到达 script 标签的时候,发生下面四件事情


  1. html解析器停止解析,

  2. 如果是外部脚本,就从外部网络获取脚本代码

  3. 将控制权交给js引擎,执行js代码

  4. 恢复html解析器的控制权


由此可以得到第一个结论 1


  • 由于<script>标签是阻塞解析的,将脚本放在网页尾部会加速代码渲染。

  • deferasync属性也能有助于加载外部脚本。

  • defer使得脚本会在dom完整构建之后执行;

  • async标签使得脚本只有在完全available才执行,并且是以非阻塞的方式进行的


2.2 第二步:解析 CSS 标签,构建 CSSOM 树


  • 我们已经看到html解析器碰到脚本后会做的事情,接下来我们看下html解析器碰到样式表会发生的情况

  • js会阻塞解析,因为它会修改文档(document)。css不会修改文档的结构,如果这样的话,似乎看起来css样式不会阻塞浏览器html解析。但是事实上 css样式表是阻塞的。阻塞是指当cssom树建立好之后才会进行下一步的解析渲染


通过以下手段可以减轻 cssom 带来的影响


  • script脚本放在页面底部

  • 尽可能快的加载css样式表

  • 将样式表按照media typemedia query区分,这样有助于我们将css资源标记成非阻塞渲染的资源。

  • 非阻塞的资源还是会被浏览器下载,只是优先级较低


2.3 第三步:把 DOM 和 CSSOM 组合成渲染树(render tree)



2.4 第四步:在渲染树的基础上进行布局,计算每个节点的几何结构


布局(layout):定位坐标和大小,是否换行,各种position, overflow, z-index属性


2.5 调用 GPU 绘制,合成图层,显示在屏幕上


将渲染树的各个节点绘制到屏幕上,这一步被称为绘制painting


3. 渲染优化相关


3.1 Load 和 DOMContentLoaded 区别


  • Load 事件触发代表页面中的 DOMCSSJS,图片已经全部加载完毕。

  • DOMContentLoaded 事件触发代表初始的 HTML 被完全加载和解析,不需要等待 CSSJS,图片加载


3.2 图层


一般来说,可以把普通文档流看成一个图层。特定的属性可以生成一个新的图层。不同的图层渲染互不影响,所以对于某些频繁需要渲染的建议单独生成一个新图层,提高性能。但也不能生成过多的图层,会引起反作用。


通过以下几个常用属性可以生成新图层


  • 3D 变换:translate3dtranslateZ

  • will-change

  • videoiframe 标签

  • 通过动画实现的 opacity 动画转换

  • position: fixed


3.3 重绘(Repaint)和回流(Reflow)


重绘和回流是渲染步骤中的一小节,但是这两个步骤对于性能影响很大


  • 重绘是当节点需要更改外观而不会影响布局的,比如改变 color 就叫称为重绘

  • 回流是布局或者几何属性需要改变就称为回流。


回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变深层次的节点很可能导致父节点的一系列回流


以下几个动作可能会导致性能问题


  • 改变 window 大小

  • 改变字体

  • 添加或删除样式

  • 文字改变

  • 定位或者浮动

  • 盒模型


很多人不知道的是,重绘和回流其实和 Event loop 有关


  • Event loop 执行完Microtasks 后,会判断 document 是否需要更新。因为浏览器是 60Hz 的刷新率,每 16ms 才会更新一次。

  • 然后判断是否有 resize 或者 scroll ,有的话会去触发事件,所以 resizescroll 事件也是至少 16ms才会触发一次,并且自带节流功能。

  • 判断是否触发了 media query

  • 更新动画并且发送事件

  • 判断是否有全屏操作事件

  • 执行 requestAnimationFrame 回调

  • 执行 IntersectionObserver 回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好

  • 更新界面

  • 以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行 requestIdleCallback 回调


常见的引起重绘的属性


  • color

  • border-style

  • visibility

  • background

  • text-decoration

  • background-image

  • background-position

  • background-repeat

  • outline-color

  • outline

  • outline-style

  • border-radius

  • outline-width

  • box-shadow

  • background-size


3.4 常见引起回流属性和方法


任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发重排,下面列一些栗子


  • 添加或者删除可见的DOM元素;

  • 元素尺寸改变——边距、填充、边框、宽度和高度

  • 内容变化,比如用户在input框中输入文字

  • 浏览器窗口尺寸改变——resize事件发生时

  • 计算 offsetWidthoffsetHeight 属性

  • 设置 style 属性的值


回流影响的范围


由于浏览器渲染界面是基于流失布局模型的,所以触发重排时会对周围 DOM 重新排列,影响的范围有两种


  • 全局范围:从根节点html开始对整个渲染树进行重新布局。

  • 局部范围:对渲染树的某部分或某一个渲染对象进行重新布局


全局范围回流


<body>  <div class="hello">    <h4>hello</h4>    <p><strong>Name:</strong>BDing</p>    <h5>male</h5>    <ol>      <li>coding</li>      <li>loving</li>    </ol>  </div></body>
复制代码


p节点上发生reflow时,hellobody也会重新渲染,甚至h5ol都会收到影响


局部范围回流


用局部布局来解释这种现象:把一个dom的宽高之类的几何信息定死,然后在dom内部触发重排,就只会重新渲染该dom内部的元素,而不会影响到外界


3.5 减少重绘和回流


使用 translate 替代 top


<div class="test"></div><style>    .test {        position: absolute;        top: 10px;        width: 100px;        height: 100px;        background: red;    }</style><script>    setTimeout(() => {        // 引起回流        document.querySelector('.test').style.top = '100px'    }, 1000)</script>
复制代码


  • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)

  • DOM 离线后修改,比如:先把 DOMdisplay:none (有一次 Reflow),然后你修改100次,然后再把它显示出来

  • 不要把 DOM 结点的属性值放在一个循环里当成循环里的变量


for(let i = 0; i < 1000; i++) {    // 获取 offsetTop 会导致回流,因为需要去获取正确的值    console.log(document.querySelector('.test').style.offsetTop)}
复制代码


  • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局

  • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame

  • CSS选择符从右往左匹配查找,避免 DOM深度过深

  • 将频繁运行的动画变为图层,图层能够阻止该节点回流影响别的元素。比如对于 video标签,浏览器会自动将该节点变为图层。



僵尸进程和孤儿进程是什么?

  • 孤儿进程:父进程退出了,而它的一个或多个进程还在运行,那这些子进程都会成为孤儿进程。孤儿进程将被 init 进程(进程号为 1)所收养,并由 init 进程对它们完成状态收集工作。

  • 僵尸进程:子进程比父进程先结束,而父进程又没有释放子进程占用的资源,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵死进程。

meta 标签:自动刷新/跳转

假设要实现一个类似 PPT 自动播放的效果,你很可能会想到使用 JavaScript 定时器控制页面跳转来实现。但其实有更加简洁的实现方法,比如通过 meta 标签来实现:


<meta http-equiv="Refresh" content="5; URL=page2.html">
复制代码


上面的代码会在 5s 之后自动跳转到同域下的 page2.html 页面。我们要实现 PPT 自动播放的功能,只需要在每个页面的 meta 标签内设置好下一个页面的地址即可。


另一种场景,比如每隔一分钟就需要刷新页面的大屏幕监控,也可以通过 meta 标签来实现,只需去掉后面的 URL 即可:


<meta http-equiv="Refresh" content="60">
复制代码


meta viewport 相关


<!DOCTYPE html>  <!--H5标准声明,使用 HTML5 doctype,不区分大小写--><head lang=”en”> <!--标准的 lang 属性写法--><meta charset=’utf-8′>    <!--声明文档使用的字符编码--><meta http-equiv=”X-UA-Compatible” content=”IE=edge,chrome=1″/>   <!--优先使用 IE 最新版本和 Chrome--><meta name=”description” content=”不超过150个字符”/>       <!--页面描述--><meta name=”keywords” content=””/>     <!-- 页面关键词--><meta name=”author” content=”name, email@gmail.com”/>    <!--网页作者--><meta name=”robots” content=”index,follow”/>      <!--搜索引擎抓取--><meta name=”viewport” content=”initial-scale=1, maximum-scale=3, minimum-scale=1, user-scalable=no”> <!--为移动设备添加 viewport--><meta name=”apple-mobile-web-app-title” content=”标题”> <!--iOS 设备 begin--><meta name=”apple-mobile-web-app-capable” content=”yes”/>  <!--添加到主屏后的标题(iOS 6 新增)是否启用 WebApp 全屏模式,删除苹果默认的工具栏和菜单栏--><meta name=”apple-itunes-app” content=”app-id=myAppStoreID, affiliate-data=myAffiliateData, app-argument=myURL”><!--添加智能 App 广告条 Smart App Banner(iOS 6+ Safari)--><meta name=”apple-mobile-web-app-status-bar-style” content=”black”/><meta name=”format-detection” content=”telphone=no, email=no”/>  <!--设置苹果工具栏颜色--><meta name=”renderer” content=”webkit”> <!-- 启用360浏览器的极速模式(webkit)--><meta http-equiv=”X-UA-Compatible” content=”IE=edge”>     <!--避免IE使用兼容模式--><meta http-equiv=”Cache-Control” content=”no-siteapp” />    <!--不让百度转码--><meta name=”HandheldFriendly” content=”true”>     <!--针对手持设备优化,主要是针对一些老的不识别viewport的浏览器,比如黑莓--><meta name=”MobileOptimized” content=”320″>   <!--微软的老式浏览器--><meta name=”screen-orientation” content=”portrait”>   <!--uc强制竖屏--><meta name=”x5-orientation” content=”portrait”>    <!--QQ强制竖屏--><meta name=”full-screen” content=”yes”>              <!--UC强制全屏--><meta name=”x5-fullscreen” content=”true”>       <!--QQ强制全屏--><meta name=”browsermode” content=”application”>   <!--UC应用模式--><meta name=”x5-page-mode” content=”app”>   <!-- QQ应用模式--><meta name=”msapplication-tap-highlight” content=”no”>    <!--windows phone 点击无高亮设置页面不缓存--><meta http-equiv=”pragma” content=”no-cache”><meta http-equiv=”cache-control” content=”no-cache”><meta http-equiv=”expires” content=”0″>
复制代码

掌握页面的加载过程

网页加载流程


  • 当我们打开网址的时候,浏览器会从服务器中获取到 HTML 内容

  • 浏览器获取到 HTML 内容后,就开始从上到下解析 HTML 的元素

  • <head>元素内容会先被解析,此时浏览器还没开始渲染页面

  • 我们看到<head>元素里有用于描述页面元数据的<meta>元素,还有一些<link>元素涉及外部资源(如图片、CSS 样式等),此时浏览器会去获取这些外部资源。除此之外,我们还能看到<head>元素中还包含着不少的<script>元素,这些<script>元素通过src属性指向外部资源

  • 当浏览器解析到这里时(步骤 3),会暂停解析并下载 JavaScript 脚本

  • 当 JavaScript 脚本下载完成后,浏览器的控制权转交给 JavaScript 引擎。当脚本执行完成后,控制权会交回给渲染引擎,渲染引擎继续往下解析 HTML 页面

  • 此时<body>元素内容开始被解析,浏览器开始渲染页面


  • 在这个过程中,我们看到<head>中放置的<script>元素会阻塞页面的渲染过程:把 JavaScript 放在<head>里,意味着必须把所有 JavaScript 代码都下载、解析和解释完成后,才能开始渲染页面

  • 如果外部脚本加载时间很长(比如一直无法完成下载),就会造成网页长时间失去响应,浏览器就会呈现“假死”状态,用户体验会变得很糟糕

  • 因此,对于对性能要求较高、需要快速将内容呈现给用户的网页,常常会将 JavaScript 脚本放在<body>的最后面。这样可以避免资源阻塞,页面得以迅速展示。我们还可以使用defer/async/preload等属性来标记<script>标签,来控制 JavaScript 的加载顺序


延迟加载的方式有哪些


js 的加载、解析和执行会阻塞页面的渲染过程,因此我们希望 js 脚本能够尽可能的延迟加载,提高页面的渲染速度。


几种方式是:


  • 将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行

  • 给 js 脚本添加 defer 属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样

  • 给 js 脚本添加 async属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行

  • 动态创建 DOM 标签的方式,我们可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本



怎么判断页面是否加载完成


  • Load 事件触发代表页面中的 DOMCSSJS,图片已经全部加载完毕。

  • DOMContentLoaded 事件触发代表初始的 HTML 被完全加载和解析,不需要等待 CSSJS,图片加载


用户头像

loveX001

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
美团前端常见面试题整理_JavaScript_loveX001_InfoQ写作社区