前端面试知识整理(持续更新)

javascript/jquery

浏览数:241

2019-4-19

AD:资源代下载服务

文章旨在整理收集前端基础、计算机网络、浏览器、vue、react源码以及算法相关知识点。内容来自平时的阅读学习以及实践理解,由于内容量巨大,会进行持续的更新和补充。文章中涉及大量优秀博文的引用,大都在文末注明引用,如有遗漏或侵权,请联系作者处理。

版本 操作时间 操作说明
v_0.1.0 2019-4-16 20:57 初版
v_0.1.1 2019-4-17 13:20 正则表达式
v_0.1.2 2019-4-18 17:20 调整原型链相关表述
v_0.1.3 2019-4-18 21:11 跨域解决方案、设计模式、回流重绘

不得不说,思否的web端文章编辑功能真是做的不行,特别卡顿!用了一年多了还是一如既往的卡啊,本文首发于前端笔记–前端必备知识清单(持续更新)。希望及时优化啊

一、CSS

1.css盒模型

  • w3c盒模型:content + padding + border + margin。元素宽高(css)等于content的宽高。
  • IE盒模型:元素的宽高(css)等于content + padding + border的宽度。

2.BFC块级格式化上下文

  • 形成条件:根元素;position: absolute/fixed; float;overflow:not visible;display: inline-block / table。
  • 应用:清除浮动

3.居中布局

  • 水平居中
    行内元素:text-align: center;块级元素:margin:auto;
  • 垂直居中
    line-height; absolute; flex;
  • 水平垂直居中
    absolute;伪元素形成行内元素居中;flex + justify-content + align-items

4.清除浮动

设置父元素高度;形成bfc;伪元素clear: both;

5.flex常用api

引用:Flex 布局教程:语法篇
容器元素display: flex;
容器属性

flex-direction // 决定主轴的方向(即项目的排列方向)。
flex-wrap // 如果一条轴线排不下,如何换行。
flex-flow // 是flex-direction属性和flex-wrap属性的简写形式,默认值为row nowrap。
justify-content // 定义了项目在主轴上的对齐方式。
align-items // 属性定义项目在交叉轴上如何对齐。
align-content // 属性定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。

元素属性

order // 属性定义项目的排列顺序。数值越小,排列越靠前,默认为0。
flex-grow // 属性定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大。
flex-shrink // 属性定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小
flex-basis // 属性定义了在分配多余空间之前,项目占据的主轴空间(main size)
flex // flex-grow, flex-shrink 和 flex-basis的简写,默认值为0 1 auto
align-self // align-self属性允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items属性。

二、JavaScript

1.原型与继承

原型:prototype

函数的对象属性,包含一个constructor属性,指向函数自身:Fun.prototype.constructor === Fun。

原型链

原型链由原型对象构成,是js对象继承的机制。每个对像都有[[prototype]]属性,主流浏览器以__proto__暴露给用户。__proto__指向对象的构造函数的原型属性(prototype)。
prototype的__proto__又指向它prototype对象(注意,这个prototype是一个对象结构)的构造函数的原型;通过__proto__属性,形成一个引用链。在访问对象的属性时,如果对象本身不存在,就会沿着这条[[prototype]]链式结构一层层的访问。

构造函数

可以通过new来新建一个对象的函数。

原型与继承

函数拥有原型(prototype);而对象可以通过原型链关系([[prototype]])实现继承。函数是一种特殊对象,也拥有__proto__,即函数也会继承。函数的原型链是:函数静态方法—>原生函数类型的原型(拥有函数原生方法)—>原生对象原型(拥有对象原生方法) —> null
class A {}; class B extends A {};

类的继承

B.__proto__ = A.prototype; B.prototype.__proto__ = A.prototype;

new Fun操作符原理

  • Object.create(Fun.prototype);
  • 执行Fun,即类似类中的调用constructor()(为什么是执行constructor,因为constructor指向函数本身);
  • 如果constructor没有明确返回函数或对象,则返回第一步创建的对象。

2.函数执行相关

堆内存与栈内存

  1. 栈内存:由编译器自动分配与释放。我们可以直接操作栈内存中的值。js的基本类型存放在栈内存中,按值引用;
  2. 堆内存:链表结构的类型,可以动态分配大小,js引用类型占用内存空间的大小不固定,存储在堆内存中。由于JS不允许直接访问堆内存中的位置,因此我们不能直接操作js的引用类型。而是生成一个指针,并将它放到栈内存中,通过这个指针来操作引用类型。

垃圾回收

类型:标记引用(无法解决循环引用问题);标记清除(现在主流回收算法)。
标记清除算法的核型概念是:从根部(在JS中就是全局对象)出发定时扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用的。那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。

内存泄漏

对于不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。
常见内存泄漏:

  1. 不合理的计时器(引用过时的外部变量)
  2. 被共享的闭包作用域(函数内部的函数共享一个闭包环境)
  3. 脱离dom引用(dom中元素被清除,但是变量还保持这对dom元素的引用)
  4. 意外的全局变量(未声明或者指向全局的this.xxx)

事件循环

事件循环是指: 执行一个宏任务,然后执行清空微任务列表,循环再执行宏任务,再清微任务列表

  • 微任务 microtask(jobs): promise / ajax / Object.observe(该方法已废弃)
  • 宏任务 macrotask(task): setTimout / script / IO / UI Rendering

每次事件循环:

  1. 执行完主执行线程中的任务。
  2. 取出micro-task中任务执行直到清空。
  3. 取出macro-task中一个任务执行。
  4. 取出micro-task中任务执行直到清空。
  5. 重复3和4。

执行环境及作用域

执行环境

执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境斗鱼一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。
全局执行环境是最外围的执行环境,在web浏览器中,全局执行环境被认为是window对象,因此所有全局变量和函数都是作为window对象的属性和方法被创建的。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁。(全局执行环境直到应用程序推出时才会被销毁)。

作用域链

当代码在一个环境中执行时,会创建变量对象的一个作用域链,保证对执行环境有权访问的所有变量和函数的有序访问。
作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象作为变量对象,其在最开始的时候只包含一个变量:arguments。作用域链的下一个变量来自包含环境,而️再下一个变量对象则来自下一个包含环境一直延续到全局执行环境。作用域链在创建的时候就会被生成,保存在内部的[[scope]]属性当中。其本质是一个指向变量对象的指针列表,它指引用但不实际包含变量对象。

[[scope]]

函数创建过程中,会创建一个预先包含外部活动对象的作用域链,并始终包含全局环境的变量对象。这个作用域链被保存在内部的[[scope]]属性中,当函数执行时,会通过复制该属性中的对象构建起执行环境的作用域链。

执行上下文

定义

执行上下文是指当前Javascript代码被解析和执行时所在环境的抽象概念,JavaScript 任何代码的运行都是在执行上下文中。

类型

  1. 全局执行上下文
  2. 函数执行上下文
  3. eval函数执行上下文

如何工作

执行上下文分为两个过程:

  1. 创建阶段
  2. 执行阶段
创建阶段

主要做了三件事

  1. this绑定(这解释了为什么this的值取决于函数的调用方式)
  2. 创建词法环境。(创建活动对象,变量和函数声明的位置标记(声明提升过程);根据[[scope]]创建作用域链)
  3. 创建变量环境(变量环境也是词法法环境,用于存储var声明的变量。)
执行阶段

变量赋值,语句执行等。

闭包

闭包是指有权访问另一个函数作用域中的变量的函数。其本质是函数的作用域链中保存着外部函数变量对象的引用。可以通过[[scope]]属性查看。因此,即使外部函数被销毁,但是由于外部环境中定义的变量在闭包的scope中还保持着引用,所以这些变量暂时不会被垃圾回收机制回收,因此依然可以在闭包函数中正常访问。注意:同一个函数内部的闭包函数共享同一个外部闭包环境。从下图可以看出,即使返回的闭包里面没有引用c,d变量,但是由于内部函数closure1中引用链,所以即使closure1未调用,闭包作用域中依然保存这些变量的引用。

3.js引用方式

  • html 静态<script>引入
  • js 动态插入<script>
  • <script defer>: 延迟加载,元素解析完成后执行
  • <script async>: 异步加载,但执行时会阻塞元素渲染


其中蓝色代表js脚本网络加载时间,红色代表js脚本执行时间,绿色代表html解析。
更多分析见下文浏览器->页面的加载与渲染

4.对象的拷贝

浅拷贝

拷贝一层,内部如果是对象,则只是单纯的复制指针地址。

  1. Object.assign()
  2. { …obj }

深拷贝

  1. JSON.parse(JSON.stringify(source)),缺点层次太深会爆栈,无法解决循环引用问题。
  2. 参考深拷贝的终极探索(99%的人都不知道),采用循环代替递归实现深层次的拷贝。大致思想如下:
// 保持引用关系
function cloneForce(x) {
    // =============
    const uniqueList = []; // 用来去重
    // =============

    let root = {};

    // 循环数组
    const loopList = [
        {
            parent: root,
            key: undefined,
            data: x,
        }
    ];

    while(loopList.length) {
        // 深度优先
        const node = loopList.pop();
        const parent = node.parent;
        const key = node.key;
        const data = node.data;

        // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
        let res = parent;
        if (typeof key !== 'undefined') {
            res = parent[key] = {};
        }
        
        // =============
        // 数据已经存在
        let uniqueData = find(uniqueList, data);
        if (uniqueData) {
            parent[key] = uniqueData.target;
            break; // 中断本次循环
        }

        // 数据不存在
        // 保存源数据,在拷贝数据中对应的引用
        uniqueList.push({
            source: data,
            target: res,
        });
        // =============
    
        for(let k in data) {
            if (data.hasOwnProperty(k)) {
                if (typeof data[k] === 'object') {
                    // 下一次循环
                    loopList.push({
                        parent: res,
                        key: k,
                        data: data[k],
                    });
                } else {
                    res[k] = data[k];
                }
            }
        }
    }

    return root;
}

function find(arr, item) {
    for(let i = 0; i < arr.length; i++) {
        if (arr[i].source === item) {
            return arr[i];
        }
    }

    return null;
}

5.模块化

  • es6模块
  • commonJS/AMD模块

es6模块与CommonJS模块的差异

  1. CommonJS模块输出的是一个值的拷贝,ES6模块输出的是值的引用。
  2. CommonJS模块是运行时加载,ES6模块是编译时输出接口。

6.防抖和节流

防抖 (debounce)

将多次高频操作优化为只在最后一次执行,通常使用的场景是:用户输入,只需再输入完成后做一次输入校验即可

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

    return function(...args) {
        if (immediate && !timer) {
            fn.apply(this, args);
        }

        if (timer) clearTimeout(timer)
        timer = setTimeout(() => {
            fn.apply(this, args);
        }, wait);
    };
}

节流(throttle)

每隔一段时间后执行一次,也就是降低频率,将高频操作优化成低频操作,通常使用场景: 滚动条事件 或者 resize 事件,通常每隔 100~500 ms执行一次即可。

function throttle(fn, wait, immediate) {
    let timer = null;
    let callNow = immediate;

    return function(...args) {
        if (callNow) {
            fn.apply(this, args);
            callNow = false;
        }

        if (!timer) {
            timer = setTimeout(() => {
                fn.apply(this, args)
                timer = null;
            }, wait);
        }
    }
}

7.ES6/ES7

let/const

let和const可以形成块级作用域,并且不存在声明提升。并且在同一个块级作用域内不可重复声明。

解构赋值

比如 let [a, b, c] = [1, 2, 3]; let { foo } = { foo: 'bar' };

函数扩展

  1. 参数默认值
  2. rest参数
  3. 箭头函数

数组扩展

  1. Array.from() // 将类数组转化为数组
  2. Array.of() // 更语义明确的数组构建函数:用于将一组值,转换为数组。
  3. entries(),keys() 和 values()
  4. 实例方法includes
  5. flat(num?)// 数组扁平化
  6. 数组空位,es6明确将数组空位转化成undefined

对象扩展

  1. 属性简写
  2. 属性名表达式
  3. 可枚举性

    • for…in循环:只遍历对象自身的和继承的可枚举的属性。
    • Object.keys():返回对象自身的所有可枚举的属性的键名。
    • JSON.stringify():只串行化对象自身的可枚举的属性。
    • Object.assign(): 忽略enumerable为false的属性,只拷贝对象自身的可枚举
  4. Object.is()
  5. Object.assign()
  6. Object.keys(),Object.values(),Object.entries()

Symbol

let s = Symbol();

typeof s
// "symbol"

Symbol.for(), Symbol.keyFor()

Set,Map

Set

结构类似数组,成员唯一。

const s = new Set();

[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));

for (let i of s) {
  console.log(i);
}
// 2 3 5 4

操作方法:

  • add(value):添加某个值,返回 Set 结构本身。
  • delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • has(value):返回一个布尔值,表示该值是否为Set的成员。
  • clear():清除所有成员,没有返回值。

WeapSet

WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。
首先,WeakSet 的成员只能是对象,而不能是其他类型的值。

const ws = new WeakSet();
ws.add(1)
// TypeError: Invalid value used in weak set
ws.add(Symbol())
// TypeError: invalid value used in weak set

其次,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。

Map

类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。

const m = new Map();
const o = {p: 'Hello World'};

m.set(o, 'content')
m.get(o) // "content"

m.has(o) // true
m.delete(o) // true
m.has(o) // false

WeakMap

WeakMap与Map的区别有两点。

首先,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。

其次,WeakMap的键名所指向的对象,不计入垃圾回收机制。

Proxy

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写
ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。

var proxy = new Proxy(target, handler);

Promise

实现一个简单的Promise

const FULLFILLED = 'FULLFILLED';
const REJECTED = 'REJECTED';
const PENDING = 'PENDING';
const isFn = val => typeof val === 'function';

class Promise {
    constructor(handler) {
        this._state = PENDING;
        this._value = null;
        this.FULLFILLED_CBS = [];
        this.REJECTED_CBS = [];

        try {
            handler(this._resolve.bind(this), this._reject.bind(this));
        } catch (err) {
            this._reject(err);
        }
    }

    _resolve(value) {
        const async = () => {
            if (this._state !== PENDING) {
                return;
            }
            this._state = FULLFILLED;
            
            const fullfilLed = (val) => {
                this._value = val;
                this.FULLFILLED_CBS.forEach(cb => cb(val));
            };
            const rejected = (err) => {
                this._value = err;
                this.REJECTED_CBS.forEach(cb => cb(err));
            };

            if (value instanceof Promise) {
                value.then(fullfilLed, rejected);
            } else {
                fullfilLed(value);
            }
        }

        requestAnimationFrame(async);
    }

    _reject(err) {
        const async = () => {
            if (this._state !== PENDING) {
                return;
            }
            this._state = REJECTED;
            this._value = err;
            this.REJECTED_CBS.forEach(cb => cb(err));
        }
        requestAnimationFrame(async);
    }

    then(onFullfilled, onRejected) {
        return new Promise((resolve, reject) => {
            const handerResolve = (value) => {
                try {
                    if (!isFn(onFullfilled)) {
                        resolve(value);
                    }
                    const res = onFullfilled(value);
    
                    if (res instanceof Promise) {
                        res.then(resolve, reject);
                    } else {
                        resolve(res);
                    }
                } catch (err) {
                    reject(err);
                }
            };
            const handlerReject = (err) => {
                try {
                    if (!isFn(onRejected)) {
                        reject(err);
                    }
                    const res  = onRejected(err);
                    if (res instanceof Promise) {
                        res.then(resolve, reject);
                    } else {
                        reject(res);
                    }
                } catch (err) {
                    reject(err);
                }
            };
            switch(this._state) {
                case PENDING:
                    this.FULLFILLED_CBS.push(handerResolve);
                    this.REJECTED_CBS.push(handlerReject);
                    break;
                case FULLFILLED:
                    handerResolve(this._value);
                    break;
                case REJECTED:
                    handlerReject(this._value);
                    break;
                default: break;
            }
        });
    }

    catch(onRejected) {
        return this.then(undefined, onRejected);
    }

    finally(cb) {
        const P = this.constructor;
        return this.then(
            /** 使用then保证promise的流是正常的,因为promise的下一步总是建立在上一步执行完的基础上 */
            value  => P.resolve(cb()).then(() => value),
            reason => P.resolve(cb()).then(() => { throw reason })
        );
    }

    static all(list) {
        return new Promise(resolve, reject) {
            let count = 0;
            const values = [];
            const P = this.constructor;

            for (let [key, fn] of list) {
                P.resolve(fn).then(res => {
                    values[key] = res;
                    count++;
                    if (count === list.length) {
                        resolve(values);
                    }
                }, err => reject(err));
            }
        }
    }

    static race(list) {
        return new Promise((resolve, reject) => {
            const values = [];
            let count = 0;
            const P = this.constructor;
            for (let fn of list) {
                P.resolve(fn).then(res => {
                    resolve(res);
                }, err => {
                    reject(err);
                })
            }
        })
    }

    static resolve(value) {
        if (value instanceof Promise) {
            return value;
        }

        return new Promise(resolve => resolve(value));
    }

    static reject(value) {
        return new Promise((resolve, reject) => reject(value));
    }
}

async/await

async 函数是什么?一句话,它就是 Generator 函数的语法糖。

async函数返回一个promise对象。async函数内部return语句返回的值,会成为then方法回调函数的参数。

Class

es6的类语法是es5构造函数的语法糖,但是也有一些不同点

  1. class的原型属性不可枚举
  2. 不使用new操作符,class会报错
  3. class新增存取函数
  4. 不存在声明提升

Class的继承

抓住两点即可:假设A为父类,B为子类那么

B.__proto__ = A; // 子类的__proto__属性,表示构造函数的继承,总是指向父类。
B.prototype.__proto__ = A.prototype; // 子类prototype属性的__proto__属性,表示方法(实例)的继承,总是指向父类的prototype属性。

Module

  • es6模块
  • commonJS/AMD模块

es6模块与CommonJS模块的差异

  1. CommonJS模块输出的是一个值的拷贝,ES6模块输出的是值的引用。
  2. CommonJS模块是运行时加载,ES6模块是编译时输出接口。

8.函数柯里化

在一个函数中,首先填充几个参数,然后再返回一个新的函数的技术,称为函数的柯里化。通常可用于在不侵入函数的前提下,为函数 预置通用参数,供多次重复调用。

const add = function add(x) {
    return function (y) {
        return x + y
    }
}

const add1 = add(1)

add1(2) === 3
add1(20) === 21

9.正则表达式

匹配模式

  1. 两种模糊匹配:{m,n} [abc]
  2. 字符组: [a-z] [^a-z];常见简写模式:

    \d // 数字 digit
    \D // 非数字
    \w // 表示数字、大小写字母和下划线word
    \W // 非单词
    \s // [ \t\v\n\r\f] 空白符,space
    \S // 非空白符
    . // 通配符,表示几乎任意字符。换行符、回车符、行分隔符和段分隔符除外
  3. 量词 {m, } {m}, ?, +, *

    • 贪婪匹配:尽可能多的匹配

      var regex = /\d{2,5}/g;
      var string = "123 1234 12345 123456";
      console.log( string.match(regex) ); 
      // => ["123", "1234", "12345", "12345"]
    • 惰性匹配:量词后面加个问号,匹配最少能满足要求的字符串

      var regex = /\d{2,5}?/g;
      var string = "123 1234 12345 123456";
      console.log( string.match(regex) ); 
      // => ["12", "12", "34", "12", "34", "12", "34", "56"]
  4. 分支结构 /good|nice/

匹配位置

锚字符:

^ // 匹配开头 
$ // 匹配结尾
\b // 匹配单词边界
\B // 匹配非单词边界
(?=p) // 正向先行断言 其中p是一个子模式,即p前面的位置。
(?!p) // 负向先行断言 其中p是一个子模式,即不是p的前面的位置

实例分析:

数字的千位分隔符表示法:比如把”12345678″,变成”12,345,678″。

const reg = /\B(?=(\d{3})+\b)/g

这个正则的意思是匹配1到多组3个相连的数字前面的位置|123 // 123前面的|代表这个位置且这些数字的右侧是单词的边界,比如
123|`123|.456,左侧是非单词边界,即数字前还是单词比如9|123`。

正则的括号

  1. 分组和分支结构括号提供分组功能,如(ab)+;(JavaScript|Regular Expression)
  2. 捕获分组 单纯的括号在正则里面是捕获分组的意思
  3. 反向引用 使用+序号来引用当前正则里面的捕获项,比如\1引用第一个捕获

    var regex = /\d{4}(-|\/|\.)\d{2}\1\d{2}/;
    var string1 = "2017-06-12";
    var string2 = "2017/06/12";
    var string3 = "2017.06.12";
    var string4 = "2016-06/12";
    console.log( regex.test(string1) ); // true
    console.log( regex.test(string2) ); // true
    console.log( regex.test(string3) ); // true
    console.log( regex.test(string4) ); // false
  4. 非捕获数组 (?:p) 想用括号又不想产生匹配项的首选

正则回溯与性能

正则在涉及贪婪量词,惰性量词以及分支的时候往往会造成回溯,更多细节:JS正则表达式完整教程(略长)

10.设计模式

设计模式

3.浏览器

1.页面的加载与渲染

从输入 url 到展示的过程

  1. DNS查找相应的ip地址
    浏览器DNS缓存->电脑本地host文件查找->运营商DNS的解析
  2. 三次握手

    • 客户端发送 syn(同步序列编号) 请求,进入 syn_send 状态,等待确认
    • 服务端接收并确认 syn 包后发送 syn+ack 包,进入 syn_recv 状态
    • 客户端接收 syn+ack 包后,发送 ack 包,双方进入 established 状态
  3. 发送请求,分析 url,设置请求报文(头,主体)
  4. 服务器返回请求的文件 (html)
  5. 浏览器渲染

    • HTML parser –> DOM Tree
    • CSS parser –> Style Tree
    • attachment –> Render Tree
    • layout: 布局

浏览器加载与渲染

浏览器下载html文件并进行编译,转化成类似下面的结构


图片来源再谈 load 与 DOMContentLoaded
浏览器会对转化后的数据结构自上而下进行分析:首先开启下载线程,对所有的资源进行优先级排序下载(注意,这里仅仅是下载,区别于解析过程,解析过程可能被阻塞)。同时主线程会对文档进行解析

css文件的解析

  • header中的css的下载不会阻塞html的解析,当css文件下载完成,将阻塞html解析,优先解析css,会阻碍渲染。
  • body中的css下载和解析都不会阻碍html的解析,会阻碍渲染。(存在特殊情况,见下文)

js文件的解析

1. 普通的script:

遇到 script 标签时,首先阻塞后续内容的解析(但是不会阻塞下载,chrome对同一个域名支持6个http同时下载),当下载完成后,便执行js文件中的同步代码。然后接着解析html

2. defer和async

如果script标签设置了async或者defer标签,下载过程不会阻塞文档解析。defer执行将会放到html解析完成之后,dcl事件之前。async则是下载完就执行,并且阻塞解析。

body中的首个script/link

在 body 中第一个 script 资源下载完成之前,浏览器会进行首次渲染。将该 script 标签前面的 DOM 树和 CSSOM 合并成一棵 Render 树,渲染到页面中。这是页面从白屏到首次渲染的时间节点,比较关键。如果是第一个标签是link,表现有点奇怪,下载和解析会阻塞html的解析,并有可能进行首次渲染,也可能不渲染。

渲染过程大致为:计算样式
Recalculate style->布局:layout->更新布局树update layer tree -> 绘制paint

3.相关概念

dom构建

DOM 构建的意思是,将文档中的所有 DOM 元素构建成一个树型结构。

css构建

将文档中的所有 css 资源合并。生成css rule tree

render 树

将 DOM 树和 CSS 合并成一棵渲染树,render 树在合适的时机会被渲染到页面中。(比如遇到 script 时, 该 script 还没有下载到本地时)。

4.总结

如下图:

  1. 浏览器首先下载该地址所对应的 html 页面。(如果html很大,浏览器并不会等待html完全下载完,而是采用分段下载,分段解析)
  2. 浏览器解析 html 页面的 DOM 结构。
  3. 开启下载线程对文档中的所有资源按优先级排序下载。
  4. 主线程继续解析文档,到达 head 节点 ,head 里的外部资源无非是外链样式表和外链 js。

    • 发现有外链 css 或者外链 js,如果是外链 js ,则停止解析后续内容,等待该资源下载,下载完后立刻执行。如果是外链 css,继续解析后续内容。
  5. 解析到 body

在 body 中第一个 script 资源下载完成之前,浏览器会进行首次渲染。将该 script 标签前面的 DOM 树和 CSSOM 合并成一棵 Render 树,渲染到页面中。这是页面从白屏到首次渲染的时间节点,比较关键。如果是第一个标签是link,表现有点奇怪,下载和解析会阻塞html的解析,并有可能进行首次渲染,也可能不渲染。随后js的下载和执行依然会阻塞解析,但是css的下载和解析则不会阻塞html的解析。

  1. 文档解析完毕,页面重新渲染。当页面引用的所有 js 同步代码执行完毕,触发 DOMContentLoaded 事件。然后执行布局:layout->更新布局树update layer tree -> 绘制paint
  2. html 文档中的图片资源,js 代码中有异步加载的 css、js 、图片资源都加载完毕之后,load 事件触发(与DOMContentLoaded没有必然的先后顺序关系)。

2.跨域解决方案

1.jsonp跨域

由于html页面通过相应的标签从不同的域名下加载静态资源文件是被浏览器允许的,所以可以通过动态创建script标签的方式,来实现跨域通信,缺点是只支持get请求

let script = document.createElement('script');
script.src = 'http://www.example.com?args=agrs&callback=callback';
document.body.appendChild(script);
function callback(res) {  console.log(res);}

2.document.domain + iframe 跨域

3.window.name + iframe 跨域

4.location.hash + iframe 跨域

5.postMessage跨域

6.跨域资源共享 CORS

RS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing)。 它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。IE8+:IE8/9需要使用XDomainRequest对象来支持CORS。
Access-Control-Allow-Origin: http://www.example.com,通过Access-Control-Allow-Credentials: true控制是否允许发送cookie

7.WebSocket协议跨域

8.node代理跨域,服务器转发

9.ngnix代理跨域

正向代理

正向代理(forward)是一个位于客户端【用户A】和原始服务器(origin server)【服务器B】之间的服务器【代理服务器Z】,为了从原始服务器取得内容,用户 A 向代理服务器 Z 发送一个请求并指定目标(服务器B),然后代理服务器 Z 向服务器 B 转交请求并将获得的内容返回给客户端。客户端必须要进行一些特别的设置才能使用正向代理。

反向代理

对于客户端而言代理服务器就像是原始服务器,并且客户端不需要进行任何特别的设置。客户端向反向代理的命名空间(name-space)中的内容发送普通请求,接着反向代理将判断向何处(原始服务器)转交请求,并将获得的内容返回给客户端。

ngnix

Nginx 是一个高性能的HTTP和反向代理服务器,同时也是一个 IMAP/POP3/SMTP 代理服务器。

搭建静态站点

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;
    server_names_hash_max_size 512;
    server_names_hash_bucket_size 128;

    server {
        # 端口
        listen   8888;
        # 匹配请求中的host值
        server_name  localhost;
        
        # 监听请求路径
        location / {
            # 查找目录
            root D:/www/work/branch/test/;
            # 默认查找
            index index.html index.htm;
        }
        # 反向代理接口
        location /h5nc {
            # 代理服务器
            proxy_pass http://test.xxx.com;
        }
    }
}

反向代理解决跨域

 # 反向代理接口
location /h5nc {
    # 代理服务器
    proxy_pass http://test.xxx.com;
}

负载均衡

负载均衡是Nginx 比较常用的一个功能,可优化资源利用率,最大化吞吐量,减少延迟,确保容错配置,将流量分配到多个后端服务器。

这里举出常用的几种策略

  1. 轮询(默认),请求过来后,Nginx 随机分配流量到任一服务器

    upstream backend {
        server 127.0.0.1:3000;
        server 127.0.0.1:3001;
    }
  2. weight=number 设置服务器的权重,默认为1,权重大的会被优先分配

    upstream backend {
        server 127.0.0.1:3000 weight=2;
        server 127.0.0.1:3001 weight=1;
    }
  3. backup 标记为备份服务器。当主服务器不可用时,将传递与备份服务器的连接。

    upstream backend {
        server 127.0.0.1:3000 backup;
        server 127.0.0.1:3001;
    }
  4. ip_hash 保持会话,保证同一客户端始终访问一台服务器。

    upstream backend {
        ip_hash;  
        server 127.0.0.1:3000 backup;
        server 127.0.0.1:3001;
    }
  5. least_conn 优先分配最少连接数的服务器,避免服务器超载请求过多。

    upstream backend {
        least_conn;
        server 127.0.0.1:3000;
        server 127.0.0.1:3001;
    }

3.回流与重绘

回流

当元素的尺寸、结构或触发某些属性时,浏览器会重新渲染页面,称为回流。此时,浏览器需要重新经过计算,计算后还需要重新页面布局,因此是较重的操作。

从上图中可以看到回流过程中发生了
recalculate style ->
layout ->
update layer tree ->
paint->
composite layers;

重绘

当元素样式的改变不影响布局时,浏览器将使用重绘对元素进行更新,此时由于只需要UI层面的重新像素绘制,因此损耗较少。

从上图中可以看到重绘过程中发生了
recalculate style ->
update layer tree ->
paint->
composite layers;

从以上图中对比可以看出,重绘的过程比回流要少做一步操作,即layout

4.计算机网络基础

5.前端工程化

6.Vue源码相关

7.React源码相关

8.算法相关

引用参考

  1. 中高级前端大厂面试秘籍,为你保驾护航金三银四,直通大厂(上)
  2. JS事件循环
  3. Flex 布局教程:语法篇
  4. 前端笔记–JS执行上下文
  5. script标签中defer和async属性的区别
  6. [译]页面生命周期:DOMContentLoaded, load, beforeunload, unload解析
  7. 再谈 load 与 DOMContentLoaded
  8. 面试带你飞:这是一份全面的 计算机网络基础 总结攻略
  9. 深拷贝的终极探索(99%的人都不知道)
  10. JS正则表达式完整教程(略长)
  11. 正确面对跨域,别慌
  12. Nginx学习笔记(反向代理&搭建集群)
  13. 前端想要了解的Nginx
  14. 你真的了解回流和重绘吗