前端基础知识储备

706 阅读45分钟

css相关

1. 盒模型

页面渲染时,dom 元素所采用的 布局模型。可通过box-sizing进行设置。根据计算宽高的区域可分为:

  • content-box (W3C 标准盒模型)
  • border-box (IE 盒模型)
  • padding-box (FireFox 曾经支持)
  • margin-box (浏览器未实现)
image.png

Tips: 理论上是有上面 4 种盒子,但现在 w3c 与 mdn 规范中均只支持 content-boxborder-box

2. BFC

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

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

  • 触发条件:

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

    • 属于同一个 BFC 的两个相邻 Box 垂直排列
    • 属于同一个 BFC 的两个相邻 Box 的 margin 会发生重叠
    • BFC 中子元素的 margin box 的左边, 与包含块 (BFC) border box的左边相接触 (子元素 absolute 除外)
    • BFC 的区域不会与 float 的元素区域重叠
    • 计算 BFC 的高度时,浮动子元素也参与计算
    • 文字层不会被浮动层覆盖,环绕于周围
  • 应用:

    • 阻止margin重叠
    • 可以包含浮动元素 —— 清除内部浮动(清除浮动的原理是两个div都位于同一个 BFC 区域之中)
    • 自适应两栏布局
    • 可以阻止元素被浮动元素覆盖

3.层叠上下文

元素提升为一个比较特殊的图层,在三维空间中 (z轴) 高出普通元素一等。

  • 触发条件

    • 根层叠上下文(html)

    • position

    • css3属性

      • flex
      • transform
      • opacity
      • filter
      • will-change
      • -webkit-overflow-scrolling
  • 层叠等级:层叠上下文在z轴上的排序

    • 在同一层叠上下文中,层叠等级才有意义
    • z-index的优先级最高

4. 居中布局

  • 水平居中

    • 行内元素: text-align: center
    • 块级元素: margin: 0 auto
    • absolute + transform
    • flex + justify-content: center
  • 垂直居中

    • line-height: height
    • absolute + transform
    • flex + align-items: center
    • table
  • 水平垂直居中

    • absolute + transform
    • flex + justify-content + align-items

5. 选择器优先级

  • !important > 行内样式 > #id > .class > tag > * > 继承 > 默认
  • 选择器 从右往左 解析

6.去除浮动影响,防止父级高度塌陷

  • 通过增加尾元素清除浮动

    • :after / <br> : clear: both
  • 创建父级 BFC

  • 父级设置高度

7.link 与 @import 的区别

  • link功能较多,可以定义 RSS,定义 Rel 等作用,而@import只能用于加载 css
  • 当解析到link时,页面会同步加载所引的 css,而@import所引用的 css 会等到页面加载完才被加载
  • @import需要 IE5 以上才能使用
  • link可以使用 js 动态引入,@import不行

8. CSS预处理器(Sass/Less/Postcss)

CSS预处理器的原理: 是将类 CSS 语言通过 Webpack 编译 转成浏览器可读的真正 CSS。在这层编译之上,便可以赋予 CSS 更多更强大的功能,常用功能:

  • 嵌套
  • 变量
  • 循环语句
  • 条件语句
  • 自动前缀
  • 单位转换
  • mixin复用

面试中一般不会重点考察该点,一般介绍下自己在实战项目中的经验即可~

9.CSS动画

  • transition: 过渡动画

    • transition-property: 属性
    • transition-duration: 间隔
    • transition-timing-function: 曲线
    • transition-delay: 延迟
    • 常用钩子: transitionend
  • animation / keyframes

    • animation-name: 动画名称,对应@keyframes

    • animation-duration: 间隔

    • animation-timing-function: 曲线

    • animation-delay: 延迟

    • animation-iteration-count: 次数

      • infinite: 循环动画
    • animation-direction: 方向

      • alternate: 反向播放
    • animation-fill-mode: 静止模式

      • forwards: 停止时,保留最后一帧
      • backwards: 停止时,回到第一帧
      • both: 同时运用 forwards / backwards
    • 常用钩子: animationend

  • 动画属性: 尽量使用动画属性进行动画,能拥有较好的性能表现

    • translate
    • scale
    • rotate
    • skew
    • opacity
    • color

10 :移动端适配

rem 是相对于html的font-size,font-size是动态计算的,设置为26.66667vm,正好是100px。100vm=375px

通常,CSS 并不是重点的考察领域,但这其实是由于现在国内业界对 CSS 的专注不够导致的,真正精通并专注于 CSS 的团队和人才并不多。因此如果能在 CSS 领域有自己的见解和经验,反而会为相当的加分和脱颖而出。

JavaScript

1. 原型 / 构造函数 / 实例

  • 原型(prototype): 一个简单的对象,用于实现对象的 属性继承。可以简单的理解成对象的爹。在 Firefox 和 Chrome 中,每个JavaScript对象中都包含一个__proto__ (非标准)的属性指向它爹(该对象的原型),可obj.__proto__进行访问。
  • 构造函数: 可以通过new新建一个对象 的函数。
  • 实例: 通过构造函数和new创建出来的对象,便是实例。 实例通过__proto__指向原型,通过constructor指向构造函数

说了一大堆,大家可能有点懵逼,这里来举个栗子,以Object为例,我们常用的Object便是一个构造函数,因此我们可以通过它构建实例。

// 实例
const instance = new Object()

则此时, 实例为instance, 构造函数为Object,我们知道,构造函数拥有一个prototype的属性指向原型,因此原型为:

// 原型
const prototype = Object.prototype

这里我们可以来看出三者的关系:

实例.__proto__ === 原型

原型.constructor === 构造函数

构造函数.prototype === 原型

// 这条线其实是是基于原型进行获取的,可以理解成一条基于原型的映射线
// 例如: 
// const o = new Object()
// o.constructor === Object   --> true
// o.__proto__ = null;
// o.constructor === Object   --> false
// 注意: 其实实例上并不是真正有 constructor 这个指针,它其实是从原型链上获取的
//      instance.hasOwnProperty('constructor') === false   (感谢 刘博海 Brian 童鞋🥳)
实例.constructor === 构造函数


 const f = new FO()     FO--->构造函数   f--->实例
 f.__proto__ =  FO.prototype;   --->   f.__proto__ = null
 (FO.prototype). constructor = FO    ===>    原型.constructor=(构造函数)

image.png

放大来看,我画了张图供大家彻底理解:

2.原型链:

原型链是由原型对象组成,每个对象都有 __proto__ 属性,指向了创建该对象的构造函数的原型,__proto__ 将对象连接起来组成了原型链。是一个用来实现继承和共享属性的有限的对象链。

  • 属性查找机制: 当查找对象的属性时,如果实例对象自身不存在该属性,则沿着原型链往上一级查找,找到时则输出,不存在时,则继续沿着原型链往上一级查找,直至最顶级的原型对象Object.prototype,如还是没找到,则输出 undefined
  • 属性修改机制: 只会修改实例对象本身的属性,如果不存在,则进行添加该属性,如果需要修改原型的属性时,则可以用: b.prototype.x = 2;但是这样会造成所有继承于该对象的实例的属性发生改变。

3. 执行上下文(EC)

执行上下文可以简单理解为一个对象:

  • 它包含三个部分:

    • 变量对象(VO)
    • 作用域链(词法作用域)
    • this指向
  • 它的类型:

    • 全局执行上下文
    • 函数执行上下文
    • eval执行上下文
  • 代码执行过程:

    • 创建 全局上下文 (global EC)
    • 全局执行上下文 (caller) 逐行 自上而下 执行。遇到函数时,函数执行上下文 (callee) 被push到执行栈顶层
    • 函数执行上下文被激活,成为 active EC, 开始执行函数中的代码,caller 被挂起
    • 函数执行完后,callee 被pop移除出执行栈,控制权交还全局上下文 (caller),继续执行

2.变量对象

变量对象,是执行上下文中的一部分,可以抽象为一种 数据作用域,其实也可以理解为就是一个简单的对象,它存储着该执行上下文中的所有 变量和函数声明(不包含函数表达式)

活动对象 (AO): 当变量对象所处的上下文为 active EC 时,称为活动对象。

3. 作用域

执行上下文中还包含作用域链。理解作用域之前,先介绍下作用域。作用域其实可理解为该上下文中声明的 变量和声明的作用范围。可分为 块级作用域函数作用域

特性:

  • 声明提前: 一个声明在函数体内都是可见的, 函数优先于变量
  • 非匿名自执行函数,函数变量为 只读 状态,无法修改
let foo = function() { console.log(1) };
(function foo() {
    foo = 10  // 由于foo在函数中只为可读,因此赋值无效
    console.log(foo)
}()) 

// 结果打印:  ƒ foo() { foo = 10 ; console.log(foo) }

4.作用域链

我们知道,我们可以在执行上下文中访问到父级甚至全局的变量,这便是作用域链的功劳。作用域链可以理解为一组对象列表,包含 父级和自身的变量对象,因此我们便能通过作用域链访问到父级里声明的变量或者函数。

  • 由两部分组成:

    • [[scope]]属性: 指向父级变量对象和作用域链,也就是包含了父级的[[scope]]AO
    • AO: 自身活动对象

如此 [[scopr]]包含[[scope]],便自上而下形成一条 链式作用域

5. 闭包

闭包是指有权访问另一个函数作用域中的变量的函数--《JavaScript高级程序设计》

原因内部的函数存在外部作用域的引用就会导致闭包

密闭的容器,类似于set、map容器,存储数据的。闭包中的变量存储的位置是堆内存。 闭包是一个对象,存放数据的格式为 key-value 形式

应用:

  1. return 回一个函数
  2. 函数作为参数
  3. IIFE(自执行函数)
  4. 循环赋值
  5. 使用回调函数就是在使用闭包-
  6. 节流防抖
  7. 柯里化实现
//4. 循环赋值
for(var i = 0; i<10; i++){
  (function(j){
       setTimeout(function(){
        console.log(j)
    }, 1000) 
  })(i)
}

//5.  使用回调函数就是在使用闭包
window.name = '林一一'
setTimeout(function timeHandler(){
  console.log(window.name);
}, 100)

函数柯里化

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

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

    const add1 = add(1)

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

6. script 引入方式:

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

7. 对象的拷贝

  • 浅拷贝: 以赋值的形式拷贝引用对象,仍指向同一个地址,修改时原对象也会受到影响

    • Object.assign
    • 展开运算符(...)
  • 深拷贝: 完全拷贝一个新对象,修改时原对象不再受到任何影响

    • JSON.parse(JSON.stringify(obj)): 性能最快

      • 具有循环引用的对象时,报错
      • 当值为函数、undefined、或symbol时,无法拷贝
    • 递归进行逐一赋值

8. new运算符的执行过程

  • 新生成一个对象
  • 链接到原型: obj.__proto__ = Con.prototype
  • 绑定this: apply
  • 返回新对象(如果构造函数有自己 retrun 时,则返回该值)

9. instanceof原理

能在实例的 原型对象链 中找到该构造函数的prototype属性所指向的 原型对象,就返回true。即:

// __proto__: 代表原型对象链
    instance.[__proto__...] === instance.constructor.prototype

 // return true

10. 代码的复用

当你发现任何代码开始写第二遍时,就要开始考虑如何复用。一般有以下的方式:

  • 函数封装
  • 继承
  • 复制extend
  • 混入mixin
  • 借用apply/call

11. 继承

在 JS 中,继承通常指的便是 原型链继承,也就是通过指定原型,并可以通过原型链继承原型上的属性或者方法。

  • 最优化: 圣杯模式
 var inherit = (function(c,p){
    	var F = function(){};
    	return function(c,p){
    		F.prototype = p.prototype;
    		c.prototype = new F();
    		c.uber = p.prototype;
    		c.prototype.constructor = c;
    	}
    })();
  • 使用 ES6 的语法糖 class / extends

12. 类型转换

大家都知道 JS 中在使用运算符号或者对比符时,会自带隐式转换,规则如下:

  • -、*、/、% :一律转换成数值后计算

  • +:

    • 数字 + 字符串 = 字符串, 运算顺序是从左到右
    • 数字 + 对象, 优先调用对象的valueOf -> toString
    • 数字 + boolean/null -> 数字
    • 数字 + undefined -> NaN
  • [1].toString() === '1'

  • {}.toString() === '[object object]'

  • NaN !== NaN+undefined 为 NaN

13. 类型判断

判断 Target 的类型,单单用 typeof 并无法完全满足,这其实并不是 bug,本质原因是 JS 的万物皆对象的理论。因此要真正完美判断时,我们需要区分对待:

  • 基本类型(null): 使用 String(null)
  • 基本类型(string / number / boolean / undefined) + function: 直接使用 typeof即可
  • 其余引用类型(Array / Date / RegExp Error): 调用toString后根据[object XXX]进行判断

很稳的判断封装:

let class2type = {}
    'Array Date RegExp Object Error'.split(' ').forEach(e => class2type[ '[object ' + e + ']' ] = e.toLowerCase()) 

    function type(obj) {
        if (obj == null) return String(obj)
        return typeof obj === 'object' ? class2type[ Object.prototype.toString.call(obj) ] || 'object' : typeof obj
    }

基本类型(值类型): Number(数字),String(字符串),Boolean(布尔),Symbol(符号),null(空),undefined(未定义)在内存中占据固定大小,保存在栈内存

引用类型(复杂数据类型): Object(对象)、Function(函数)。其他还有Array(数组)、Date(日期)、RegExp(正则表达式)、特殊的基本包装类型(String、Number、Boolean) 以及单体内置对象(Global、Math)等 引用类型的值是对象 保存在堆内存中,栈内存存储的是对象的变量标识符以及对象在堆内存中的存储地址。

14. 模块化

模块化开发在现代开发中已是必不可少的一部分,它大大提高了项目的可维护、可拓展和可协作性。通常,我们 在浏览器中使用 ES6 的模块化支持,在 Node 中使用 commonjs 的模块化支持。

  • 分类:

    • es6: import / export
    • commonjs: require / module.exports / exports
    • amd: require / defined
  • requireimport的区别

    • require支持 动态导入import不支持,正在提案 (babel 下可支持)
    • require同步 导入,import属于 异步 导入
    • require值拷贝,导出值变化不会影响导入值;import指向 内存地址,导入值会随导出值而变化

15. 防抖与节流

防抖与节流函数是一种最常用的 高频触发优化方式,能对性能有较大的帮助。

  • 防抖 (debounce) : 多次触发事件,事件处理函数只能执行一次,并且是在触发操作结束时执行。

也就是说,当一个事件被触发准备执行事件函数前,会等待一定的时间(这时间是码农自己去定义的,比如 1 秒),如果没有再次被触发,那么就执行,如果被触发了,那就本次作废,重新从新触发的时间开始计算,并再次等待 1 秒,直到能最终执行!

/**
 * 防抖函数  一个需要频繁触发的函数,在规定时间内,只让最后一次生效,前面的不生效
 * @param fn要被节流的函数
 * @param delay规定的时间
 */
function debounce(fn, delay) {
    //记录上一次的延时器
    var timer = null;
    return function () {
       //清除上一次的演示器
        clearTimeout(timer);
        //重新设置新的延时器
        timer = setTimeout(()=>{
            //修正this指向问题
            fn.apply(this);
        }, delay); 
    }
}
document.getElementById('btn').onclick = debounce(function () {
    console.log('按钮被点击了' + Date.now());
}, 1000);
  • 节流(throttle) : 规定的时间内,函数只能被调用一次,且是最先被触发调用的那次。
  • 应用场景: 滚动加载更多、搜索框搜的索联想功能、高频点击、表单重复提交……(通常每隔 100~500 ms执行一次即可)。
//节流函数 一个函数执行一次后,只有大于设定的执行周期才会执行第二次。
//有个需要频繁触发的函数,出于优化性能的角度,在规定时间内,只让函数触发的第一次生效,后面的不生效。
//@param fn要被节流的函数 
//@param delay规定的时间

function throttle(fn, delay) {
    //记录上一次函数触发的时间
    var lastTime = 0;
    return function(){
        //记录当前函数触发的时间
        var nowTime = Date.now();
        if(nowTime - lastTime > delay){
            //修正this指向问题
            fn.call(this);
            //同步执行结束时间
            lastTime = nowTime;
        }
    }
}
document.onscroll = throttle(function () {
    console.log('scllor事件被触发了' + Date.now());
}, 200); 

16. 函数执行改变this

由于 JS 的设计原理: 在函数中,可以引用运行环境中的变量。因此就需要一个机制来让我们可以在函数体内部获取当前的运行环境,这便是this

因此要明白 this 指向,其实就是要搞清楚 函数的运行环境,说人话就是,谁调用了函数。例如:

  • obj.fn(),便是 obj 调用了函数,既函数中的 this === obj
  • fn(),这里可以看成 window.fn(),因此 this === window

但这种机制并不完全能满足我们的业务需求,因此提供了三种方式可以手动修改 this 的指向:

  • call: fn.call(target, 1, 2)
  • apply: fn.apply(target, [1, 2])
  • bind: fn.bind(target)(1,2)

17. ES6/ES7

由于 Babel 的强大和普及,现在 ES6/ES7 基本上已经是现代化开发的必备了。通过新的语法糖,能让代码整体更为简洁和易读。

  • 声明

    • let / const: 块级作用域、不存在变量提升、暂时性死区、不允许重复声明
    • const: 声明常量,无法修改
  • 解构赋值

  • class / extend: 类声明与继承

  • Set / Map: 新的数据结构

  • 异步解决方案:

    • Promise的使用与实现

      1、接收一个 Promise 实例的数组或具有 Iterator 接口的对象,

      2、如果元素不是 Promise 对象,则使用 Promise.resolve 转成 Promise 对象

      3、如果全部成功,状态变为 resolved,返回值将组成一个数组传给回调

      4、只要有一个失败,状态就变为 rejected,返回值将直接传递给回调
      all() 的返回值也是新的 Promise 对象

// promise.all 的实现

function promiseAll(promises) {
  return new Promise(function(resolve, reject) {
    if (!isArray(promises)) {
      return reject(new TypeError('arguments must be an array'));
    }
    var resolvedCounter = 0;
    var promiseNum = promises.length;
    var resolvedValues = new Array(promiseNum);
    for (var i = 0; i < promiseNum; i++) {
      (function(i) {
        Promise.resolve(promises[i]).then(function(value) {
          resolvedCounter++
          resolvedValues[i] = value
          if (resolvedCounter == promiseNum) {
            return resolve(resolvedValues)
          }
        }, function(reason) {
          return reject(reason)
        })
      })(i)
    }
  })
}

*   `generator`:

    *   `yield`: 暂停代码
    *   `next()`: 继续执行代码

<!---->

    function* helloWorld() {
      yield 'hello';
      yield 'world';
      return 'ending';
    }

    const generator = helloWorld();

    generator.next()  // { value: 'hello', done: false }

    generator.next()  // { value: 'world', done: false }

    generator.next()  // { value: 'ending', done: true }

    generator.next()  // { value: undefined, done: true }

*   `await / async`: 是`generator`的语法糖, babel中是基于`promise`实现。

<!---->

    async function getUserByAsync(){
       let user = await fetchUser();
       return user;
    }

    const user = await getUserByAsync()
    console.log(user)复制代码

18. AST

抽象语法树 (Abstract Syntax Tree) ,是将代码逐字母解析成 树状对象 的形式。这是语言之间的转换、代码语法检查,代码风格检查,代码格式化,代码高亮,代码错误提示,代码自动补全等等的基础。例如: 转存失败,建议直接上传图片文件 function square(n){ return n * n }复制代码

通过解析转化成的AST如下图:

19. babel编译原理

  • babylon 将 ES6/ES7 代码解析成 AST
  • babel-traverse 对 AST 进行遍历转译,得到新的 AST
  • 新 AST 通过 babel-generator 转换成 ES5

21. 数组(array)

  • map: 遍历数组,返回回调返回值组成的新数组

  • forEach: 无法break,可以用try/catchthrow new Error来停止

  • filter: 过滤

  • some: 有一项返回true,则整体为true

  • every: 有一项返回false,则整体为false

  • join: 通过指定连接符生成字符串

  • find: 返回符合传入测试(函数)条件的数组元素 ----(返回值)

  • findIndex:返回符合传入测试(函数)条件的数组元素索引 ----(返回索引)

  • push / pop: 末尾推入和弹出,改变原数组, push 返回数组长度, pop 返回原数组最后一项;

  • unshift / shift: 头部推入和弹出,改变原数组,unshift 返回数组长度,shift 返回原数组第一项 ;

  • sort(fn) / reverse: 排序与反转,改变原数组

  • concat: 连接数组,不影响原数组, 浅拷贝

  • slice(start, end): 返回截断后的新数组,不改变原数组

  • splice(start, number, value...): 返回删除元素组成的数组,value 为插入项,改变原数组

  • indexOf / lastIndexOf(value, fromIndex): 查找数组项,返回对应的下标

  • array.reduce((t, v, i, a) => {}, initValue)

    • 参数

    • callback:回调函数(必选)

    • initValue:初始值(可选)

  • 回调函数的参数
    • total(t):累计器完成计算的返回值(必选)
    • value(v):当前元素(必选)
    • index(i):当前元素的索引(可选)
    • array(a):当前元素所属的数组对象(可选)
  • reduce / reduceRight(fn(prev, cur), defaultPrev): 两两执行,prev 为上次化简函数的return值,cur 为当前值

    • 当传入 defaultPrev 时,从第一项开始;
    • 当未传入时,则为第二项
  • 数组乱序:

var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
arr.sort(function () {
    return Math.random() - 0.5;
});复制代码
  • 数组拆解: flat: [1,[2,3]] --> [1, 2, 3]
Array.prototype.flat = function() {
    return this.toString().split(',').map(item => +item )
}复制代码

浏览器

session和cookie的区别

(1)session保存在服务器端(客户端仅保存一个sessionID),Cookie保存在客户端。

(2)session保存的是对象,Cookie保存的是字符串。cookie的存储限制数据量不大于4KB,而session的容量则是无限量的。

(3)session不能区分路径,同一个用户在访问同一个Web应用程序期间,所有的session在任何路径都可访问。Cookie中如果设置了路径参数,则在其他路径下无法访问。

(4)session默认保存SeesionID的方法是Cookie,如果客户端禁用了Cookie,服务器端应采用其他的替代方法替代SessionID。

(5)session在会话结束后就会关闭,但是Cookie可以经过持久化而长期保存在客户端的本地硬盘上。所以session也更安全。

1. 跨标签页通讯

不同标签页间的通讯,本质原理就是去运用一些可以 共享的中间介质,因此比较常用的有以下方法:

  • 通过父页面window.open()和子页面postMessage

    • 异步下,通过 window.open('about: blank')tab.location.href = '*'
  • 设置同域下共享的localStorage与监听window.onstorage

    • 重复写入相同的值无法触发
    • 会受到浏览器隐身模式等的限制
  • 设置共享cookie与不断轮询脏检查(setInterval)

  • 借助服务端或者中间层实现

2. 浏览器架构

  • 用户界面
  • 主进程
  • 内核
    • 渲染引擎
    • JS 引擎
      • 执行栈
    • 事件触发线程
      • 消息队列
        • 微任务
        • 宏任务
    • 网络异步线程
    • 定时器线程

3. 浏览器下事件循环(Event Loop)

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

  • 微任务 microtask(jobs) : promise / Async,Await(实际就是promise) / process.nextTick / ajax / Object.observe(该方法已废弃)
  • 宏任务 macrotask(task) : script(js整体代码) / setTimout /setInterval/ IO操作 / UI Rendering(ui交互)/ postMessage
image.png

async2 end => Promise => async1 end => promise1 => promise2 => setTimeout 但是,对于async/await

4. 从输入 url 到展示的过程

  • URL释义

  • 缓存处理

  • 域名解析

  • TCP 三次握手

  • HTTP 请求

  • HTTP 响应

  • TCP 四次挥手

  • 浏览器渲染

    • HTML parser --> DOM Tree

      • 标记化算法,进行元素状态的标记
      • dom 树构建
    • CSS parser --> Style Tree

      • 解析 css 代码,生成样式树
    • attachment --> Render Tree

      • 结合 dom树 与 style树,生成渲染树
    • layout: 布局

    • GPU painting: 像素绘制页面

URL释义

scheme://host.domain:port/path/filename

各部分解释如下: scheme:定义因特网服务的类型。常见的协议有 http、https、ftp、file,其中最常见的类型是 http,而 https 则是进行加密的网络传输。

host:定义域主机(http 的默认主机是 www)

domain:定义因特网域名,比如 mozilla.org

port:定义主机上的端口号(http 的默认端口号是 80,https 的默认端口号是 443)

path:定义服务器上的路径(如果省略,则文档必须位于网站的根目录中)。

filename:定义文档/资源的名称

TCP 三次握手

在客户端发送数据之前会用客户端 IP 地址发起 TCP 三次握手用以同步客户端和服务端的序列号和确认号,并交换 TCP 窗口大小信息来建立 TCP 连接。

先清楚一个概念,http请求与TCP连接之间的关系:在客户端向服务端请求和返回的过程中,是需要去创建一个TCP connection,因为http是不存在连接这样一个概念的,它只有请求和响应这样一个概念,请求和响应都是一个数据包,中间要通过一个传输通道,这个传输通道就是在TCP里面创建了一个从客户端发起和服务端接收的一个连接。

握手时序 TCP标示

SYN(synchronous 建立联机)

ACK(acknowledgement 确认)

Seq(Sequence number 顺序号码)

第一次握手: 建立连接,客户端发送SYN=1、随机产生Seq=client_isn的数据包到服务器主机,等待服务器确认。

第二次握手: 服务器主机收到请求后确认联机(可以接受数据),发起第二次握手请求,ACK=(A的Seq+1)、SYN=1,随机产生Seq=server_isn的数据包到客户端。

第三次握手: 客户端收到后检查ACK是否正确,若正确,客户端会再发送确认包。ACK=服务器主机的Seq+1、Seq=client_isn,服务器主机收到后确认Seq值与ACK值,若正确,则建立连接。

如果是https连接,要在tcp三次握手之后创建加密连接,来加密通信的上层数据(建立 SSL/TLS 连接)

TCP/1.1 浏览器为每一个域名维护了6个TCP连接,超过排队等待。 TCP/2.0 浏览器为每个域名维护1个TCP持久连接。

image.png

浏览器渲染

渲染进程中主要包含以下线程:

主线程 Main thread 工作线程 Worker thread 排版线程 Compositor thread 光栅线程 Raster thread 其主要作流程如下所示

  1. 浏览器创建Document对象并解析HTML,将解析到的元素和⽂本节点添加到⽂档中,此时 document.readystate 为 loading(正在加载)。
  2. HTML解析器遇到没有 asyncdefer 的 script 时,将他们添加到⽂档中,然后执⾏⾏内或外部脚本。这些脚本会同步执⾏,并且在脚本下载和执⾏时解析器会暂停。这样就可以⽤ document.write() 把⽂本插⼊到输⼊流中。同步脚本经常简单定义函数和注册事件处理程序,他们可以遍历和操作script和他们之前的⽂档内容。
  3. 当解析器遇到设置了 async 属性的 script 时,开始下载脚本并继续解析⽂档。脚本会在它下载完成后尽快执⾏,但是解析器不会停下来等它下载。异步脚本禁止使⽤ document.write(),它们可以访问⾃⼰script和之前的⽂档元素
  4. 当⽂档完成解析,document.readystate 变成 interactive(可交互)
  5. 所有 defer 脚本会按照在⽂档出现的顺序执⾏,延迟脚本能访问完整⽂档树,禁止使⽤document.write()
  6. 浏览器在 Document 对象上触发 DOMContentLoaded 事件
  7. 此时⽂档完全解析完成,浏览器可能还在等待如图⽚等内容加载,等这些内容完成载⼊ 并且所有异步脚本完成载⼊和执⾏,document.readState 变为complete(完成),window 触发 load 事件。
image.png

5. 重绘与回流

当元素的样式发生变化时,浏览器需要触发更新,重新绘制元素。这个过程中,有两种类型的操作,即重绘与回流。

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

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

    • 页面初次渲染

    • 浏览器窗口大小改变

    • 元素尺寸、位置、内容发生改变

    • 元素字体大小变化

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

    • 激活 CSS 伪类(例如::hover)

    • 查询某些属性或调用某些方法

      • clientWidth、clientHeight、clientTop、clientLeft
      • offsetWidth、offsetHeight、offsetTop、offsetLeft
      • scrollWidth、scrollHeight、scrollTop、scrollLeft
      • getComputedStyle()
      • getBoundingClientRect()
      • scrollTo()

回流必定触发重绘,重绘不一定触发回流。重绘的开销较小,回流的代价较高。

最佳实践:
  • css

    • 避免使用table布局
    • 将动画效果应用到position属性为absolutefixed的元素上
  • javascript

    • 避免频繁操作样式,可汇总后统一 一次修改
    • 尽量使用class进行样式修改
    • 减少dom的增删次数,可使用 字符串 或者 documentFragment 一次性插入
    • 极限优化时,修改样式可将其display: none后修改
    • 避免多次触发上面提到的那些会触发回流的方法,可以的话尽量用 变量存住

6. 存储

我们经常需要对业务中的一些数据进行存储,通常可以分为 短暂性存储 和 持久性储存。

  • 短暂性的时候,我们只需要将数据存在内存中,只在运行时可用

  • 持久性存储,可以分为 浏览器端 与 服务器端

    • 浏览器:

      • cookie: 通常用于存储用户身份,登录状态等

        • http 中自动携带, 体积上限为 4K, 可自行设置过期时间
      • localStorage / sessionStorage: 长久储存/窗口关闭删除, 体积限制为 4~5M

      • indexDB

    • 服务器:

      • 分布式缓存 redis
      • 数据库

7. Web Worker

现代浏览器为JavaScript创造的 多线程环境。可以新建并将部分任务分配到worker线程并行运行,两个线程可 独立运行,互不干扰,可通过自带的 消息机制 相互通信。

基本用法:

// 创建 worker
const worker = new Worker('work.js');

// 向 worker 线程推送消息
worker.postMessage('Hello World');

// 监听 worker 线程发送过来的消息
worker.onmessage = function (event) {
  console.log('Received message ' + event.data);
}复制代码

限制:

  • 同源限制
  • 无法使用 document / window / alert / confirm
  • 无法加载本地资源

8. V8垃圾回收机制

垃圾回收: 将内存中不再使用的数据进行清理,释放出内存空间。V8 将内存分成 新生代空间老生代空间

  • 新生代空间: 用于存活较短的对象

    • 又分成两个空间: from 空间 与 to 空间

    • Scavenge GC算法: 当 from 空间被占满时,启动 GC 算法

      • 存活的对象从 from space 转移到 to space
      • 清空 from space
      • from space 与 to space 互换
      • 完成一次新生代GC
  • 老生代空间: 用于存活时间较长的对象

    • 从 新生代空间 转移到 老生代空间 的条件

      • 经历过一次以上 Scavenge GC 的对象
      • 当 to space 体积超过25%
    • 标记清除算法: 标记存活的对象,未被标记的则被释放

      • 增量标记: 小模块标记,在代码执行间隙执,GC 会影响性能
      • 并发标记(最新技术): 不阻塞 js 执行
    • 压缩算法: 将内存中清除后导致的碎片化对象往内存堆的一端移动,解决 内存的碎片化

9. 内存泄露

  • 意外的全局变量: 无法被回收
  • 定时器: 未被正确关闭,导致所引用的外部变量无法被释放
  • 事件监听: 没有正确销毁 (低版本浏览器可能出现)
  • 闭包: 会导致父级中的变量无法被释放
  • dom 引用: dom 元素被删除时,内存中的引用未被正确清空

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

服务端与网络

1. http/https 协议

  • 1.0 协议缺陷:

    • 无法复用链接,完成即断开,重新慢启动和 TCP 3次握手
    • head of line blocking: 线头阻塞,导致请求之间互相影响
  • 1.1 改进:

    • 长连接(默认 keep-alive),复用

    • host 字段指定对应的虚拟站点

    • 新增功能:

      • 断点续传

      • 身份认证

      • 状态管理

      • cache 缓存

        • Cache-Control
        • Expires
        • Last-Modified
        • Etag
  • 2.0:

    • 多路复用
    • 二进制分帧层: 应用层和传输层之间
    • 首部压缩
    • 服务端推送
  • https: 较为安全的网络传输协议

    • 证书(公钥)
    • SSL 加密
    • 端口 443
  • TCP:

    • 三次握手

    • 四次挥手

    • 滑动窗口: 流量控制

    • 拥塞处理

      • 慢开始
      • 拥塞避免
      • 快速重传
      • 快速恢复
  • 缓存策略: 可分为 强缓存协商缓存

    强缓存会直接从浏览器里面拿数据,协商缓存会先访问服务器看缓存是否过期,再决定是否从浏览器里面拿数据。

    控制强缓存的字段有:Expires和Cache-Control,Expires 和 Cache-Control。

    控制协商缓存的字段是:Last-Modified / If-Modified-Since 和 Etag / If-None-Match,其中 Etag / If-None-Match的优先级比Last-Modified / If-Modified-Since高。

2. 常见状态码

  • 1xx: 接受,继续处理
  • 200: 成功,并返回数据
  • 201: 已创建
  • 202: 已接受
  • 203: 成为,但未授权
  • 204: 成功,无内容
  • 205: 成功,重置内容
  • 206: 成功,部分内容
  • 301: 永久移动,重定向
  • 302: 临时移动,可使用原有URI
  • 304: 资源未修改,可使用缓存
  • 305: 需代理访问
  • 400: 请求语法错误
  • 401: 要求身份认证
  • 403: 拒绝请求
  • 404: 资源不存在
  • 500: 服务器错误

3. get / post

  • get: 缓存、请求长度受限、会被历史保存记录

    • 无副作用(不修改资源),幂等(请求次数与资源无关)的场景
  • post: 安全、大数据、更多编码类型

两者详细对比如下图:

image.png

4. Websocket

Websocket 是一个 持久化的协议, 基于 http , 服务端可以 主动 push

  • 兼容:

    • FLASH Socket
    • 长轮询: 定时发送 ajax
    • long poll: 发送 --> 有消息时再 response
  • new WebSocket(url)

  • ws.onerror = fn

  • ws.onclose = fn

  • ws.onopen = fn

  • ws.onmessage = fn

  • ws.send()

5. TCP三次握手

建立连接前,客户端和服务端需要通过握手来确认对方:

  • 客户端发送 syn(同步序列编号) 请求,进入 syn_send 状态,等待确认
  • 服务端接收并确认 syn 包后发送 syn+ack 包,进入 syn_recv 状态
  • 客户端接收 syn+ack 包后,发送 ack 包,双方进入 established 状态

6. TCP四次挥手

  • 客户端 -- FIN --> 服务端, FIN—WAIT
  • 服务端 -- ACK --> 客户端, CLOSE-WAIT
  • 服务端 -- ACK,FIN --> 客户端, LAST-ACK
  • 客户端 -- ACK --> 服务端,CLOSED

7. Node 的 Event Loop: 6个阶段

  • timer 阶段: 执行到期的setTimeout / setInterval队列回调

  • I/O 阶段: 执行上轮循环残流的callback

  • idle, prepare

  • poll: 等待回调

      1. 执行回调
      1. 执行定时器
      • 如有到期的setTimeout / setInterval, 则返回 timer 阶段
      • 如有setImmediate,则前往 check 阶段
  • check

    • 执行setImmediate
  • close callbacks

跨域

  • JSONP: 利用<script>标签不受跨域限制的特点,缺点是只能支持 get 请求
function jsonp(url, jsonpCallback, success) {
  const script = document.createElement('script')
  script.src = url
  script.async = true
  script.type = 'text/javascript'
  window[jsonpCallback] = function(data) {
    success && success(data)
  }
  document.body.appendChild(script)
}复制代码
  • 设置 CORS: Access-Control-Allow-Origin:*
  • postMessage

安全

  • XSS攻击: 注入恶意代码

    • cookie 设置 httpOnly
    • 转义页面上的输入内容和输出内容
  • CSRF: 跨站请求伪造,防护:

    • get 不修改数据
    • 不被第三方网站访问到用户的 cookie
    • 设置白名单,不被第三方网站请求
    • 请求校验

框架:Vue

  • key 的作用:key是给每一个vnode的唯一id,可以依靠key,更准确, 更的拿到oldVnode中对应的vnode节点。

1. nextTick 的作用是什么?他的实现原理是什么?

作用:vue 更新 DOM 是异步更新的,数据变化,DOM 的更新不会马上完成,nextTick 的回调是在下次 DOM 更新循环结束之后执行的延迟回调。

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

  • Promise:可以将函数延迟到当前函数调用栈最末端
  • MutationObserver :是 H5 新加的一个功能,其功能是监听 DOM 节点的变动,在所有 DOM 变动完成后,执行回调函数
  • setImmediate:用于中断长时间运行的操作,并在浏览器完成其他操作(如事件和显示更新)后立即运行回调函数
  • 如果以上都不行则采用 setTimeout 把函数延迟到 DOM 更新之后再使用,原因是宏任务消耗大于微任务,优先使用微任务,最后使用消耗最大的宏任务。

Vue 实现双向数据绑定原理是什么?

Vue2.x 采用数据劫持结合发布订阅模式(PubSub 模式)的方式,通过 Object.defineProperty 来劫持各个属性的 setter、getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

当把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项时,Vue 将遍历它的属性,用 Object.defineProperty 将它们转为 getter/setter。用户看不到 getter/setter,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。

Vue 的数据双向绑定整合了 ObserverCompileWatcher 三者,通过 Observer 来监听自己的 model 的数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化->视图更新,视图交互变化(例如 input 操作)->数据 model 变更的双向绑定效果。

Vue3.x 放弃了 Object.defineProperty ,使用 ES6 原生的 Proxy,来解决以前使用 Object.defineProperty 所存在的一些问题。

2. 生命周期

  • vue 生命周期有几个阶段

它可以总共分为 8 个阶段:创建前/后, 载入前/后,更新前/后,销毁前/销毁后。

  1. beforeCreate:是 new Vue( ) 之后触发的第一个钩子,在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问。
  2. created:在实例创建完成后发生,当前阶段已经完成了数据观测,也就是可以使用数据,更改数据,在这里更改数据不会触发 updated 函数。可以做一些初始数据的获取,在当前阶段无法与 DOM 进行交互,如果非要想,可以通过 vm.$nextTick 来访问 DOM 。
  3. beforeMount:发生在挂载之前,在这之前 template 模板已导入渲染函数编译。而当前阶段虚拟 DOM 已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发 updated。
  4. mounted:在挂载完成后发生,在当前阶段,真实的 DOM 挂载完毕,数据完成双向绑定,可以访问到 DOM 节点,使用 $refs 属性对 DOM 进行操作。
  5. beforeUpdate:发生在更新之前,也就是响应式数据发生更新,虚拟 DOM 重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重渲染。
  6. updated:发生在更新完成之后,当前阶段组件 DOM 已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。
  7. beforeDestroy:发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。
  8. destroyed:发生在实例销毁之后,这个时候只剩下了 DOM 空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁。
  • 第一次页面加载会触发哪几个钩子

会触发 4 个钩子,分别是:beforeCreate、created、beforeMount、mounted

  • DOM 渲染在哪个周期就已经完成

DOM 渲染是在 mounted 阶段完成,此阶段真实的 DOM 挂载完毕,数据完成双向绑定,可以访问到 DOM 节点。

  • 多组件(父子组件)中生命周期的调用顺序说一下

组件的调用顺序都是先父后子,渲染完成的顺序是先子后父。组件的销毁操作是先父后子,销毁完成的顺序是先子后父。

  • 加载渲染阶段:父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted

  • 挂载阶段 父created->子created->子mounted->父mounted

  • 父组件更新阶段 父beforeUpdate->父updated

  • 子组件更新阶段 父beforeUpdate->子beforeUpdate->子updated->父updated

  • 销毁阶段:父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

  • vue 生命周期的作用是什么

在生命周期的过程中会运行着一些叫做生命周期的函数,给予了开发者在不同的生命周期阶段添加业务代码的能力。

其实和回调是一个概念,当系统执行到某处时,检查是否有 hook(钩子),有的话就会执行回调。

通俗的说,hook 就是在程序运行中,在某个特定的位置,框架的开发者设计好了一个钩子来告诉我们当前程序已经运行到特定的位置了,会触发一个回调函数,并提供给我们,让我们可以在生命周期的特定阶段进行相关业务代码的编写。

image.png

watch 与 computed 的区别是什么?以及他们的使用场景分别是什么?

区别

  • 都是观察数据变化的(相同)
  • computed计算属性将会混入到 vue 的实例中,所以需要监听自定义变量;watch 监听 data 、props 里面数据的变化;
  • computed 有缓存,它依赖的值变了才会重新计算,watch 没有;
  • watch 支持异步,computed 不支持;
  • watch 是一对多(监听某一个值变化,执行对应操作);computed 是多对一(监听属性依赖于其他属性)
  • watch 监听函数接收两个参数,第一个是最新值,第二个是输入之前的值;
  • computed 属性是函数时,都有 get 和 set 方法,默认走 get 方法,get 必须有返回值(return)

-watch 的 参数

  • deep:深度监听
  • immediate :组件加载立即触发回调函数执行

-computed 缓存原理

conputed本质是一个惰性的观察者;当计算数据存在于 data 或者 props里时会被警告;

vue 初次运行会对 computed 属性做初始化处理(initComputed),初始化的时候会对每一个 computed 属性用 watcher 包装起来 ,这里面会生成一个 dirty 属性值为 true;然后执行 defineComputed 函数来计算,计算之后会将 dirty 值变为 false,这里会根据 dirty 值来判断是否需要重新计算;如果属性依赖的数据发生变化,computed 的 watcher 会把 dirty 变为 true,这样就会重新计算 computed 属性的值。

v-if 与 v-show 的区别

  • 共同点:都是动态显示 DOM 元素

  • 区别点:

    1、手段

v-if 是动态的向 DOM 树内添加或者删除 DOM 元素

v-show 是通过设置 DOM 元素的 display 样式属性控制显隐

2、编译过程

v-if 切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件

v-show 只是简单的基于 css 切换

3、编译条件

v-if 是惰性的,如果初始条件为假,则什么也不做。只有在条件第一次变为真时才开始局部编译

v-show 是在任何条件下(首次条件是否为真)都被编译,然后被缓存,而且 DOM 元素保留

4、性能消耗

v-if 有更高的切换消耗

v-show 有更高的初始渲染消耗

5、使用场景

v-if 适合运营条件不大可能改变

v-show 适合频繁切换

v-for和v-if

区别

  • 这个v-if是条件渲染,v-for是列表渲染
  • V2当中,v-for的优先级更高,而在V3当中,则是v-if的优先级更高

keep-alive

作用:实现组件缓存,保持这些组件的状态,以避免反复渲染导致的性能问题。 需要缓存组件 频繁切换,不需要重复渲染

场景:tabs标签页 后台导航,vue性能优化

相关的生命周期函数:其内部所有嵌套的组件都具有两个生命周期钩子函数,activated 和 deactivated,它们分别在组件激活和失活时触发。第一次 activated 触发是在 mounted 之后

相关属性:keep-alive 具有 include 和 exclude 属性,通过它们可以控制哪些组件进入缓存。另外它还提供了 max 属性,通过它可以设置最大缓存数,当缓存的实例超过该数时,vue 会移除最久没有使用的组件缓存。

原理Vue.js内部将DOM节点抽象成了一个个的VNode节点,keep-alive组件的缓存也是基于VNode节点的而不是直接存储DOM结构。它将满足条件(pruneCache与pruneCache)的组件在cache对象中缓存起来,在需要重新渲染的时候再将vnode节点从cache对象中取出并渲染。

3. 数据响应(数据劫持)

看完生命周期后,里面的watcher等内容其实是数据响应中的一部分。数据响应的实现由两部分构成: 观察者( watcher )依赖收集器( Dep ) ,其核心是 defineProperty这个方法,它可以 重写属性的 get 与 set 方法,从而完成监听数据的改变。

  • Observe (观察者)观察 props 与 state

    • 遍历 props 与 state,对每个属性创建独立的监听器( watcher )
  • 使用 defineProperty 重写每个属性的 get/set(defineReactive

    • get: 收集依赖

      • Dep.depend()

        • watcher.addDep()
    • set: 派发更新

      • Dep.notify()
      • watcher.update()
      • queenWatcher()
      • nextTick
      • flushScheduleQueue
      • watcher.run()
      • updateComponent()

大家可以先看下面的数据相应的代码实现后,理解后就比较容易看懂上面的简单脉络了。

let data = {a: 1}
// 数据响应性
observe(data)

// 初始化观察者
new Watcher(data, 'name', updateComponent)
data.a = 2

// 简单表示用于数据更新后的操作
function updateComponent() {
    vm._update() // patchs
}

// 监视对象
function observe(obj) {
	 // 遍历对象,使用 get/set 重新定义对象的每个属性值
    Object.keys(obj).map(key => {
        defineReactive(obj, key, obj[key])
    })
}

function defineReactive(obj, k, v) {
    // 递归子属性
    if (type(v) == 'object') observe(v)
    
    // 新建依赖收集器
    let dep = new Dep()
    // 定义get/set
    Object.defineProperty(obj, k, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
        	  // 当有获取该属性时,证明依赖于该对象,因此被添加进收集器中
            if (Dep.target) {
                dep.addSub(Dep.target)
            }
            return v
        },
        // 重新设置值时,触发收集器的通知机制
        set: function reactiveSetter(nV) {
            v = nV
            dep.nofify()
        },
    })
}

// 依赖收集器
class Dep {
    constructor() {
        this.subs = []
    }
    addSub(sub) {
        this.subs.push(sub)
    }
    notify() {
        this.subs.map(sub => {
            sub.update()
        })
    }
}

Dep.target = null

// 观察者
class Watcher {
    constructor(obj, key, cb) {
        Dep.target = this
        this.cb = cb
        this.obj = obj
        this.key = key
        this.value = obj[key]
        Dep.target = null
    }
    addDep(Dep) {
        Dep.addSub(this)
    }
    update() {
        this.value = this.obj[this.key]
        this.cb(this.value)
    }
    before() {
        callHook('beforeUpdate')
    }
}复制代码

4. virtual dom 原理实现

  • 创建 dom 树

  • 树的diff,同层对比,输出patchs(listDiff/diffChildren/diffProps)

    • 没有新的节点,返回

    • 新的节点tagNamekey不变, 对比props,继续递归遍历子树

      • 对比属性(对比新旧属性列表):

        • 旧属性是否存在与新属性列表中
        • 都存在的是否有变化
        • 是否出现旧列表中没有的新属性
    • tagNamekey值变化了,则直接替换成新节点

  • 渲染差异

    • 遍历patchs, 把需要更改的节点取出来
    • 局部更新dom
// diff算法的实现
function diff(oldTree, newTree) {
	 // 差异收集
    let pathchs = {}
    dfs(oldTree, newTree, 0, pathchs)
    return pathchs
}

function dfs(oldNode, newNode, index, pathchs) {
    let curPathchs = []
    if (newNode) {
        // 当新旧节点的 tagName 和 key 值完全一致时
        if (oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {
        	  // 继续比对属性差异
            let props = diffProps(oldNode.props, newNode.props)
            curPathchs.push({ type: 'changeProps', props })
            // 递归进入下一层级的比较
            diffChildrens(oldNode.children, newNode.children, index, pathchs)
        } else {
        	  // 当 tagName 或者 key 修改了后,表示已经是全新节点,无需再比
            curPathchs.push({ type: 'replaceNode', node: newNode })
        }
    }

	 // 构建出整颗差异树
    if (curPathchs.length) {
    		if(pathchs[index]){
    			pathchs[index] = pathchs[index].concat(curPathchs)
    		} else {
    			pathchs[index] = curPathchs
    		}
    }
}

// 属性对比实现
function diffProps(oldProps, newProps) {
    let propsPathchs = []
    // 遍历新旧属性列表
    // 查找删除项
    // 查找修改项
    // 查找新增项
    forin(olaProps, (k, v) => {
        if (!newProps.hasOwnProperty(k)) {
            propsPathchs.push({ type: 'remove', prop: k })
        } else {
            if (v !== newProps[k]) {
                propsPathchs.push({ type: 'change', prop: k , value: newProps[k] })
            }
        }
    })
    forin(newProps, (k, v) => {
        if (!oldProps.hasOwnProperty(k)) {
            propsPathchs.push({ type: 'add', prop: k, value: v })
        }
    })
    return propsPathchs
}

// 对比子级差异
function diffChildrens(oldChild, newChild, index, pathchs) {
		// 标记子级的删除/新增/移动
    let { change, list } = diffList(oldChild, newChild, index, pathchs)
    if (change.length) {
        if (pathchs[index]) {
            pathchs[index] = pathchs[index].concat(change)
        } else {
            pathchs[index] = change
        }
    }

	 // 根据 key 获取原本匹配的节点,进一步递归从头开始对比
    oldChild.map((item, i) => {
        let keyIndex = list.indexOf(item.key)
        if (keyIndex) {
            let node = newChild[keyIndex]
            // 进一步递归对比
            dfs(item, node, index, pathchs)
        }
    })
}

// 列表对比,主要也是根据 key 值查找匹配项
// 对比出新旧列表的新增/删除/移动
function diffList(oldList, newList, index, pathchs) {
    let change = []
    let list = []
    const newKeys = getKey(newList)
    oldList.map(v => {
        if (newKeys.indexOf(v.key) > -1) {
            list.push(v.key)
        } else {
            list.push(null)
        }
    })

    // 标记删除
    for (let i = list.length - 1; i>= 0; i--) {
        if (!list[i]) {
            list.splice(i, 1)
            change.push({ type: 'remove', index: i })
        }
    }

    // 标记新增和移动
    newList.map((item, i) => {
        const key = item.key
        const index = list.indexOf(key)
        if (index === -1 || key == null) {
            // 新增
            change.push({ type: 'add', node: item, index: i })
            list.splice(i, 0, key)
        } else {
            // 移动
            if (index !== i) {
                change.push({
                    type: 'move',
                    form: index,
                    to: i,
                })
                move(list, index, i)
            }
        }
    })

    return { change, list }
}复制代码

5. Proxy 相比于 defineProperty 的优势

  • 数组变化也能监听到
  • 不需要深度遍历监听
let data = { a: 1 }
let reactiveData = new Proxy(data, {
	get: function(target, name){
		// ...
	},
	// ...
})复制代码

6. vue-router

  • mode

    • hash
    • history
  • 跳转

    • this.$router.push()
    • <router-link to=""></router-link>
  • 占位

    • <router-view></router-view>

7. vuex

  • state: 状态中心
  • mutations: 更改状态
  • actions: 异步更改状态
  • getters: 获取状态
  • modules: 将state分成多个modules,便于管理

React

先写类组件 useState 同步/异步

1、react 类组件和函数组件

react无状态组件和class类组件的区别

  • 直观区别,函数组件代码量较少,相比类组件更加简洁
  • 函数组件看似只是一个返回react元素的函数,其实体现的是无状态组件的思想,函数组件中没有this没有state,也没有生命周期,这就决定了函数组件都是展示性组件,接收props,渲染dom,而不关注其他逻辑
  • 因为函数组件不需要考虑组件状态和组件生命周期方法中的各种比较校验,所以有很大的性能提升空间

React 有三种构建组件的方式

  • React.createClass
  • ES6 class
  • 无状态函数

React.createClass 是 React 最传统、兼容性最好的方法。该方法构建一个组件对象, 当组件被调用时,就会创建几个组件实例

ES6 class 方式和 createClass 类似,只是从调用内部方法变成了用类来实现。

无状态组件创建时始终保持一个实例,避免了不必要的检查和内存分配。

生命周期(新、旧)

image.png React 的生命周期主要分为三个阶段:MOUNTING、RECEIVE_PROPS、UNMOUNTING
  • 组件挂载时(组件状态的初始化,读取初始 state 和 props 以及两个生命周期方法,只会在初始化时运行一次)

    • constructor 初始化阶段,可以进行state和props的初始化
    • static getDerivedStateFromProps 静态方法,不能获取this
    • render 创建虚拟DOM的阶段
    • componentDidMount第一次渲染后调用,挂载到页面生成真实DOM,可以访问DOM,进行异步请求和定时器、消息订阅
  • 组件更新时(组件的更新过程是指父组件向下传递 props 或者组件自身执行 setState 方法时发生的一系列更新的动作)

    • 组件自身的 state 更新,依次执行

      • shouldComponentUpdate(会接收需要更新的 props 和 state,让开发者增加必要的判断条件,在其需要的时候更新,不需要的时候不更新。如果返回的是 false,那么组件就不再向下执行生命周期方法。)
      • componentWillUpdate
      • render(能获取到最新的 this.state)
    • 父组件更新 props 而更新

      • componentWillReceiveProps(在此调用 setState,是不会触发 re-render 的,而是会进行 state 的合并。因此此时的 this.state 不是最新的,在 render 中才可以获取更新后的 this.state。
      • shouldComponentUpdate
      • componentWillUpdate
      • render
      • shouldComponentUpdate 返回一个布尔值,默认返回true,可以通过这个生命周期钩子进行性能优化,确认不需要更新组件时调用
  • 组件卸载时

    • componentWillMount(我们常常会在组件的卸载过程中执行一些清理方法,比如事件回收、清空定时器)
  • 错误捕获

    • static getDerivedStateFromError 在errorBoundary中使用

    • componentDidCatch

setState 同步/异步

image.png 在了解`setState`之前,我们先来简单了解下 React 一个包装结构: **Transaction**:
  • 事务 (Transaction):

    • 是 React 中的一个调用结构,用于包装一个方法,结构为: initialize - perform(method) - close。通过事务,可以统一管理一个方法的开始与结束;处于事务流中,表示进程正在执行一些操作;
image.png
  • setState: React 中用于修改状态,更新视图。它具有以下特点:

  • 异步与同步: setState并不是单纯的异步或同步,这其实与调用时的环境相关:

    • 合成事件(React事件) 和 生命周期钩子(除 componentDidUpdate) 中,setState是"异步"的;

      • 原因: 因为在setState的实现中,有一个判断: 当更新策略正在事务流的执行中时,该组件更新会被推入dirtyComponents队列中等待执行;否则,开始执行batchedUpdates队列更新;

        • 在生命周期钩子调用中,更新策略都处于更新之前,组件仍处于事务流中,而componentDidUpdate是在更新之后,此时组件已经不在事务流中了,因此则会同步执行;
        • 在合成事件中,React 是基于 事务流完成的事件委托机制 实现,也是处于事务流中;
      • 问题: 无法在setState后马上从this.state上获取更新后的值。

      • 解决: 如果需要马上同步去获取新值,setState其实是可以传入第二个参数的。setState(updater, callback),在回调中即可获取最新值;

    • 原生事件setTimeout 中,setState是同步的,可以马上获取更新后的值;

      • 原因: 原生事件是浏览器本身的实现,与事务流无关,自然是同步;而setTimeout是放置于定时器线程中延后执行,此时事务流已结束,因此也是同步;
  • 批量更新: 在 合成事件生命周期钩子 中,setState更新队列时,存储的是 合并状态(Object.assign)。因此前面设置的 key 值会被后面所覆盖,最终只会执行一次更新;

  • 函数式: 由于 Fiber 及 合并 的问题,官方推荐可以传入 函数 的形式。setState(fn),在fn中返回新的state对象即可,例如this.setState((state, props) => newState);

    • 使用函数式,可以用于避免setState的批量更新的逻辑,传入的函数将会被 顺序调用
  • 注意事项:

    • setState 合并,在 合成事件 和 生命周期钩子 中多次连续调用会被优化为一次;

    • 当组件已被销毁,如果再次调用setState,React 会报错警告,通常有两种解决办法:

      • 将数据挂载到外部,通过 props 传入,如放到 Redux 或 父级中;

      • 在组件内部维护一个状态量 (isUnmounted),componentWillUnmount中标记为 true,在setState前进行判断;

HOC高阶组件

HOC(Higher Order Componennt) 是在 React 机制下社区形成的一种组件模式,在很多第三方开源库中表现强大。

  • 简述:

    • 高阶组件不是组件,是 增强函数,可以输入一个元组件,返回出一个新的增强组件;
    • 高阶组件的主要作用是 代码复用操作 状态和参数;
  • 用法:

    • 属性代理 (Props Proxy) : 返回出一个组件,它基于被包裹组件进行 功能增强

      • 默认参数: 可以为组件包裹一层默认参数;
function proxyHoc(Comp) {
	return class extends React.Component {
		render() {
			const newProps = {
				name: 'tayde',
				age: 1,
			}
			return <Comp {...this.props} {...newProps} />
		}
	}
}
  • 提取状态: 可以通过 props 将被包裹组件中的 state 依赖外层,例如用于转换受控组件:
function withOnChange(Comp) {
	return class extends React.Component {
		constructor(props) {
			super(props)
			this.state = {
				name: '',
			}
		}
		onChangeName = () => {
			this.setState({
				name: 'dongdong',
			})
		}
		render() {
			const newProps = {
				value: this.state.name,
				onChange: this.onChangeName,
			}
			return <Comp {...this.props} {...newProps} />
		}
	}
}

使用姿势如下,这样就能非常快速的将一个 Input 组件转化成受控组件。

const NameInput = props => (<input name="name" {...props} />)
export default withOnChange(NameInput)
  • 包裹组件: 可以为被包裹元素进行一层包装,
function withMask(Comp) {
              return class extends React.Component {
                  render() {
            		  return (
                                <div>
            			 <Comp {...this.props} />
                                    <div style={{
                                        width: '100%',
                                        height: '100%',
                                        backgroundColor: 'rgba(0, 0, 0, .6)',
                                      }}/>
                                </div>
            		  )
            	  }
              }
    }

  • 反向继承 (Inheritance Inversion): 返回出一个组件,继承于被包裹组件,常用于以下操作:
function IIHoc(Comp) {
    return class extends Comp {
        render() {
            return super.render();
        }
    };
}
  • 渲染劫持 (Render Highjacking)

    • 条件渲染: 根据条件,渲染不同的组件
function withLoading(Comp) {
    return class extends Comp {
        render() {
            if(this.props.isLoading) {
                return <Loading />
            } else {
                return super.render()
            }
        }
    };
}
  • 可以直接修改被包裹组件渲染出的 React 元素树

    • 操作状态 (Operate State): 可以直接通过 this.state 获取到被包裹组件的状态,并进行操作。但这样的操作容易使 state 变得难以追踪,不易维护,谨慎使用。
  • 应用场景:

    • 权限控制,通过抽象逻辑,统一对页面进行权限判断,按不同的条件进行页面渲染:
function withAdminAuth(WrappedComponent) {
    return class extends React.Component {
		constructor(props){
			super(props)
			this.state = {
		    	isAdmin: false,
			}
		} 
		async componentWillMount() {
		    const currentRole = await getCurrentUserRole();
		    this.setState({
		        isAdmin: currentRole === 'Admin',
		    });
		}
		render() {
		    if (this.state.isAdmin) {
		        return <Comp {...this.props} />;
		    } else {
		        return (<div>您没有权限查看该页面,请联系管理员!</div>);
		    }
		}
    };
}
  • 性能监控,包裹组件的生命周期,进行统一埋点:
function withTiming(Comp) {
    return class extends Comp {
        constructor(props) {
            super(props);
            this.start = Date.now();
            this.end = 0;
        }
        componentDidMount() {
            super.componentDidMount && super.componentDidMount();
            this.end = Date.now();
            console.log(`${WrappedComponent.name} 组件渲染时间为 ${this.end - this.start} ms`);
        }
        render() {
            return super.render();
        }
    };
}

  • 代码复用,可以将重复的逻辑进行抽象。
  • 使用注意:

      1. 纯函数: 增强函数应为纯函数,避免侵入修改元组件;
      1. 避免用法污染: 理想状态下,应透传元组件的无关参数与事件,尽量保证用法不变;
      1. 命名空间: 为 HOC 增加特异性的组件名称,这样能便于开发调试和查找问题;
      1. 引用传递: 如果需要传递元组件的 refs 引用,可以使用React.forwardRef
      1. 静态方法: 元组件上的静态方法并无法被自动传出,会导致业务层无法调用;解决:
      • 函数导出
      • 静态方法赋值
      1. 重新渲染: 由于增强函数每次调用是返回一个新组件,因此如果在 Render 中使用增强函数,就会导致每次都重新渲染整个HOC,而且之前的状态会丢失;

prue component

React 通信

react组件间通信常见的几种情况:

    1. 父组件向子组件通信
    1. 子组件向父组件通信
    1. 跨级组件通信
    1. 非嵌套关系的组件通信

1)父组件向子组件通信

父组件通过 props 向子组件传递需要的信息。父传子是在父组件中直接绑定一个正常的属性,这个属性就是指具体的值,在子组件中,用props就可以获取到这个值

// 子组件: Child
const Child = props =>{
  return <p>{props.name}</p>
}

// 父组件 Parent
const Parent = ()=>{
    return <Child name="京程一灯"></Child>
}

ref

类组件

函数组件:

useImperativeHandle 可以配合 forwardRef 自定义暴露给父组件的实例值。这个很有用,我们知道,对于子组件,如果是 class 类组件,我们可以通过 ref 获取类组件的实例,但是在子组件是函数组件的情况,如果我们不能直接通过 ref 的,那么此时 useImperativeHandle 和 forwardRef 配合就能达到效果。

function Son (props,ref) {     //useImperativeHandle + forwardRef
    console.log(props)
    const inputRef = useRef(null)
    const [ inputValue , setInputValue ] = useState('')
    useImperativeHandle(ref,()=>{
       const handleRefs = {
           /* 声明方法用于聚焦input框 */
           onFocus(){
              inputRef.current.focus()
           },
           /* 声明方法用于改变input的值 */
           onChangeValue(value){
               setInputValue(value)
           }
       }
       return handleRefs
    },[])
    return <div>
        <input
            placeholder="请输入内容"
            ref={inputRef}
            value={inputValue}
        />
    </div>
}

const ForwarSon = forwardRef(Son)

class Index extends React.Component{      //ref 
    inputRef = null
    handerClick(){
       const { onFocus , onChangeValue } =this.cur
       onFocus()
       onChangeValue('let us learn React!')
    }
    render(){
        return <div style={{ marginTop:'50px' }} >
            <ForwarSon ref={node => (this.inputRef = node)} />
            <button onClick={this.handerClick.bind(this)} >操控子组件</button>
        </div>
    }
}

2)子组件向父组件通信

props + 回调的方式,使用公共组件进行状态提升。子传父是先在父组件上绑定属性设置为一个函数,当子组件需要给父组件传值的时候,则通过props调用该函数将参数传入到该函数当中,此时就可以在父组件中的函数中接收到该参数了,这个参数则为子组件传过来的值

// 子组件: Child
const Child = props =>{
  const cb = msg =>{
      return ()=>{
          props.callback(msg)
      }
  }
  return (
      <button onClick={cb("京程一灯欢迎你!")}>京程一灯欢迎你</button>
  )
}

// 父组件 Parent
class Parent extends Component {
    callback(msg){
        console.log(msg)
    }
    render(){
        return <Child callback={this.callback.bind(this)}></Child>    
    }
}

3)跨级组件通信

即父组件向子组件的子组件通信,向更深层子组件通信。

  • 使用props,利用中间组件层层传递,但是如果父组件结构较深,那么中间每一层组件都要去传递props,增加了复杂度,并且这些props并不是中间组件自己需要的。
  • 使用context,context相当于一个大容器,我们可以把要通信的内容放在这个容器中,这样不管嵌套多深,都可以随意取用,对于跨越多层的全局数据可以使用context实现。
// context方式实现跨级组件通信 
// Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据

const BatteryContext = createContext();

//  子组件的子组件 
class GrandChild extends Component {
    render(){
        return (
            <BatteryContext.Consumer>
                {
                    color => <h1 style={{"color":color}}>我是红色的:{color}</h1>
                }
            </BatteryContext.Consumer>
        ) 
    }
}

//  子组件
const Child = () =>{
    return (
        <GrandChild/>
    )
}
// 父组件
class Parent extends Component {
      state = {
          color:"red"
      }
      render(){
          const {color} = this.state
          return (
          <BatteryContext.Provider value={color}>
              <Child></Child>
          </BatteryContext.Provider> 
          )
      }
}

4)非嵌套关系的组件通信

即没有任何包含关系的组件,包括兄弟组件以及不在同一个父级中的非兄弟组件。

    1. 可以使用自定义事件通信(发布订阅模式),使用pubsub-js
    1. 可以通过redux等进行全局状态管理
    1. 如果是兄弟组件通信,可以找到这两个兄弟节点共同的父节点, 结合父子间通信方式进行通信。
    1. 也可以new一个 Vue 的 EventBus,进行事件监听,一边执行监听,一边执行新增 VUE的eventBus 就是发布订阅模式,是可以在React中使用的;

Hooks

Fiber

结语

由于精力时间及篇幅有限,这篇就先写到这。大家慢慢来不急。。🤪。下篇打算准备以下内容,我也得补补课先:

  • Webpack相关

    • 原理
    • Loader
    • Plugin
  • 项目性能优化

    • 首屏渲染优化
    • 用户体验优化
    • webpack 性能优化
  • Hybrid 与 Webview

    • webview 加载过程
    • bridge 原理
    • hybrid app 经验
image.png转存失败,建议直接上传图片文件