好记性不如烂笔头---记下来再说~
目录
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性能 优化
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
- Boolean:布尔值:有2个值分别是:
true
和false
.- null : 一个表明 null 值的特殊关键字。
- undefined : 表示变量未赋值时的属性。
- Number:整数或浮点数,
- BigInt::任意精度的整数
- String:字符串是一串表示文本值的字符序列
- Symbol:一种实例是唯一且不可改变的数据类型。
- Record: 只读的object
- Tuple:只读的array
- var 声明的变量有变量提升 未声明先使用不会报错,该变量的值是 undefined
- var 声明的全局变量会自动挂载到 window 上,let const 不会
- var不存在块级作用域,let和const存在块级作用域
- let const 声明的变量不存在变量提升,但是会有暂时性死区,就是未声明先使用,会报错
- let const 在同一个作用域中不能重复声明同一个变量 var 可以
- let var 声明变量可以不初始化,默认为 undefined const 声明必须初始化
- const是let的增强,声明是常量 如果是基本数据类型,变量不可修改 如果是引用数据类型 变量的属性可以修改,地址不能改
- undefined,null 都是基本数据类型中一种,他们的值都只有一个 就是 undefined 和 null
- undefined 变量已经声明但是并未初始化的值就是 undefined
- null 一般用来表示给将要返回为对象的变量的初始化
- typeof undefined 为 undefined
- typeof null 为 object 计算机语言是二进制(因为 js 底层用前三位占位符为 000 来表示对象, null 二进制全是 0)
- null 是javaScript的关键字,undefined不是关键字,是全局变量中的一个只读属性
形参:在声明函数时,函数定义的参数
实参:在调用函数时,传递给函数的参数
不管对于值类型还是引用类类型,都是按值传递的
对于 js 中的变量,值类型存放在栈中,引用类型的地址存放在栈中,对应的值存放在堆中。当传参发生的时候,值类型会直接将栈中的值进行复制,形参和实参此时实际上是两个完全不相干的变量。对于引用类型,传参发生时,会将实参变量位于栈中的地址进行复制,此时栈中会有两个指向同一个堆地址的指针。
也称之为深拷贝/浅拷贝
浅拷贝:浅拷贝仅仅复制所拷贝的对象,而不复制它所引用的对象。
深拷贝:要复制的对象所引用的对象都复制了一遍
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
}
JSON.parse(JSON.stringify(target))
事件流:事件流描述的是从页面中接收事件的顺序,有以下三个阶段。
事件捕获阶段
处于目标阶段
事件冒泡阶段
addEventListener:addEventListener是DOM2 级事件新增的指定事件处理程序的操作,这个方法接收3个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。最后这个布尔值参数如果是true,表示在捕获阶段调用事件处理程序;如果是false,表示在冒泡阶段调用事件处理程序。
在es6之前,用var 声明的变量存在变量提升
变量提升:指变量的声明会提升到变量所在作用域的最顶端,赋值则是留在代码所在的位置
函数提升:只有函数声明式的函数才会提升,函数字面量(函数表达式)不会提升
函数优先原则: 函数提升优先于变量提升,同名变量提升不会覆盖函数,只有赋值的时候才会覆盖
原型也叫原型对象,是用来存放实例间共享属性和方法的
当我们访问一个对象的属性的时候,首先会在该对象本身中来找这个属性,如果找到了,就返回这个属性所映射的值
如果找不到,就会在这个对象的原型上去找,就是__proto__上去找,如果还找不到,会接着去对象原型的原型上去找
xx.__proto.__proto__中去找,这样子就形成了原型链.ECMA规定,原型链的顶端,即是Object.prototype.__proto__ === nul 找到这还找不到这属性,就会返回undefined了
总结:原型链就是用来查找属性的
作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,我们可以访问到外层环境的变量和函数。
作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。
当我们查找一个变量时,如果当前执行环境中没有找到,我们可以沿着作用域链向后查找。
总结:作用域链是用来查找变量的
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指向:
})
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
}
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
}
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
}
浏览器并不能直接通过域名找到对应的服务器,而是要通过 IP 地址。
判断是否命中强缓存,如果命中,则不发送请求,直接请求缓存设备
如果没有命中强缓存,则再判断是否命中协商缓存,如果没命中,则发送请求
为了防止已经失效的连接请求报文段突然又传送到了服务器端,从而产生错误。
请求报文由请求行、请求头和请求体三部分组成。
1)HTML解析,处理HTML标记并构建DOM树。
2)CSS解析,处理CSS标记并构建CSSOM树。
3)将DOM树和CSSOM合并称render tree(渲染树)。将每条css规则按照【从右至左】的方 式在dom树上进行逆向匹配,然后生成具有样式规则描述的渲染树。
4)渲染树布局,计算每个节点的集合信息。包括repaint和reflow。
5)渲染树绘制,将每个节点绘制到屏幕上。
理解重排和重绘,先了解CRP
关键渲染路径(Critical Rendering Path)是浏览器将 HTML,CSS 和 JavaScript 转换为屏幕上的像素所经历的步骤序列。优化关键渲染路径可提高渲染性能。
浏览器把我们的代码渲染到屏幕上的像素点的步骤:
重排发生在layout阶段,重绘发生在paint阶段,所以重排一定会引起重绘,重绘不一定会引起重排
1:传参方式的区别
2:在缓存方面的区别
3:HTTP协议没有规定get/post 请求参数的长度,对get请求参数的限制是来源与浏览器或web服务器,浏览器或web服务器限制了url的长度
localStorage:localStorage 在所有同源窗口中都是共享的;始终有效,窗口或浏览器关闭也一直保存,因此用作持久数据;(key:同源窗口都会共享,并且不会失效,不管窗口或者浏览器关闭与否都会始终生效)
// 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'
// 核心是基于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)
最后再将 AST 转换成渲染函数 render
编译过程:v-if
切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show
只是简单的基于css切换
编译条件:v-if
是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染
控制手段:v-show
隐藏则是为该元素添加css--display:none
,dom
元素依旧还在。v-if
显示隐藏是将dom
元素整个添加或删除
v-show
由false
变为true
的时候不会触发组件的生命周
v-if
由false
变为true
的时候,触发组件的beforeCreate
、create
、beforeMount
、mounted
钩子,由true
变为false
的时候触发组件的beforeDestory
、destoryed
方法
从源码上来看:
含有 v-if 的节点在转换成 ast 抽象语法树,再将 AST 转成渲染函数 render 的代码来看,是一个三元表达式,只有为 true 的时候,才会去创建元素false 的时候 是调用的创建空元素的方法
含有 v-show 的节点 从 AST 转成的渲染函数 的代码中可以知道 v-show 被截取,被放到了指令的集合中去,当表达式为 false 的时候,设置节点的 display 属性为 none
从源码来看
有 v-for 和 v-if 的节点转换成的 render 函数中代码中
先 loop 节点,再在每个节点中的用一个三元运算符来描述 v-if 的作用,所以是先进行遍历,再考虑是否渲染
如果遍历的数量太大,都要进行三元运算,所以性能狠低建议使用 computed,先对数据进行过滤
computed:是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
watch:没有缓存性,更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;当我们需要深度监听对象中的属性时,可以打开deep:true选项,这样便会对对象中的每一项进行监听;watch选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
$bus
= new Vue组件的调用顺序都是先父后子,渲染完成的顺序是先子后父。
组件的销毁操作是先父后子,销毁完成的顺序是先子后父。
事件循环机制
nextTick 主要是使用了宏任务和微任务定义了一个异步方法首先修改数据,这是同步任务。同一事件循环的所有的同步任务都在主线程上执行,形成一个执行栈,此时还未涉及 DOM 。
Vue 开启一个异步队列,并缓冲在此事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。同步任务执行完之后,才会去任务队列中读取回调函数,等待主线程执行,然后更新 dom
如果环境支持,nextTick 内部是用 promise.then,MutationObserver, setImmediate 等
如果环境不支持,采用的 setTimeout 来实现异步
hash 模式:
#后面 hash 值的变化,不会导致浏览器向服务器发出请求,浏览器不发出请求,就不会刷新页面
通过监听 hashchange 事件可以知道 hash 发生了哪些变化,然后根据 hash 变化来实现更新页面部分内容的操作。
history 模式:
history 模式的实现,主要是 HTML5 标准发布的两个 API,pushState 和replaceState,这两个 API 可以在改变 url,但是不会发送请求。这样就可以监听 url 变化来实现更新页面部分内容的操作
区别
url 展示上,hash 模式有“#”,history 模式没有
刷新页面时,hash 模式可以正常加载到 hash 值对应的页面,而 history 没有处理的话,会返回 404,一般需要后端将所有页面都配置重定向到首页路由
兼容性,hash 可以支持低版本浏览器和 IE。
React是一个UI库,以声明式编写 UI,构建管理自身状态的封装组件,然后对其组合以构成复杂的 UI。开发者不需要关心界面是如何被渲染的,只需要关心数据的生成和传递
真实dom是我们在浏览器开发者模式下看到的dom tree
虚拟dom是一个普通的js对象,是用来描述真实的dom结构,有以下几个主要属性:
当 DOM
操作(渲染更新)比较频繁时,
React 底层会先将前后两次的虚拟DOM
树进行对比,
定位出具体需要更新的部分,生成一个补丁集
,
最后只把“补丁”打在需要更新的那部分真实DOM
上,实现精准的“差量更新”。
频繁操作真实 DOM,会引起页面的重绘和重排,代价昂贵,性能低下,虚拟dom是一个js对象,操作对象性能开销极小;
解决了扩平台开发的问题,因为虚拟 DOM 描述的东西可以是真实 DOM,也可以是安卓界面。IOS 界面等等,这就可以对接不同平台的渲染逻辑。从而实现"一次编码,多端运行"(如 React,React Native)
react16.4之后 生命周期分为三个阶段:
创建阶段主要分为以下几个生命周期方法:
实例过程中自动调用的方法,在方法内部通过super
关键字获取来自父组件的props
在该方法中,通常的操作为初始化state
状态或者在this
上挂载方法
该方法是新增的生命周期方法,是一个静态的方法,因此不能访问到组件的实例
执行时机:组件创建和更新阶段,不论是props
变化还是state
变化,也会调用
在每次render
方法前调用,第一个参数为即将更新的props
,第二个参数为上一个状态的state
,可以比较props
和 state
来加一些限制条件,防止无用的state更新
该方法需要返回一个新的对象作为新的state
或者返回null
表示state
状态不需要更新
类组件必须实现的方法,用于渲染DOM
结构,可以访问组件state
与prop
属性
注意: 不要在 render
里面 setState
, 否则会触发死循环导致内存崩溃
组件挂载到真实DOM
节点后执行,其在render
方法之后执行
此方法多用于执行一些数据获取,事件监听等操作
该阶段的函数主要为如下方法:
用于告知组件本身基于当前的props
和state
是否需要重新渲染组件,默认情况返回true
执行时机:到新的props或者state时都会调用,通过返回true或者false告知组件更新与否
一般情况,不建议在该周期方法中进行深层比较,会影响效率
同时也不能调用setState
,否则会导致无限循环调用更
该周期函数在render
后执行,执行之时DOM
元素还没有被更新
该方法返回的一个Snapshot
值,作为componentDidUpdate
第三个参数传入
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log('#enter getSnapshotBeforeUpdate');
return 'foo';
}
componentDidUpdate(prevProps, prevState, snapshot) {
console.log('#enter componentDidUpdate snapshot = ', snapshot);
}
此方法的目的在于获取组件更新前的一些信息,比如组件的滚动位置之类的,在组件更新后可以根据这些信息恢复一些UI视觉上的状态
执行时机:组件更新结束后触发
在该方法中,可以根据前后的props
和state
的变化做相应的操作,如获取数据,修改DOM
样式等
此方法用于组件卸载前,清理一些注册是监听事件,或者取消订阅的网络请求等
一旦一个组件实例被卸载,其不会被再次挂载,而只可能是被重新创建
在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。
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
还可以使用缓存(客户端缓存、服务端缓存)优化