Fork me on GitHub
image frame

Walter

面朝大海 春暖花开

手写Event.EventEmitter

手写Event.EventEmitter

一个EventEmitter需要实现以下方法:

  • addEventListener
  • removeEventlistener
  • removeAllEventListener
  • emit
  • once

EventEmitter

function EventEmitter(){
    this.events = new Map()
}
//工具函数
const wrapCallBack = (fn,once = false) => ({ callback:fn,once })

addEventListener

EventEmitter.prototype.addEventListener = function(type,fn,once = false){
    //从events获取事件类型为type的事件回调
    let handler = this.events.get(type);
    // console.log(handler)
    if(!handler){
        //如果不存在 则添加
        this.events.set(type,wrapCallBack(fn,once))
    }else if(handler && typeof handler.callback === 'function'){
        //如果只绑定一个回调
        this.events.set(type,[handler,wrapCallBack(fn,once)])
    }else {
        //绑定类多个回调
        this.events.set(type,[...handler,wrapCallBack(fn,once)])
    }
}

removeEventListener

EventEmitter.prototype.removeListener = function(type,listener){
    let handler = this.events.get(type);
    if(!handler) return;
    if(!Array.isArray(handler)){
      //简单比较回调函数是否相同
        if(String(handler.callback) === String(listener)){
            this.events.delete(type)
        }else {
            return
        }
    }else{
      //循环回调函数队列
        for (let i = 0; i < handler.length; i++) {
            const item = handler[i];
            //简单比较回调函数是否相同
            if(String(item.callback) === String(listener)){
                handler.splice(i,1);
                //避免回调函数数组变化 导致数组访问错误
                i--;
            }
        }
        //如果数组为空 删除监听的事件即可
        if(handler.length === 0){
            this.events.delete(type)
        }else if(handler.length === 1){
              //如果数组只有一个元素 只需要以对象的形式保存即可
            this.events.set(type, handler[0]);
        }
    }
}

removeAllEventListener

EventEmitter.prototype.removeAllListener = function(type){
    let handler = this.events.get(type);
    if(!handler) return 
    this.events.delete(type)
}

emit

EventEmitter.prototype.emit = function(type,...args){
    let handler = this.events.get(type),
        eventsArray = [];
      //如果是回调队列为数组的形式 循环遍历
    if(Array.isArray(handler)){
        //简单实现 避免后续数组长度出现变化 对数组的访问出错
        //优化:可以考虑写一个工具函数 深拷贝
        for (let i = 0; i < handler.length; i++) {
            eventsArray.push(handler[i])
        }
          //遍历type对应的回调队列 触发每一个cb
        for (let i = 0; i < eventsArray.length; i++) {
            const item = eventsArray[i];
            //执行回调
            item.callback.apply(this,args);
            if(item.once){
                  //如果回调函数只执行一次 则删除该回调函数
                this.removeListener(type,item.callback)
            }
        }
    }else {
          //否则直接执行即可
        handler.callback.apply(this,args)
    }
    return true
}

once

EventEmitter.prototype.once = function(type,fn){
    this.addEventListener(type,fn,true)
}

深拷贝 VS 浅拷贝

深拷贝 VS 浅拷贝

深拷贝:拷贝实例。浅拷贝:拷贝引用。

不知道大家还记不记得高程上对数据类型的定义:
基本数据类型:
布尔类型、字符串类型、数字类型。这些类型属于基本数据类型,它们的值通常都是直接存放在栈中,所以平时我们在给给变量赋值的时候,就是直接将值直接复制给变量,新变量的更改不会影响原变量。
引用类型:
对于对象(包含object、array、date、function等等),这些对象的值都存放在堆中,值的引用存放在栈中。

举个🌰:

let a = 10;
let b = a;//10
b = 20;
console.log(a,b);//10,20

let obj = { a:10,b:20 }
let obj2 = obj1;
obj2.a = 100;
console.log(obj.a)//100

image.png
如图,深拷贝就是在内存中重新开辟一个新的内存,将一个对象从内存中完整的读出来,存放到新的内存中,两者互不影响。

话不多说,先撸为敬。

乞丐版

在业务场景中,我们使用的最多的就是下面的方式,但是这样的方式存在着潜在的风险,对于一下循环嵌套的对象、数组等,无法进行有效的复制。具体请看这一篇文章

JSON.parse(JSON.stringify())

基础版本(一)

function deeClone(obj){
    let res = {}
    for (const key in obj) {
      const element = obj[key];
      res[key] = element
    }
    return res
}

let obj = {
    a:10,
    b:[1,3,4,{ c:20 }],
    d:true
}

console.log(deeClone(obj));//{ a: 10, b: [ 1, 3, 4, { c: 20 } ], d: true }

基础版本(二)

创建一个新对象,遍历目标对象,将原对象上的属性和属性值一次挂载到新对象上。
深拷贝,由于我们不知道原对象会嵌套多少层,所以我们需要使用递归来解决。
如果属性值为基本类型,直接将属性和属性值挂载到新对象上。
如果属性值为对象类型,则递归遍历该属性的属性值,然后挂载到新对象上。

function deepClone(target){
    if(typeof target === 'object'){
        let res = Array.isArray(target) ? [] : {};
        for (const key in target) {
          const element = target[key];
          if(typeof element === 'object'){
            res[key] = deepClone(element)
          }else {
            res[key] = element;
          }
        }
        return res;
    }else {
        return target
    }
}

let obj = {
    a: 1,
    b: {
        c: {
            d: function() {
                console.log(1)
            }
        }
    },
    f:[1,2,3,{ g:function(){} }]
};
let obj1 = deepClone(obj);
obj1.a = 2;
console.log(obj);
console.log(obj1)

基础版本(三)

兼容循环引用。

我们借用上面的函数,去对一个有循环引用的对象进行深拷贝:

let obj = {
    a: 1,
    b: {
        c: {
            d: function() {
                console.log(1)
            }
        }
    },
    f:[1,2,3,{ g:function(){} }]
};
obj.obj = obj;
let obj1 = deepClone(obj);
obj1.a = 2;
console.log(obj);
console.log(obj1)

image.png

如上所示,如果存在对象直接或者间接引用自身的情况,就会出现内存溢出的情况。

为了解决这种情况,我们需要在内存中开辟一个新的空间,来存储当前对象和拷贝对象的关系,当拷贝当前对象的时候,现在存储空间中查找,如果存在,直接返回存储中的值,否则,继续拷贝。

function deepClone3(target, map = new WeakMap()) {
    if (typeof target === 'object') {
        let res = Array.isArray(target) ? [] : {};
        if(map.get(target)){
            return map.get(target)
        }
        map.set(target,res);
        for (const key in target) {
          const element = target[key];
          res[key] = deepClone3(element,map);
        }
        return res;
    } else {
        return target
    }
}

let obj = {
    a: 1,
    b: {
        c: {
            b:100,
            d: function() {
                console.log(1)
            }
        }
    },
    f:[1,2,3,{ g:function(){} }]
}

obj.obj = obj;
let obj1 = deepClone3(obj)
console.log(obj1)

再次执行拷贝:我们可以看到obj.obj值为 Circular,即循环引用的意思。

image.png

Map

JavaScript的对象本质上只能是键值对的集合,且键只能为字符串。为了解决这个问题,在ES6中提出了一种新的数据结构:Map。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。
Map的API:

  1. size()
  2. set(key,value)
  3. get(key)
  4. has(key)
  5. delete(key)
  6. clear()

Map的遍历方法:

  • Map.prototype.keys():返回键名的遍历器。
  • Map.prototype.values():返回键值的遍历器。
  • Map.prototype.entries():返回所有成员的遍历器。
  • Map.prototype.forEach():遍历 Map 的所有成员。

WeakMap

WeakMap是Map的一种变形。但是WeakMap只接受对象作为键(除null外),且WeakMap的键对应的对象,不会被计入垃圾回收机制。
使用WeakMap的优势:它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。

总之,WeakMap的专用场合就是,它的键所对应的对象,可能会在将来消失。WeakMap结构有助于防止内存泄漏。
WeakMap的API:

  1. get(key)
  2. set(key,value)
  3. has(key)
  4. delete(key)

兼容其他类型

每一个对象都有对应的toString()方法,如果该方法未被重写,则调用的就是Object.prototype上的toString(),返回的就是’[object type]’,type就是该对象对应的数据类型。
所以,为了防止toString()方法被重写,我们直接调用Object.prototype上的方法,获取数据类型:

//判断类型
function getType(target) {
    return Object.prototype.toString.call(target).slice(8, -1);
}

前面提到过,对应可引用的数据类型,我们要递归遍历;对应不可引用的数据类型,直接复制即可:

//判断是否是可引用的数据类型 
function isRefrenceType(target) {
    let type = typeof target;
    return (target !== null && (type === 'object' || type === 'function'))
}

这里大致对一些数据类型进行划分:

//引用类型
const mapTag = 'Map';

const setTag = 'Set';

const arrayTag = 'Array';

const objectTag = 'Object';


//不可引用类型
const boolTag = 'Boolean';

const dateTag = 'Date';

const errorTag = 'Error';

const numberTag = 'Number';

const regexpTag = 'RegExp';

const stringTag = 'String';

const symbolTag = 'Symbol';

const bufferTag = 'Uint8Array';
//判断类型
function getType(target) {
    return Object.prototype.toString.call(target).slice(8, -1);
}
//判断是否是原始类型类型 
function isRefrenceType(target) {
    let type = typeof target;
    return (target !== null && (type === 'object' || type === 'function'))
}
//获取原型上的方法
function getInit(target) {
    let ClassNames = target.constructor;
    return new ClassNames();
}
//引用类型
const mapTag = 'Map';

const setTag = 'Set';

const arrayTag = 'Array';

const objectTag = 'Object';


//不可引用类型
const boolTag = 'Boolean';

const dateTag = 'Date';

const errorTag = 'Error';

const numberTag = 'Number';

const regexpTag = 'RegExp';

const stringTag = 'String';

const symbolTag = 'Symbol';

const bufferTag = 'Uint8Array';

let deepTag = [mapTag, setTag, arrayTag, objectTag];
function deepClone4(target, map = new WeakMap()) {
    let type = getType(target);
    let isOriginType = isRefrenceType(target);
    if (!isOriginType) {
        return target
    }
    let cloneTarget;
    if (deepTag.includes(type)) {
        cloneTarget = getInit(target);
    }

    //防止循环引用
    if (map.get(target)) {
        return map.get(target)
    }
    map.set(target, cloneTarget);

    //如果是mapTag 类型
    if (type === mapTag) {
        console.log(target, cloneTarget, 'target')
        target.forEach((v, key) => {
            cloneTarget.set(key, deepClone4(v, map))
        });
        return cloneTarget;
    }

    //如果是setTag 类型
    if (type === setTag) {
        target.forEach((v) => {
            cloneTarget.add(deepClone4(v, map))
        });
        return cloneTarget;
    }

    //如果是arrayTag 类型
    if (type === arrayTag) {
        target.forEach((v, i) => {
            cloneTarget[i] = deepClone4(v, map)
        });
        return cloneTarget;
    }

    //objectTag 类型
    if (type === objectTag) {
        let array = Object.keys(target);
        array.forEach((i, v) => {
            cloneTarget[i] = deepClone4(target[i], map)
        });
        return cloneTarget;
    }
}

const map = new Map();

map.set('key', 'value');

map.set('name', 'kaka')



const set = new Set();

set.add('11').add('12')

const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2,8],
    empty: null,
    map,
    set
};
target.target = target;
const target1 = deepClone4(target)
target1.a = "a"
console.log('🍎', target)
console.log('🍌', target1)

web常见的攻击手段

常见的攻击手段

DOS

DOS(Denial Service)拒绝服务式攻击,它是一种实现即简单很有效的针对服务器进行的攻 击方式,它的攻击目的就是让被攻击的主机和服务器拒绝用户正常访问,破坏系统正常运行 ,从而达到互联网用户无法连接被攻击的服务器和主机,造成服务器瘫痪。它的攻击过程,首先攻击者向被攻击的服务器发生大量带有虚假IP地址的服务请求,被攻击者在接收到请求 后返回确认信息,等待攻击者确认,此过程需要TCP的三次交换。由于攻击者发送的请求信 息是虚假的,所以被攻击服务器无法接受到信息确认,一直处于等待状态,而分配给这次请 求的资源却始终没有被释放。当被攻击者等待一定的时间后,连接会因超时而被断开,这是 攻击者再次发送新的虚假信息请求,就这样最终服务器资源被耗尽,直到瘫痪。

DDOS

DDOS(Distributed Denial Service)分布拒绝式攻击,它是在DOS基础上进行的大规模,大 范围的攻击模式,DOS只是单机和单机之间的攻击模式,而DDOS是利用一批受控制的僵尸主 机向一台服务器主机发起的攻击,其攻击的强度和造成的威胁要比DOS严重很多,更具破坏 性。首先DDOS攻击者要寻找僵尸主机,在互联网上寻找一些有后门漏洞的主机,然后入侵系 统安装控制程序,入侵的越多,控制的僵尸主机就越多,攻击源就更多,然后把入侵的主机 分配,一部分充当攻击的主要控制端,一部分充当攻击源,各负其责,在攻击者统一指挥下 对被攻击的服务器发起攻击,由于这个攻击模式是在幕后操作,所以很难被监控系统跟踪, 身份不容易被发现。

XSS

XSS(Cross Site Scripting),跨站脚本攻击。指的是通过网页开发过程中留下的漏洞,通过一些方法将恶意指令代码注入到网页中,使用户加载并执行攻击者的恶意程序指令。这些恶意网页程序通常是JavaScript,但实际上也可以包括Java、 VBScript、ActiveX、 Flash 或者甚至是普通的HTML。攻击成功后,攻击者可能得到包括但不限于更高的权限(如执行一些操作)、私密网页内容、会话和cookie等各种内容。

反射型XSS

攻击者的恶意代码并没有注入到网站中,而是诱使用户点击目标网站的链接来实施攻击。
反射型 XSS 跟存储型 XSS 的区别是:存储型 XSS 的恶意代码存在数据库里,反射型 XSS 的恶意代码存在 URL 里。
反射型 XSS 漏洞常见于通过 URL 传递参数的功能,如网站搜索、跳转等。

存储型XSS

攻击者的恶意代码被保存到目标网站的服务器中,这种攻击的持久性和稳定性较强。

DOM型XSS

DOM型指的就是攻击者输入恶意的代码前端没有做任何过滤被执行了,然后恶意代码发送到攻击者的网站,模拟用户的行为。

  1. 攻击者构造出特殊的 URL,其中包含恶意代码。
  2. 用户打开带有恶意代码的 URL。
  3. 用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执行。
  4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作

反射型XSS和存储型XSS防范

存储型和反射型 XSS 都是在服务端取出恶意代码后,插入到响应 HTML 里的,攻击者刻意编写的“数据”被内嵌到“代码”中,被浏览器所执行。
预防这两种漏洞,有两种常见做法:

  • 改成纯前端渲染,把代码和数据分隔开。
  • 对 HTML 做充分转义。

DOM型XSS防范

DOM 型 XSS 攻击,实际上就是网站前端 JavaScript 代码本身不够严谨,把不可信的数据当作代码执行了。
在使用 .innerHTML.outerHTMLdocument.write() 时要特别小心,不要把不可信的数据作为 HTML 插到页面上,而应尽量使用 .textContent.setAttribute() 等。
如果用 Vue/React 技术栈,并且不使用 v-html/dangerouslySetInnerHTML 功能,就在前端 render 阶段避免 innerHTMLouterHTML 的 XSS 隐患。
DOM 中的内联事件监听器,如 locationonclickonerroronloadonmouseover 等,<a> 标签的 href 属性,JavaScript 的 eval()setTimeout()setInterval() 等,都能把字符串作为代码运行。如果不可信的数据拼接到字符串中传递给这些 API,很容易产生安全隐患,请务必避免。

常见的XSS注入方法

  • 在 HTML 中内嵌的文本中,恶意内容以 script 标签形成注入。
  • 在内联的 JavaScript 中,拼接的数据突破了原本的限制(字符串,变量,方法名等)。
  • 在标签属性中,恶意内容包含引号,从而突破属性值的限制,注入其他属性或者标签。
  • 在标签的 href、src 等属性中,包含 javascript: 等可执行代码。
  • 在 onload、onerror、onclick 等事件中,注入不受控制代码。
  • 在 style 属性和标签中,包含类似 background-image:url("javascript:..."); 的代码(新版本浏览器已经可以防范)。
  • 在 style 属性和标签中,包含类似 expression(...) 的 CSS 表达式代码(新版本浏览器已经可以防范)

vue常见面试题

vue常见面试题

1、你知道vue的模板语法用的是哪个web模板引擎的吗?说说你对这模板引擎的理解。

使用的Mustache模版。
模板引擎:
负责组装数据,以另外一种形式或外观展现数据。
优点:

  1. 可维护性(后期改起来方便);
  2. 可扩展性(想要增加功能,增加需求方便);
  3. 开发效率提高(程序逻辑组织更好,调试方便);
  4. 看起来舒服(不容易写错)

2、你知道v-model的原理吗?

v-model只是一个语法糖,其内部实现原理就是使用v-bindinput事件监听值的改变。

changeValue(e){
    value = e.target.value
}

3、使用过vue开发过多语言项目吗?说说你的做法

使用vue-i18n。具体使用请参考vue-i18n

  1. 安装:npm install vue-i18n –save-dev
  2. 创建语言文件:language-zh.js和language-en.js 分别对应中文和英文。
  3. 在main.js中引入。
  4. 切换语言。

language-zh.js:

export const lang = {
    home:'首页‘,
  name:'姓名'
}

language-en.js:

export const lang = {
    home:'home‘,
  name:'name'
}

main.js:

import VueI18n from 'vue-i18n'
Vue.use(VueI18n) // 通过插件的形式挂载,通过全局方法 Vue.use() 使用插件const i18n = new VueI18n({
  locale: 'zh', // 语言标识 //this.$i18n.locale // 通过切换locale的值来实现语言切换
  messages: {
    'zh': require('./VueI18n/language-zh'),  //
    'en': require('./VueI18n/language-en')
  }
})
Vue.config.productionTip = false

new Vue({
  el: '#app',
  router,
  i18n,
  components: { App },
  template: '<App/>'
})

切换语言:

changeLaguages () {
  console.log(this.$i18n.locale)
  let lang = this.$i18n.locale === 'zh' ? 'en' : 'zh'
  this.$i18n.locale = lang
}

4、在使用计算属性时,函数名可以和data数据源中的属性同名吗?为什么?

不可以。因为无论是计算属性、data、还是props,最终都会挂载到vm实例上,因此三者不可以同名。
src/core/instance/state.js#L202这里,会做一个检查。
image.png
而且在初始化vm的时候,会依次初始化props、mehtods、data,computed等。这是初始化的源码:

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

5、vue中data数据源中的属性可以和methods中的方法名重名吗?为什么?

不可以。
在初始化data的时候,程序会进行检查,src/core/instance/state.js#L113,在initState函数:

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {//这里会对同名属性和方法进行警告
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {//这里会对同名的props和属性进行警告
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

5、怎么给vue定义全局方法?

  1. 挂载到Vue的原型上,即:Vue.prototype.methodsName = function(){}。
  2. 使用mixin,即Vue.use(mixins)。

6、vue2.0中不再支持在v-html中使用过滤器了怎么办?

  1. 使用全局方法。推荐
  2. 使用filters过滤器。
  3. 使用computed。
  4. 使用methods。

全局方法:

Vue.prototype.msg = function(msg){ 
 return msg.replace("\n""<br>"}
<div v-html="msg(content)"></div>

filters过滤器:

filters:{
  msg:function(msg){
    return msg.replace(/\n/g,"<br>")
  }
}

computed:

computed:{
  msg:function(msg){
    return msg.replace(/\n/g,"<br>")
  }
}

mehtods:

methods:{
  msg:function(msg){
    return msg.replace(/\n/g,"<br>")
  }
}

7、使用vue后怎么针对搜索引擎做SEO优化?

  1. SSR服务端渲染
  2. NUXT同构
  3. prerender-spa-plugin 预渲染

8、和keep-alive相关的生命周期有哪些?描述下这些生命周期

activated 和 deactivated。

  1. activated:当页面进入的时候,依次触发:created –> mounted –> activated。
  2. deactivated:当退出页面的时候触发deactivated,当在此前进或者后退的时候之后触发activated。

9、你知道vue中key的原理吗?

src\core\vdom\patch.js - updateChildren()

1、key的作用主要是为了高效的更新虚拟DOM,其原理是vue在patch过程中通过key可以精准判断两个节点是否是同一个,从而避免频繁更新不同元素,使得整个patch过程更加高效,减少DOM操作量,提高性能。
2、同时,在vue中使用相同标签名的元素过渡的时候需要使用key进行区分,否则vue之后更新其内部属性而不会触发过渡效果。

10、 vue如何重置data?

this.$data: 获取当前状态下的data。
this.$options.data():获取当前组件初始化状态下的data。
so:Object.assgin( this.$data,this.$options.data() )

11、谈谈你对mvc 、mvp和mvvm的理解?

在web1.0时代,并没有web前端的概念,当时开发一个web应用基本是由asp、.net、java等后台人员完成。项目通常包含html、css、js等文件。这样做的优势就是开发简单快捷。缺点就是jsp文件难以维护。

  • mvc:为了解决上述问题,使前后端职责更加清晰、代码更容易维护、于是mvc开发模式和框架应运而生。在这个模式下,前端主要负责v,即视图层view,前端使用html模版渲染html。后台负责c层和m层,即控制层和数据层。当用户发起请求时,后端根据用户请求路径,返回相应的页面。这样的缺点就是前端页面开发效率不高、前后端职责还不够清晰明确。

image.png
在web2.0时代,随着ajax的出现,让前端可以使用ajax和后台进行交互,前后端职责更加清晰。于是浏览器和服务器之间的整体架构变成了这样:
image.png

页面通过ajax与服务器进行交互,前端人员只负责页面不分,数据部分由后台获取。而且使用ajax可以局部刷新页面,大大降低了服务端压力和流量消耗,而且对用户端体验也更加友好。

  • mvp:随着业务的增加,我们对项目的不断跌倒,就会导致view层越来越庞大,controller层显得越发单薄。于是人们就这v层和m层中间添加了p层。由p层负责m层和v层之间的数据流动,防止二者直接进行数据流动。

image.png

我们可以通过看到,presenter负责和Model进行双向交互,还和View进行双向交互。这种交互方式,相对于MVC来说少了一些灵活,VIew变成了被动视图,并且本身变得很小。虽然它分离了View和Model。但是应用逐渐变大之后,导致presenter的体积增大,难以维护。

  • mvvm:首先,何为MVVM呢?MVVM可以分解成(Model-View-VIewModel)。ViewModel可以理解为在presenter基础上的进阶版。如图所示:

image.png

ViewModel通过实现一套数据响应式机制自动响应Model中数据变化;
同时Viewmodel会实现一套更新策略自动将数据变化转换为视图更新;
通过事件监听响应View中用户交互修改Model中数据。
这样在ViewModel中就减少了大量DOM操作代码。
MVVM在保持View和Model松耦合的同时,还减少了维护它们关系的代码,使用户专注于业务逻辑,兼顾开发效率和可维护性。

总结:

  • 这三者都是框架模式,它们设计的目标都是为了解决Model和View的耦合问题。
  • MVC模式出现较早主要应用在后端,如Spring MVC、ASP.NET MVC等,在前端领域的早期也有应用,如Backbone.js。它的优点是分层清晰,缺点是数据流混乱,灵活性带来的维护性问题。
  • MVP模式在是MVC的进化形式,Presenter作为中间层负责MV通信,解决了两者耦合问题,但P层过于臃肿会导致维护问题。
  • MVVM模式在前端领域有广泛应用,它不仅解决MV耦合问题,还同时解决了维护两者映射关系的大量繁杂代码和DOM操作代码,在提高开发效率、可读性同时还保持了优越的性能表现。

12、你知道style加scope属性的原理和作用吗

用途:防止全局同名CSS污染
原理:在标签加上v-data-something属性,再在选择器时加上对应[v-data-something],即CSS带属性选择器,以此完成类似作用域的选择方式。

13、vue边界情况有哪些

  1. 访问根实例:this.$root
  2. 访问父组件:this.$parent
  3. 访问子组件:this.$refs
  4. 依赖注入:provide inject
  5. 组件递归引用

14、子组件如何访问父组件

  1. 直接在子组件中使用this.$parent.methods
  2. 子组件使用$emit发送一个事件,在父组件中设置监听
  3. 父组件将事件通过属性传递给子组件

15、watch/methods中使用箭头函数会怎么样?

不能使用箭头函数。因为箭头函数的this默认绑定父级作用域,而不是vue实例。如果使用的是npm安装的vue,则此时this指向当前组件:
image.png
如果使用的是script标签,则this指向window。

16 、vue如何配置favicon

  • 在插件配置中的HtmlWebpackPlugin中配置
new HtmlWebpackPlugin({
  template: 'src/index.html',
  favicon:''
})
  • 在html模版中配置:
<link rel="icon" href="/assets/title.png" type="image/x-icon" />

17、vue的错误处理你知道多少?

  • errorHandler

具体请看:errorHandler

Vue.config.errorHandler = function (err, vm, info) {
  // handle error
  // `info` is a Vue-specific error info, e.g. which lifecycle hook
  // the error was found in. Only available in 2.2.0+
}

其中:err指的是error对象本身,info指的是错误信息,vm指的是vue实例。

  • warnHandler
Vue.config.warnHandler = function (msg, vm, trace) {
  // `trace` is the component hierarchy trace
  console.log(`Warn: ${msg}\nTrace: ${trace}`);
}

trace代表组件树。
例如:我们引入一个不存在的变量

<div id="app" v-cloak>
  Hello, {{name}}
</div>

就会提示:

Warn: Property or method 'name' is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property. See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.
Trace: 

(found in <Root>)
  • renderError:和前面两个不同,这个技巧不适用于全局,和组件相关。并且只适用于非生产环境。
const app = new Vue({
  el:'#app',
  renderError (h, err) {
    return h('pre', { style: { color: 'red' }}, err.stack)
  }
})

举个栗子:

<div id="app" v-cloak>
  Hello, {{name2}}
</div>

<script>
const app = new Vue({
  el:'#app',
  computed:{
    name2() {
      return x;
    }
  }
})
</script>
ReferenceError: x is not defined
    at Vue.name2 (pen.js:45:7)
    at Watcher.get (https://unpkg.com/vue@2.6.11/dist/vue.js:4478:27)
    at Watcher.evaluate (https://unpkg.com/vue@2.6.11/dist/vue.js:4583:23)
    at Proxy.computedGetter (https://unpkg.com/vue@2.6.11/dist/vue.js:4832:19)
    at Proxy.eval (eval at createFunction (https://unpkg.com/vue@2.6.11/dist/vue.js:11649:14), <anonymous>:3:68)
    at Vue._render (https://unpkg.com/vue@2.6.11/dist/vue.js:3551:24)
    at Vue.updateComponent (https://unpkg.com/vue@2.6.11/dist/vue.js:4067:23)
    at Watcher.get (https://unpkg.com/vue@2.6.11/dist/vue.js:4478:27)
    at new Watcher (https://unpkg.com/vue@2.6.11/dist/vue.js:4467:14)
    at mountComponent (https://unpkg.com/vue@2.6.11/dist/vue.js:4074:5)
  • errorCaptured

类型(err: Error, vm: Component, info: string) => ?boolean
当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false 以阻止该错误继续向上传播。

18、vue文件中style、script是必须的吗

style不是必须的。但是script是必须的:export default {}

19、如果vue变量名出现以_或者$开头的时候,会出现什么情况?

会报undefined。因为在initState的时候,vm会代理所有的data,如果出现有_或者$开头的变量名,可能会和vm内部属性冲突,vue不会代理该属性。可以通过vm.$data.访问。

20、vue使用v-for遍历对象的时候,是按什么顺序遍历的?如何保证遍历顺序?

首先,会判断对象是否有iterator接口,如果有,则循环执行next()方法。
没有iterator接口的情况下,使用Object.keys()遍历,但是在不同浏览器的情况下,遍历的结果不可能完全一致。
可以将对象放在数组中,遍历数组可以保证遍历顺序。

21、vue想扩展组件时该怎么做?

  • Vue.mixin混入
  • Vue.extend
  • solt

22、vue中$attrs 和 $listeners的使用场景

组件传值的时候会用到 爷爷在父亲组件传递值,父亲组件会通过$attrs获取到不在父亲props里面的所有属性,父亲组件通过在孙子组件上绑定$attrs 和 $listeners 使孙组件获取爷爷传递的值并且可以调用在爷爷那里定义的方法
A组件:爷爷级组件

<template>
  <div>
    <B :a="a" :b="b" :c="c" v-bind="$attrs" @test="test" @test2="test2" v-on="$listeners"/>
  </div>
</template>

<script>
import B from './B.vue'
export default {
    name:"A",
    components:{
        B
    },
    data(){
        return {
            a:1,
            b:2,
            c:3
        }
    },
    methods:{
        test(){
            alert(0)
        },
        test2(){
            alert(2)
        }
    }
}
</script>

B组件:父亲级组件

<template>
  <div>
      <p>attrs: {{ $attrs }}</p>
      <p style="background:#ccc;"  @click="$listeners.test">props: {{ a }} </p>
      <C v-bind="$attrs"/>
  </div>
</template>

<script>
import C from './C.vue'
export default {
    components:{
        C
    },
    name:"B",
    props:['a'],
    data(){
        return {

        }
    }
}
</script>

C:孙子级组件

<template>
  <div>
      <p>C : {{$attrs }}</p>
  </div>
</template>

<script>
export default {
    name:"C",
    props:['name'],
    inheritAttrs:false,
    data(){
        return {

        }
    }
}
</script>

23、vue为什么要求组件模版只能有一个根结点

因为vue组件最终是要被各种loader打包解析的,而我们必须为loader指定一个入口。而且,模版最终是会被编译成vdom的,所以必须有一个根结点来递归遍历其子节点,渲染成一个“树”。

防抖节流

防抖节流

在前端开发的过程中,我们经常会需要绑定一些持续触发的事件,如 resize、scroll、mousemove 等等,但有些时候我们并不希望在事件持续触发的过程中那么频繁地去执行函数。
通常这种情况下我们怎么去解决的呢?一般来讲,防抖和节流是比较好的解决方案。

防抖(debounce)

防抖就是在规定时间内函数只会触发一次,如果再次触发,会重新计算时间。

/*** 
 * @description 防抖函数
 * @param func 函数
 * @param wait 延迟执行毫秒数
 * @param immediate 是否立即执行
 * */
function debouncing(func, wait = 1000, immediate = true) {
    let timer = null;
    return function () {
        let args = arguments;
        let context = this;
        if (timer) {
            clearTimeout(timer);
        }
        if (!immediate) {
            //第一种:n秒之后执行,n秒内再次触发会重新计算时间
            timer = setTimeout(() => {
                //确保this指向不会改变
                func.apply(context, [...args]);
            }, wait);
        } else {
            //第二种:立即执行,n秒内不可再次触发
            let callnew = !timer;
            timer = setTimeout(() => {
                timer = null;
                console.log('kaka')
            }, wait);
            if (callnew) func.apply(context, [...args])
        }
    }
}

function fn() {
    console.log('debluncing')
}

let f1 = debouncing(fn, 1000);

setInterval(() => {
    f1()
}, 1000);

节流(throttle)

节流指的是函数一定时间内不会再次执行,用作稀释函数的执行频率。

/**
 * @description 节流函数
 * @param func 函数
 * @param wait 延迟执行毫秒数
 * @param type 1:时间戳版本 2: 定时器版本
 *  */
function throttle(func, wait = 1000, type = 1) {
    if (type === 1) {
        let timeout = null;
        return function () {
            const context = this;
            const args = arguments;
            if (!timeout) {
                timeout = setTimeout(() => {
                    timeout = null;
                    func.apply(context, [...args]);
                }, wait);
            }
        }
    } else {
        let previous = 0;
        return function () {
            const context = this;
            const args = arguments;
            let newDate = new Date().getTime();
            if (newDate - previous > wait) {
                func.apply(context, [...args]);
                previous = newDate;
            }
        }
    }

}

function fn() {
    console.log('throttle')
}

const f1 = throttle(fn);

setInterval(() => {
    f1()
}, 100);

柯里化

柯里化

柯里化

什么是函数柯里化?

函数柯里化就是使函数先接受一部分参数,使用闭包缓存已接受的函数,返回一个新的函数,在新的函数中接受剩余参数。常常用作实现参数复用、尽量使代码函数化,增加可读性。

实现函数柯里化

先给一个常见的面试题:

add(1)//1
add(1)(2)//3
add(1)(2)(3)//6
/**
 * @description curry 函数柯里化
 * @param func 函数
 * @return 函数
 */
function curry(func){
    let outerargs = Array.from(arguments).slice(1);
  function inner(){
      const innerargs = Array.from(arguments);
    outerargs = outerargs.concat(innerargs);
    return inner;
  }
  inner.toString = function(){
      return fn.apply(null,outerargs)
  }
  return inner;
}

反柯里化

实现call、apply和bind

实现call、apply和bind

三者的异同

  1. 都可以改变this指向。
  2. 接受的第一个参数为改变后的this指向。
  3. call、bind接受的参数,以队列的形式。apply接受数组形式的传参。
  4. call和apply可以多次改变this指向。而bind只可使用一次,再次使用无效,绑定的仍然是第一次的对象。
  5. call和apply调用时,会直接执行function,而bind会返回一个新的函数,且函数名为”bound xxx” 。

思路

在JavaScript中this的指向问题中提到,作为对象方法的调用。函数还可以作为某个对象的方法调用,这时this就指这个上级对象。也就是我们平时说的,谁调用,this就指向谁。

实现方法:在传入的参数中传入一个方法,然后执行这个方法,最后删除该方法(为了保持对象的前后一致性)。

实现

call

Function.prototype.newCall = function(context,...rest){
  //获取构造函数 即this
    const constructor = this;
  //获取对象 即新的this指向
  if(context){
      context = context || window;
  }else {
      context = Object.cerate(null);
  }
  //使用Symbol()确保不会覆盖对象obj的原有的属性
  const fn = Symbol();
  context[fn] = constructor;
  context[fn](...rest);
  //删除添加的属性
  delete context[fn]
}

const obj = {
    a:10,
    b:20,
    getMoney(){
        console.log(this.a)
    }
}

function fn(){
    console.log(this.a)
}
fn();//undefined
fn.newCall(obj);//10

apply

Function.prototype.newApply = function(context,rest){
  //获取构造函数 即this
    const constructor = this;
  //获取对象 即新的this指向
  if(context){
      context = context || window;
  }else {
      context = Object.cerate(null);
  }
  //使用Symbol()确保不会覆盖对象obj的原有的属性
  const fn = Symbol();
  context[fn] = constructor;
  context[fn](rest);
  //删除添加的属性
  delete context[fn]
}

const obj = {
    a:10,
    b:20,
    getMoney(){
        console.log(this.a)
    }
}

function fn(){
    console.log(this.a)
}
fn();//undefined
fn.newApply(obj);//10

bind

Function.prototype.newBind = function(context,...params){
    console.log(params)
    const Constructor = this;
    return function(...rest){
        Constructor.apply(context,[...params,...rest])
    }
}

const obj = {
    a:10,
    b:100
}

function fn(){
    console.log(this.a)
}

const f1 = fn.newBind(obj,1,2,3,4);

f1();//10

观察者模式和发布订阅模式

观察者模式和发布订阅模式

观察者模式

观察者模式定义了对象见一种一对多的依赖关系,当一个对象的状态发生改变的时候,所以依赖它的对象都将接收到通知,并作出更新。

发布订阅模式

发布订阅模式,个人理解为是观察者模式的一种特例。最初的时候,订阅目标和订阅对象是联系在一起的,当订阅目标发生改变的时候,会逐个通知订阅者。
在现在的发布订阅模式中,发布者不在直接将消息发送给订阅者,而是是通过一个第三方组件,也称为消息中心或者调度中心,由它来维持着发布者和订阅者之间的通讯,发布方和订阅方互不知道对方的存在。

区别

image.png

观察者模式

订阅者直接订阅发布者的事件,当发布者的状态改变时,会直接触发订阅者的回调事件。

发布订阅模式

订阅者首先将即将订阅的事件注册到调度中心,当发布者发布该事件到调度中心,也就是该事件触发时,由调度中心统一调度订阅者注册到调度中心的处理代码。

举例

观察者模式

function Hunter(level,name){
    this.name = name;
    this.level = level;
    this.list = [];
};
//发布
Hunter.prototype.publish = function(money){
    console.log(this.level + '猎人,' + this.name + '寻求帮助!!!');
    this.list.forEach(item => {
        item(money)
    })
};
//订阅
Hunter.prototype.subscribe = function(target,fn){
    console.log(this.name + '订阅了' + target.name);
    target.list.push(fn);
};

let kaka = new Hunter('黄金','kaka');
let hudie = new Hunter('钻石','hudie');
let dida = new Hunter('王者','dida');

hudie.subscribe(kaka,(money) => {
    console.log('观察到kaka的动态,向您汇报!')
});

kaka.publish(200)

这上面的代码中,我们定义来一个函数Hunter,在其原型上注册了发布和订阅的函数。在订阅函数中,设置了两个参数,target对象和回调函数fn,然后在发布者的执行队列中将回调函数注入进去。在发布函数中,一旦发布者改变了状态,则会触发其执行队列中的回调函数。

发布订阅模式

let HunterUnion = {
    type:'hunter',
    topics:Object.create(null),
    publish:function(topic,money){
        if(!this.topics[topic]){
            return 
        };
        this.topics[topic].forEach(item => {
            item(money);
        })
    },
    subscribe:function(topic,fn){
        if(!this.topics[topic]){
            this.topics[topic] = [];
        }
        this.topics[topic].push(fn);
    }
}


function Hunter(level,name){
    this.name = name;
    this.level = level;
    this.list = [];
};
//发布
Hunter.prototype.publish = function(topic,money){
    console.log(this.level + '猎人,' + this.name + '发布tiger任务!!!');
    HunterUnion.publish(topic,money)
};
//订阅
Hunter.prototype.subscribe = function(topic,fn){
    console.log(this.name + '订阅了' + topic);
    HunterUnion.subscribe(topic,fn)
    // target.list.push(fn);
};

let kaka = new Hunter('黄金','kaka');
let hudie = new Hunter('钻石','hudie');
let dida = new Hunter('王者','dida');

hudie.subscribe('tiger',(money) => {
    console.log('hudie,' + '观察到tiger的动态,向您汇报!')
});

dida.subscribe('tiger',(money) => {
    console.log('dida,' + '观察到tiger的动态,向您汇报!')
});

kaka.publish('tiger',200)

在上面我们定义了一个调度中心HunterUnion,所有的发布订阅的动作,都在其内部完成。其他大致和上面的观察者差不多。

总结

观察者模式和发布订阅模式最大的区别就是发布订阅模式有个事件调度中心。
观察者模式由具体目标调度,每个被订阅的目标里面都需要有对观察者的处理,这种处理方式比较直接粗暴,但是会造成代码的冗余。
而发布订阅模式中统一由调度中心进行处理,订阅者和发布者互不干扰,消除了发布者和订阅者之间的依赖。这样一方面实现了解耦,还有就是可以实现更细粒度的一些控制。比如发布者发布了很多消息,但是不想所有的订阅者都接收到,就可以在调度中心做一些处理,类似于权限控制之类的。还可以做一些节流操作。

手写JSON.stringify

手写JSON.stringify

根据JSON.stringify()的定义,可得知该方法的实现过程:

  1. 如果对象有自定义的toJSON()方法,则使用自定义方法;
  2. 布尔、数字、字符串被转换为对应的原始值;
  3. undefined function symbol转换时被忽略,如果出现在数组中,则返回null。
function JSONString(obj) {
    //获取传入的值的数据类型
    let type = typeof obj;
    if (type !== 'object') {
        if (!/(function|undefined|symbol)/.test(type)) {
            return '"' + obj + '"'
        }
    } else {
        let arr = [];
        //是否是数组
        let isArray = obj && obj.constructor === Array;
        loop: for (let key in obj) {
            let value = obj[key];
            let typeValue = Object.prototype.toString.call(value);
            if (typeValue === '[object Symbol]' || typeValue === '[object Undefined]' || typeValue === '[object Function]') { //symbol 类型 
                continue loop;
            } else if (typeValue === '[object Object]' || typeValue === '[object Array]') {
                value = JSONString(value);
            }
            let v = isArray ? `${String(value)}` : `"${key}":${String(value)}`;
            arr.push(v)
        };
        let str = isArray ? (`"[${String(arr)}]"`) : (`{${String(arr)}}`);
        return str;
    }

};

let obj = {
    a: 10,
    b: function() {},
    c: true,
    d: Symbol(),
    e: [1, 2, 3, { a: 100 }],
    f: null,
    g: undefined
};
let s = JSONString(obj);
console.log(s);

手写new操作符

手写new操作符

new操作符做了什么?

  1. 新建一个对象;
  2. 将继承构造函数的原型;
  3. 改变构造函数的this指向;
  4. 返回对象(如果函数没有返回对象类型,则返回该对象的引用)。
//手写new操作符
Function.prototype.new = function() {
    //获取参数
    const args = Array.prototype.slice.call(arguments);
    //获取构造函数 即this
    let Controuctor = this;
    //新建对象
    let obj = Object.create(null);
    Object.setPrototypeOf(obj, Controuctor.prototype);
    //改变构造函数this指向
    let res = Controuctor.apply(obj, args);
    if (res !== null && (typeof res === 'function' || typeof res === 'object')) {
        return res;
    };
    return obj
};

let obj = {
    a: 10,
    b: 20
};

function fn() {
    let args = Array.prototype.slice.call(arguments);
    this.getName = function() {
        console.log(args)
    }
};

let f = fn.new(1, 2, 3);

f.getName()

浏览器输入URL到页面加载完成 置顶

从输入URL到页面加载完成的过程

主干流程

  1. 浏览器接收url,开启一个新的网络请求线程(这部分涉及到浏览器进程与线程之间的关系)。
  2. 从开启网络请求线程到发起一个http请求(这部分涉及到dns解析、五层因特网协议栈、tcp/ip协议等)。
  3. 从服务器接收到一个请求到后台收到请求(这部分涉及到负载均衡、安全拦截、后台程序内部处理等知识)。
  4. 服务端和浏览器之间的交互(如http请求头、状态码、请求报文、cookie、session等)。
  5. http缓存(包括强缓存、协商缓存等)。
  6. CSS的可视化模型解析(元素的渲染规则、包含块、控制块、BFC、IFC等)。
  7. JS的解析过程(JS的解释过程、预处理过程、执行阶段的GO、VO、AO、作用域和作用域连等)。
  8. 其它(如安全策略、跨域等)

从浏览器接收url到开启网络请求线程

多进程的浏览器

浏览器是多进程的,有一个主控进程,每一个tab页都会新开一个进程。
这些进程可能包括:主控进程、插件进程、GPU、tab页等。

  • Browser进程:浏览器的主进程,负责调控,只有一个。
  • GPU进程:负责3D渲染,最多只有一个。
  • 第三方插件进程:负责管理第三方插件,每一个插件都有一个对应的进程,仅当该插件创建时创建。
  • 浏览器渲染进程(浏览器内核):默认每个tab页一个进程,互不干扰,负责控制页面渲染、脚本执行、事件处理等。

浏览器内核的五大线程

每一个tab页都可以看作一个浏览器内核进程,而该进程包含多个线程:

  • GUI线程:负责页面渲染、重绘、CSS解析等。
  • JS引擎线程:负责程序脚本的解析和执行。
  • 事件触发线程:负责管理事件循环,管理事件任务队列。
  • 定时触发器线程:setTImeout 和 setInterval 所在的线程。定时器的计时不是有JS引擎控制,而是由该线程控制。
  • http请求线程:负责http请求。

QA

Q:为什么JS引擎线程和GUI线程互斥?
A:因为JS是可以操作DOM的,如果在JS操作DOM的同时,GUI线程也在渲染页面,那么元素最终的呈现可能就不会和我们预期的一致了。
Q:为什么JS引擎是单线程?
A:1、创建JavaScript语言的时候,多进程多线程并不流行,硬件支持度不高。
2、多进程多线程使用时需要加锁,实施成本高。
3、如果多个线程同时操作一个DOM元素,可能元素的最终呈现不会如预期一样。
4、线程之间资源共享。
具体可以参考这篇文章

解析URL

当用户输入URL之后,会进行解析(URL的本质是统一资源标识符)。
URL一般包含这几部分:

  • protocol:协议头,如http、https 、ftp等。
  • host:指定主机域名或者IP地址。
  • port:端口号。
  • path:资源路径。
  • query:查询参数。
  • fragment:即#之后的hash值,一般用来定位到某个位置。

网络请求都是单独的线程

每次网络请求都会开辟一个新的线程进行,譬如如果URL解析到http协议,就会新建一个网络线程去处理资源下载,
因此浏览器会根据解析到的http协议去单独开辟出新的网络线程,前往请求资源。

从开启网络线程到发起http请求

这一部分主要包含dns解析、tcp/ip请求构建、五层因特网协议栈等。

DNS解析

如果用户输入的是域名,那么就需要进行dns解析获取对应的IP地址,具体步骤如下:

  1. 如果浏览器有缓存,就使用浏览器缓存;否则使用本机缓存,再没有就使用host。
  2. 如果本地没有缓存,就向dns服务器查询,(查询中可能会使用路由等,路由也是有缓存的,如果有,则使用),获取IP地址。

注意:域名查询可能经过dns调度器的。而且dns查询是很耗时,如果解析域名较多,那么网站的首屏加载就会变得很慢,这时可以考试使用dns-prefetch,这一点请参考这里

TCP/IP请求

http请求本质上还是tcp/ip请求。
tcp将http长报文划分为短报文,通过三次握手与服务器建立连接,进行可靠传输。

三次握手

image.png
TCP报头中的源端口号和目的端口号同IP数据报中的源IP与目的IP唯一确定一条TCP连接。TCP在发送数据前必须在彼此间建立连接,这里连接意思是:双方需要内保存对方信息(例如:IP,Port…)。

报文主要段的意思:

  • 序号seq:发送的数据字节流,确保TCP连接传输有序,对每个字节编号。序列号seq就是这个报文段中的第一个字节的数据编号。
  • 确认序号ack:发送方期待接收的下一序列号,接收成功后序列号加1,只有ACK为1时,才有效。
  • 确认ACK:仅当ACK为1时确认号才有效,为0时无效。
  • 同步SYN:连接建立时用于同步序号。当SYN=1,ACK=0时,指的是这是一个请求报文段,若统一连接,则SYN=1,ACK=1。因此,SYN=1,表示这是一个请求连接,或者连接接收报文。SYN只有在建立连接时才会被置为1,握手结束之后置为0.
  • 终止FIN:用于释放连接。当FIN=1时,表示此报文段的发送方的数据已经发送完毕,并要求释放连接。

PS:ACK、SYN和FIN这些大写的单词表示标志位,其值要么是1,要么是0;ack、seq小写的单词表示序号。

字段 含义
URG 紧急指针是否有效。为1,表示某一位需要被优先处理
ACK 确认号是否有效,一般置为1。
PSH 提示接收端应用程序立即从TCP缓冲区把数据读走。
RST 对方要求重新建立连接,复位。
SYN 请求建立连接,并在其序列号的字段进行序列号的初始值设定。建立连接,设置为1
FIN 希望断开连接。

image.png

  1. 第一次握手:建立连接时,客户端发送初始序号seq=x,SYN=1请求标志。让服务器知道客户端发送,自己接收正常。
  2. 第二次握手:服务器发送请求标志SYN,发送确认标志ACK,服务器自己的序号seq,发送客户端的确认标志ack:SYN=1,ACK=1,seq=y,ack=x + 1。让客户端知道自己接收、发送正常,服务器接收、发送正常。
  3. 第三次握手:客户端发送确认标志ACK,自己的序号seq,发送服务端的确认序号ack:ACK=1,seq=x+1,ack=y+1。让服务器知道客户端发送、接收,自己发送、接收正常。

从上面可知,三次握手是让服务器和客户端知道彼此双方发送、接收能力正常的最少握手次数。

四次挥手

image.png

四次挥手过程解析:

  1. 第一次挥手:客户端主动请求断开,向服务端发送FIN=1,seq=u,进入FIN-WAIT-1状态。
  2. 第二次挥手:服务端接收到客户端发送的信息,发送确认序号ACK=1,客户端的确认号ack=u+1,自己的序号seq=v。并且进入CLOSE-WAIT状态。
  3. 第三次挥手:客户端接收到服务端的信息后,进入FIN-WAIT-2状态。此时服务器发送释放FIN=1信号,发送确认标志ACK=1,自己的序号seq=w,客户端的确认号ack=u+1。并且自己进入LAST-ACT状态(最好确认状态)。
  4. 第四次挥手:客户端收到服务端信息后,发送确认标志ACK=1,自己的序号seq=u+1,服务端的确认号ack=w+1。客户端进入TIME-WAIT状态。等待2MSL(2个最长报文段寿命)之后,进入CLOSE状态。服务端收到确认后,也进入CLOSE状态。

QA

Q:为什么三次握手和四次挥手?
A:三次握手时,服务器同时把ACK和SYN放在一起发送到了客户端那里。四次挥手时,当收到对方的 FIN 报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方是否现在关闭发送数据通道,需要上层应用来决定,因此,己方 ACK 和 FIN 一般都会分开发送。
Q:为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
A:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。
Q:如果已经建立了连接,但是客户端突然出现故障了怎么办?
A:TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。

tcp/ip的并发限制

浏览器针对同一个域名下的tcp连接是有限制的(一般是5-20个)。而且在http1.0中,一个资源下载就对应着一个tcp/ip连接。

get和post的区别

get和post本质上都是tcp/ip,但是两者除了在http层面上的不同,在tcp/ip层面也有区别。
get会产生一个tcp包,而post会产生两个:

  • get请求时会把headers和data一起发送出去,服务器响应200。
  • post请求会先把headers发送出去,服务器响应100 continue,然后浏览器在发送data,服务器响应200。

其他区别:

分类 GET POST
可否被缓存 可以被缓存 不可以被缓存
后退或刷新 无害 重新提交请求
编码类型 application/x-www-form-urlencoded application/x-www-form-urlencoded 或 multipart/form-data。为二进制数据使用多重编码。
历史 会被保留在浏览器历史记录中 不会被保留在浏览器历史记录中
对数据类型的限制 只允许ASCII字符 无限制
安全性 与POST比,安全性较差,因为发送的数据会暴露在地址栏中 POST比GET较安全
可见性 可见 不可见

五层因特网协议

从客户端发出http请求到服务器接收,这中间会经过一系列的过程:
从应用层发出http请求,到传输层经过三次握手建立tcp连接,再到网络层的IP寻址,再到数据链路层封装成帧,最后经由物理层传输。
五层因特网协议栈其实就是:

  1. 应用层(dns http)DNS解析IP并发送http请求。
  2. 传输层(tcp udp)经过三次握手,建立tcp连接。
  3. 网络层(IP ARP)IP寻址。
  4. 数据链路层(PPP)封装成帧。
  5. 物理层(利用无力介质传输比特流)物理传输(然后传输的时候通过双绞线,电磁波等介质)。

相比于完整的IOS/OSI七层框架,少了表示层和会话层。
OSI七层框架:物理层数据链路层网络层传输层会话层表示层应用层

  1. 表示层:主要处理两个通讯系统之间的信息交换的表示方式,包括数据格式交换、数据加密解密等。
  2. 会话层:它管理不同用户的进程之间的对话,如登录、注销等。

从服务器接收请求到后台接收到请求

负载均衡

对于一个大型项目来说,它的并发访问量可能会很大,所以如果只有一台服务器可能是吃不消的,所以一般会有若干个服务器组成一个集群,然后配合反向代理服务器实现负载均衡。
简单来说:就是用户的请求都会指向一个反向代理服务器,如nginx,然后代理服务器按照指定的算法,分配不同的请求给对应集群中的服务器执行,然后反响代理服务器等待服务器的http响应,并将其反应给用户。
**

后台处理

一般后台都是部署到一个容器中,所以:

  • 先是容器接收到请求(如tomcat),
  • 然后对应容器中的程序接收到请求(如java、php等),
  • 然后就是后台程序中的内部处理,处理完成之后响应结果。

一般后台都会有统一的验证:安全拦截、跨域处理等,如果验证不通过,则直接拒绝请求。如果验证通过,才会进入后台程序,后台程序进行对应的操作,如数据查询等。等程序执行完毕之后,会返回一个http包,然后将这个包发送给前端,完成交互。

后台和前台的交互

前后端交互的时候,http报文作为最重要的信息载体。

http报文结构

报文一般包括了通用头部、请求/响应头部、请求/响应体

通用头部
  • Request Url:请求的web服务器地址。
  • Request Method:请求方式(GET、POST、PUT、DELETE、HEAD、CONNECT、TRACE)。
  • Status Code:请求返回的状态码。
  • Remote Address:请求的远程服务器地址(会被转为IP)。

其中methods一般可以分为两批次:

  • HTTP1.0定义了三种请求方法:GET、POST和HEAD方法以及几种额外的请求方式(PUT、DELETE、LINK和UNLINK)。HTTP1.0具体定义请参考:https://tools.ietf.org/html/rfc1945
  • HTTP1.1定义了八种请求方式:GET、POST、PUT、DELETE、HEAD、CONNECT、TRACE。HTTP1.1具体定义请参考:https://tools.ietf.org/html/rfc2616
常用状态码
200-请求成功。请求的资源已返回客户端。
301-永久重定向。
302-临时重定向。
304-表明自上次请求之后,资源未做变更,请使用本地缓存的资源。
400-客户端请求出错。
401-请求未经授权。
403-禁止访问。
404-请求资源不存在。
500-服务器内部错误。
503-服务器不可用。
......

最后列举下大致不同范围的状态码:

1xx:指示信息,表示请求已接受,正在处理。
2xx:请求已被成功接收。
3xx:重定向,要完成请求需要进一步的操作。
4xx:客户端错误,请求语法错误或者请求无法实现。
5xx:服务端错误,服务器实现合法的请求。

请求/响应头部

常用的请求头部:

Accept:接收类型。表示浏览器可以支持的MIME类型(对标服务器返回的Content-Type)。
Accept-Encodeing:浏览器支持的压缩类型,如GZip,超出类型不能接收。
ConContent-Type:客户端发出去的实体内容的类型。
Cache-Control:指定请求和响应应遵循的缓存机制,如no-cache等。
If-Modifiy-Since:对应服务端的Last-Modified,用于比较服务器资源文件是否发生变动,精确到1s,http1.0。
Expires:缓存控制。设置资源的有效期,有效时间内可以直接使用缓存。使用的是服务端时间,http1.0。
Max-age:代表资源在本地的有效时间,有效时间内不会请求,http1.1。
If-None-Match:对应服务端的ETag,用于匹配文件内容是否发生改变,比较精确,http1.1。
Cookie:有cookie且同域名访问时会带上。
Connection:当浏览器与服务器通信时对于长连接如何进行处理,如keep-alive。
Host:请求的服务器地址。
Origin:请求最初从哪里发起的。Origin比Referer更尊重隐私。
Referer:该页面的来源URL(CSRF拦截常用该字段)。
User-Agent:用户客户端的信息。

常用的响应头部:

Access-Control-Allow-Headers:服务器允许的请求headers。
Access-Control-Allow-Methods:服务器允许的请求方式。
Access-Control-Allow-Methods:服务器允许的请求Origin头部(如*)。
Content-Type:服务器返回的响应内容实体的类型。
Date:数据从服务器返回的时间。
Cache-Control:告诉浏览器或其他客户,什么环境可以安全的缓存文档。
Last-Modified:请求资源最后的修改时间。
Expires:告诉浏览器应该在什么时候放弃使用资源,从而不在缓存它。
Max-age:告诉浏览器应该缓存多长时间,开启Cache-Control有效。
ETag:请求资源的文件指纹。
Set-cookie:设置该页面的cookie,服务器通过这个设置浏览器页面的cookie。
Keep-Alive:如果浏览器有keep-alive,那么服务器也有对应的keep-alive。
Server:服务器的一些信息。

一般来说,请求头部和响应头部都是匹配分析的
如:请求头部的Accept要和响应头部的Content-Type是要匹配的,否则就会报错。
如:跨域请求时,请求头部的Origin要和响应头部的Access-Control-Allow-Origin匹配,否则回报跨域错误。
再比如:缓存时,请求头部的If-Modified-Since和ETag,要与响应头部的Last-Modified和If-None-Match相匹配。

请求和响应实体

http请求时,除了头部,还有消息实体,一般来说
请求实体中会将一些需要的参数都放入进入(用于post请求)。
譬如实体中可以放参数的序列化形式(a=1&b=2这种),或者直接放表单对象(Form Data对象,上传时可以夹杂参数以及文件),等等
而一般响应实体中,就是放服务端需要传给客户端的内容
一般现在的接口请求时,实体中就是对于的信息的json格式,而像页面请求这种,里面就是直接放了一个html字符串,然后浏览器自己解析并渲染。
image.png

cookie及相关优化

cookie 是一种本地存储方式,主要用于浏览器和服务器通信,常用于验证身份,和服务端的session搭配使用。
场景如下(简述):

在登陆页面,用户登陆了
此时,服务端会生成一个session,session中有对于用户的信息(如用户名、密码等)
然后会有一个sessionid(相当于是服务端的这个session对应的key)
然后服务端在登录页面中写入cookie,值就是:jsessionid=xxx
然后浏览器本地就有这个cookie了,以后访问同域名下的页面时,自动带上cookie,自动检验,在有效时间内无需二次登陆

上述就是cookie的常用场景简述(当然了,实际情况下得考虑更多因素)。
一般来说,cookie中是不允许存放敏感信息的,如帐号,密码等。如果非要存储,一定要在cookie 中这只httpOnly,这样就无法通过js操作获取cookie了。
另外,在同域名操作时,总会默认带上该域名下所有的cookie。针对这种情况,在某些场景中是需要优化的:

比如以下场景:

客户端在域名A下有cookie(这个可以是登陆时由服务端写入的)
然后在域名A下有一个页面,页面中有很多依赖的静态资源(都是域名A的,譬如有20个静态资源)
此时就有一个问题,页面加载,请求这些静态资源时,浏览器会默认带上cookie
也就是说,这20个静态资源的http请求,每一个都得带上cookie,而实际上静态资源并不需要cookie验证
此时就造成了较为严重的浪费,而且也降低了访问速度(因为内容更多了)

针对这种场景,我们可以使用域名拆分的方案:将静态资源分组,分别存放到不同的服务器中。
而在移动端,如果请求的域名过多时,会降低请求速度(因为域名解析是很耗时的,而且移动端的带宽大都不如pc),这时我们可以使用dns-prefetch,让浏览器空闲时提前解析域名。
关于cookie 的操作大致可以如下图所示:
image.png

gzip压缩

首先,明确gzip是一种压缩格式,需要浏览器支持才有效(不过一般现在浏览器都支持),
而且gzip压缩效率很好(高达70%左右)
然后gzip一般是由apachetomcat等web服务器开启
当然服务器除了gzip外,也还会有其它压缩格式(如deflate,没有gzip高效,且不流行)
所以一般只需要在服务器上开启了gzip压缩,然后之后的请求就都是基于gzip压缩格式的,
非常方便。

长连接与短连接

首先看tcp/ip层面的定义:

  • 长连接:一个tcp/ip连接上可以连续发送多个数据包,在tcp连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接,一般需要自己做在线维持(类似于心跳包)
  • 短连接:通信双方有数据交互时,就建立一个tcp连接,数据发送完成后,则断开此tcp连接

然后在http层面:

  • http1.0中,默认使用的是短连接,也就是说,浏览器没进行一次http操作,就建立一次连接,任务结束就中断连接,譬如每一个静态资源请求时都是一个单独的连接
  • http1.1起,默认使用长连接,使用长连接会有这一行Connection: keep-alive,在长连接的情况下,当一个网页打开完成后,客户端和服务端之间用于传输http的tcp连接不会关闭,如果客户端再次访问这个服务器的页面,会继续使用这一条已经建立的连接。

注意: keep-alive不会永远保持,它有一个持续时间,一般在服务器中配置(如apache),另外长连接需要客户端和服务器都支持时才有效。

http2.0

http2.0不是https,它是http的下一代规范。
简述一下http2.0和http1.1的区别:

  • http1.1中,每请求一个资源,都需要新开一个tcp/ip连接,所以对应的结果就是每一个资源都占用一个tcp/ip请求,而tcp/ip请求并发数量是有限制的,当请求资源数量一多,请求速度就会慢下来。
  • http2.0中,一个tcp/ip请求可以请求多个资源,也就是说,只要一次tcp/ip请求,就可以请求诺干个资源,分割成更小的帧请求,速度会有显著提升。

所以,如果http2.0得到全面的使用,那么http1.1中的很多优化手段就不必在使用了,比如:使用雪碧图,静态资源多域名拆分等。
http2.0的特性:

  1. 多路复用(即一个tcp/ip请求可以请求多个资源)。
  2. 首部压缩(http头部压缩,减少体积)。
  3. 二进制分帧(在应用层和传输层使用二进制分帧,改善传输性能,实现低延迟和高吞吐量)。
  4. 服务器推送(服务端可以对客户端的一个请求发出多个响应,可以主动通知客户端)。
  5. 请求优先级(如果流被赋予了优先级,那么它就会基于这个优先级被处理,且由服务器决定使用多少资源来处理这个流)。

https

https是安全版本的http。譬如一些支付等操作都是基于https的,http的安全性太低。
简单来说,https和http的区别就是在请求建立连接之前,先建立ssl连接,确保之后的通信都是加密的,无法被轻易截取分析。
一般来说,如果要将网站升级成https,需要后端支持(后端需要申请证书等),然后https的开销也比http要大(因为需要额外建立安全链接以及加密等),所以一般来说http2.0配合https的体验更佳(因为http2.0更快了)。

1、浏览器请求建立SSL链接,并向服务端发送一个随机数–Client random和客户端支持的加密方法,比如RSA加密,此时是明文传输。 

2. 服务端从中选出一组加密算法与Hash算法,回复一个随机数–Server random,并将自己的身份信息以证书的形式发回给浏览器
(证书里包含了网站地址,非对称加密的公钥,以及证书颁发机构等信息)

3. 浏览器收到服务端的证书后

    - 验证证书的合法性(颁发机构是否合法,证书中包含的网址是否和正在访问的一样),如果证书信任,则浏览器会显示一个小锁头,否则会有提示

    - 用户接收证书后(不管信不信任),浏览会生产新的随机数–Premaster secret,然后证书中的公钥以及指定的加密方法加密`Premaster secret`,发送给服务器。

    - 利用Client random、Server random和Premaster secret通过一定的算法生成HTTP链接数据传输的对称加密key-`session key`

    - 使用约定好的HASH算法计算握手消息,并使用生成的`session key`对消息进行加密,最后将之前生成的所有信息发送给服务端。 

4. 服务端收到浏览器的回复

    - 利用已知的加解密方式与自己的私钥进行解密,获取`Premaster secret`

    - 和浏览器相同规则生成`session key`

    - 使用`session key`解密浏览器发来的握手消息,并验证Hash是否与浏览器发来的一致

    - 使用`session key`加密一段握手消息,发送给浏览器

5. 浏览器解密并计算握手消息的HASH,如果与服务端发来的HASH一致,此时握手过程结束。

之后所有的https通信数据将由之前浏览器生成的session key并利用对称加密算法进行加密。
image.png

图片来源于阮一峰的图解SSL/TSL协议

http缓存

缓存可以被简单的划分为两类:强缓存(200 from cache)和协商缓存(304)。
区别:

  • 强缓存:如果浏览器判断本地缓存未过期的时候,将会直接使用本地缓存,不会发起请求。
  • 协商缓存:浏览器会向服务器发起请求,在请求头部携带资源文件的缓存信息,如If-Modified-Since、ETag等,由服务器端判断,资源自从上次访问是否发生过变更,如果发生过变更,则返回状态码200,并将新的资源文件和缓存规则返回;如果未变更,返回304状态码,告诉浏览器使用本地缓存。

缓存头部概述

上面提到的强缓存和协商缓存,那么它们是如何进行区分的呢?
答案是通过不同的http头部控制。
这里列举一些常用的缓存头部:
用于强缓存控制的:

  • http1.0: Pragma/Expires
  • http1.1: Cache-Control/Max-Age

注意:Max-Age不是一个头部,它是Cache-Control头部的值。

用于协商缓存控制的:

  • http1.0: Last-Modified/If-Modified-Since
  • http1.1:E-tag/If-None-Match

再提一点,其实HTML页面中也有一个meta标签可以控制缓存方案-Pragma

<META HTTP-EQUIV="Pragma" CONTENT="no-cache">

不过,这种方案还是比较少用到,因为支持情况不佳,譬如缓存代理服务器肯定不支持,所以不推荐。

头部的区别

http1.0的缓存控制
  • Pragma:严格来说,它不属于专门的缓存控制头部,但是它设置no-cache时可以让本地强缓存失效(属于编译控制,来实现特定的指令,主要是因为兼容http1.0,所以以前又被大量应用)。
  • Expires:服务端配置的,属于强缓存,在规定的时间之前,浏览器端不会发起请求,而是使用本地缓存。注意,Expires一般对应服务器端时间,如Expires:Fri, 30 Oct 1998 14:19:41
  • If-Modified-Since/Last-Modified:这两个是成堆出现的,属于协商缓存的内容。其中属于浏览器的头部内容是If-Modified-Since,而属于服务端的是Last-Modified。它的作用是,当服务器接收到请求时,如果二者匹配,则说明服务端资源未改变,可以使用本地缓存,只返回头部,状态码304。
http1.1的缓存控制
  • Cache-Control:缓存控制头部,属于强缓存,有no-cache、max-age等多个值。
  • Max-Age:服务端配置的,用来控制强缓存,在规定的时间之内,浏览器无需发出请求,直接使用本地缓存,注意,Max-Age是Cache-Control头部的值,不是独立的头部,譬如Cache-Control: max-age=3600,而且它值得是绝对时间,由浏览器自己计算。
  • If-None-Match/E-tag:这两个是成对出现的,属于协商缓存的内容,其中浏览器的头部是If-None-Match,而服务端的是E-tag,同样,发出请求后,如果If-None-MatchE-tag匹配,则代表内容未变,通知浏览器使用本地缓存,和Last-Modified不同,E-tag更精确,它是类似于指纹一样的东西,基于FileEtag INode Mtime Size生成,也就是说,只要文件变,指纹就会变,而且没有1s精确度的限制。

Max-Age和Expires

Expires使用的是服务器端的时间
但是有时候会有这样一种情况-客户端时间和服务端不同步
那这样,可能就会出问题了,造成了浏览器本地的缓存无用或者一直无法过期
所以一般http1.1后不推荐使用Expires
Max-Age使用的是客户端本地时间的计算,因此不会有这个问题
因此推荐使用Max-Age
注意,如果同时启用了Cache-ControlExpiresCache-Control优先级高。

E-tag和Last-Modified

Last-Modified

  • 表明服务端的文件最后何时改变的
  • 它有一个缺陷就是只能精确到1s,
  • 然后还有一个问题就是有的服务端的文件会周期性的改变,导致缓存失效

E-tag

  • 是一种指纹机制,代表文件相关指纹
  • 只有文件变才会变,也只要文件变就会变,
  • 也没有精确时间的限制,只要文件一遍,立马E-tag就不一样了

如果同时带有E-tagLast-Modified,服务端会优先检查E-tag
各大缓存头部的整体关系如下图:
image.png

解析页面

前面有提到http交互,那么接下来就是浏览器获取到html,然后解析,渲染。

流程简述

浏览器内核拿到内容后,渲染步骤大致分为一下几步:

1、解析HTML,构建DOM Tree
2、解析CSS,构建CSS Rules Tree
3、合并DOM Tree 和 CSS Rules Tree(其实是将CSS Rules Tree附在DOM Tree之上),生成render Tree。
4、布局人的人 Tree(Layout/reflow),负责计算各元素的尺寸和位置的计算。
5、绘制render Tree(paint),绘制页面像素信息。
6、浏览器将各层的信息发送给GPU,由GPU将各层合层(composite),显示在屏幕上。

如下图:
image.png

HTML解析 构建DOM

整个渲染步骤中,HTML解析是第一步。
简单的理解,这一步的流程是这样的:浏览器解析HTML,构建DOM树。
但实际上,在分析整体构建时,却不能一笔带过,得稍微展开。
解析HTML到构建出DOM当然过程可以简述如下:
Bytes → characters → tokens → nodes → DOM
譬如假设有这样一个HTML页面:(以下部分的内容出自参考来源,修改了下格式)

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg"></div>
  </body>
</html>

浏览器的处理如下:
image.png
列举其中的一些重点过程:

  1. Conversion转换:浏览器将获得的HTML内容(Bytes)基于它的编码格式转换为字符串。
  2. Tokeizing分词:浏览器按照HTML规范将这些字符串转换未不同的标记token,每一个token都有自己独特的含义和规则。
  3. Lexing词法分析:分词的结果就是获得一堆token,此时将他们转换为对象,为他们定义它们的属性和规则。
  4. DOM构建:因为HTML标记的就是不同标签之间的关系,这个关系就像树形结构一样。

最终的DOM树如下图所示:
image.png

生成CSS规则

同理,CSS规则树的生成也是类似。简述为:
Bytes → characters → tokens → nodes → CSSOM

body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }

最终生成的CSS Rules Tree:
image.png

构建渲染树

当DOM树和CSSOM都有了后,就要开始构建渲染树了
一般来说,渲染树和DOM树相对应的,但不是严格意义上的一一对应
因为有一些不可见的DOM元素不会插入到渲染树中,如head这种不可见的标签或者display: none
整体来说可以看图:
image.png

渲染

有了render树,接下来就是开始渲染,基本流程如下:
image.png

图中重要的四个步骤就是:

1. 计算CSS样式
2. 构建渲染树
3. 布局,主要定位坐标和大小,是否换行,各种position overflow z-index属性
4. 绘制,将图像绘制出来

然后,图中的线与箭头代表通过js动态修改了DOM或CSS,导致了重新布局(Layout)或渲染(Repaint)。
这里Layout和Repaint的概念是有区别的:

  • Layout,也称为Reflow,即回流。一般意味着元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树
  • Repaint,即重绘。意味着元素发生的改变只是影响了元素的一些外观之类的时候(例如,背景色,边框颜色,文字颜色等),此时只需要应用新样式绘制这个元素就可以了

注意:回流的成本要远高于重绘,因为回流会造成同级元素和子节点的回流。所以,一定要尽可能的避免回流。

什么会造成回流

  • 页面渲染初始化
  • DOM结构改变,如删除某个节点
  • render Tree发生变化,如padding改变
  • 窗口resize
  • 最复杂的一种:获取元素的某个属性:
    • offsetTop/Left/Width/Height
    • scrollTop/Left/Width/Height
    • clientTop/Left/Width/Height
    • width/height
    • 调用了getComputedStyle()或者IE的currentStyle

回流一定会伴随着重绘,重绘却不一定造成回流,这是一个充分必要条件。

优化方案
  1. 减少逐项更改样式,最好一次性更改,或者使用定义样式class一次性更改。
  2. 避免循环操作dom,创建一个documentFragment或div,在它上面应用所有DOM操作,最后再把它添加到window.document。
  3. 避免多次调用读取offset等属性,必要的时候可以缓存到变量中。
  4. 将复杂的元素决定定位或者固定定位,使其脱离文档流,否则造成的回流代价太大。

注意:改变字体大小会引发回流

复合层与简单层

浏览器渲染的图层一般分为两大类:普通图层和复合图层。

  • DOM中每一个节点都对应一个普通图层。
  • 复合图层就是普通图层的合并。一个页面一般来说只要一个复合图层。

浏览器什么时候会创建复合图层

  • 3D 或者 CSS transform

  • CSS filters

  • 元素覆盖时,比如使用了 z-index 属性

如何变成复合图层

将一个元素变为复合图层,就是使用传说中的硬件加速。

  • transform:translate3d(0,0,0),或者translateZ(0)。
  • opacity属性/过度动画(需要动画执行的过程中才会创建合成层,动画没有开始或结束后元素还会回到之前的状态)
  • will-chang属性(这个比较偏僻),一般配合opacity与translate使用(而且经测试,除了上述可以引发硬件加速的属性外,其它属性并不会变成复合层),will-change的作用就是告诉浏览器该元素会有那些发生变化的方法,让浏览器为该元素的变化做好优化准备,具体请移步

复合图层的作用

一般一个元素开启硬件加速后会变成复合图层,可以独立于普通文档流中,改动后可以避免整个页面重绘,提升性能
但是尽量不要大量使用复合图层,否则由于资源消耗过度,页面反而会变的更卡。

使用硬件加速的问题

  1. 内存。如果GPU加载了大量的纹理,那么很容易就会发生内容问题,这一点在移动端浏览器上尤为明显,所以,一定要牢记不要让页面的每个元素都使用硬件加速。
  2. 使用GPU渲染会影响字体的抗锯齿效果。这是因为GPU和CPU具有不同的渲染机制。即使最终硬件加速停止了,文本还是会在动画期间显示得很模糊。

注意:使用硬件加速时,尽可能的使用index,防止浏览器默认给后续的元素创建复合层渲染。
具体的原理时这样的:
webkit CSS3中,如果这个元素添加了硬件加速,并且index层级比较低,
那么在这个元素的后面其它元素(层级比这个元素高的,或者相同的,并且releative或absolute属性相同的),
会默认变为复合层渲染,如果处理不当会极大的影响性能

简单点理解,其实可以认为是一个隐式合成的概念:如果a是一个复合图层,而且b在a上面,那么b也会被隐式转为一个复合图层,这点需要特别注意

QA

Q:为什么开启transform的元素不会引起回流和重绘?
A:因为transform属性支持的位移函数translate()、缩放比例函数scale()、斜切函数skew()、旋转函数rotate()都支持线性映射的形式,也就是matrix( )表示的方式,简单来说就是所有transform实现的效果都可以对原坐标系中的点[x,y]按照如下的齐次矩阵进行计算得到变换后的点坐标[x',y']
image.png
齐次矩阵的系数是设定transform变换时传入的,是一个已知项,而使用三维的齐次矩阵是因为二维坐标的点在变换时会产生常数项(主要是平移变换),而如果以二维矩阵作为参数来计算时,以x坐标变换为例,结果的形式就是x'=ax+by,其中是没有常量的,所以只能采用一个三维齐次矩阵来表示,但计算中的第三个坐标实际上并不需要使用。更多的关于变换的数学原理,感兴趣的读者可以自行查阅资料。
所以translform在动画过程中不是使用缓存,而是在图层合成时遍历当前层的点,然后利用上述方法计算出新的坐标点即可。它可以视为一种与图层内容无关的变换,图层中的元素首次生成的位图信息缓存可以被反复使用。比如一段平移动画,如果使用绝对定位+改变left值的方式来实现,就需要不断计算动画元素的布局并更新它的像素信息,但如果使用translate来实现,动画元素在文档流中的位置并不需要改变,无论后续平移到多远,都可以使用位图缓存中保存的初始位置信息,再加上变换矩阵的影响在层合并时计算出来,同样既不影响布局,也不需要重绘,这就是它高性能的原因。
Q:为什么使用opacity的元素不会引起回流和重绘?
A:opacity单词意思为透明度,直观视觉效果就是颜色变淡了,但最终显示的颜色其实仍然可以用RGB三个通道来表示,从数值运算的角度来看,它实际上表示了它采用一般混合策略和其他颜色进行混合时的比例,也就是:

例如在网页默认的白底色上rgba(255,255,255)显示一个包含透明度的rgba(218,89,97,0.8)颜色, 那么颜色的RGB分量都按照上述公式进行计算就得到rgb(225,122,128),用取色器拾取一下渲染出来的点,结果和上述理论是一致的:
image.png
所以opacity这个属性本身就是用在重叠部分颜色处理的过程中使用的,对于分层的图原来说就可以看作是与图层内容无关的系数,因为合成过程中当前层中所有像素都需要经历上面的颜色混合公式,所以opacity的动画过程既不会影响布局,也不需要重绘。这样图层中保存的RGB像素数据的缓存在动画过程中也就不需要更新了,如果不使用opacity属性的话,每一帧对于变化部分都需要手动重计算RGB颜色值(这也就相当于是重绘了),因为这些区域的像素颜色一直都在变化,缓存也就没有意义。现在再来看看opacity的性能优势,就相对容易理解了。

小结
opacitytransform动画的高性能是由于其数学原理决定了可以使用缓存信息,而并不是因为它被硬件加速了。

参考:高性能的Web动画和渲染原理
使用CSS开启硬件加速提高性能
复合图层和简单图层

Chrome中的调试

Chrome的开发者工具中,Performance中可以看到详细的渲染过程:
image.png
image.png

资源外链的下载

上面介绍了html解析,渲染流程。但实际上,在解析html时,会遇到一些资源连接,此时就需要进行单独处理了
简单起见,这里将遇到的静态资源分为一下几大类(未列举所有):

  • CSS样式资源
  • JS脚本资源
  • img图片类资源

当遇到外链时的处理

当遇到上述的外链时,会单独开启一个线程去下载资源(http1.1中每一个资源的下载都对应一个tcp/ip连接)。

遇到CSS资源外链

CSS资源的处理有几个特点:

  • CSS下载时异步,不会阻塞浏览器构建DOM树
  • 但是会阻塞渲染,也就是在构建render时,会等到css下载解析完毕后才进行(这点与浏览器优化有关,防止css规则不断改变,避免了重复的构建)
  • 有例外,media query声明的CSS是不会阻塞渲染的

遇到脚本资源

JS脚本资源的处理有几个特点:

  • 阻塞浏览器的解析,也就是说发现一个外链脚本时,需等待脚本下载完成并执行后才会继续解析HTML
  • 浏览器的优化,一般现代浏览器有优化,在脚本阻塞时,也会继续下载其它资源(当然有并发上限),但是虽然脚本可以并行下载,解析过程仍然是阻塞的,也就是说必须这个脚本执行完毕后才会接下来的解析,并行下载只是一种优化而已
  • defer与async,普通的脚本是会阻塞浏览器解析的,但是可以加上defer或async属性,这样脚本就变成异步了,可以等到解析完毕后再执行

注意,defer和async是有区别的: defer是延迟执行,而async是异步执行。
简单的说(不展开):

  • async是异步执行,异步下载完毕后就会执行,不确保执行顺序,一定在onload前,但不确定在DOMContentLoaded事件的前或后
  • defer是延迟执行,在浏览器看起来的效果像是将脚本放在了body后面一样(虽然按规范应该是在DOMContentLoaded事件前,但实际上不同浏览器的优化效果不一样,也有可能在它后面)

遇到图片资源

遇到图片等资源时,直接就是异步下载,不会阻塞解析,下载完毕后直接用图片替换原有src的地方

loaded和domcontentloaded

简单的对比:

  • DOMContentLoaded 事件触发时,仅当DOM加载完成,不包括样式表,图片(譬如如果有async加载的脚本就不一定完成)。等同于$(document).ready()
  • load 事件触发时,页面上所有的DOM,样式表,脚本,图片都已经加载完成了

CSS的可视化模型

这一部分内容很多参考《精通CSS-高级Web标准解决方案》以及参考来源
前面提到了整体的渲染概念,但实际上文档树中的元素是按什么渲染规则渲染的,是可以进一步展开的,此部分内容即: CSS的可视化格式模型
先了解:

  • CSS中规定每一个元素都有自己的盒子模型(相当于规定了这个元素如何显示)
  • 然后可视化格式模型则是把这些盒子按照规则摆放到页面上,也就是如何布局
  • 换句话说,盒子模型规定了怎么在页面里摆放盒子,盒子的相互作用等等

说到底: CSS的可视化格式模型就是规定了浏览器在页面中如何处理文档树
关键字:

包含块(Containing Block)
控制框(Controlling Box)
BFC(Block Formatting Context)
IFC(Inline Formatting Context)
定位体系
浮动
...

另外,CSS有三种定位机制:普通流浮动绝对定位,如无特别提及,下文中都是针对普通流中的。

包含块(Containing Block)

一个元素的box的定位和尺寸,会与某一矩形框有关,这个框就称之为包含块。
元素会为它的子孙元素创建包含块,但是,并不是说元素的包含块就是它的父元素,元素的包含块与它的祖先元素的样式等有关系
譬如:

  • 根元素是最顶端的元素,它没有父节点,它的包含块就是初始包含块
  • static和relative的包含块由它最近的块级、单元格或者行内块祖先元素的内容框(content)创建
  • fixed的包含块是当前可视窗口
  • absolute的包含块由它最近的position 属性为absoluterelative或者fixed的祖先元素创建
    • 如果其祖先元素是行内元素,则包含块取决于其祖先元素的direction特性
    • 如果祖先元素不是行内元素,那么包含块的区域应该是祖先元素的内边距边界

控制框(Controlling Box)

块级元素和块框以及行内元素和行框的相关概念
块框:

  • 块级元素会生成一个块框(Block Box),块框会占据一整行,用来包含子box和生成的内容
  • 块框同时也是一个块包含框(Containing Box),里面要么只包含块框,要么只包含行内框(不能混杂),如果块框内部有块级元素也有行内元素,那么行内元素会被匿名块框包围

关于匿名块框的生成,示例:

<DIV>
Some text
  <P>More text</P>
</DIV>

div生成了一个块框,包含了另一个块框p以及文本内容Some text,此时Some text文本会被强制加到一个匿名的块框里面,被div生成的块框包含(其实这个就是IFC中提到的行框,包含这些行内框的这一行匿名块形成的框,行框和行内框不同)
换句话说:
如果一个块框在其中包含另外一个块框,那么我们强迫它只能包含块框,因此其它文本内容生成出来的都是匿名块框(而不是匿名行内框)
**行内框:

  • 一个行内元素生成一个行内框
  • 行内元素能排在一行,允许左右有其它元素

关于匿名行内框的生成,示例:

<P>Some <EM>emphasized</EM> text</P>

P元素生成一个块框,其中有几个行内框(如EM),以及文本Sometext,此时会专门为这些文本生成匿名行内框。
display属性的影响
display的几个属性也可以影响不同框的生成:

  • block,元素生成一个块框
  • inline,元素产生一个或多个的行内框
  • inline-block,元素产生一个行内级块框,行内块框的内部会被当作块块来格式化,而此元素本身会被当作行内级框来格式化(这也是为什么会产生BFC
  • none,不生成框,不再格式化结构中,当然了,另一个visibility: hidden则会产生一个不可见的框

总结:

  • 如果一个框里,有一个块级元素,那么这个框里的内容都会被当作块框来进行格式化,因为只要出现了块级元素,就会将里面的内容分块几块,每一块独占一行(出现行内可以用匿名块框解决)
  • 如果一个框里,没有任何块级元素,那么这个框里的内容会被当成行内框来格式化,因为里面的内容是按照顺序成行的排列。

BFC(Block Formatting Context)

块级格式化上下文。
FC即格式上下文,它定义框内部的元素渲染规则,比较抽象,譬如:

FC像是一个大箱子,里面装有很多元素
箱子可以隔开里面的元素和外面的元素(所以外部并不会影响FC内部的渲染)
内部的规则可以是:如何定位,宽高计算,margin折叠等等

不同类型的框参与的FC类型不同,譬如块级框对应BFC,行内框对应IFC
注意,并不是说所有的框都会产生FC,而是符合特定条件才会产生,只有产生了对应的FC后才会应用对应渲染规则
BFC规则:

在块格式化上下文中
每一个元素左外边与包含块的左边相接触(对于从右到左的格式化,右外边接触右边)
即使存在浮动也是如此(所以浮动元素正常会直接贴近它的包含块的左边,与普通元素重合)
除非这个元素也创建了一个新的BFC

总结几点BFC特点:

  1. 内部box在垂直方向,一个接一个的放置
  2. box的垂直方向由margin决定,属于同一个BFC的两个box间的margin会重叠
  3. BFC区域不会与float box重叠(可用于排版)
  4. BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此
  5. 计算BFC的高度时,浮动元素也参与计算(不会浮动坍塌)

如何触发BFC?

  1. 根元素
  2. float属性不为none
  3. positionabsolutefixed
  4. displayinline-block, flex, inline-flextabletable-celltable-caption
  5. overflow不为visible

这里提下,display: table,它本身不产生BFC,但是它会产生匿名框(包含display: table-cell的框),而这个匿名框产生BFC。
更多请自行在网上搜索。

IFC(Inline Formatting Context)

IFC即行内框产生的格式上下文
IFC规则

在行内格式化上下文中
框一个接一个地水平排列,起点是包含块的顶部。
水平方向上的 margin,border 和 padding 在框之间得到保留
框在垂直方向上可以以不同的方式对齐:它们的顶部或底部对齐,或根据其中文字的基线对齐

行框
包含那些框的长方形区域,会形成一行,叫做行框
行框的宽度由它的包含块和其中的浮动元素决定,高度的确定由行高度计算规则决定
行框的规则:

如果几个行内框在水平方向无法放入一个行框内,它们可以分配在两个或多个垂直堆叠的行框中(即行内框的分割)
行框在堆叠时没有垂直方向上的分割且永不重叠
行框的高度总是足够容纳所包含的所有框。不过,它可能高于它包含的最高的框(例如,框对齐会引起基线对齐)
行框的左边接触到其包含块的左边,右边接触到其包含块的右边。

结合补充下IFC规则:

浮动元素可能会处于包含块边缘和行框边缘之间
尽管在相同的行内格式化上下文中的行框通常拥有相同的宽度(包含块的宽度),它们可能会因浮动元素缩短了可用宽度,而在宽度上发生变化
同一行内格式化上下文中的行框通常高度不一样(如,一行包含了一个高的图形,而其它行只包含文本)
当一行中行内框宽度的总和小于包含它们的行框的宽,它们在水平方向上的对齐,取决于 `text-align` 特性
空的行内框应该被忽略
即不包含文本,保留空白符,margin/padding/border非0的行内元素,
以及其他常规流中的内容(比如,图片,inline blocks 和 inline tables),
并且不是以换行结束的行框,
必须被当作零高度行框对待

总结:

  • 行内元素总是会应用IFC渲染规则
  • 行内元素会应用IFC规则渲染,譬如text-align可以用来居中等
  • 块框内部,对于文本这类的匿名元素,会产生匿名行框包围,而行框内部就应用IFC渲染规则
  • 行内框内部,对于那些行内元素,一样应用IFC渲染规则
  • 另外,inline-block,会在元素外层产生IFC(所以这个元素是可以通过text-align水平居中的),当然,它内部则按照BFC规则渲染

相比BFC规则来说,IFC可能更加抽象(因为没有那么条理清晰的规则和触发条件)
但总的来说,它就是行内元素自身如何显示以及在框内如何摆放的渲染规则,这样描述应该更容易理解。

其它

当然还有有一些其它内容:

  • 譬如常规流,浮动,绝对定位等区别
  • 譬如浮动元素不包含在常规流中
  • 譬如相对定位,绝对定位,Fixed定位等区别
  • 譬如z-index的分层显示机制等

这里不一一展开,更多请参考:https://bbs.csdn.net/topics/340204423

JS引擎解析过程

前面有提到遇到JS脚本时,会等到它的执行,实际上是需要引擎解析的,这里展开描述(介绍主干流程)。

JS的解释阶段

首先得明确: JS是解释型语言,所以它无需提前编译,而是由解释器实时运行
引擎对JS的处理过程可以简述如下:

  1. 读取代码,进行词法分析(Lexical analysis),然后将代码分解为词元token。
  2. 对词元token进行语法分析(parsing),然后将代码整理出语法树(syntax tree)。
  3. 使用翻译器(translator),将代码转换为字节码(bytecode)
  4. 使用字节码解释器(bytecode interpreter),将字节码转为机器码。

最终计算机执行的就是机器码。
为了提高运行速度,现代浏览器一般采用即时编译(JIT-Just In Time compiler)。
即字节码只在运行时编译,用到哪一行就编译哪一行,并且把编译结果缓存(inline cache)。
这样整个程序的运行速度能得到显著提升。
而且,不同浏览器策略可能还不同,有的浏览器就省略了字节码的翻译步骤,直接转为机器码(如chrome的v8)
总结起来可以认为是: 核心的JIT编译器将源码编译成机器码运行。

JS的预处理阶段

上述将的是解释器的整体过程,这里再提下在正式执行JS前,还会有一个预处理阶段
(譬如变量提升,分号补全等)
预处理阶段会做一些事情,确保JS可以正确执行,这里仅提部分:
分号补全
JS执行是需要分号的,但为什么以下语句却可以正常运行呢?

console.log('a')
console.log('b')

原因就是JS解释器有一个Semicolon Insertion规则,它会按照一定规则,在适当的位置补充分号
譬如列举几条自动加分号的规则:

  • 当有换行符(包括含有换行符的多行注释),并且下一个token没法跟前面的语法匹配时,会自动补分号。
  • 当有}时,如果缺少分号,会补分号。
  • 程序源代码结束时,如果缺少分号,会补分号。

于是,上述的代码就变成了

console.log('a');
console.log('b');

所以可以正常运行
当然了,这里有一个经典的例子:

function b() {
    return
    {
        a: 'a'
    };
}

由于分号补全机制,所以它变成了:

function b() {
    return;
    {
        a: 'a'
    };
}

所以运行后是undefined

变量提升

一般包括函数提升和变量提升
譬如:

a = 1;
b();
function b() {
    console.log('b');
}
var a;

经过变量提升后,就变成:

function b() {
    console.log('b');
}
var a;
a = 1;
b();

这里没有展开,其实展开也可以牵涉到很多内容的
譬如可以提下变量声明,函数声明,形参,实参的优先级顺序,以及es6中let有关的临时死区等。

JS的执行阶段

此阶段的内容中的图片来源:深入理解JavaScript系列(10):JavaScript核心(晋级高手必读篇)
这里还有一篇文章可以帮助大家理解:作用域与作用域链
解释器解释完语法规则后,就开始执行,然后整个执行流程中大致包含以下概念:

  • 执行上下文,执行堆栈概念(如全局上下文,当前活动上下文)
  • VO(变量对象)和AO(活动对象)
  • 作用域链
  • this机制等

这些概念如果深入讲解的话内容过多,因此这里仅提及部分特性
执行上下文简单解释

  • JS有执行上下文
  • 浏览器首次载入脚本,它将创建全局执行上下文,并压入执行栈栈顶(不可被弹出)
  • 然后每进入其它作用域就创建对应的执行上下文并把它压入执行栈的顶部
  • 一旦对应的上下文执行完毕,就从栈顶弹出,并将上下文控制权交给当前的栈。
  • 这样依次执行(最终都会回到全局执行上下文)

譬如,如果程序执行完毕,被弹出执行栈,然后有没有被引用(没有形成闭包),那么这个函数中用到的内存就会被垃圾处理器自动回收:
image.png
然后执行上下文与VO,作用域链,this的关系是:
每一个执行上下文,都有三个重要属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

image.png)image.png)image.png
image.png
VO与AO
VO是执行上下文的属性(抽象概念),但是只有全局上下文的变量对象允许通过VO的属性名称来间接访问(因为在全局上下文里,全局对象自身就是变量对象)
AO(activation object),当函数被调用者激活,AO就被创建了
可以理解为:

  • 在函数上下文中:VO === AO
  • 在全局上下文中:VO === this === global

总的来说,VO中会存放一些变量信息(如声明的变量,函数,arguments参数等等)
作用域链
它是执行上下文中的一个属性,原理和原型链很相似,作用很重要。
譬如流程简述:

在函数上下文中,查找一个变量foo
如果函数的VO中找到了,就直接使用
否则去它的父级作用域链中(__parent__)找
如果父级中没找到,继续往上找
直到全局上下文中也没找到就报错

image.png

this指针
这也是JS的核心知识之一,由于内容过多,这里就不展开,仅提及部分
注意:this是执行上下文环境的一个属性,而不是某个变量对象的属性
因此:

  • this是没有一个类似搜寻变量的过程
  • 当代码中使用了this,这个 this的值就直接从执行的上下文中获取了,而不会从作用域链中搜寻
  • this的值只取决中进入上下文时的情况

所以经典的例子:

var baz = 200;
var bar = {
    baz: 100,
    foo: function() {
        console.log(this.baz);
    }
};
var foo = bar.foo;
// 进入环境:global
foo(); // 200,严格模式中会报错,Cannot read property 'baz' of undefined
// 进入环境:global bar
bar.foo(); // 100

就要明白了上面this的介绍,上述例子很好理解
更多参考:
深入理解JavaScript系列(13):This? Yes,this!

回收机制

JS有垃圾处理器,所以无需手动回收内存,而是由垃圾处理器自动处理。
一般来说,垃圾处理器有自己的回收策略。
譬如对于那些执行完毕的函数,如果没有外部引用(被引用的话会形成闭包),则会回收。(当然一般会把回收动作切割到不同的时间段执行,防止影响性能)
常用的两种垃圾回收规则是:

  • 标记清除
  • 引用计数

Javascript引擎基础GC方案是(simple GC):mark and sweep(标记清除),简单解释如下:

  1. 遍历所有可访问的对象。
  2. 回收已不可访问的对象。

譬如:(出自javascript高程)
当变量进入环境时,例如,在函数中声明一个变量,就将这个变量标记为“进入环境”。> 从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。

而当变量离开环境时,则将其标记为“离开环境”。
垃圾回收器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。
然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记(闭包,也就是说在环境中的以及相关引用的变量会被去除标记)。
而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。
最后,垃圾回收器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。

关于引用计数,简单点理解:
跟踪记录每个值被引用的次数,当一个值被引用时,次数+1,减持时-1,下次垃圾回收器会回收次数为0的值的内存(当然了,容易出循环引用的bug)
GC的缺陷
和其他语言一样,javascript的GC策略也无法避免一个问题: GC时,停止响应其他操作
这是为了安全考虑。
而Javascript的GC在100ms甚至以上
对一般的应用还好,但对于JS游戏,动画对连贯性要求比较高的应用,就麻烦了。
这就是引擎需要优化的点: 避免GC造成的长时间停止响应。
GC优化策略
这里介绍常用到的:分代回收(Generation GC)
目的是通过区分“临时”与“持久”对象:

  • 多回收“临时对象”区(young generation
  • 少回收“持久对象”区(tenured generation
  • 减少每次需遍历的对象,从而减少每次GC的耗时。

像node v8引擎就是采用的分代回收(和java一样,作者是java虚拟机作者。)
更多可以参考:
V8 内存浅析

其他

可以提到跨域

譬如发出网络请求时,会用AJAX,如果接口跨域,就会遇到跨域问题
可以参考:
ajax跨域,这应该是最全的解决方案了

可以提到web安全

譬如浏览器在解析HTML时,有XSSAuditor,可以延伸到web安全相关领域
可以参考:
AJAX请求真的不安全么?谈谈Web安全与AJAX的关系。

更多

如可以提到viewport概念,讲讲物理像素,逻辑像素,CSS像素等概念
如熟悉Hybrid开发的话可以提及一下Hybrid相关内容以及优化

感谢

感谢撒网要见鱼的分享。

多页面打包配置

webpack4.0(七):多页面打包配置

webpack.config.js

const path = require('path');
const fs = require('fs');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');


/**
 * @param { Object } 
 * @return { Object }
 */
const makeHtml = (configs) => {
    let pluginsDll = [
        new CleanWebpackPlugin(),
    ];
    Object.keys(configs.entry).forEach(key => {
        console.log(key)
        pluginsDll.push(
            new HtmlWebpackPlugin({
                filename:`${key}.html`,
                template:'./src/index.html',
                chunks:['lodash','react',key]
            })
        )
    })
    const files = fs.readdirSync(path.resolve(__dirname,'../dll'));
    files.forEach(file => {
        if(/.*\.dll.js$/.test(file)){
            pluginsDll.push(
                new AddAssetHtmlWebpackPlugin({
                    filepath:path.resolve(__dirname,'../dll',file)
                })
            )
        }
        if(/.*\.manifest.json$/.test(file)){
            new webpack.DllReferencePlugin({
                manifest: require(path.resolve(__dirname,'../dll',file)),
            })
        }
    });
    return pluginsDll;
}



const config = {
    entry:{
        index:'./src/index.js',
        list:'./src/list.js'
    },
    output:{
        path:path.resolve(__dirname,'../dist')
    },
    module:{
        rules:[
            {
                test:/\.jsx?$/,
                loader:'babel-loader'
            }
        ]
    }
};

config.plugins = makeHtml(config);

module.exports = config;

webpack.dll.js

const path = require('path');
const webpack = require('webpack');

module.exports = {
    mode:"production",
    entry:{
        react:['react','react-dom','react-router-dom'],
        lodash:['lodash-es']
    },
    output:{
        filename:'[name].dll.js',
        path : path.resolve(__dirname,'../dll'),
        library:'_dll_[name]'
    },
    plugins:[
        new webpack.DllPlugin({
            name: "_dll_[name]",
            path: path.join(__dirname, "../dll/[name].manifest.json"),
        })
    ]
}