关于前端:2023最新前端面试总结

6次阅读

共计 17591 个字符,预计需要花费 44 分钟才能阅读完成。

介绍 plugin

插件零碎是 Webpack 胜利的一个关键性因素。在编译的整个生命周期中,Webpack 会触发许多事件钩子,Plugin 能够监听这些事件,依据需要在相应的工夫点对打包内容进行定向的批改。

一个最简略的 plugin 是这样的:

class Plugin{
      // 注册插件时,会调用 apply 办法
      // apply 办法接管 compiler 对象
      // 通过 compiler 上提供的 Api,能够对事件进行监听,执行相应的操作
      apply(compiler){
          // compilation 是监听每次编译循环
          // 每次文件变动,都会生成新的 compilation 对象并触发该事件
        compiler.plugin('compilation',function(compilation) {})
      }
}

注册插件:

// webpack.config.js
module.export = {
    plugins:[new Plugin(options),
    ]
}

事件流机制:

Webpack 就像工厂中的一条产品流水线。原材料通过 Loader 与 Plugin 的一道道解决,最初输入后果。

  • 通过链式调用,按程序串起一个个 Loader;
  • 通过事件流机制,让 Plugin 能够插入到整个生产过程中的每个步骤中;

Webpack 事件流编程范式的外围是根底类 Tapable,是一种 观察者模式 的实现事件的订阅与播送:

const {SyncHook} = require("tapable")

const hook = new SyncHook(['arg'])

// 订阅
hook.tap('event', (arg) => {
    // 'event-hook'
    console.log(arg)
})

// 播送
hook.call('event-hook')

Webpack 中两个最重要的类 CompilerCompilation 便是继承于 Tapable,也领有这样的事件流机制。

  • Compiler : 能够简略的了解为 Webpack 实例,它蕴含了以后 Webpack 中的所有配置信息,如 options,loaders, plugins 等信息,全局惟一,只在启动时实现初始化创立,随着生命周期逐个传递;
  • Compilation: 能够称为 编译实例。当监听到文件产生扭转时,Webpack 会创立一个新的 Compilation 对象,开始一次新的编译。它蕴含了以后的输出资源,输入资源,变动的文件等,同时通过它提供的 api,能够监听每次编译过程中触发的事件钩子;
  • 区别:

    • Compiler 全局惟一,且从启动生存到完结;
    • Compilation对应每次编译,每轮编译循环均会从新创立;
  • 罕用 Plugin:

    • UglifyJsPlugin: 压缩、混同代码;
    • CommonsChunkPlugin: 代码宰割;
    • ProvidePlugin: 主动加载模块;
    • html-webpack-plugin: 加载 html 文件,并引入 css / js 文件;
    • extract-text-webpack-plugin / mini-css-extract-plugin: 抽离款式,生成 css 文件;DefinePlugin: 定义全局变量;
    • optimize-css-assets-webpack-plugin: CSS 代码去重;
    • webpack-bundle-analyzer: 代码剖析;
    • compression-webpack-plugin: 应用 gzip 压缩 js 和 css;
    • happypack: 应用多过程,减速代码构建;
    • EnvironmentPlugin: 定义环境变量;
  • 调用插件 apply 函数传入 compiler 对象
  • 通过 compiler 对象监听事件

loader 和 plugin 有什么区别?

webapck 默认只能打包 JS 和 JOSN 模块,要打包其它模块,须要借助 loader,loader 就能够让模块中的内容转化成 webpack 或其它 laoder 能够辨认的内容。

  • loader就是模块转换化,或叫加载器。不同的文件,须要不同的 loader 来解决。
  • plugin是插件,能够参加到整个 webpack 打包的流程中,不同的插件,在适合的机会,能够做不同的事件。

webpack 中都有哪些插件,这些插件有什么作用?

  • html-webpack-plugin 主动创立一个 HTML 文件,并把打包好的 JS 插入到 HTML 文件中
  • clean-webpack-plugin 在每一次打包之前,删除整个输入文件夹下所有的内容
  • mini-css-extrcat-plugin 抽离 CSS 代码,放到一个独自的文件中
  • optimize-css-assets-plugin 压缩 css

用过 TypeScript 吗?它的作用是什么?

为 JS 增加类型反对,以及提供最新版的 ES 语法的反对,是的利于团队合作和排错,开发大型项目

Cookie 有哪些字段,作用别离是什么

Cookie 由以下字段组成:

  • Name:cookie 的名称
  • Value:cookie 的值,对于认证 cookie,value 值包含 web 服务器所提供的拜访令牌;
  • Size:cookie 的大小
  • Path:能够拜访此 cookie 的页面门路。比方 domain 是 abc.com,path 是 /test,那么只有/test 门路下的页面能够读取此 cookie。
  • Secure:指定是否应用 HTTPS 平安协定发送 Cookie。应用 HTTPS 平安协定,能够爱护 Cookie 在浏览器和 Web 服务器间的传输过程中不被窃取和篡改。该办法也可用于 Web 站点的身份甄别,即在 HTTPS 的连贯建设阶段,浏览器会查看 Web 网站的 SSL 证书的有效性。然而基于兼容性的起因(比方有些网站应用自签订的证书)在检测到 SSL 证书有效时,浏览器并不会立刻终止用户的连贯申请,而是显示平安危险信息,用户仍能够抉择持续拜访该站点。
  • Domain:能够拜访该 cookie 的域名,Cookie 机制并未遵循严格的同源策略,容许一个子域能够设置或获取其父域的 Cookie。当须要实现单点登录计划时,Cookie 的上述个性十分有用,然而也减少了 Cookie 受攻打的危险,比方攻击者能够借此动员会话定置攻打。因此,浏览器禁止在 Domain 属性中设置.org、.com 等通用顶级域名、以及在国家及地区顶级域下注册的二级域名,以减小攻打产生的范畴。
  • HTTP:该字段蕴含 HTTPOnly 属性,该属性用来设置 cookie 是否通过脚本来拜访,默认为空,即能够通过脚本拜访。在客户端是不能通过 js 代码去设置一个 httpOnly 类型的 cookie 的,这种类型的 cookie 只能通过服务端来设置。该属性用于避免客户端脚本通过document.cookie 属性拜访 Cookie,有助于爱护 Cookie 不被跨站脚本攻打窃取或篡改。然而,HTTPOnly 的利用仍存在局限性,一些浏览器能够阻止客户端脚本对 Cookie 的读操作,但容许写操作;此外大多数浏览器仍容许通过 XMLHTTP 对象读取 HTTP 响应中的 Set-Cookie 头。
  • Expires/Max-size:此 cookie 的超时工夫。若设置其值为一个工夫,那么当达到此工夫后,此 cookie 生效。不设置的话默认值是 Session,意思是 cookie 会和 session 一起生效。当浏览器敞开(不是浏览器标签页,而是整个浏览器) 后,此 cookie 生效。

总结: 服务器端能够应用 Set-Cookie 的响应头部来配置 cookie 信息。一条 cookie 包含了 5 个属性值 expires、domain、path、secure、HttpOnly。其中 expires 指定了 cookie 生效的工夫,domain 是域名、path 是门路,domain 和 path 一起限度了 cookie 可能被哪些 url 拜访。secure 规定了 cookie 只能在确保安全的状况下传输,HttpOnly 规定了这个 cookie 只能被服务器拜访,不能应用 js 脚本拜访。

JS 隐式转换,显示转换

个别非根底类型进行转换时会先调用 valueOf,如果 valueOf 无奈返回根本类型值,就会调用 toString

字符串和数字

  • “+” 操作符,如果有一个为字符串,那么都转化到字符串而后执行字符串拼接
  • “-” 操作符,转换为数字,相减 (-a, a * 1 a/1) 都能进行隐式强制类型转换
[] + {} 和 {} + []

布尔值到数字

  • 1 + true = 2
  • 1 + false = 1

转换为布尔值

  • for 中第二个
  • while
  • if
  • 三元表达式
  • ||(逻辑或)&&(逻辑与)右边的操作数

符号

  • 不能被转换为数字
  • 能被转换为布尔值(都是 true)
  • 能够被转换成字符串 “Symbol(cool)”

宽松相等和严格相等

宽松相等容许进行强制类型转换,而严格相等不容许

字符串与数字

转换为数字而后比拟

其余类型与布尔类型

  • 先把布尔类型转换为数字,而后持续进行比拟

对象与非对象

  • 执行对象的 ToPrimitive(对象)而后持续进行比拟

假值列表

  • undefined
  • null
  • false
  • +0, -0, NaN
  • “”

数组扁平化

ES5 递归写法 —— isArray()、concat()

function flat11(arr) {var res = [];
    for (var i = 0; i < arr.length; i++) {if (Array.isArray(arr[i])) {res = res.concat(flat11(arr[i]));
        } else {res.push(arr[i]);
        }
    }
    return res;
}

如果想实现第二个参数(指定“拉平”的层数),能够这样实现,前面的几种能够本人相似实现:

function flat(arr, level = 1) {var res = [];
    for(var i = 0; i < arr.length; i++) {if(Array.isArray(arr[i]) || level >= 1) {res = res.concat(flat(arr[i]), level - 1);
        }
        else {res.push(arr[i]);
        }
    }
    return res;
}

ES6 递归写法 — reduce()、concat()、isArray()

function flat(arr) {
    return arr.reduce((pre, cur) => pre.concat(Array.isArray(cur) ? flat(cur) : cur), []);
}

ES6 迭代写法 — 扩大运算符(…)、some()、concat()、isArray()

ES6 的扩大运算符(…) 只能扁平化一层

function flat(arr) {return [].concat(...arr);
}

全副扁平化 :遍历原数组,若arr 中含有数组则应用一次扩大运算符,直至没有为止。

function flat(arr) {while(arr.some(item => Array.isArray(item))) {arr = [].concat(...arr);
    }
    return arr;
}

toString/join & split

调用数组的 toString()/join() 办法(它会主动扁平化解决),将数组变为字符串而后再用 split 宰割还原为数组。因为 split 宰割后造成的数组的每一项值为字符串,所以须要用一个 map 办法遍历数组将其每一项转换为数值型。

function flat(arr){return arr.toString().split(',').map(item => Number(item));
    // return arr.join().split(',').map(item => Number(item));
}

应用正则

JSON.stringify(arr).replace(/[|]/g, '') 会先将数组 arr 序列化为字符串,而后应用 replace() 办法将字符串中所有的[] 替换成空字符,从而达到扁平化解决,此时的后果为 arr 不蕴含 [] 的字符串。最初通过JSON.parse() 解析字符串。

function flat(arr) {return JSON.parse("[" + JSON.stringify(arr).replace(/\[|\]/g,'') +"]");
}

类数组转化为数组

类数组是具备 length 属性,但不具备数组原型上的办法。常见的类数组有 arguments、DOM 操作方法返回的后果 (如document.querySelectorAll('div')) 等。

扩大运算符(…)

留神:扩大运算符只能作用于 iterable 对象,即领有 Symbol(Symbol.iterator) 属性值。

let arr = [...arrayLike]

Array.from()

let arr = Array.from(arrayLike);

Array.prototype.slice.call()

let arr = Array.prototype.slice.call(arrayLike);

Array.apply()

let arr = Array.apply(null, arrayLike);

concat + apply

let arr = Array.prototype.concat.apply([], arrayLike);

代码输入后果

function runAsync (x) {const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
  return p
}
Promise.race([runAsync(1), runAsync(2), runAsync(3)])
  .then(res => console.log('result:', res))
  .catch(err => console.log(err))

输入后果如下:

1
'result:' 1
2
3

then 只会捕捉第一个胜利的办法,其余的函数尽管还会继续执行,然而不是被 then 捕捉了。

参考 前端进阶面试题具体解答

实现节流函数和防抖函数

函数防抖的实现:

function debounce(fn, wait) {
  var timer = null;

  return function() {
    var context = this,
      args = [...arguments];

    // 如果此时存在定时器的话,则勾销之前的定时器从新记时
    if (timer) {clearTimeout(timer);
      timer = null;
    }

    // 设置定时器,使事件间隔指定事件后执行
    timer = setTimeout(() => {fn.apply(context, args);
    }, wait);
  };
}

函数节流的实现:

// 工夫戳版
function throttle(fn, delay) {var preTime = Date.now();

  return function() {
    var context = this,
      args = [...arguments],
      nowTime = Date.now();

    // 如果两次工夫距离超过了指定工夫,则执行函数。if (nowTime - preTime >= delay) {preTime = Date.now();
      return fn.apply(context, args);
    }
  };
}

// 定时器版
function throttle (fun, wait){
  let timeout = null
  return function(){
    let context = this
    let args = [...arguments]
    if(!timeout){timeout = setTimeout(() => {fun.apply(context, args)
        timeout = null 
      }, wait)
    }
  }
}

如何判断元素是否达到可视区域

以图片显示为例:

  • window.innerHeight 是浏览器可视区的高度;
  • document.body.scrollTop || document.documentElement.scrollTop 是浏览器滚动的过的间隔;
  • imgs.offsetTop 是元素顶部间隔文档顶部的高度(包含滚动条的间隔);
  • 内容达到显示区域的:img.offsetTop < window.innerHeight + document.body.scrollTop;

map 和 weakMap 的区别

(1)Map map 实质上就是键值对的汇合,然而一般的 Object 中的键值对中的键只能是字符串。而 ES6 提供的 Map 数据结构相似于对象,然而它的键不限度范畴,能够是任意类型,是一种更加欠缺的 Hash 构造。如果 Map 的键是一个原始数据类型,只有两个键严格雷同,就视为是同一个键。

实际上 Map 是一个数组,它的每一个数据也都是一个数组,其模式如下:

const map = [["name","张三"],
     ["age",18],
]

Map 数据结构有以下操作方法:

  • sizemap.size 返回 Map 构造的成员总数。
  • set(key,value):设置键名 key 对应的键值 value,而后返回整个 Map 构造,如果 key 曾经有值,则键值会被更新,否则就新生成该键。(因为返回的是以后 Map 对象,所以能够链式调用)
  • get(key):该办法读取 key 对应的键值,如果找不到 key,返回 undefined。
  • has(key):该办法返回一个布尔值,示意某个键是否在以后 Map 对象中。
  • delete(key):该办法删除某个键,返回 true,如果删除失败,返回 false。
  • clear():map.clear()革除所有成员,没有返回值。

Map 构造原生提供是三个遍历器生成函数和一个遍历办法

  • keys():返回键名的遍历器。
  • values():返回键值的遍历器。
  • entries():返回所有成员的遍历器。
  • forEach():遍历 Map 的所有成员。
const map = new Map([["foo",1],
     ["bar",2],
])
for(let key of map.keys()){console.log(key);  // foo bar
}
for(let value of map.values()){console.log(value); // 1 2
}
for(let items of map.entries()){console.log(items);  // ["foo",1]  ["bar",2]
}
map.forEach((value,key,map) => {console.log(key,value); // foo 1    bar 2
})

(2)WeakMap WeakMap 对象也是一组键值对的汇合,其中的键是弱援用的。其键必须是对象,原始数据类型不能作为 key 值,而值能够是任意的。

该对象也有以下几种办法:

  • set(key,value):设置键名 key 对应的键值 value,而后返回整个 Map 构造,如果 key 曾经有值,则键值会被更新,否则就新生成该键。(因为返回的是以后 Map 对象,所以能够链式调用)
  • get(key):该办法读取 key 对应的键值,如果找不到 key,返回 undefined。
  • has(key):该办法返回一个布尔值,示意某个键是否在以后 Map 对象中。
  • delete(key):该办法删除某个键,返回 true,如果删除失败,返回 false。

其 clear()办法曾经被弃用,所以能够通过创立一个空的 WeakMap 并替换原对象来实现革除。

WeakMap 的设计目标在于,有时想在某个对象下面寄存一些数据,然而这会造成对于这个对象的援用。一旦不再须要这两个对象,就必须手动删除这个援用,否则垃圾回收机制就不会开释对象占用的内存。

而 WeakMap 的 键名所援用的对象都是弱援用 ,即垃圾回收机制不将该援用思考在内。因而,只有所援用的对象的其余援用都被革除,垃圾回收机制就会开释该对象所占用的内存。也就是说,一旦不再须要,WeakMap 外面的 键名对象和所对应的键值对会主动隐没,不必手动删除援用

总结:

  • Map 数据结构。它相似于对象,也是键值对的汇合,然而“键”的范畴不限于字符串,各种类型的值(包含对象)都能够当作键。
  • WeakMap 构造与 Map 构造相似,也是用于生成键值对的汇合。然而 WeakMap 只承受对象作为键名(null 除外),不承受其余类型的值作为键名。而且 WeakMap 的键名所指向的对象,不计入垃圾回收机制。

代码输入问题

function Parent() {
    this.a = 1;
    this.b = [1, 2, this.a];
    this.c = {demo: 5};
    this.show = function () {console.log(this.a , this.b , this.c.demo);
    }
}

function Child() {
    this.a = 2;
    this.change = function () {this.b.push(this.a);
        this.a = this.b.length;
        this.c.demo = this.a++;
    }
}

Child.prototype = new Parent();
var parent = new Parent();
var child1 = new Child();
var child2 = new Child();
child1.a = 11;
child2.a = 12;
parent.show();
child1.show();
child2.show();
child1.change();
child2.change();
parent.show();
child1.show();
child2.show();

输入后果:

parent.show(); // 1  [1,2,1] 5

child1.show(); // 11 [1,2,1] 5
child2.show(); // 12 [1,2,1] 5

parent.show(); // 1 [1,2,1] 5

child1.show(); // 5 [1,2,1,11,12] 5

child2.show(); // 6 [1,2,1,11,12] 5

这道题目值得神帝,他波及到的知识点很多,例如 this 的指向、原型、原型链、类的继承、数据类型 等。

解析:

  1. parent.show(),能够间接取得所需的值,没啥好说的;
  2. child1.show(),Child的构造函数本来是指向 Child 的,题目显式将 Child 类的原型对象指向了 Parent 类的一个实例,须要留神 Child.prototype 指向的是 Parent 的实例 parent,而不是指向Parent 这个类。
  3. child2.show(),这个也没啥好说的;
  4. parent.show(),parent是一个 Parent 类的实例,Child.prorotype指向的是 Parent 类的另一个实例,两者在堆内存中互不影响,所以上述操作不影响 parent 实例,所以输入后果不变;
  5. child1.show(),child1执行了 change() 办法后,产生了怎么的变动呢?
  6. this.b.push(this.a),因为 this 的动静指向个性,this.b 会指向 Child.prototype 上的 b 数组,this.a 会指向 child1a属性, 所以 Child.prototype.b 变成了[1,2,1,11];
  7. this.a = this.b.length,这条语句中 this.athis.b的指向与上一句统一,故后果为 child1.a 变为4;
  8. this.c.demo = this.a++,因为 child1 本身属性并没有 c 这个属性,所以此处的 this.c 会指向 Child.prototype.cthis.a 值为 4,为原始类型,故赋值操作时会间接赋值,Child.prototype.c.demo 的后果为 4,而this.a 随后自增为5(4 + 1 = 5)。
  9. child2执行了 change() 办法, 而 child2child1均是 Child 类的实例,所以他们的原型链指向同一个原型对象 Child.prototype, 也就是同一个parent 实例,所以 child2.change() 中所有影响到原型对象的语句都会影响 child1 的最终输入后果。
  10. this.b.push(this.a),因为 this 的动静指向个性,this.b 会指向 Child.prototype 上的 b 数组,this.a 会指向 child2a属性, 所以 Child.prototype.b 变成了[1,2,1,11,12];
  11. this.a = this.b.length,这条语句中 this.athis.b的指向与上一句统一,故后果为 child2.a 变为5;
  12. this.c.demo = this.a++,因为 child2 本身属性并没有 c 这个属性,所以此处的 this.c 会指向 Child.prototype.c,故执行后果为Child.prototype.c.demo 的值变为 child2.a 的值 5,而child2.a 最终自增为6(5 + 1 = 6)。

Promise.all 和 Promise.race 的区别的应用场景

(1)Promise.all Promise.all能够将多个 Promise 实例包装成一个新的 Promise 实例。同时,胜利和失败的返回值是不同的,胜利的时候返回的是 一个后果数组 ,而失败的时候则返回 最先被 reject 失败状态的值

Promise.all 中传入的是数组,返回的也是是数组,并且会将进行映射,传入的 promise 对象返回的值是依照程序在数组中排列的,然而留神的是他们执行的程序并不是依照程序的,除非可迭代对象为空。

须要留神,Promise.all 取得的胜利后果的数组外面的数据程序和 Promise.all 接管到的数组程序是统一的,这样当遇到发送多个申请并依据申请程序获取和应用数据的场景,就能够应用 Promise.all 来解决。

(2)Promise.race

顾名思义,Promse.race 就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])外面哪个后果取得的快,就返回那个后果,不论后果自身是胜利状态还是失败状态。当要做一件事,超过多长时间就不做了,能够用这个办法来解决:

Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})

GET 和 POST 的申请的区别

Post 和 Get 是 HTTP 申请的两种办法,其区别如下:

  • 利用场景: GET 申请是一个幂等的申请,个别 Get 申请用于对服务器资源不会产生影响的场景,比如说申请一个网页的资源。而 Post 不是一个幂等的申请,个别用于对服务器资源会产生影响的情景,比方注册用户这一类的操作。
  • 是否缓存: 因为两者利用场景不同,浏览器个别会对 Get 申请缓存,但很少对 Post 申请缓存。
  • 发送的报文格式: Get 申请的报文中实体局部为空,Post 申请的报文中实体局部个别为向服务器发送的数据。
  • 安全性: Get 申请能够将申请的参数放入 url 中向服务器发送,这样的做法绝对于 Post 申请来说是不太平安的,因为申请的 url 会被保留在历史记录中。
  • 申请长度: 浏览器因为对 url 长度的限度,所以会影响 get 申请发送数据时的长度。这个限度是浏览器规定的,并不是 RFC 规定的。
  • 参数类型: post 的参数传递反对更多的数据类型。

Vue 通信

1.props 和 $emit
2. 地方事件总线 EventBus(根本不必)
3.vuex(官网举荐状态管理器)
4.$parent 和 $children
当然还有一些其余方法,但根本不罕用,或者用起来太简单来。介绍来通信的形式,还能够扩大说一下应用
场景,如何应用,注意事项之类的。

HTTP2 的头部压缩算法是怎么的?

HTTP2 的头部压缩是 HPACK 算法。在客户端和服务器两端建设“字典”,用索引号示意反复的字符串,采纳哈夫曼编码来压缩整数和字符串,能够达到 50%~90% 的高压缩率。

具体来说:

  • 在客户端和服务器端应用“首部表”来跟踪和存储之前发送的键值对,对于雷同的数据,不再通过每次申请和响应发送;
  • 首部表在 HTTP/ 2 的连贯存续期内始终存在,由客户端和服务器独特渐进地更新;
  • 每个新的首部键值对要么被追加到以后表的开端,要么替换表中之前的值。

例如下图中的两个申请,申请一发送了所有的头部字段,第二个申请则只须要发送差别数据,这样能够缩小冗余数据,升高开销。

渲染机制

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标签,浏览器会主动将该节点变为图层。

说一说什么是跨域,怎么解决

因为浏览器出于平安思考,有同源策略。也就是说,如果协定、域名或者端口有一个不同就是跨域,Ajax 申请会失败。为来避免 CSRF 攻打
1.JSONP
    JSONP 的原理很简略,就是利用 <script> 标签没有跨域限度的破绽。通过 <script> 标签指向一个须要拜访的地址并提供一个回调函数来接收数据当须要通信时。<script src="http://domain/api?param1=a&param2=b&callback=jsonp"></script>
    <script>
        function jsonp(data) {console.log(data)        }    </script>
    JSONP 应用简略且兼容性不错,然而只限于 get 申请。2.CORS
    CORS 须要浏览器和后端同时反对。IE 8 和 9 须要通过 XDomainRequest 来实现。3.document.domain
    该形式只能用于二级域名雷同的状况下,比方 a.test.com 和 b.test.com 实用于该形式。只须要给页面增加 document.domain = 'test.com' 示意二级域名都雷同就能够实现跨域
4.webpack 配置 proxyTable 设置开发环境跨域
5.nginx 代理跨域
6.iframe 跨域
7.postMessage
    这种形式通常用于获取嵌入页面中的第三方页面数据。一个页面发送音讯,另一个页面判断起源并接管音讯

let、const、var 的区别

(1)块级作用域: 块作用域由 {}包含,let 和 const 具备块级作用域,var 不存在块级作用域。块级作用域解决了 ES5 中的两个问题:

  • 内层变量可能笼罩外层变量
  • 用来计数的循环变量泄露为全局变量

(2)变量晋升: var 存在变量晋升,let 和 const 不存在变量晋升,即在变量只能在申明之后应用,否在会报错。

(3)给全局增加属性: 浏览器的全局对象是 window,Node 的全局对象是 global。var 申明的变量为全局变量,并且会将该变量增加为全局对象的属性,然而 let 和 const 不会。

(4)反复申明: var 申明变量时,能够反复申明变量,后申明的同名变量会笼罩之前申明的遍历。const 和 let 不容许反复申明变量。

(5)暂时性死区: 在应用 let、const 命令申明变量之前,该变量都是不可用的。这在语法上,称为 暂时性死区。应用 var 申明的变量不存在暂时性死区。

(6)初始值设置: 在变量申明时,var 和 let 能够不必设置初始值。而 const 申明变量必须设置初始值。

(7)指针指向: let 和 const 都是 ES6 新增的用于创立变量的语法。let 创立的变量是能够更改指针指向(能够从新赋值)。但 const 申明的变量是不容许扭转指针的指向。

区别 var let const
是否有块级作用域 × ✔️ ✔️
是否存在变量晋升 ✔️ × ×
是否增加全局属性 ✔️ × ×
是否反复申明变量 ✔️ × ×
是否存在暂时性死区 × ✔️ ✔️
是否必须设置初始值 × × ✔️
是否扭转指针指向 ✔️ ✔️ ×

BFC

块级格式化上下文,是一个独立的渲染区域,让处于 BFC 外部的元素与内部的元素互相隔离,使内外元素的定位不会相互影响。

IE 下为 Layout,可通过 zoom:1 触发

触发条件:

  • 根元素
  • position: absolute/fixed
  • display: inline-block / table
  • float 元素
  • ovevflow !== visible

规定:

  • 属于同一个 BFC 的两个相邻 Box 垂直排列
  • 属于同一个 BFC 的两个相邻 Boxmargin 会产生重叠
  • BFC 中子元素的 margin box 的右边,与蕴含块 (BFC) border box的右边相接触 (子元素 absolute 除外)
  • BFC 的区域不会与 float 的元素区域重叠
  • 计算 BFC 的高度时,浮动子元素也参加计算
  • 文字层不会被浮动层笼罩,盘绕于四周

利用:

  • 阻止 margin 重叠
  • 能够蕴含浮动元素 —— 革除外部浮动 (革除浮动的原理是两个div 都位于同一个 BFC 区域之中)
  • 自适应两栏布局
  • 能够阻止元素被浮动元素笼罩

高低垂直居中计划

  • 定高:marginposition + margin(负值)
  • 不定高:position + transformflexIFC + vertical-align:middle
/* 定高计划 1 */
.center {
  height: 100px;
  margin: 50px 0;   
}
/* 定高计划 2 */
.center {
  height: 100px;
  position: absolute;
  top: 50%;
  margin-top: -25px;
}
/* 不定高计划 1 */
.center {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
}
/* 不定高计划 2 */
.wrap {
  display: flex;
  align-items: center;
}
.center {width: 100%;}
/* 不定高计划 3 */
/* 设置 inline-block 则会在外层产生 IFC,高度设为 100% 撑开 wrap 的高度 */
.wrap::before {
  content: '';
  height: 100%;
  display: inline-block;
  vertical-align: middle;
}
.wrap {text-align: center;}
.center {
  display: inline-block;  
  vertical-align: middle;
}

二分查找 – 工夫复杂度 log2(n)

题目形容: 如何确定一个数在一个有序数组中的地位

实现代码如下:

function search(arr, target, start, end) {
  let targetIndex = -1;

  let mid = Math.floor((start + end) / 2);

  if (arr[mid] === target) {
    targetIndex = mid;
    return targetIndex;
  }

  if (start >= end) {return targetIndex;}

  if (arr[mid] < target) {return search(arr, target, mid + 1, end);
  } else {return search(arr, target, start, mid - 1);
  }
}
// const dataArr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
// const position = search(dataArr, 6, 0, dataArr.length - 1);
// if (position !== -1) {//   console.log(` 指标元素在数组中的地位:${position}`);
// } else {//   console.log("指标元素不在数组中");
// }
正文完
 0