前端基础高频面试题(呕心沥血) 持续更新

好记性不如烂笔头---记下来再说~

目录

JavaScript部分

闭包

节流 

 防抖

继承

JavaScript数据类型  

let const 和var的区别

 undefined 和 null 的区别

 实参/形参

 JS 中的传参策略

深度克隆/浅克隆 

 JavaScript中的事件循环机制

事件流

说说箭头函数

变量提升/函数提升

简单说说原型/原型链

作用域/作用域链的区别

this指向

手写 call函数

手写 apply 函数

手写 new 方法

从输入url到页面展现发生了什么?

重排和重绘

get 和 post请求方式的区别

Cookie、sessionStorage、localStorage的区别

vue部分

响应式原理

手写vue2响应式原理

手写vue3响应式原理 

 模板编译

v-if 和 v-show 的区别

 v-for 和 v-if 为什么不能连用

 computed 和 watch 的区别

Vue2.x组件通信

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

Vue中组件生命周期调用顺序是什么样的?

接口请求一般放在哪个生命周期中?

 nextTick 原理

vue路由hash模式和history模式实现原理分别是什么,他们的区别是什么?

React部分 

说说对React的理解,有哪些特性

虚拟DOM是什么

虚拟 DOM 大概是如何工作的

虚拟 DOM 的优点

说说对React生命周期的理解

创建阶段

constructor

getDerivedStateFromProps

render

componentDidMount

更新阶段

shouldComponentUpdate

getSnapshotBeforeUpdate

componentDidUpdate

卸载阶段

componentWillUnmount

React 中 setState 什么时候是同步的,什么时候是异步的?

性能优化部分

 webpack 优化

vue性能 优化


JavaScript部分

闭包

1:定义

MDN上的定义:一个函数和对其 lexical environmet 词法环境的引用捆绑在一起 就叫做闭包

 2:自己的理解:

闭包是一种现象;

  • 函数嵌套
  • 内部函数引用外部函数的变量
  • 外部函数被调用

具备这三个现象就形成了闭包

闭包的形成与函数定义无关,是在函数被调用执行的时候才被确认创建的

闭包的形成,与作用域链的访问顺序有直接关系。

3:可以用来干什么!

1:创建私有变量,避免变量全局污染

2:内部函数能够访问到外部函数的作用域

3:模块化,主动暴露出属性的getter,setter方法供外部访问

4:缺点

 由于变量会一直保存在内存中,使用不善会造成内存溢出

节流 

1:定义

单位时间触发多次事件,只执行一次其事件对应的回调函数,就叫函数节流

实现1:

利用延时器 实现原理:提前定义容器变量用来保存setTimeout的返回值,在每次触发事件,准备开启新的setTimeout之前,先检查容器变量中是否保存有setTimeout的返回值,如果有,那么不再开启setTimeout,保证同一时间只有一个setTimeout存在。setTimeout执行完毕之后,手动清空容器变量的返回值

function throttle(fun,delay){
    let timer = null
    return function(){
        if(timer){
            return
        }
        timer = setTimeout({
        fun.apply(this,arguments)
            timer = null
        },delay)
    }
}

实现2:

利用时间戳 实现原理:提前设定变量,准备存储事件结束后的时间戳,在事件开启之后,立即保存时间戳,并判断当前时间戳和事件结束后的时间戳的差值,决定是否需要执行本次事件。事件执行完毕之后,保存事件结束时的时间戳,以供下次开启事件时计算差值

function throttle(fn, wait=300){
    let last = 0;
    return function(){
        var now = new Date().getTime();;
        if (now - last > wait) {
            fn.call(this);
            last = new Date().getTime();;
        }
    }
}

 防抖

1:定义:

触发事件后,在规定的时间后执行其对应的回调函数,如果在规定的时间内又触发了事件,则重新计时

2:实现

 将 setTimeout 的返回值挂载到 window 上,每次触发事件都会把上次的 timer 清除了,直到最后次无法清除,再执行回调函数

function debunce(fun,delay){
    let timer = null
    return function(){
        if(timer){
            cleanTimeout(timer)
            timer = null
        }
        timer = setTimeout({
            fun.apply(this,arguments)
        },delay)
    }
}

传送门:JavaScript 函数节流与防抖https://blog.csdn.net/weixin_41718879/article/details/121361862?spm=1001.2014.3001.5501https://blog.csdn.net/weixin_41718879/article/details/121361862?spm=1001.2014.3001.5501 

继承

解决实例对象间属性共享的方案

  • 原型链继承

核心: 子类原型指向父类实例

缺点1: 子类所有实例都共享父类原型上的属性,如果一个实例修改了父类原型上的引用变量,其他实例也会共享这个修改

缺点2: 实例化的时候无法向父类构造函数传参,也就无法初始化父类的属性

child.prototype = new Father()
  •  构造函数继承:

核心:在子类的构造函数中调用父类构造函数

缺点 1:无法共享父类原型链上的属性

function Children(){
    Father.apply(this,arguments)
}
  •  组合继承

核心: 子类的构造函数中调用父类构造函数;子类原型指向父类的实例

优点 1:可以向父类构造函数传参
优点 2:避免了父类中引用属性在子类中共享的问题
优点 3:父类原型链上的方法子类也可以共享
缺点:调用父类两次构造函数(实例化一次,子类 prototype 属性指向父类实例一次)

function Father(){

}

function Children(){
    // 子类构造函数中调用父类构造函数
    Father.apply(this,arguments)
}

// 子类原型指向父类实例
Children.prototype = new Father()

Children.prototype.constructor = Children

传送门: JavaScript 继承详解https://blog.csdn.net/weixin_41718879/article/details/121361627?spm=1001.2014.3001.5501https://blog.csdn.net/weixin_41718879/article/details/121361627?spm=1001.2014.3001.5501

JavaScript数据类型  

  • 9种基本数据类型(原始类型):
  1. Boolean:布尔值:有2个值分别是:true 和 false.
  2. null : 一个表明 null 值的特殊关键字。 
  3. undefined : 表示变量未赋值时的属性。
  4. Number:整数或浮点数,
  5. BigInt::任意精度的整数
  6. String:字符串是一串表示文本值的字符序列
  7. Symbol:一种实例是唯一且不可改变的数据类型。
  8. Record: 只读的object
  9. Tuple:只读的array
  • 对象(Object)(引用类型):数组/函数/对象

let const 和var的区别

  1. var 声明的变量有变量提升 未声明先使用不会报错,该变量的值是 undefined
  2. var 声明的全局变量会自动挂载到 window 上,let const 不会
  3. var不存在块级作用域,let和const存在块级作用域
  4. let const 声明的变量不存在变量提升,但是会有暂时性死区,就是未声明先使用,会报错
  5. let const 在同一个作用域中不能重复声明同一个变量 var 可以
  6. let var 声明变量可以不初始化,默认为 undefined const 声明必须初始化
  7. const是let的增强,声明是常量 如果是基本数据类型,变量不可修改 如果是引用数据类型 变量的属性可以修改,地址不能改

 undefined 和 null 的区别

  1. undefined,null 都是基本数据类型中一种,他们的值都只有一个 就是 undefined 和 null
  2. undefined 变量已经声明但是并未初始化的值就是 undefined
  3.  null 一般用来表示给将要返回为对象的变量的初始化
  4.  typeof undefined 为 undefined
  5.  typeof null 为 object 计算机语言是二进制(因为 js 底层用前三位占位符为 000 来表示对象, null 二进制全是 0)
  6. null 是javaScript的关键字,undefined不是关键字,是全局变量中的一个只读属性

 实参/形参

形参:在声明函数时,函数定义的参数
实参:在调用函数时,传递给函数的参数

 JS 中的传参策略

不管对于值类型还是引用类类型,都是按值传递的
对于 js 中的变量,值类型存放在栈中,引用类型的地址存放在栈中,对应的值存放在堆中。当传参发生的时候,值类型会直接将栈中的值进行复制,形参和实参此时实际上是两个完全不相干的变量。对于引用类型,传参发生时,会将实参变量位于栈中的地址进行复制,此时栈中会有两个指向同一个堆地址的指针。

深度克隆/浅克隆 

也称之为深拷贝/浅拷贝

浅拷贝:浅拷贝仅仅复制所拷贝的对象,而不复制它所引用的对象。

深拷贝:要复制的对象所引用的对象都复制了一遍

  • 实现深拷贝1:
  • function copy(target){
      let result
        if(typeof target !='object' || target === null){
            return target
        }
        result = Array.isArray(target)?[]:{}
        for(let key in target){
            result[key] = typeof target[key] === 'object' ?copy(target(key)) : target(key)
        }
        return result
    }
  •  实现深拷贝2: 
JSON.parse(JSON.stringify(target))

 JavaScript中的事件循环机制

  • 浏览器内核大致可以分为 GUI 渲染引擎,js 引擎,时间管理引擎,回调函数引擎,http 请求引擎
  • 其中 js 引擎是专门处理 js 脚本任务的
  • javaScript 是单线程的,但是他有一个特点是”非阻塞”的
  • 首先 js 处理任务是有一个主线程和一个调用栈的
  • 调用栈是先进后出,执行的任务会置顶,执行完会就会移除
  • js 任务我们可以分为同步任务和异步任务
  • 同步任务会进入调用栈排队等待执行,遇到异步任务,依次把异步任务放入一个队列,并且会在 eventTable 中注册回调函数
  • 当主线程处理完同步任务后,调用栈被清空的时候,会去任务队列中依次读取任务,取出相应的回调函数进行执行
  • 任务队列是先进先出的
  • 如此的读取执行 读取执行 反复操作 就是这个事件循环机制了

  • 那么除了广义上区分的同步任务和异步任务 还可以分为宏任务和微任务
  • 当 js 引擎执行第一次事件循环的时候,也就是循环整个 script 代码的时候,中途遇到了微任务,会把微任务放入微任务队列,当这个宏任务执行完之后,会去微任务队列读取任务,如果有,则依次读取微任务,取出起相应的回调函数并执行
  • 只有当当前微任务队列中的任务执行完了之后,浏览器才会去渲染 UI 或者才会去执行下一个宏任务,如此反复,就是 js 的事件循环机制了
  • 宏任务:script,setTimeout,setInterval,setImmediate,I/O,UI Rending
  • 微任务:process.nextTick,promise.then,mutationObserve

事件流

事件流:事件流描述的是从页面中接收事件的顺序,有以下三个阶段。

  • 事件捕获阶段

  • 处于目标阶段

  • 事件冒泡阶段

addEventListeneraddEventListener是DOM2 级事件新增的指定事件处理程序的操作,这个方法接收3个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。最后这个布尔值参数如果是true,表示在捕获阶段调用事件处理程序;如果是false,表示在冒泡阶段调用事件处理程序。

说说箭头函数

  • 箭头函数是一种简写方法,没有this对象,箭头函数中的this取决于它所在的上下文中的this
  • 箭头函数没有原型对象prototype
  • 因为没有原型对象,所以不能作为构造函数使用,不能使用new 关键字
  • 没有arguments对象
  • call apply bind对其无效

变量提升/函数提升

在es6之前,用var 声明的变量存在变量提升

变量提升:指变量的声明会提升到变量所在作用域的最顶端,赋值则是留在代码所在的位置

函数提升:只有函数声明式的函数才会提升,函数字面量(函数表达式)不会提升 

 函数优先原则: 函数提升优先于变量提升,同名变量提升不会覆盖函数,只有赋值的时候才会覆盖

简单说说原型/原型链

原型也叫原型对象,是用来存放实例间共享属性和方法的

  • js中任何函数(除了箭头函数)都有一个属性叫prototype,它指向这个函数的原型Prototype,这个原型我们可以称之为原型对象 默认指向一个object空对象
  • js中任何一个对象(null除外)都有一个属性叫__proto__ 我们称之为隐式原型
  • 实例对象或者函数对象的__proto__ === 其对应的构造函数的prototype属性的
  • 原型对象是一个普通对象 其构造函数是Object()
  • 函数对象是通过Function创建的
  • Object函数也是Function创建的

当我们访问一个对象的属性的时候,首先会在该对象本身中来找这个属性,如果找到了,就返回这个属性所映射的值

如果找不到,就会在这个对象的原型上去找,就是__proto__上去找,如果还找不到,会接着去对象原型的原型上去找

 xx.__proto.__proto__中去找,这样子就形成了原型链.ECMA规定,原型链的顶端,即是Object.prototype.__proto__ === nul 找到这还找不到这属性,就会返回undefined了

总结:原型链就是用来查找属性的

作用域/作用域链的区别

作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,我们可以访问到外层环境的变量和函数。

作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。

当我们查找一个变量时,如果当前执行环境中没有找到,我们可以沿着作用域链向后查找。

总结:作用域链是用来查找变量的

this指向

this 永远指向最后调用函数的对象

this 指向是在函数调用的时候确定的,而不是在函数声明的时候确定的

  1.  无直接调用者则指向window(非严格模式下) .严格模式下会报错
  2. 构造函数中的this指向该构造函数实例化出来的对象
  3. dom事件中的this指向dom元素
  4. apply/call/bind / 指向第一个参数,若无参数,也指向window
  5. 箭头函数中的this是其上层应用上下文中的this
    var name = "global_name";

    function a() {
        var name = "local_name";
        console.log(this.name)
    }
    // 无直接调用者 this指向window  相当于window.a()
    a(); // global_name
    var name = "global_name";

    function a() {
        var name = "local_name";
        fn:function(){
            console.log(this.name)
        }
    }
    // 直接调用者为a对象 所以this指向a
    a.fn(); // local_name
var name = "global_name";
    var a = {
        name: "local_name",
        fn : function () {
            console.log(this.name);      
        }
    }
    // 直接调用者为对象a 所以输出local_name
    window.a.fn(); // local_name
    var name = "global_name";

    var a = {
        fn : function () {
            console.log(this.name);      
        }
    }

    var f = a.fn;
    // 无直接调用者 this指向window
    f(); //global_name
    var name = "global_name";

    function fn() {
        var name = 'local_name';
        fn1();
        function fn1() {
            console.log(this.name);      // global_name
        }
    }
    
    fn()  
    // 输出global_name  因为fn1()无直接调用者 this指向window

 


 const input = document.getElementById('input')

 input.addEventListener('input', function () {
       console.log('this指向:', this)  //this指向:  
  })

手写 call函数

Function.prototype.myCall = function (context) {
  if (typeof this != 'function') {
    return 'type Error'
  }
  let args = [...arguments].slice(1)
  context = context || window
  context.fn = this
  let result = null
  result = context.fn(...args)
  delete context.fn
  return result
}

手写 apply 函数

Funciton.prototype.myApply = function (context) {
  if (typeof this != 'function') {
    return 'type error'
  }
  context = context || window
  context.fn = this
  if (argumetns[1]) {
    result = context.fn(arguments)
  } else {
    result = context.fn()
  }
  return result
}

手写 new 方法

function myNew() {
  let obj = Object.create(null) // 创建新对象
  let context = Array.prototype.shift.call(arguments) // 获取构造函数
  if (typeof context === 'function') {
    Object.setPrototypeOf(obj, context.prototype) //设置原型
  } else {
    Object.setPrototypeOf(obj, null)
  }
  let result = context.apply(obj, arguments) // 改变this指向
  return typeof result == 'object' ? result : obj
}

从输入url到页面展现发生了什么?

  • 浏览器的地址栏输入URL并按下回车;
  • DNS 解析:将域名解析成 IP 地址;

        浏览器并不能直接通过域名找到对应的服务器,而是要通过 IP 地址。

  • 强缓存

        判断是否命中强缓存,如果命中,则不发送请求,直接请求缓存设备

  • 协商缓存

        如果没有命中强缓存,则再判断是否命中协商缓存,如果没命中,则发送请求

  • TCP 连接:TCP 三次握手;

        为了防止已经失效的连接请求报文段突然又传送到了服务器端,从而产生错误。

  • 发送 HTTP 请求;

        请求报文由请求行、请求头和请求体三部分组成。

  • 服务器处理请求并返回 HTTP 报文;
  • 浏览器解析渲染页面(CRP--关键渲染路径)

        1)HTML解析,处理HTML标记并构建DOM树。

        2)CSS解析,处理CSS标记并构建CSSOM树。

        3)将DOM树和CSSOM合并称render tree(渲染树)。将每条css规则按照【从右至左】的方                  式在dom树上进行逆向匹配,然后生成具有样式规则描述的渲染树。

        4)渲染树布局,计算每个节点的集合信息。包括repaint和reflow。

        5)渲染树绘制,将每个节点绘制到屏幕上。

  • 断开连接:TCP 四次挥手

重排和重绘

理解重排和重绘,先了解CRP

关键渲染路径(Critical Rendering Path)是浏览器将 HTML,CSS 和 JavaScript 转换为屏幕上的像素所经历的步骤序列。优化关键渲染路径可提高渲染性能。

浏览器把我们的代码渲染到屏幕上的像素点的步骤:

  1. 构建DOM(document object model)
  2. 构建cssOM(css Object model)
  3. 浏览器引擎结合两者创建渲染树(Render Tree)
  4. Layout 布局 确定页面上元素的大小和位置
  5. Paint 绘制到屏幕上

前端基础高频面试题(呕心沥血) 持续更新_第1张图片

  • 重排(Reflow):元素的 位置发生变动 时发生重排,也叫回流。此时在 Layout 阶段,计算每一个元素在设备视口内的确切位置和大小。当一个元素位置发生变化时,其父元素及其后边的元素位置都可能发生变化,代价极高。
  • 重绘(Repaint): 元素的 样式发生变动 ,但是位置没有改变。此时在关键渲染路径中的 Paint 阶段,将渲染树中的每个节点转换成屏幕上的实际像素,这一步通常称为绘制或栅格化。

重排发生在layout阶段,重绘发生在paint阶段,所以重排一定会引起重绘,重绘不一定会引起重排

get 和 post请求方式的区别

1:传参方式的区别

  • get方式传参是以key=value的形式拼接在url后面
  • post方式是将参数放在body中

2:在缓存方面的区别

  • get请求类似于查找的过程,用户获取数据,可以不用每次都与数据库连接,所以可以使用缓存。
  • post不同,post做的一般是修改和删除的工作,所以必须与数据库交互,所以不能使用缓存。因此get请求适合于请求缓存

3:HTTP协议没有规定get/post 请求参数的长度,对get请求参数的限制是来源与浏览器或web服务器,浏览器或web服务器限制了url的长度

Cookie、sessionStorage、localStorage的区别

  • Cookie:cookie数据始终在同源的http请求中携带(即使不需要),即cookie在浏览器和服务器间来回传递。而sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存。cookie只在设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭。cookie是在所有同源窗口中都是共享的。
  • sessionStorage:仅在当前浏览器窗口关闭前有效,自然也就不可能持久保持,l
  • localStorage:localStorage 在所有同源窗口中都是共享的;始终有效,窗口或浏览器关闭也一直保存,因此用作持久数据;(key:同源窗口都会共享,并且不会失效,不管窗口或者浏览器关闭与否都会始终生效)

前端基础高频面试题(呕心沥血) 持续更新_第2张图片

vue部分

响应式原理

  • 首先vue会对 data option 中的数据进行观察
  • 如果观测的数据是一个对象
  1. 递归遍历当前对象属性,通过 object.defineProperty 的形式给每个属性添加 get,set 方法;也给每个属性实例化 Dep,每个属性都有自己的 dep 实例
  2.  get 中收集依赖 watcher
  3. 在 set 中通知 watcher 更新 UI
  • 如果观测的数据是一个数组
  1. 会劫持数组原型上的方法,进行重写
  2. 在重写的方法中会收集当前数组的依赖
  3. 然后在观测阶段把数组的原型设置为重写后方法

手写vue2响应式原理

// vue2.0响应式原理
let oldArrayPrototype = Array.prototype
let proto = Object.create(oldArrayPrototype) // 继承
;['push', 'shift', 'unshift'].forEach(method => {
  proto[method] = function() {
    //函数劫持,把函数进行重写, 内部继续调用老的方法
    updateView()
    oldArrayPrototype[method].call(this, ...arguments)
  }
})

function observer(target) {
  if (typeof target !== 'object' || target == null) {
    return target
  }
  if (Array.isArray(target)) {
    // 劫持数组
    // target.__proto__ =proto;
    Object.setPrototypeOf(target, proto)
  }
  for (let key in target) {
    defineReactive(target, key, target[key])
  }
}
_toString = Object.prototype.toString

function defineReactive(target, key, value) {
  observer(value) //递归
  Object.defineProperty(target, key, {
    get() {
      return value
    },
    set(newValue) {
      if (_toString.call(value).slice(8, -1) === 'Object') {
        observer(value) //递归
      }
      if (newValue !== value) {
        observer(newValue)
        updateView()
        value = newValue
      }
    }
  })
}

function updateView() {
  console.log('数据变化,更新视图')
}

let data = { name: 'zf', age: { number: 100 } }
observer(data)
data.age = { number: 20 }
data.sex = 'nan'

手写vue3响应式原理 

// 核心是基于proxy的
//  触发视图更新
const toProxy = new WeakMap() // 存放是代理后的对象
const toRow = new WeakMap() // 存放的是代理前的对象
function trigger() {
  console.log('视图更新')
}
function reactive(target) {
  if (!isObject(target)) {
    return target
  }
  if (toProxy.get(target)) {
    // 如果代理表中已经存在了 就返回代理结果 (重复代理)
    return toProxy.get(target)
  }
  if (toRow.has(target)) {
    // 如果这个对象已经被代理过了,就返回对象 (传入的就是代理后的对象)
    return target
  }
  // 触发的方法
  const handlers = {
    set(target, key, value, receiver) {
      if (target.hasOwnProperty(key)) {
        trigger()
      }
      return Reflect.set(target, key, value) //设置值
    },
    get(target, key, value, receiver) {
      const res = Reflect.get(target, key)
      if (isObject(target[key])) {
        return reactive(res) // 递归 如果取的值还是一个对象的话 继续代理
      }
      return res
    },
    deleteProperty(target, key, value, receiver) {
      return Reflect.deleteProperty(target, key)
    },
  }
  let observed = new Proxy(target, handlers)
  toProxy.set(target, observed) // 远对象 代理后的结果
  toRow.set(observed, target)
  return observed
}

function isObject(target) {
  return typeof target === 'object' && target != null
}
let obj = {
  name: 'yh',
  a: [1, 2],
}

let p = reactive(obj)
p.a.push(33)
console.log(obj)

 模板编译

  1. 模板解析,将用户的模板通过正则等一系列方式转换成 AST 抽象语法树
  2. 遍历优化 AST,标记静态节点和静态根节点(在 diff 的 patching 算法中可以跳过静态节点)
  3. 最后再将 AST 转换成渲染函数 render

v-if 和 v-show 的区别

  • 编译过程不同
  • 编译条件不同
  • 控制手段不同

编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换

编译条件:v-if是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染

控制手段:v-show隐藏则是为该元素添加css--display:nonedom元素依旧还在。v-if显示隐藏是将dom元素整个添加或删除

  • v-show 由false变为true的时候不会触发组件的生命周

  • v-iffalse变为true的时候,触发组件的beforeCreatecreatebeforeMountmounted钩子,由true变为false的时候触发组件的beforeDestorydestoryed方法

从源码上来看:

含有 v-if 的节点在转换成 ast 抽象语法树,再将 AST 转成渲染函数 render 的代码来看,是一个三元表达式,只有为 true 的时候,才会去创建元素false 的时候 是调用的创建空元素的方法

含有 v-show 的节点 从 AST 转成的渲染函数 的代码中可以知道 v-show 被截取,被放到了指令的集合中去,当表达式为 false 的时候,设置节点的 display 属性为 none

 v-for 和 v-if 为什么不能连用

从源码来看
有 v-for 和 v-if 的节点转换成的 render 函数中代码中
先 loop 节点,再在每个节点中的用一个三元运算符来描述 v-if 的作用,所以是先进行遍历,再考虑是否渲染
如果遍历的数量太大,都要进行三元运算,所以性能狠低

建议使用 computed,先对数据进行过滤

 computed 和 watch 的区别

computed:是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;

watch:没有缓存性,更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;当我们需要深度监听对象中的属性时,可以打开deep:true选项,这样便会对对象中的每一项进行监听;watch选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

Vue2.x组件通信

  1. 父子组件通信
    • 事件机制(父--->子props ||  子--->父 $on,$emit)
    • 获取父子组件实例 $parent $children
    • Ref 获取实例的方式调用组件的属性或者方法
    • Provide、inject 
  2. 兄弟组件通信
    • eventBus: Vue.prototype.$bus = new Vue
    • Vuex
  3. 跨级组件通信
    • Vuex
    • $attrs/$listeners
    • Provide/inject

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

  1. 一个组件被复用多次的话,会通过一个构造函数创建多个实例。
  2. 如果data是对象的话,对象是引用类型,所有实例都共享对象属性的变化。所以为了保证组件不同的实例之间data不冲突,data必须是一个函数,调用函数都会创建一个新的对象,保证了同一个复用组件间的作用域不同.

Vue中组件生命周期调用顺序是什么样的?

  • 组件的调用顺序都是先父后子,渲染完成的顺序是先子后父。

  • 组件的销毁操作是先父后子,销毁完成的顺序是先子后父。

接口请求一般放在哪个生命周期中?

  1. 可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。
  2. 推荐在 created 钩子函数中调用异步请求

 nextTick 原理

事件循环机制
nextTick 主要是使用了宏任务和微任务定义了一个异步方法

首先修改数据,这是同步任务。同一事件循环的所有的同步任务都在主线程上执行,形成一个执行栈,此时还未涉及 DOM 。
Vue 开启一个异步队列,并缓冲在此事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。

同步任务执行完之后,才会去任务队列中读取回调函数,等待主线程执行,然后更新 dom
如果环境支持,nextTick 内部是用 promise.then,MutationObserver, setImmediate 等
如果环境不支持,采用的 setTimeout 来实现异步

vue路由hash模式和history模式实现原理分别是什么,他们的区别是什么?

  • hash 模式:

    • #后面 hash 值的变化,不会导致浏览器向服务器发出请求,浏览器不发出请求,就不会刷新页面

    • 通过监听 hashchange 事件可以知道 hash 发生了哪些变化,然后根据 hash 变化来实现更新页面部分内容的操作。

  • history 模式:

    • history 模式的实现,主要是 HTML5 标准发布的两个 API,pushState 和replaceState,这两个 API 可以在改变 url,但是不会发送请求。这样就可以监听 url 变化来实现更新页面部分内容的操作

  • 区别

    • url 展示上,hash 模式有“#”,history 模式没有

    • 刷新页面时,hash 模式可以正常加载到 hash 值对应的页面,而 history 没有处理的话,会返回 404,一般需要后端将所有页面都配置重定向到首页路由

    • 兼容性,hash 可以支持低版本浏览器和 IE。

React部分 

说说对React的理解,有哪些特性

React是一个UI库,以声明式编写 UI,构建管理自身状态的封装组件,然后对其组合以构成复杂的 UI。开发者不需要关心界面是如何被渲染的,只需要关心数据的生成和传递

  • 使用类似html的JSX语法来描述视图,提倡CSS-IN-JS方式
  • 通过虚拟dom 来描述和修改 真实dom
  • 在不同的生命周期阶段做不同的事情
  • 对真实DOM事件进行封装,使用事件委托的方式来捕获DOM事件

虚拟DOM是什么

真实dom是我们在浏览器开发者模式下看到的dom tree

虚拟dom是一个普通的js对象,是用来描述真实的dom结构,有以下几个主要属性:

  • type:是什么标签/元素
  • props:标签/元素的属性
  • children:是否有子节点

虚拟 DOM 大概是如何工作的

当 DOM 操作(渲染更新)比较频繁时,

React 底层会先将前后两次的虚拟DOM 树进行对比,

定位出具体需要更新的部分,生成一个补丁集

最后只把“补丁”打在需要更新的那部分真实DOM 上,实现精准的“差量更新”。

虚拟 DOM 的优点

  1. 频繁操作真实 DOM,会引起页面的重绘和重排,代价昂贵,性能低下,虚拟dom是一个js对象,操作对象性能开销极小;

  2. 解决了扩平台开发的问题,因为虚拟 DOM 描述的东西可以是真实 DOM,也可以是安卓界面。IOS 界面等等,这就可以对接不同平台的渲染逻辑。从而实现"一次编码,多端运行"(如 React,React Native)

说说对React生命周期的理解

react16.4之后  生命周期分为三个阶段:

  • 创建阶段
  • 更新阶段
  • 卸载阶段

创建阶段

创建阶段主要分为以下几个生命周期方法:

  • constructor
  • getDerivedStateFromProps
  • render
  • componentDidMount

constructor

实例过程中自动调用的方法,在方法内部通过super关键字获取来自父组件的props

在该方法中,通常的操作为初始化state状态或者在this上挂载方法

getDerivedStateFromProps

该方法是新增的生命周期方法,是一个静态的方法,因此不能访问到组件的实例

执行时机:组件创建和更新阶段,不论是props变化还是state变化,也会调用

在每次render方法前调用,第一个参数为即将更新的props,第二个参数为上一个状态的state,可以比较props 和 state来加一些限制条件,防止无用的state更新

该方法需要返回一个新的对象作为新的state或者返回null表示state状态不需要更新

render

类组件必须实现的方法,用于渲染DOM结构,可以访问组件stateprop属性

注意: 不要在 render 里面 setState, 否则会触发死循环导致内存崩溃

componentDidMount

组件挂载到真实DOM节点后执行,其在render方法之后执行

此方法多用于执行一些数据获取,事件监听等操作

更新阶段

该阶段的函数主要为如下方法:

  • getDerivedStateFromProps
  • shouldComponentUpdate
  • render
  • getSnapshotBeforeUpdate
  • componentDidUpdate

shouldComponentUpdate

用于告知组件本身基于当前的propsstate是否需要重新渲染组件,默认情况返回true

执行时机:到新的props或者state时都会调用,通过返回true或者false告知组件更新与否

一般情况,不建议在该周期方法中进行深层比较,会影响效率

同时也不能调用setState,否则会导致无限循环调用更

getSnapshotBeforeUpdate

该周期函数在render后执行,执行之时DOM元素还没有被更新

该方法返回的一个Snapshot值,作为componentDidUpdate第三个参数传入

getSnapshotBeforeUpdate(prevProps, prevState) {
    console.log('#enter getSnapshotBeforeUpdate');
    return 'foo';
}

componentDidUpdate(prevProps, prevState, snapshot) {
    console.log('#enter componentDidUpdate snapshot = ', snapshot);
}

此方法的目的在于获取组件更新前的一些信息,比如组件的滚动位置之类的,在组件更新后可以根据这些信息恢复一些UI视觉上的状态

componentDidUpdate

执行时机:组件更新结束后触发

在该方法中,可以根据前后的propsstate的变化做相应的操作,如获取数据,修改DOM样式等

卸载阶段

componentWillUnmount

此方法用于组件卸载前,清理一些注册是监听事件,或者取消订阅的网络请求等

一旦一个组件实例被卸载,其不会被再次挂载,而只可能是被重新创建

附上一张图:前端基础高频面试题(呕心沥血) 持续更新_第3张图片

React 中 setState 什么时候是同步的,什么时候是异步的?

在React中,如果是由React引发的事件处理(比如通过onClick引发的事件处理),调用setState不会同步更新this.state,除此之外的setState调用会同步执行this.state。所谓“除此之外”,指的是绕过React通过addEventListener直接添加的事件处理函数,还有通过setTimeout/setInterval产生的异步调用。

原因:在React的setState函数实现中,会根据一个变量isBatchingUpdates判断是直接更新this.state还是放到队列中回头再说,而isBatchingUpdates默认是false,也就表示setState会同步更新this.state,但是,有一个函数batchedUpdates,这个函数会把isBatchingUpdates修改为true,而当React在调用事件处理函数之前就会调用这个batchedUpdates,造成的后果,就是由React控制的事件处理过程setState不会同步更新this.state

性能优化部分

 webpack 优化

  1. 自带优化:生产环境会使用 tree-shaking 分析文件的依赖关系,没使用的代码不打包
  2. scope-hoisting —> 作用域提升,变量的结果会直接打包在文件中
  3. 打包速度的优化:开启多线程打包
  4. 打包体积的优化
  5. 开启gzip压缩
  • 按需引入,使用的第三方 UI 可以按需引入,比如 echart,elementUI 等
  • 语言国际包:一般都有很多其他国家的语言(momendjs)
  • 使用 babel-preset 处理 es6 语法的时候,默认会把所有的 es6 转换后的语法都打包到文件中去,使用useBuiltIn:’usage’方式 把使用到的语法转换后打进文件中去
  • 动态链接库 CDN 的方式引入 把体积大的包放到 cdn 上 webpack.dllPlugin(生成动态链接库) DllReferencePlugins(引用动态链接库

vue性能 优化

1)代码层面 

  • 尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher

  • 如果需要使用v-for给每项元素绑定事件时使用事件代理

  • SPA 页面采用keep-alive缓存组件

  • 在更多的情况下,使用v-if替代v-show

  • key保证唯一

  • 使用路由懒加载、异步组件

  • 防抖、节流

  • 第三方模块按需导入

  • 长列表滚动到可视区域动态加载

  • 图片懒加载

2)打包优化

  • 压缩代码

  • Tree Shaking/Scope Hoisting

  • 使用cdn加载第三方模块

  • 多线程打包happypack

  • splitChunks抽离公共文件

  • sourceMap优化

3)用户体验

  • 骨架屏

  • PWA

  • 还可以使用缓存(客户端缓存、服务端缓存)优化

你可能感兴趣的:(notes,前端)