JS 中的 8 种数据类型及区别

包括值类型(基本对象类型)和引用类型(复杂对象类型)

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

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

JS 中的数据类型检测方案

1.typeof

1
2
3
4
5
6
7
8
9
10
console.log(typeof 1) // number
console.log(typeof true) // boolean
console.log(typeof 'mc') // string
console.log(typeof function () {}) // function
console.log(typeof console.log()) // function
console.log(typeof []) // object
console.log(typeof {}) // object
console.log(typeof null) // object
console.log(typeof undefined) // undefined
复制代码

优点:能够快速区分基本数据类型

缺点:不能将 Object、Array 和 Null 区分,都返回 object

2.instanceof

1
2
3
4
5
6
7
console.log(1 instanceof Number) // false
console.log(true instanceof Boolean) // false
console.log('str' instanceof String) // false
console.log([] instanceof Array) // true
console.log(function () {} instanceof Function) // true
console.log({} instanceof Object) // true
复制代码

优点:能够区分 Array、Object 和 Function,适合用于判断自定义的类实例对象

缺点:Number,Boolean,String 基本数据类型不能判断

3.Object.prototype.toString.call()

1
2
3
4
5
6
7
8
9
10
var toString = Object.prototype.toString
console.log(toString.call(1)) //[object Number]
console.log(toString.call(true)) //[object Boolean]
console.log(toString.call('mc')) //[object String]
console.log(toString.call([])) //[object Array]
console.log(toString.call({})) //[object Object]
console.log(toString.call(function () {})) //[object Function]
console.log(toString.call(undefined)) //[object Undefined]
console.log(toString.call(null)) //[object Null]
复制代码

优点:精准判断数据类型

缺点:写法繁琐不容易记,推荐进行封装后使用

var && let && const

ES6 之前创建变量用的是 var,之后创建变量用的是 let/const

三者区别

  1. var 定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问。
    let 定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问。
    const 用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,且不能修改。
  2. var 可以先使用,后声明,因为存在变量提升;let 必须先声明后使用。
  3. var 是允许在相同作用域内重复声明同一个变量的,而 let 与 const 不允许这一现象。
  4. 在全局上下文中,基于 let 声明的全局变量和全局对象 GO(window)没有任何关系 ;
    var 声明的变量会和 GO 有映射关系;
  5. 会产生暂时性死区

暂时性死区是浏览器的 bug:检测一个未被声明的变量类型时,不会报错,会返回 undefined
如:console.log(typeof a) //undefined
而:console.log(typeof a)//未声明之前不能使用
let a

  1. let /const/function 会把当前所在的大括号(除函数之外)作为一个全新的块级上下文,应用这个机制,在开发项目的时候,遇到循环事件绑定等类似的需求,无需再自己构建闭包来存储,只要基于 let 的块作用特征即可解决

JS 垃圾回收机制

  1. 项目中,如果存在大量不被释放的内存(堆/栈/上下文),页面性能会变得很慢。当某些代码操作不能被合理释放,就会造成内存泄漏。我们尽可能减少使用闭包,因为它会消耗内存。

  2. 浏览器垃圾回收机制/内存回收机制:

    浏览器的Javascript具有自动垃圾回收机制(GC:Garbage Collecation),垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。

    标记清除:在js中,最常用的垃圾回收机制是标记清除:当变量进入执行环境时,被标记为“进入环境”,当变量离开执行环境时,会被标记为“离开环境”。垃圾回收器会销毁那些带标记的值并回收它们所占用的内存空间。
    谷歌浏览器:“查找引用”,浏览器不定时去查找当前内存的引用,如果没有被占用了,浏览器会回收它;如果被占用,就不能回收。
    IE 浏览器:“引用计数法”,当前内存被占用一次,计数累加 1 次,移除占用就减 1,减到 0 时,浏览器就回收它。

  3. 优化手段:内存优化 ; 手动释放:取消内存的占用即可。

    (1)堆内存:fn = null 【null:空指针对象】

    (2)栈内存:把上下文中,被外部占用的堆的占用取消即可。

  4. 内存泄漏

    在 JS 中,常见的内存泄露主要有 4 种,全局变量、闭包、DOM 元素的引用、定时器

作用域和作用域链

创建函数的时候,已经声明了当前函数的作用域==>当前创建函数所处的上下文。如果是在全局下创建的函数就是[[scope]]:EC(G),函数执行的时候,形成一个全新的私有上下文EC(FN),供字符串代码执行(进栈执行)

定义:简单来说作用域就是变量与函数的可访问范围,由当前环境与上层环境的一系列变量对象组成 1.全局作用域:代码在程序的任何地方都能被访问,window 对象的内置属性都拥有全局作用域。 2.函数作用域:在固定的代码片段才能被访问

作用:作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

作用域链参考链接一般情况下,变量到 创建该变量 的函数的作用域中取值。但是如果在当前作用域中没有查到,就会向上级作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链。

闭包的两大作用:保存/保护

  • 闭包的概念

    函数执行时形成的私有上下文 EC(FN),正常情况下,代码执行完会出栈后释放;但是特殊情况下,如果当前私有上下文中的某个东西被上下文以外的事物占用了,则上下文不会出栈释放,从而形成不销毁的上下文。 函数执行函数执行过程中,会形成一个全新的私有上下文,可能会被释放,可能不会被释放,不论释放与否,他的作用是:

(1)保护:划分一个独立的代码执行区域,在这个区域中有自己私有变量存储的空间,保护自己的私有变量不受外界干扰(操作自己的私有变量和外界没有关系);

(2)保存:如果当前上下文不被释放【只要上下文中的某个东西被外部占用即可】,则存储的这些私有变量也不会被释放,可以供其下级上下文中调取使用,相当于把一些值保存起来了;

我们把函数执行形成私有上下文,来保护和保存私有变量机制称为闭包

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

稍全面的回答: 在 js 中变量的作用域属于函数作用域, 在函数执行完后,作用域就会被清理,内存也会随之被回收,但是由于闭包函数是建立在函数内部的子函数, 由于其可访问上级作用域,即使上级函数执行完, 作用域也不会随之销毁, 这时的子函数(也就是闭包),便拥有了访问上级作用域中变量的权限,即使上级函数执行完后作用域内的值也不会被销毁。

  • 闭包的特性

    • 1、内部函数可以访问定义他们外部函数的参数和变量。(作用域链的向上查找,把外围的作用域中的变量值存储在内存中而不是在函数调用完毕后销毁)设计私有的方法和变量,避免全局变量的污染。

      1.1.闭包是密闭的容器,,类似于 set、map 容器,存储数据的

      1.2.闭包是一个对象,存放数据的格式为 key-value 形式

    • 2、函数嵌套函数

    • 3、本质是将函数内部和外部连接起来。优点是可以读取函数内部的变量,让这些变量的值始终保存在内存中,不会在函数被调用之后自动清除

  • 闭包形成的条件

    1. 函数的嵌套
    2. 内部函数引用外部函数的局部变量,延长外部函数的变量生命周期
  • 闭包的用途

    1. 模仿块级作用域
    2. 保护外部函数的变量 能够访问函数定义时所在的词法作用域(阻止其被回收)
    3. 封装私有化变量
    4. 创建模块
  • 闭包应用场景

    闭包的两个场景,闭包的两大作用:保存/保护。 在开发中, 其实我们随处可见闭包的身影, 大部分前端 JavaScript 代码都是“事件驱动”的,即一个事件绑定的回调方法; 发送 ajax 请求成功|失败的回调;setTimeout 的延时回调;或者一个函数内部返回另一个匿名函数,这些都是闭包的应用。

  • 闭包的优点:延长局部变量的生命周期

  • 闭包缺点:会导致函数的变量一直保存在内存中,过多的闭包可能会导致内存泄漏

JS 中 this 的五种情况

  1. 作为普通函数执行时,this指向window
  2. 当函数作为对象的方法被调用时,this就会指向该对象
  3. 构造器调用,this指向返回的这个对象
  4. 箭头函数 箭头函数的this绑定看的是this所在函数定义在哪个对象下,就绑定哪个对象。如果有嵌套的情况,则 this 绑定到最近的一层对象上。
  5. 基于 Function.prototype 上的 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。apply接收参数的是数组,call接受参数列表,`` bind方法通过传入一个对象,返回一个this绑定了传入对象的新函数。这个函数的 this指向除了使用new `时会被改变,其他情况下都不会改变。若为空默认是指向全局对象 window。

原型 && 原型链

原型关系:

  • 每个 class 都有显示原型 prototype
  • 每个实例都有隐式原型 _ proto_
  • 实例的* proto*指向对应 class 的 prototype

原型: 在 JS 中,每当定义一个对象(函数也是对象)时,对象中都会包含一些预定义的属性。其中每个函数对象都有一个prototype 属性,这个属性指向函数的原型对象

原型链:函数的原型链对象 constructor 默认指向函数本身,原型对象除了有原型属性外,为了实现继承,还有一个原型链指针proto,该指针是指向上一层的原型对象,而上一层的原型对象的结构依然类似。因此可以利用proto一直指向 Object 的原型对象上,而 Object 原型对象用 Object.prototype.** proto** = null 表示原型链顶端。如此形成了 js 的原型链继承。同时所有的 js 对象都有 Object 的基本防范

特点: JavaScript对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与之相关的对象也会继承这一改变。

new 运算符的实现机制

  1. 首先创建了一个新的空对象
  2. 设置原型,将对象的原型设置为函数的prototype对象。
  3. 让函数的this指向这个对象,执行构造函数的代码(为这个新对象添加属性)
  4. 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。

EventLoop 事件循环

1
JS`是单线程的,为了防止一个函数执行时间过长阻塞后面的代码,所以会先将同步代码压入执行栈中,依次执行,将异步代码推入异步队列,异步队列又分为宏任务队列和微任务队列,因为宏任务队列的执行时间较长,所以微任务队列要优先于宏任务队列。微任务队列的代表就是,`Promise.then`,`MutationObserver`,宏任务的话就是`setImmediate setTimeout setInterval

JS 运行的环境。一般为浏览器或者 Node。 在浏览器环境中,有 JS 引擎线程和渲染线程,且两个线程互斥。 Node 环境中,只有 JS 线程。 不同环境执行机制有差异,不同任务进入不同 Event Queue 队列。 当主程结束,先执行准备好微任务,然后再执行准备好的宏任务,一个轮询结束。

浏览器中的事件环(Event Loop)

事件环的运行机制是,先会执行栈中的内容,栈中的内容执行后执行微任务,微任务清空后再执行宏任务,先取出一个宏任务,再去执行微任务,然后在取宏任务清微任务这样不停的循环。

  • eventLoop 是由 JS 的宿主环境(浏览器)来实现的;

  • 事件循环可以简单的描述为以下四个步骤:

    1. 函数入栈,当 Stack 中执行到异步任务的时候,就将他丢给 WebAPIs,接着执行同步任务,直到 Stack 为空;
    2. 此期间 WebAPIs 完成这个事件,把回调函数放入队列中等待执行(微任务放到微任务队列,宏任务放到宏任务队列)
    3. 执行栈为空时,Event Loop 把微任务队列执行清空;
    4. 微任务队列清空后,进入宏任务队列,取队列的第一项任务放入 Stack(栈)中执行,执行完成后,查看微任务队列是否有任务,有的话,清空微任务队列。重复 4,继续从宏任务中取任务执行,执行完成之后,继续清空微任务,如此反复循环,直至清空所有的任务。

    事件循环流程

  • 浏览器中的任务源(task):

    • 宏任务(macrotask)
      宿主环境提供的,比如浏览器
      ajax、setTimeout、setInterval、setTmmediate(只兼容 ie)、script、requestAnimationFrame、messageChannel、UI 渲染、一些浏览器 api
    • 微任务(microtask)
      语言本身提供的,比如 promise.then
      then、queueMicrotask(基于 then)、mutationObserver(浏览器提供)、messageChannel 、mutationObersve

Node 环境中的事件环(Event Loop)

Node是基于 V8 引擎的运行在服务端的JavaScript运行环境,在处理高并发、I/O 密集(文件操作、网络操作、数据库操作等)场景有明显的优势。虽然用到也是 V8 引擎,但由于服务目的和环境不同,导致了它的 API 与原生 JS 有些区别,其 Event Loop 还要处理一些 I/O,比如新的网络连接等,所以 Node 的 Event Loop(事件环机制)与浏览器的是不太一样。

2020120317343116.png 执行顺序如下:

  • timers: 计时器,执行 setTimeout 和 setInterval 的回调
  • pending callbacks: 执行延迟到下一个循环迭代的 I/O 回调
  • idle, prepare: 队列的移动,仅系统内部使用
  • poll轮询: 检索新的 I/O 事件;执行与 I/O 相关的回调。事实上除了其他几个阶段处理的事情,其他几乎所有的异步都在这个阶段处理。
  • check: 执行setImmediate回调,setImmediate 在这里执行
  • close callbacks: 执行close事件的callback,一些关闭的回调函数,如:socket.on(‘close’, …)

setTimeout、Promise、Async/Await 的区别

  1. setTimeout

    settimeout 的回调函数放到宏任务队列里,等到执行栈清空以后执行。

  2. Promise

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    console.log('script start')
    let promise1 = new Promise(function (resolve) {
    console.log('promise1')
    resolve()
    console.log('promise1 end')
    }).then(function () {
    console.log('promise2')
    })
    setTimeout(function () {
    console.log('settimeout')
    })
    console.log('script end')
    // 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout
    复制代码
  3. async/await

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    async function async1() {
    console.log('async1 start')
    await async2()
    console.log('async1 end')
    }
    async function async2() {
    console.log('async2')
    }

    console.log('script start')
    async1()
    console.log('script end')

    // 输出顺序:script start->async1 start->async2->script end->async1 end
    复制代码

Async/Await 如何通过同步的方式实现异步

Async/Await 就是一个自执行的 generate 函数。利用 generate 函数的特性把异步的代码写成“同步”的形式,第一个请求的返回值作为后面一个请求的参数,其中每一个参数都是一个 promise 对象.

介绍节流防抖原理、区别以及应用

节流:事件触发后,规定时间内,事件处理函数不能再次被调用。也就是说在规定的时间内,函数只能被调用一次,且是最先被触发调用的那次。

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

使用场景
节流:滚动加载更多、搜索框搜的索联想功能、高频点击、表单重复提交……
防抖:搜索框搜索输入,并在输入完以后自动搜索、手机号,邮箱验证输入检测、窗口大小 resize 变化后,再重新渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* 节流函数 一个函数执行一次后,只有大于设定的执行周期才会执行第二次。有个需要频繁触发的函数,出于优化性能的角度,在规定时间内,只让函数触发的第一次生效,后面的不生效。
* @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)

/**
* 防抖函数 一个需要频繁触发的函数,在规定时间内,只让最后一次生效,前面的不生效
* @param fn要被节流的函数
* @param delay规定的时间
*/
function debounce(fn, delay) {
//记录上一次的延时器
var timer = null
return function () {
//清除上一次的演示器
clearTimeout(timer)
//重新设置新的延时器
timer = setTimeout(function () {
//修正this指向问题
fn.apply(this)
}, delay)
}
}
document.getElementById('btn').onclick = debounce(function () {
console.log('按钮被点击了' + Date.now())
}, 1000)
复制代码

简述 MVVM

什么是 MVVM?

视图模型双向绑定,是Model-View-ViewModel的缩写,也就是把MVC中的Controller演变成ViewModel。Model层代表数据模型,View代表 UI 组件,ViewModelViewModel层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。以前是操作 DOM 结构更新视图,现在是数据驱动视图

MVVM 的优点:

1.低耦合。视图(View)可以独立于 Model 变化和修改,一个 Model 可以绑定到不同的 View 上,当 View 变化的时候 Model 可以不变化,当 Model 变化的时候 View 也可以不变; 2.可重用性。你可以把一些视图逻辑放在一个 Model 里面,让很多 View 重用这段视图逻辑。 3.独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计。 4.可测试

Vue 底层实现原理

vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty()来劫持各个属性的 setter 和 getter,在数据变动时发布消息给订阅者,触发相应的监听回调
Vue 是一个典型的 MVVM 框架,模型(Model)只是普通的 javascript 对象,修改它则试图(View)会自动更新。这种设计让状态管理变得非常简单而直观

Observer(数据监听器) : Observer 的核心是通过 Object.defineProprtty()来监听数据的变动,这个函数内部可以定义 setter 和 getter,每当数据发生变化,就会触发 setter。这时候 Observer 就要通知订阅者,订阅者就是 Watcher

Watcher(订阅者) : Watcher 订阅者作为 Observer 和 Compile 之间通信的桥梁,主要做的事情是:

  1. 在自身实例化时往属性订阅器(dep)里面添加自己
  2. 自身必须有一个 update()方法
  3. 待属性变动 dep.notice()通知时,能调用自身的 update()方法,并触发 Compile 中绑定的回调

Compile(指令解析器) : Compile 主要做的事情是解析模板指令,将模板中变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加鉴定数据的订阅者,一旦数据有变动,收到通知,更新试图

谈谈对 vue 生命周期的理解?

每个Vue实例在创建时都会经过一系列的初始化过程,vue的生命周期钩子,就是说在达到某一阶段或条件时去触发的函数,目的就是为了完成一些动作或者事件

  • create阶段:vue 实例被创建
    beforeCreate: 创建前,此时 data 和 methods 中的数据都还没有初始化
    created: 创建完毕,data 中有值,未挂载
  • mount阶段: vue 实例被挂载到真实 DOM 节点
    beforeMount:可以发起服务端请求,去数据
    mounted: 此时可以操作 DOM
  • update阶段:当 vue 实例里面的 data 数据变化时,触发组件的重新渲染
    beforeUpdate :更新前
    updated:更新后
  • destroy阶段:vue 实例被销毁
    beforeDestroy:实例被销毁前,此时可以手动销毁一些方法
    destroyed:销毁后

组件生命周期

生命周期(父子组件) 父组件 beforeCreate –> 父组件 created –> 父组件 beforeMount –> 子组件 beforeCreate –> 子组件 created –> 子组件 beforeMount –> 子组件 mounted –> 父组件 mounted –>父组件 beforeUpdate –>子组件 beforeDestroy–> 子组件 destroyed –> 父组件 updated

加载渲染过程 父 beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted

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

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

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

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

computed与watch

通俗来讲,既能用 computed 实现又可以用 watch 监听来实现的功能,推荐用 computed, 重点在于 computed 的缓存功能 computed 计算属性是用来声明式的描述一个值依赖了其它的值,当所依赖的值或者变量 改变时,计算属性也会跟着改变; watch 监听的是已经在 data 中定义的变量,当该变量变化时,会触发 watch 中的方法。

watch 属性监听 是一个对象,键是需要观察的属性,值是对应回调函数,主要用来监听某些特定数据的变化,从而进行某些具体的业务逻辑操作,监听属性的变化,需要在数据变化时执行异步或开销较大的操作时使用

computed 计算属性 属性的结果会被缓存,当computed中的函数所依赖的属性没有发生改变的时候,那么调用当前函数的时候结果会从缓存中读取。除非依赖的响应式属性变化时才会重新计算,主要当做属性来使用 computed中的函数必须用return返回最终的结果 computed更高效,优先使用。data 不改变,computed 不更新。

使用场景 computed:当一个属性受多个属性影响的时候使用,例:购物车商品结算功能 watch:当一条数据影响多条数据的时候使用,例:搜索数据

组件中的 data 为什么是一个函数?

1.一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。 2.如果 data 是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间 data 不冲突,data 必须是一个函数。

为什么 v-for 和 v-if 不建议用在一起

1.当 v-for 和 v-if 处于同一个节点时,v-for 的优先级比 v-if 更高,这意味着 v-if 将分别重复运行于每个 v-for 循环中。如果要遍历的数组很大,而真正要展示的数据很少时,这将造成很大的性能浪费 2.这种场景建议使用 computed,先对数据进行过滤

React/Vue 项目中 key 的作用

  • key 的作用是为了在 diff 算法执行时更快的找到对应的节点,提高diff速度,更高效的更新虚拟DOM;

    vue 和 react 都是采用 diff 算法来对比新旧虚拟节点,从而更新节点。在 vue 的 diff 函数中,会根据新节点的 key 去对比旧节点数组中的 key,从而找到相应旧节点。如果没找到就认为是一个新增节点。而如果没有 key,那么就会采用遍历查找的方式去找到对应的旧节点。一种一个 map 映射,另一种是遍历查找。相比而言。map 映射的速度更快。

  • 为了在数据变化时强制更新组件,以避免“就地复用”带来的副作用。

    当 Vue.js 用 v-for 更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。重复的 key 会造成渲染错误。

vue 组件的通信方式

  • props/$emit 父子组件通信

    父->子props,子->父 $on、$emit 获取父子组件实例 parent、children Ref 获取实例的方式调用组件的属性或者方法 父->子孙 Provide、inject 官方不推荐使用,但是写组件库时很常用

  • $emit/$on 自定义事件 兄弟组件通信

    Event Bus 实现跨组件通信 Vue.prototype.$bus = new Vue() 自定义事件

  • vuex 跨级组件通信

    Vuex、$attrs、$listeners Provide、inject

nextTick 的实现

  1. nextTickVue提供的一个全局API,是在下次DOM更新循环结束之后执行延迟回调,在修改数据之后使用$nextTick,则可以在回调中获取更新后的DOM
  2. Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue将开启 1 个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个watcher被多次触发,只会被推入到队列中-次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作是非常重要的。nextTick方法会在队列中加入一个回调函数,确保该函数在前面的 dom 操作完成后才调用;
  3. 比如,我在干什么的时候就会使用 nextTick,传一个回调函数进去,在里面执行 dom 操作即可;
  4. 我也有简单了解nextTick实现,它会在callbacks里面加入我们传入的函数,然后用timerFunc异步方式调用它们,首选的异步方式会是Promise。这让我明白了为什么可以在nextTick中看到dom操作结果。

nextTick 的实现原理是什么?

在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后立即使用 nextTick 来获取更新后的 DOM。 nextTick 主要使用了宏任务和微任务。 根据执行环境分别尝试采用 Promise、MutationObserver、setImmediate,如果以上都不行则采用 setTimeout 定义了一个异步方法,多次调用 nextTick 会将方法存入队列中,通过这个异步方法清空当前队列。

使用过插槽么?用的是具名插槽还是匿名插槽或作用域插槽

vue 中的插槽是一个非常好用的东西 slot 说白了就是一个占位的 在 vue 当中插槽包含三种一种是默认插槽(匿名)一种是具名插槽还有一种就是作用域插槽 匿名插槽就是没有名字的只要默认的都填到这里具名插槽指的是具有名字的

keep-alive 的实现

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

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

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

mixin

mixin 项目变得复杂的时候,多个组件间有重复的逻辑就会用到 mixin 多个组件有相同的逻辑,抽离出来 mixin 并不是完美的解决方案,会有一些问题 vue3 提出的 Composition API 旨在解决这些问题【追求完美是要消耗一定的成本的,如开发成本】 场景:PC 端新闻列表和详情页一样的右侧栏目,可以使用 mixin 进行混合 劣势:1.变量来源不明确,不利于阅读 2.多 mixin 可能会造成命名冲突 3.mixin 和组件可能出现多对多的关系,使得项目复杂度变高

Vuex 的理解及使用场景

Vuex 是一个专为 Vue 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。

  1. Vuex 的状态存储是响应式的;当 Vue 组件从 store 中读取状态的时候,

若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新 2. 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation, 这样使得我们可以方便地跟踪每一个状态的变化 Vuex 主要包括以下几个核心模块:

  1. State:定义了应用的状态数据
  2. Getter:在 store 中定义“getter”(可以认为是 store 的计算属性),

就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来, 且只有当它的依赖值发生了改变才会被重新计算 3. Mutation:是唯一更改 store 中状态的方法,且必须是同步函数 4. Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作 5. Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中

img

hooks 用过吗?聊聊 react 中 class 组件和函数组件的区别

类组件是使用 ES6 的 class 来定义的组件。 函数组件是接收一个单一的 props 对象并返回一个 React 元素。

关于 React 的两套 API(类(class)API 和基于函数的钩子(hooks) API)。官方推荐使用钩子(函数),而不是类。因为钩子更简洁,代码量少,用起来比较”轻”,而类比较”重”。而且,钩子是函数,更符合 React 函数式的本质。

函数一般来说,只应该做一件事,就是返回一个值。 如果你有多个操作,每个操作应该写成一个单独的函数。而且,数据的状态应该与操作方法分离。根据函数这种理念,React 的函数组件只应该做一件事情:返回组件的 HTML 代码,而没有其他的功能。函数的返回结果只依赖于它的参数。不改变函数体外部数据、函数执行过程里面没有副作用。

类(class)是数据和逻辑的封装。 也就是说,组件的状态和操作方法是封装在一起的。如果选择了类的写法,就应该把相关的数据和操作,都写在同一个 class 里面。

类组件的缺点 :

大型组件很难拆分和重构,也很难测试。
业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。
组件类引入了复杂的编程模式,比如 render props 和高阶组件。
难以理解的 class,理解 JavaScript 中 this 的工作方式。

区别

函数组件的性能比类组件的性能要高,因为类组件使用的时候要实例化,而函数组件直接执行函数取返回结果即可。

1.状态的有无
hooks 出现之前,函数组件没有实例没有生命周期没有state没有this,所以我们称函数组件为无状态组件。 hooks 出现之前,react 中的函数组件通常只考虑负责 UI 的渲染,没有自身的状态没有业务逻辑代码,是一个纯函数。它的输出只由参数 props 决定,不受其他任何因素影响。

2.调用方式的不同
函数组件重新渲染,将重新调用组件方法返回新的 react 元素。类组件重新渲染将 new 一个新的组件实例,然后调用 render 类方法返回 react 元素,这也说明为什么类组件中 this 是可变的。

3.因为调用方式不同,在函数组件使用中会出现问题
在操作中改变状态值,类组件可以获取最新的状态值,而函数组件则会按照顺序返回状态值

React Hooks(钩子的作用)

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

React Hooks 的几个常用钩子:

  1. useState() //状态钩子
  2. useContext() //共享状态钩子
  3. useReducer() //action 钩子
  4. useEffect() //副作用钩子

还有几个不常见的大概的说下,后续会专门写篇文章描述下

  • 1.useCallback 记忆函数 一般把函数式组件理解为 class 组件 render 函数的语法糖,所以每次重新渲染的时候,函数式组件内部所有的代码都会重新执行一遍。而有了 useCallback 就不一样了,你可以通过 useCallback 获得一个记忆后的函数。

    1
    2
    3
    4
    5
    6
    7
    function App() {
    const memoizedHandleClick = useCallback(() => {
    console.log('Click happened')
    }, []) // 空数组代表无论什么情况下该函数都不会发生改变
    return <SomeComponent onClick={memoizedHandleClick}>Click Me</SomeComponent>
    }
    复制代码

    第二个参数传入一个数组,数组中的每一项一旦值或者引用发生改变,useCallback 就会重新返回一个新的记忆函数提供给后面进行渲染。

  • 2.useMemo 记忆组件 useCallback 的功能完全可以由 useMemo 所取代,如果你想通过使用 useMemo 返回一个记忆函数也是完全可以的。 唯一的区别是:useCallback 不会执行第一个参数函数,而是将它返回给你,而 useMemo 会执行第一个函数并且将函数执行结果返回给你
    所以 useCallback 常用记忆事件函数,生成记忆后的事件函数并传递给子组件使用。而 useMemo 更适合经过函数计算得到一个确定的值,比如记忆组件。

  • 3.useRef 保存引用值

    useRef 跟 createRef 类似,都可以用来生成对 DOM 对象的引用。useRef 返回的值传递给组件或者 DOM 的 ref 属性,就可以通过 ref.current 值访问组件或真实的 DOM 节点,重点是组件也是可以访问到的,从而可以对 DOM 进行一些操作,比如监听事件等等。

  • 4.useImperativeHandle 穿透 Ref

    通过 useImperativeHandle 用于让父组件获取子组件内的索引

  • 5.useLayoutEffect 同步执行副作用

    大部分情况下,使用 useEffect 就可以帮我们处理组件的副作用,但是如果想要同步调用一些副作用,比如对 DOM 的操作,就需要使用 useLayoutEffect,useLayoutEffect 中的副作用会在 DOM 更新之后同步执行。

    useEffect 和 useLayoutEffect 有什么区别:简单来说就是调用时机不同,useLayoutEffect 和原来 componentDidMount&componentDidUpdate 一致,在 react 完成 DOM 更新后马上同步调用的代码,会阻塞页面渲染。而 useEffect 是会在整个页面渲染完才会调用的代码。官方建议优先使用useEffect

React 组件通信方式

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

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

1)父组件向子组件通信

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

1
2
3
4
5
6
7
8
9
10
// 子组件: Child
const Child = (props) => {
return <p>{props.name}</p>
}

// 父组件 Parent
const Parent = () => {
return <Child name='京程一灯'></Child>
}
复制代码

2)子组件向父组件通信

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 子组件: 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 实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 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 中使用的;

setState 既存在异步情况也存在同步情况

1.异步情况 在React事件当中是异步操作

2.同步情况 如果是在setTimeout事件或者自定义的dom事件中,都是同步的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
//setTimeout事件
import React,{ Component } from "react";
class Count extends Component{
constructor(props){
super(props);
this.state = {
count:0
}
}

render(){
return (
<>
<p>count:{this.state.count}</p>
<button onClick={this.btnAction}>增加</button>
</>
)
}

btnAction = ()=>{
//不能直接修改state,需要通过setState进行修改
//同步
setTimeout(()=>{
this.setState({
count: this.state.count + 1
});
console.log(this.state.count);
})
}
}

export default Count;
复制代码
//自定义dom事件
import React,{ Component } from "react";
class Count extends Component{
constructor(props){
super(props);
this.state = {
count:0
}
}

render(){
return (
<>
<p>count:{this.state.count}</p>
<button id="btn">绑定点击事件</button>
</>
)
}

componentDidMount(){
//自定义dom事件,也是同步修改
document.querySelector('#btn').addEventListener('click',()=>{
this.setState({
count: this.state.count + 1
});
console.log(this.state.count);
});
}
}

export default Count;
复制代码

生命周期

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
安装
当组件的实例被创建并插入到 DOM 中时,这些方法按以下顺序调用:

constructor()
static getDerivedStateFromProps()
render()
componentDidMount()

更新中
更新可能由道具或状态的更改引起。当重新渲染组件时,这些方法按以下顺序调用:

static getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()

卸载
当组件从 DOM 中移除时调用此方法:

componentWillUnmount()
复制代码

说一下 react-fiber

1)背景

react-fiber 产生的根本原因,是大量的同步计算任务阻塞了浏览器的 UI 渲染。默认情况下,JS 运算、页面布局和页面绘制都是运行在浏览器的主线程当中,他们之间是互斥的关系。如果 JS 运算持续占用主线程,页面就没法得到及时的更新。当我们调用setState更新页面的时候,React 会遍历应用的所有节点,计算出差异,然后再更新 UI。如果页面元素很多,整个过程占用的时机就可能超过 16 毫秒,就容易出现掉帧的现象。

2)实现原理

  • react 内部运转分三层:
    • Virtual DOM 层,描述页面长什么样。
    • Reconciler 层,负责调用组件生命周期方法,进行 Diff 运算等。
    • Renderer 层,根据不同的平台,渲染出相应的页面,比较常见的是 ReactDOM 和 ReactNative。

Fiber 其实指的是一种数据结构,它可以用一个纯 JS 对象来表示

1
2
3
4
5
6
7
const fiber = {
stateNode, // 节点实例
child, // 子节点
sibling, // 兄弟节点
return, // 父节点
}
复制代码
  • 为了实现不卡顿,就需要有一个调度器 (Scheduler) 来进行任务分配。优先级高的任务(如键盘输入)可以打断优先级低的任务(如 Diff)的执行,从而更快的生效。任务的优先级有六种:
    • synchronous,与之前的 Stack Reconciler 操作一样,同步执行
    • task,在 next tick 之前执行
    • animation,下一帧之前执行
    • high,在不久的将来立即执行
    • low,稍微延迟执行也没关系
    • offscreen,下一次 render 时或 scroll 时才执行
  • Fiber Reconciler(react )执行过程分为 2 个阶段:
    • 阶段一,生成 Fiber 树,得出需要更新的节点信息。这一步是一个渐进的过程,可以被打断。阶段一可被打断的特性,让优先级更高的任务先执行,从框架层面大大降低了页面掉帧的概率。
    • 阶段二,将需要更新的节点一次过批量更新,这个过程不能被打断。
  • Fiber 树:React 在 render 第一次渲染时,会通过 React.createElement 创建一颗 Element 树,可以称之为 Virtual DOM Tree,由于要记录上下文信息,加入了 Fiber,每一个 Element 会对应一个 Fiber Node,将 Fiber Node 链接起来的结构成为 Fiber Tree。Fiber Tree 一个重要的特点是链表结构,将递归遍历编程循环遍历,然后配合 requestIdleCallback API, 实现任务拆分、中断与恢复。

从 Stack Reconciler 到 Fiber Reconciler,源码层面其实就是干了一件递归改循环的事情

Portals

Portals 提供了一种一流的方式来将子组件渲染到存在于父组件的 DOM 层次结构之外的 DOM 节点中。结构不受外界的控制的情况下就可以使用 portals 进行创建

何时要使用异步组件?如和使用异步组件

  • 加载大组件的时候
  • 路由异步加载的时候

react 中要配合 Suspense 使用

1
2
3
4
5
// 异步懒加载
const Box = lazy(() => import('./components/Box'))
// 使用组件的时候要用suspense进行包裹
;<Suspense fallback={<div>loading...</div>}>{show && <Box />}</Suspense>
复制代码

React 事件绑定原理

React 并不是将 click 事件绑在该 div 的真实 DOM 上,而是在document处监听所有支持的事件,当事件发生并冒泡至 document 处时,React 将事件内容封装并交由真正的处理函数运行。这样的方式不仅减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件。
另外冒泡到 document 上的事件也不是原生浏览器事件,而是 React 自己实现的合成事件(SyntheticEvent)。因此我们如果不想要事件冒泡的话,调用 event.stopPropagation 是无效的,而应该调用 event.preventDefault

react事件绑定原理

什么是防抖和节流?有什么区别?如何实现?

1. 概念

节流:事件触发后,规定时间内,事件处理函数不能再次被调用。也就是说在规定的时间内,函数只能被调用一次,且是最先被触发调用的那次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function debounce(fn) {
let timeout = null // 创建一个标记用来存放定时器的返回值
return function () {
clearTimeout(timeout) // 每当用户输入的时候把前一个 setTimeout clear 掉
timeout = setTimeout(() => {
// 然后又创建一个新的 setTimeout, 这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数
fn.apply(this, arguments)
}, 500)
}
}

function sayHi() {
console.log('防抖成功')
}

var inp = document.getElementById('inp')
inp.addEventListener('input', debounce(sayHi)) // 防抖
复制代码

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function throttle(fn) {
let canRun = true // 通过闭包保存一个标记
return function () {
if (!canRun) return // 在函数开头判断标记是否为true,不为true则return
canRun = false // 立即设置为false
setTimeout(() => {
// 将外部传入的函数的执行放在setTimeout中
fn.apply(this, arguments)
// 最后在setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。当定时器没有执行的时候标记永远是false,在开头被return掉
canRun = true
}, 500)
}
}

function sayHi(e) {
console.log(e.target.innerWidth, e.target.innerHeight)
}
window.addEventListener('resize', throttle(sayHi))
复制代码

2. 使用场景:

节流:滚动加载更多、搜索框搜的索联想功能、高频点击、表单重复提交……

防抖:搜索框搜索输入,并在输入完以后自动搜索、手机号,邮箱验证输入检测、窗口大小 resize 变化后,再重新渲染。

项目优化

移除生产环境的控制台打印。方案很多,esling+pre-commit、使用插件自动去除,插件包括 babel-plugin-transform-remove-console、uglifyjs-webpack-plugin、terser-webpack-plugin。最后选择了 terser-webpack-plugin,脚手架 vue-cli 用这个插件来开启缓存和多线程打包,无需安装额外的插件,仅需在 configureWebpack 中设置 terser 插件的 drop_console 为 true 即可。最好还是养成良好的代码习惯,在开发基本完成后去掉无用的 console,vscode 中的 turbo console 就蛮好的。

第三方库的按需加载。echarts,官方文档里是使用配置文件指定使用的模块,另一种使用 babel-plugin-equire 实现按需加载。element-ui 使用 babel-plugin-component 实现按需引入。

公有样式,比如对 element-ui 部分组件(如弹框、表格、下拉选框等)样式的统一调整。公共组件,比如 date-picker、upload-file 等在 element-ui 提供的组件基本上做进一步的封装。自定义组件包括 preview-file、搜索框等。

前后端数据交换方面,推动项目组使用蓝湖、接口文档,与后端同学协商,规范后台数据返回。

雅虎军规提到的,避免css表达式、滤镜,较少DOM操作,优化图片、精灵图,避免图片空链接等

性能问题:页面加载性能、动画性能、操作性能。Performance API,记录性能数据。

winter 重学前端 优化技术方案:

缓存:客户端控制的强缓存策略

降低请求成本:DNS 由客户端控制,隔一段时间主动请求获取域名 IP,不走系统 DNS(完全看不懂)。TCP/TLS 连接复用,服务器升级到 HTTP2,尽量合并域名。

减少请求数:JS、CSS 打包到 HTML。JS 控制图片异步加载、懒加载。小型图片使用 data-uri。

较少传输体积:尽量使用 SVG\gradient 代替图片。根据机型和网络状况控制图片清晰度。对低清晰度图片使用锐化来提升体验。设计上避免大型背景图。

使用CDN加速,内容分发网络,是建立再承载网基础上的虚拟分布式网络,能够将源站内容缓存到全国或全球的节点服务器上。用户就近获取内容,提高了资源的访问速度,分担源站压力。