Javascript基础
基础数据类型
string number boolean undefined null symbol(es6)
原型链
1、在js中每个实例对象上都有一个私有属性指向这个实例对象的构造函数的原型对象,这个原型对象本质也是一个对象,它也会有一个私有属性指向它的构造函数的原型对象,就这样一只通过每个对象的私有属性链接,构成原型链。
当我们访问这个实例对象的属性时,首先会在这个实例对象上找有没有要找的属性,如果没有就会找此实例对象构造函数的原型对象有没有,就这样依次通过原型链查找,如果最后没找到,就回返回undefined
谈谈对作用域的理解
1、js中的作用域分为全局作用域、函数作用域、块级作用域
2、全局作用域:是指js全局运行的环境,比如在浏览器中玖是window所在的作用域,在全局作用的变量所有地方都可以访问
3、函数作用域:函数作用域也叫局部作用域,如果一个变量声明在函数内,那么这个变量只能在此函数作用域内访问,在函数外部无法访问。
4、块级作用域:es6引入了let、const关键字来声明变量,如果在一个大括号用let、const声明了变量,那么这变量就存在于块级作用域内,在大括号外面无法访问。
5、js遵循的是词法作用域,即在变量的声明阶段就已经确定好了此变量的作用域,而非执行阶段。
6、当在一个作用域内访问一个变量时首先会从当前作用域内查找此变量是否存在,如果不存在就会查找其上层级所在的作用域,以次类推,如果找找全局作用域还没找到,就会报错,这样就构成了一个作用域链
变量提升(JS执行过程)
1、当js运行之前,首先会对当前js脚本进行词法解析,以及进行变量的初始化操作,对于var声明的关键字,会将此变量指向undefined,函数声明的的关键字指向当前函数的运行环境
2、在js运行阶段,在对变量进行赋值操作,将变量指向实际的引用值,就这样当在声明前访问被声明的变量或者函数,是可以访问到的,这样就造成了变量提升
3、全局环境内,函数声明优先于var变量声明,局部环境内函数名->参数->变量
继承
原型链继承
实现方法 :通过让子类的prototype属性指向一个父类的实例对象实现继承
优点:简单
缺点:引用类型属性被所有实力共享、不能向父类传参
// 父类
function Person(name) {
this.name = 'person'
this.habit = ["eat", "drink", "sleep"]
}
// 父类 prototype 上的方法
Person.prototype.sayName = function () {
console.log(this.name)
}
// 子类
function Student(name) {
this.name = name;
this.job = 'student';
}
// 通过子类 prototype 指向父类一个实例
Student.prototype = new Person()
// 子类 prototype 上的方法
Student.prototype.sayHello = function () {
console.log('hello, i"m ' + this.name)
}
// 子类实例
const student = new Student('小明')
call、aplly继承
实现方法:通过在子类构造函数使用call、aplly来调用父类构造函数实现继承
优点:相对简单、避免了引用类型的属性被共享
缺点:只能继承构造器中定义的属性和方法,不能继承原型上定义的属性和方法
// 父类
function Person(name, age) {
this.name = name;
this.age = age;
this.habit = ["eat", "drink", "sleep"]
}
// 父类 prototype 上的方法
Person.prototype.sayName = function () {
console.log(this.name)
}
// 子类
function Student(name, age, sex) {
// 在子类构造函数中调用父类构造函数
Person.apply(this, [name, age])
this.sex = sex
}
// 子类 prototype 上的方法
Student.prototype.sayHello = function () {
console.log(`Hello, I'm ${this.name}, ${this.age}, ${this.sex}`)
}
const student = new Student('小明', 18, 'male')
student.sayHello();
student.sayName();
组合继承
实现方法:通过在子类构造函数使用call、aplly来调用父类构造函数继承构造器中的方法和属性,在通过让子类的prototype属性指向一个父类的实例对象继承prototype上的属性和方法
优点:引用类型的属性不被共享、还可以向父类传参,还继承了父类prototype上的属性和方法
// 父类
function Person(name, age) {
this.name = name;
this.age = age;
this.habit = ["eat", "drink", "sleep"]
}
// 父类 prototype 上的方法
Person.prototype.sayName = function () {
console.log(this.name)
}
// 子类
function Student(name, age, sex) {
// 在子类构造函数中调用父类构造函数
Person.apply(this, [name, age])
this.sex = sex
}
// 子类的prototype指向一个父类的实例
Student.prototype = new Person()
// 子类 prototype 上的方法
Student.prototype.sayHello = function () {
console.log(`Hello, I'm ${this.name}, ${this.age}, ${this.sex}`)
}
const student = new Student('小明', 18, 'male')
student.sayHello();
student.sayName();
原型式继承
实现方法:通过实现一个函数,此函数接收一个对象参数,这个对象参数就是该方法调用时返回对象的原型对象,实际就跟Object.create原理一致
优点:通过函数的封装使对象继承更容易
缺点:要确保创建多个对象时所传递的对象不能为同一个父类的实例对象
function Person(name, age) {
this.name = name;
this.age = age;
this.habit = ["eat", "drink", "sleep"]
}
function createStudentFactory(prototype) {
function Student(name, age, sex){
this.sex = sex
}
Student.prototype = prototype;
return new Student();
}
const student = createObjFactory(new Person('小明', 18))
寄生式继承
function createObj (o) {
var clone = Object.create(o);
clone.sayName = function () {
console.log('hi');
}
return clone;
}
寄生组合式继承
// 父类
function Person(name, age) {
this.name = name;
this.age = age;
this.habit = ["eat", "drink", "sleep"]
}
// 父类 prototype 上的方法
Person.prototype.sayName = function () {
console.log(this.name)
}
// 子类
function Student(name, age, sex) {
Person.apply(this, [name, age])
this.sex = sex
}
// 关联子类和父类
function prototype(child, parent) {
const prototype = Object.create(parent.prototype)
prototype.constructor = child
child.prototype = prototype
}
prototype(Student, Person)
// 子类 prototype 上的方法
Student.prototype.sayHello = function () {
console.log(`Hello, I'm ${this.name}, ${this.age}, ${this.sex}`)
}
const student = new Student('小明', 18, 'male')
student.sayHello()
class继承
- 实现方法:通过es6的class、extents来实现继承,继承的最优方法
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
this.habit = ["eat", "drink", "sleep"]
}
sayName () {
console.log(this.name)
}
}
class Student extends Person {
constructor(name, age, sex) {
super(name, age)
this.sex = sex
}
sayHello() {
console.log(`Hello, I'm ${this.name}, ${this.age}, ${this.sex}`)
}
}
const student = new Student('小明', 18, 'male')
console.log(student)
闭包
1、闭包是指有权访问另一个函数作用域变量的函数
2、正常情况下,当函数调用结束后其作用域以及作用域内的变量都会被释放
3、如果构成闭包函数,由于一个函数引用了另一个函数作用域内的变量,所以当函数调用结束后,这个变量并不会释放,这就构成了一个闭包环境
4、闭包一般会用来封装私有变量、实现模块变量私有化等
事件循环
1、当js引擎运行一段代码时,首先会将一些同步代码推入主线程调用栈进行执行,js主线程调用栈是单线程运行的。
2、当遇到一些异步代码api时,比如像setTimeout、setInterval、MutationObserve、setImmediate、Promise.then等api时,当调用栈调用完这些api后会将后面的逻辑交给异步线程进行处理,当异步线程调用完这些异步的代码,就会将微任务事件的回调放入微任务队列,宏任务事件放入宏任务队列中
3、每当主线程执行栈里同步代码执行完毕,首先会看看微任务事件队列内是否有可执行的回调函数,如果有会先执行微任务内的回调,
4、当微任务队列内所有的任务都执行完毕,就会执行宏任务队列内的回调函数,每一轮宏任务事件循环结束,也会先检查微任务队列是否有可执行任务
5、就这样一只循环往复构成事件循环
requestAnimationFram执行时机
每次浏览器执行渲染相关的任务都是有固定时机的,跟cup性能以及屏幕刷新率有关,事件循环大概会每秒执行60次渲染任务(slp)样式计算、布局计算、绘制页面,而requestAnimationFram会在这些操作之前进行执行,requestAnimationFram也会产生一个队列每当
requestIdleCallback执行时机
requestIdleCallback执行时机是跟屏幕刷新率有关的,如果一个屏幕每秒刷新60次的话,每帧执行的的时间大约为16ms,在一帧内需要执行的操作有 (处理用户的交互)、(JS 解析执行)、(窗口大小变化、滚动、屏幕分辨率变化、动画事件)、(requestAnimationFram)、(样式计算、布局计算、绘制页面),如果执行完这些任务还没有达到16ms就会执行requestIdleCallback,如果达到了就不会执行
事件模型(事件冒泡、事件代理)
1、在js执行事件时分为两个阶段:捕获阶段和冒泡阶段,当内父元素和子元素都绑定了事件的情况下,在捕获阶段会先出发父元素的事件,后触发子元素的事件,在冒泡阶段会先触发父元素的事件,后触发子元素的事件,默认情况下,事件都是在冒泡阶段执行的
2、正式由于事件冒泡的存在,可以通过给父级元素绑定事件,通过判断e.target来确定是哪个元素来进行事件处理
this指向(箭头函数中this指向)
1、对于普通函数来说,this的指向一般取决于当前函数所在上下文环境,比如如果在全局定义的函数this就指向window,如果在一个对象定义函数,this就指向这个对象,总结来说就是谁调用 this 就指向谁,但是可一通过call、apply、bind来改变this指向
2、对于肩头函数this指向其所在上下文环境父级所在的上下文环境
浮点精度
1、由于计算时会将十进制数字转换成2进制,在这过程中导致出现了无限循环的二进制数,所以在进行运算完毕再转十进制会导致计算结果出现误差
2、一般通过将小数转换成整数在进行运算,然后将结果在转换成小数,或者用第三方库
网络协议
七层网络模型
OSI七层网络模型 | TCP/IP四层网络概念 | 对应网络协议 | 作用 |
---|---|---|---|
应用层(Application) | 应用层 | HTTP、TFTP、FTP、NFS、WAIS、SMTP | 为应用程序提供网络服务 |
表示层(Presentation) | TeInet、Rlogin、SNMP、Gopher | 数据格式化、加密、解密 | |
会话层(Session) | SMTP、DNS | 建立、维护、管理回话连接 | |
传输层(Transport) | 传输层 | TCP、UDP | 建立、维护、管理端到端连接 |
网络层(Network) | 网络层 | IP、ICMP、ARP、RARP、AKP、UUCP | IP寻址和路由选择 |
数据链路层(Data Link) | 数据链路层 | FDDI、Ethernet、Arpanet、PDN、SLIP、PPP | 控制网络层与物理层的通信 |
物理层(Physical) | IEEE 802.1A、IEEE 802.2到IEEE 802.11 | 比特流传输 |
TCP与UDP协议特点
TCP连接需要进行三次握手进行数据传输前的连接确认(A->B:客户端告诉服务端要建立连接)(B->A:服务端告诉客户端收到连接请求可以建立连接)(A—>B:收到可以连接的请求),四次挥手确认连接完毕并关闭连接(A—>B:收据发送完毕,请求释放)(B->A:收到释放请求,释放客户端到服务端的请求,连接是双向的此时服务端到客户端的请求还没释放)(B->A:请求客户端释放服务端的请求)(A->B:请求收到,即将释放连接)
特点:面向连接、支持单播传输、面向字节流、可靠传输、提供拥塞控制、TCP提供全双工通信
UDP是一种无连接协议,UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。
特点:面向无连接、有单播,多播,广播的功能、UDP是面向报文的、不可靠性
彻底搞懂HTTPS
链接1
链接2
浏览器相关
浏览器工作原理(从输入URL到页面渲染过程)
链接
1、浏览器本地缓存,根据强缓存和协商缓存的规则进行判定是否使用缓存
2、DNS查询,解析URL到IP地址
3、建立TCP连接,发起请求
4、服务端响应,发送数据资源
5、客户端接收HTML资源,解析资源渲染页面
浏览器拿到资源后的渲染步骤:
1、获取HTML资源:将HTML转成可编程的文档对象模型(DOM)
在HTML解析阶段如果遇script标签(特别是没有 async 或者 defer 属性)),会停止HTML的解析
有 async,会在 HTML 文档解析时并行下载文件,并在下载完成后立即执行(暂停 HTML 解析)
有 defer,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行会在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成
css加载不会阻塞DOM树的解析
css加载会阻塞DOM树的渲染
css加载会阻塞后面js语句的执行
缓存策略(强缓存、协商缓存)
当浏览器请求资源时首先会查询强缓存是否过期,如果没过期就会直接从硬盘或者内存中加载缓存资源,不会在发起http请求,如果缓存过期并且设置有协商缓存,则先回进
强缓存
浏览器会根据响应头的两个字端进行强缓存查询Expires(过期时间)和Cache-Control(缓存控制),Cache-Control优先级高于Expires
Expires:如果客户端此次发送请求的时间在Expires之前,则会直接触发缓存,不再去发起请求
Cache-Control:这个响应头会有多个字端,代表不同的意思,如表格
指令 | 作用 |
---|---|
public | 表示响应可以被客户端和代理服务器缓存 |
private | 表示响应只可以被客户端缓存 |
max-age=30 | 缓存30秒后过期,需要重起请求 |
smax-age=30 | 覆盖max-age,作用一样,只在代理服务器中生效 |
no-store | 不缓存任何响应 |
no-cache | 资源被缓存,但立即失效,下次会发起请求验证资源是否过期(协商缓存) |
max-stale=30 | 30秒内即使缓存过期,也使用该缓存 |
min-fresh=30 | 希望在30秒内获取最新的响应 |
协商缓存
Last-Modified & If-Modified-Since
当第一次客户端向服务端发起请求时,服务端会在响应头中加入Last-Modified(最近一次文件修改时间)字段,当客户端再次发起请求时,客户端会自动设置请求头If-Modified-Since为上次请求响应头中的Last-Modified值,当服务端收到请求会取的该值并和最近一次文件修改时间进行对比,如果过期了就重新下发资源,如果没过期则告诉浏览器使用缓存资源
缺点:这种方法侦测改变的时间的最小单位为1s,这意味着如果在1s的时间内,请求的资源发生了改变,也会正常触发缓存,导致客户端无法获取到最新资源
Etag & If-None-Match
当第一次客户端向服务端发起请求时,服务端会在响应头中加入Etag(资源hash值)字段,当客户端再次发起请求时,客户端会自动设置请求头If-None-Match为上次请求响应头中的Etag值,当服务端收到请求会取的该值并和当前文件的hash值进行对比,如果一样则告诉浏览器使用缓存资源,如果不一样则重新下发资源
缺点:由于Etag需要服务端使用特定算法判断资源变化情况,所以所占用资源较多,性能上不如Last-Modified&If-Modified-Since这种判断时间戳的方式快
内存泄漏
垃圾回收机制
跨域原理
手写实现
手写call
Function.prototype.myCall = function (context, ...args) {
let ctx = (context === null || context === undefined) ? window : context;
ctx.fn = this;
const res = ctx.fn(...args)
delete ctx.fn
return res;
}
手写apply
Function.prototype.myCall = function (context, args = []) {
let ctx = (context === null || context === undefined) ? window : context;
ctx.fn = this;
const res = ctx.fn(...args)
delete ctx.fn
return res;
}
手写bind
Function.prototype.myBind = function (context, ...args) {
// 函数本体
const fn = this;
// 新函数
return function newFn(...newArgs) {
// 当作构造函数使用
if (this instanceof newFn) {
return new fn(...args, newArgs)
}
// 单做普通函数使用
return fn.apply(context, [...args, ...newArgs])
}
}
手写new
function factoryNew(Constructor, args = []) {
if (typeof Constructor !== 'function') {
throw 'first argument must be a function!'
}
// 创建一个新对象并绑定对象的__proto__和构造函数的prototype
const obj = Object.create(Constructor.prototype)
// 通过aplly方法调用构造函数,和绑定this对象
const res = Constructor.apply(obj, args)
// 判断返回值是否是函数或者对象
const isObject = typeof res === 'object' && typeof res !== null;
const isFunction = typeof res === 'function'
// 如果是对象或者函数则返回原结果
if (isObject || isFunction) {
return res
}
// 否则返回新建的对象
return obj
}
手写 instanceof
/**
* 手写 intstanceof
* 需要判断 实例的__proto__ === 构造函数的protorype
* 使用while进行循环实例原型链
* 当到原型链终点(null)终止循环并返回false
* 当实例原型链中存在构造函数返回false,终止循环
* 每次循环结束后,变量重新赋值
*/
function factoryInstanceof(instance, Constructor) {
// 获取实例proto属性
let left = instance.__proto__;
// 获取构造函数 prototype
let right = Constructor.prototype;
// 递归判断
while(true){
// 到原型链终点停止循环并返回false
if(left == null) return false
// 当实例原型链中有构造函数返回true
if(left == right) return true
// 原型链递归
left = left.__proto__
}
}
手写Object.create
/**
* 手写Object.create
*/
Object.myCreate = function (proto) {
// 创建构造函数
function F() {}
// 给构造函数绑定prototype
F.prototype = proto
// 返回构造函数实例
return new F()
}
手写Promise
手写Ajax
function request(url, method, data, async = true) {
return new Promise((resolve, reject) => {
// 1.创建 xhr 实例
const xhr = new XMLHttpRequest()
// 2.设置监听函数
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (this.status === 200) {
resolve(this)
} else {
reject(this)
}
}
}
// 3.创建链接
xhr.open(url, method, async)
// 4.发送数据
xhr.send(data)
})
}
手写节流函数
/**
* 应用场景
* 如果触发某一操作,在间隔时间内才会调用一次
*/
function throttle(fn, time) {
let flag = true
return function (...args) {
if (flag) {
flag = false
setTimeout(() => {
flag = true
fn(...args)
}, 1000)
}
}
}
const log = throttle((val) => console.log(val), 1000)
手写防抖函数
/**
* 应用场景
* 不停操作就一直不掉用函数,当停止操作时多长时间后,就开始调用
*/
function debounce(fn, time) {
let timer = null
return function (...args) {
clearTimeout(timer)
timer = setTimeout(() => {
timer = fn(...args)
}, time)
}
}
const log = debounce((val) => console.log(val), 1000)
手写字符串trim方法
String.prototype.myTrim = function () {
// 全局匹配开头和结尾出现的一次或多次空白字符串
return this.replace(/^\s+|\s+$/g,'')
}
手写deepClone
/**
* 采用递归
* 考虑循环引用问题:将每次复制的对象放入weakmap中,在下次递归时获取一下
* 基础类型数据直接返回
* 函数类型直接返回
* 考虑可遍历对象和不可遍历对象
* 考虑键是Symbol类型的键
* 考虑数组的遍历
* 考虑对象的遍历
*/
function deepClone(target, hash = new WeakMap()) {
}
手写订阅发布(观察者)模式
手写EventEmitter
工程化
什么是模块化(有哪些规范、有哪些工具)
webpack原理(插件、loader)
import 和 require 区别
前端微服务
前端框架(Vue、React)
什么是SPA以及它的优点和缺点
单页面应用就是通过监听浏览器地址栏的改变利用js和css动态更新页面内容的web应用,它不像传统多页面应用每次页面跳转都需要向服务端发起新的页面请求。
优点:
1、交互体验好,切换页面快
2、工作体验好,前后端分离各司其职,快速开发,技术要求高促进团队技术提高
3、减轻服务端页面请求压力
缺点:
1、不利于seo
2、首次加载时间长
什么是虚拟DOM
1、虚拟DOM是对真实DOM的抽象,本质上是JS对象,通过一些属性和字段对真实DOM的描述
2、通过特定的render方法可以将虚拟DOM渲染成不同平台的DOM节点
3、通过虚拟DOM可实现差量更新,节省了不必要的开销
4、操作起来更加的轻量,而且在开发体验上更加的便捷。
Diff算法
vue2 Diff算法
链接
1、通过patch函数进行新旧节点进行对比
2、通过sameVnode函数进行两个节点的比较,是否是相同的节点(key相同 && ((tag相同、isComment、data、input type相同)|| 异步组件 && asyncFactory相同))
3、如果两个节点不同则直接进行替换,创建新节点: -〉 更新父的占位符节点 -〉删除旧节点
4、如果两个节点相同调用patchVnode函数进行比较,执行以下的步骤
5、如果两个节点都是文本节点,直接进行比较是否相同,不同就直接进行替换,如果不是文本节点就进行一下步骤
6、如果新节点有子节点,旧节点没有子节点,就把新节点子节点给旧节点
7、如果新节点没有子节点,旧节点有子节点则删除旧节点子节点
8、如果新节点和旧节点都有子节点则调用updateChildren进行子节点的更新,执行以下步骤
9、头头比较、尾尾比较、头尾比较、尾头比较,如果有相同的节点则进行指针移动
10、如果没有就将所有旧的节点进行key => index的映射,然后用新头节点跟所有旧节点进行比较
React Diff算法
对比Vue和React更新DOM的机制
vue响应式原理
链接
简单来说就是当用户把一个普通的JS对象传入Vue实例作为data选项时,Vue将遍历此对象所有的属性,并使用Object.defineProperty把这些属性全部转为getter和setter,再结合发布订阅模式实现响应式系统。
具体实现是:
1、当初始化vue实例过程中传入data数据,首先会创建一个Observer实例,创建这个实例的时候利用Object.defineProperty对数据属性的getter和setter进行监听,然后会为每个属性创建一个Dep订阅器实例用来收集使用该data数据的Watcher订阅者实例。
2、在初始化模板的时候,会通过Compile一系列的操作将模版转换成render函数,同时会创建Watcher订阅者实例,并将Dep.target标识为当前的Watcher实例,如果在编译模板时使用到了data中的属性就会触发它的getter,在getter中会判断Dep.target是否有值,如果有的话会调用Dep.addSub将Watcher收集起来。
3、数据更新是就会触发变更属性的setter,然后调用Dep.notify通知所有使用到该属性的Watcher去调用更新函数
vue中有几种watcher
1、render watcher(负责试图渲染)
2、computed watcher(负责计算属性更新
3、watcher api(watcher api更新逻辑)
vue对于数组的处理
vue通过改写所依赖数组的原型链,拦截了七个可以更改原数组的方法,在调用这些方法时首先会调用数组原方法来获取结果,然后在对新增的对象类型的数据进行监听,对修改的数据调用Dep的notify函数来通知所有的watcher进行更新操作,最后返回结果
vue是怎样进行异步更新的
链接
1、修改vue中的data时,会触发queueWatcher函数
2、这个函数会将相关的 Watcher 加入一个队列。
3、然后调用 nextTick 方法,传入flushSchedulerQueue函数,这个函数负责执行队列中的watcher的更新函数,然后将这个函数push进nextTick队列中
4、在nextTick中,会进行Promise、MutationObserver、setImmediate等函数是否存在的判断,会根据浏览器实际的兼容性选择在事件循环的哪个阶段执行更新函数
nextTick原理
1、nextTick会维护一个函数队列,每次调用nextTick都会向这个队列中新增一个函数。
2、还有一个flushCallbacks函数就是负责这个函数队列中函数依次调用的。
3、在nextTick函数内部会对Promise、MutationObserver、setImmediate的兼容性进行判断,如果支持这些微任务方法的话就会调用兼容的方法将flushCallbacks函数放在下次的时间循环中进行调用
vue computed和watch的区别,以及执行顺序
computed:顾名思义计算属性,当使用一个属性值并且需要根据这个值做转换的时候会使用,结果会缓存,当依赖属性不发生变化的时候不会从新计算
watch:顾名思义监听属性,当需要监听一个属性,当这个属性值发生变化时需要进行一些列操作时使用
初次渲染:watch是在beforCreate和created中间执行,computed是在beforeMount和mounted中间执行
数据更新:watch是在beforeUpdate前执行,computed是在beforeUpdate和updated中间执行
vue computed实现原理(怎样进行缓存)
1、在初始化阶段会调用initComputed函数,在这个函数中会为每个computed属性创建一个watcher实例,在这个watcher实例中有一个value属性负责缓存这个computed属性的取值,还有一个dirty属性就负责标记当前值是不是脏数据,并且将它的初始值设置为true
2、然后通过调用defineComputed方法,利用Object.defineProperty将当前computed的属性定义为当前vm实例的属性,并且给这个属性设置get/set方法,在这个getter方法中首先会取到当前属性的watcher实例,先判断一下当前dirty标记是不是脏数据,如果是脏数据就会调用watcher实例的get方法获取最新的值,并设置watcher.value为当前的值,然后将dirty设置为false,如果不是脏数据就直接返回watcher.value
3、当访问computed属性的时候会把Dep.target设置为当前计算属性的watcher,访问计算属性的时候会访问所依赖的data属性,然后调用data属性的get方法,在get方法中会判断Dep.target是否有值,如果有值就会把当前的watcher收集到自身的subs中。
4、初次访问computed属性时会把当前计算属性的watcher因为dirty的初始值是true,所以会直接通过watcher.get获取当前值,然后设置value缓存,如果computed属性没依赖的属性没有改变的话下次在获取值因为dirty的值为false,所以就直接返回的是watcher.value。
5、当computed以来的data属性改变的话,就会调用依赖此属性的watcher的update方法,其中就有当前computed属性的watcher,当在调用以后就会获取新的值。
vue组件之间的通信
1、父传子:props provide/inject
2、子传父:通过子父组件向子组件绑定自定义方法,然后再子组件内通过this.$emit方法来触发事件传值
3、兄弟传:eventBus(on) vuex
组件中data为什么是函数
因为作为组件的话,一个组件可能在多个地方复用,如果data是对象的话,那么所有用这个组件的地方就会共享同一个对象,造成逻辑混乱,所有用函数的形式,保证每个组件内部的状态都是独立的
v-modle原理
v-model实际就是@input和$emit('input', value)的一个语法糖
keep-alive原理
链接
1、在keep-alive组件created阶段会初始化两个对象分别缓存VNode(虚拟DOM)和VNode对应的键集合
2、在mounted阶段对include和exclude参数进行监听,然后实时地更新(删除)this.cache对象数据
3、在render阶段首先获取keep-alive包裹着的第一个子组件对象及其组件名;
根据设定的黑白名单(如果有)进行条件匹配,决定是否缓存;
不匹配,直接返回组件实例(VNode);
如果匹配根据组件ID和tag生成缓存Key,并在缓存对象中查找是否已缓存过该组件实例,
如果存在,直接取出缓存值并更新该key在this.keys中的位置,如果不存在则在this.cache对象中存储该组件实例并保存key值;
之后检查缓存的实例数量是否超过max的设置值,超过则根据LRU置换策略删除最近最久未使用的实例
最后将该组件实例的keepAlive属性值设置为true
4、在组件树渲染阶段,会根据组件的abstract属性决定是否降该组件渲染在vdom树上,如果是true的话就不进行树上的渲染
5、在patch阶段,会执行createComponent函数,首先会判断vnode.componentInstance和keepAlive值是否为true,首先会执行keepAlive的render方法
为什么要写vue3,vue3对比vue2有哪些优化
响应式系统
vue2中是通过对data进行遍历递归,利用Object.defineProperty将data中的属性转换成set/get来实现的,这样做有很多缺点:
1、必须递归遍历所有需要监听的属性,如果是深层次嵌套的对象,无疑是很大的性能损耗
2、只能对预先劫持过的数据做出响应,无法对预先不存在的对象属性进行监听
3、如果对数组进行Object.defineProperty劫持,性能不好,性能代价和用户体验不成正比,所以没有对数组进行劫持监听
vue3利用Proxy对数据进行代理劫持,得到了很大的优化提升
1、不用深层次遍历递归对象属性,只有当使用了该属性时才会对属性进行代理,实现了按需响应代理
2、由于Vue3使用Proxy对数据进行代理,所以对于数据是否有过预先的定义属性都能够进行拦截
3、由于Vue3使用Proxy对数据进行代理,是惰性的,所以对数组代理不会有什么问题
Diff算法的提升
1、在vue2中数据更新触发渲染时,vue通过递归遍历两个虚拟DOM树,并比较每个节点上的每个属性,来确定实际DOM的哪些部分需要更新,
2、在vue3中通过编译阶段对静态模板的分析,
对typescriot支持
vue使用flow进行类型检查,vue3使用typescript进行重构,更好的优化了用户开发体验
打包体积
vue3中将一些全局的api进行模块化(),当实际引用的时候才会导入,这样做在编译阶段可以更好的进行tree sharking
讲讲vue-ssr(原理、作用、好处)
vue-router有哪几种模式
hash history abstract
hash和history的原理
hash:改变地址栏中的hash值,触发hashchange事件,通过hashchange事件来改变当前页面的视图
history:通过h5 history api来进行地址栏中的地址改变,同时改变当前页面试图
vue-router中常用的API
1、全局前置守卫:router.beforeEach
2、全局解析守卫:router.beforeResolve
3、全局后置守卫:router.afterEach
4、路由独享守卫:beforeEnter
5、组件内的守卫:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
vue-router导航解析流程
全局 -> 路由 -> 组件 -> 全局 类似于冒泡机制
1、导航被触发。
2、在失活的组件里调用 beforeRouteLeave 守卫。
3、调用全局的 beforeEach 守卫。
4、在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
5、在路由配置里调用 beforeEnter。
6、解析异步路由组件。
7、在被激活的组件里调用 beforeRouteEnter。
8、调用全局的 beforeResolve 守卫 (2.5+)。
9、导航被确认。
10、调用全局的 afterEach 钩子。
11、触发 DOM 更新。
12、用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。