面试题总结
vue页面跳转会经过两个钩子函数beforeEach、afterEach
组见守卫
beforeRouteEnter前置组见守卫
*beforeRouteUpdate更新之前
watch和computed区别
数据没有改变,则 computed 不会重新计算)。若没改变,计算属性会立即返回之前缓存的计算结果。
不支持异步,当 computed 内有异步操作时无效,无法监听数据的变化的值。
watch 支持异步
当一个属性发生变化时,需要执行对应的操作;一对多时,一般用 watch。
不支持缓存,数据变或者触发重新渲染时,直接会触发相应的操作。
值类型(基本类型):字符串(String)、数字(Number)、布尔(Boolean)、对空(Null)、未定义(Undefined)、Symbol。
引用数据类型:对象(Object)、数组(Array)、函数(Function)。
防抖和节流
-
节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
-
防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时
继承方式
-
原型链继承
-
构造函数继承(借助 call)
-
组合继承
-
原型式继承
-
寄生式继承
-
寄生组合式继承
宏任务和微任务
宏任务(macrotask) | 微任务(microtask) | |
---|---|---|
谁发起的 | 宿主(Node、浏览器) | JS引擎 |
具体事件 | 1. script (可以理解为外层同步代码) 2. setTimeout/setInterval 3. UI rendering/UI事件 4. postMessage,MessageChannel 5. setImmediate,I/O(Node.js) | 1. 2. MutaionObserver 3. Object.observe(已废弃;Proxy 对象替代) 4. process.nextTick(Node.js) |
谁先运行 | 后运行 | 先运行 |
会触发新一轮Tick吗 | 会 | 不会 |
SSR和SPA区别
服务端渲染SSR和客户端渲染SPA(单页面应用)的区别:
SSR (Server Side Render):最终HTML文件由服务端来生成,然后浏览器直接下载进行显示。支持SSR的框架有基于React的Next和基于Vue的Nuxt。 SPA (Single Page Application):浏览器下载的是整个单页面应用的相关代码(框架代码、业务代码)资源,最终HTML文件通过js代码在浏览器上生成。三大框架Angular、Vue、React均为SPA。 SSR主要解决SPA首屏渲染慢以及不利于SEO的问题,其优势为:
首屏渲染快。SPA会一次性下载整个单页面应用的所有代码和资源,造成首屏渲染缓慢,而SSR则对每个页面单独进行服务端渲染的HTML文件,这样访问什么页面就下载什么页面,并且由于直接下载HTML文件,文件容量相对较小,所以可以被更快地加载好,给带宽较差的访问者带来速度上的提升。 支持SEO。SPA在直到浏览器运行其JS代码前页面的数据是未知的,因此无法被爬虫,而SSR由于服务端直接输出HTML文件,因此可以解决这个问题。一些非最优的解决方案:如取巧地将某些需要SEO的页面进行预渲染,或者判断访问来源是爬虫时,才输出服务端渲染的html,否则输出SPA,但是这样做也有弊端比如爬虫抓取的数据可能和真实访问数据不一致等等。 SPA与SSR选择:
如果采取前后端分离开发,SPA效率更高,无需后端人员操心前端内容。相反,如果采用SSR,则需要后端人员也对前端的技能有一定掌握,并且前端人员做好页面后需要由后端人员来进行修改。 如果对SEO有需求,可采用SSR的方法。 总结:若无SEO需求,且前后端分离开发,首选还是SPA。而在需要加速首屏渲染时,可以采用SSR的方法或者webpack的方法等等。若需要SEO,则采用SSR或对部分页面预渲染的方法。Nuxt是一个Node程序,这是由于我们要把Vue在服务端上运行,需要Node环境。当我们访问Nuxt应用时,实际上在访问Node程序的路由,程序输出为首屏渲染内容+用于重新渲染的SPA的脚本代码。其中,路由是由Nuxt中定义好的pages文件夹来生成。
1、SSR渲染的优势:
(1)更利于SEO;
(2)更利于首屏渲染(特别是对于缓慢的网络情况或运行缓慢的设备,内容更快到达)
2、SSR渲染的缺点:
(1)服务器压力大,考虑服务器端负载。
(2)开发条件受限,只会执行到ComponentMount之前的生命周期钩子,引用第三方库不可用其他生命周期钩子,引用库选择产生很大的限制。
(3)学习成本增大,需要学习构建设置和部署的更多要求。
怎样判断对象是否为空
把对象转换为数组来进行判断
父子组件的执行顺序为,
父组件beforeCreated ->父组件created ->父组件beforeMounted ->子组件beforeCreated ->子组件created ->子组件beforeMounted ->子组件mounted -> 父组件mounted
为什么用set
由于 Vue 会在初始化实例时进行双向数据绑定,使用Object.defineProperty()对属性遍历添加 getter/setter 方法,所以属性必须在 data 对象上存在时才能进行上述过程 ,这样才能让它是响应的。如果要给对象添加新的属性,此时新属性没有进行过上述过程,不是响应式的,所以会出想数据变化,页面不变的情况。此时需要用到$set。
任务队列 “ 任务队列 " 是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空," 任务队列 " 上第一位的事件就会自动进入主线程。
所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务
异步任务指的是,不进入主线程、而进入 " 任务队列 "(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
JS如何实现异步操作
JS 的异步是通过回调函数实现的,即通过任务队列,在主线程执行完当前的任务栈(所有的同步操作),主线程空闲后轮询任务队列,并将任务队列中的任务(回调函数)取出来执行。
" 回调函数 "(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
虽然 JS 是单线程的,但是浏览器的内核是多线程的,在浏览器的内核中不同的异步操作由不同的浏览器内核模块调度执行,异步操作会将相关回调添加到任务队列中。而不同的异步操作添加到任务队列的时机也不同,如 onclick, setTimeout,ajax 处理的方式都不同,这些异步操作是由浏览器内核的 webcore 来执行的,webcore 包含上图中的3种 webAPI,分别是 DOM Binding、network、timer模块。
onclick 由浏览器内核的 DOM Binding 模块来处理,当事件触发的时候,回调函数会立即添加到任务队列中。 setTimeout 会由浏览器内核的 timer 模块来进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中。 ajax 则会由浏览器内核的 network 模块来处理,在网络请求完成返回之后,才将回调函数添加到任务队列中。 异步执行机制 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。 主线程之外,还存在一个 " 任务队列 "(task queue)。只要异步任务有了运行结果,就在 " 任务队列 " 之中放置一个事件。 一旦 " 执行栈 " 中的所有同步任务执行完毕,系统就会读取 " 任务队列 ",看看里面有哪些事件。于是那些对应的异步任务结束等待状态,进入执行栈,开始执行。 主线程不断重复上面的第三步(事件轮询) JS中事件队列的优先级 在 JS 的ES6 中新增的任务队列(promise)优先级是在事件循环之上的,事件循环每次 tick 后会查看 ES6 的任务队列中是否有任务要执行,也就是 ES6 的任务队列比事件循环中的任务(事件)队列优先级更高。
先执行同步代码再执行异步代码,先执行微任务再执行宏任务
setTimeout( fn, 0 ),它在 " 任务队列 " 的尾部添加一个事件,因此要等到同步任务和 " 任务队列 " 现有的事件都处理完,才会得到执行。
console.log(1) setTimeout(function () { console.log(6) }, 1000) new Promise(function (resolve, reject) { console.log(2) setTimeout(function () { console.log(3) }, 500) resolve() }).then(function (res) { console.log(4) }) console.log(5)
// 1 2 5 4 3 6 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 微观任务队列 微任务:语言标准(ECMAScript)提供,如process.nextTick(node)、Promise、Object.observe、MutationObserver
宏观任务队列 宏任务:由宿主环境提供,比如setTimeout、setInterval、网络请求Ajax、用户I/O、script(整体代码)、UI rendering、setImmediate(node)
call、apply、bind区别
-
三者都可以改变函数的this对象指向。
-
三者第一个参数都是this要指向的对象,如果如果没有这个参数或参数为undefined或null,则默认指向全局window。
-
三者都可以传参,但是apply是数组,而call是参数列表,且apply和call是一次性传入参数,而bind可以分为多次传入。
-
bind 是返回绑定this之后的函数,便于稍后调用;apply 、call 则是立即执行 。
query和params区别
区别: 1.首先就是写法得不同,query 得写法是 用 path 来编写传参地址,而 params 得写法是用 name 来编写传参地址,你可以看一下编写路由时候得相关属性,你也可以输出一下 路由对象信息 看一下
2.接收方法不同, 一个用 query 来接收, 一个用 params 接收 ,总结就是谁发得 谁去接收
3.query 在刷新页面得时候参数不会消失,而 params 刷新页面得时候会参数消失,可以考虑本地存储解决
4.query 传得参数都是显示在url 地址栏当中,而 params 传参不会显示在地址栏
http请求状态码
一般我们看不到,因为表示请求继续 100: 继续请求,前面的一部分内容服务端已经接受到了,正在等待后续内容 101: 请求者已经准备切换协议,服务器
2 开头的都是表示成功,本次请求成功了,只不过不一样的状态码有不一样的含义(语义化)
200: 标准请求成功(一般表示服务端提供的是网页) 201: 创建成功(一般是注册的时候,表示新用户信息已经添加到数据库) 203: 表示服务器已经成功处理了请求,但是返回的信息可能来自另一源 204: 服务端已经成功处理了请求,但是没有任何数据返回
3 开头也是成功的一种,但是一般表示重定向 301: 永久重定向 302: 临时重定向 304: 使用的是缓存的数据 305: 使用代理
4 开头表示客户端出现错误了 400: 请求的语法服务端不认识 401: 未授权(你要登录的网站需要授权登录) 403: 服务器拒绝了你的请求 404: 服务器找不到你请求的 URL
405:本页面拒绝访问
407: 你的代理没有授权 408: 请求超时 410: 你请求的数据已经被服务端永久删除
5 开头的表示服务端出现了错误 500: 服务器内部错误 503: 服务器当前不可用(过载或者维护) 505: 请求的协议服务器不支持
防抖和节流
-
节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
function throttled(fn, delay) { let timer = null let starttime = Date.now() return function () { let curTime = Date.now() // 当前时间 let remaining = delay - (curTime - starttime) // 从上一次到现在,还剩下多少多余时间 let context = this let args = arguments clearTimeout(timer) if (remaining <= 0) { fn.apply(context, args) starttime = Date.now() } else { timer = setTimeout(fn, remaining); } } }
-
防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时
function debounce(func, wait, immediate) { let timeout; return function () { let context = this; let args = arguments; if (timeout) clearTimeout(timeout); // timeout 不为null if (immediate) { let callNow = !timeout; // 第一次会立即执行,以后只有事件执行后才会再次触发 timeout = setTimeout(function () { timeout = null; }, wait) if (callNow) { func.apply(context, args) } } else { timeout = setTimeout(function () { func.apply(context, args) }, wait); } } }
hash和history区别
hash支持缓存
history不支持缓存需要有后台来配置,来做兼容
Vue-Router路由模式的选择和底层原理
.路由类型 Hash模式:丑,无法使用锚点定位。 History模式:需要后端配合,IE9不兼容(可使用强制刷新处理IE9不兼容) hash路由===》location.hash切换
windos.onhashchange监听路径的切换
hisory路由==》hisory.pushState切换
Windows.onpopstate监听路径的切换
理解:
Vue.util.defineReactive_route这个工具库把路由的信息变成了响应式数据,而defineReactive()内部就是调用了Object.defineProperty()和依赖收集实现了路由信息的响应式,而Vue的data也是通过这种方式实现响应式。 图中左边五种方式都会触发调用updateRoute这个API,这个API的功能是修改路由中的响应式数据,当响应式数据被改变后就会自动触发router-view的更新,router-view会根据url和路由匹配规则进行相应组件的展示。
hash和History原理
hash模式背后的原理是onhashchange
事件
-
前端路由的底层原理:hashchange 和H5的history API中的popState和replaceState来进行实现的
hash路由===》location.hash切换
windos.onhashchange监听路径的切换
hisory路由==》hisory.pushState切换
Windows.onpopstate监听路径的切换
import和require的区别
import是ES6标准中的模块化解决方案,require是node中遵循CommonJS规范的模块化解决方案。
后者支持动态引入,也就是require(${path}/xx.js)
,前者目前不支持,但是已有提案。
前者是关键词,后者不是。
前者是编译时加载,必须放在模块顶部,在性能上比后者好一些;后者是运行时加载,理论来说放在哪里都可以。
前者采用的是实时绑定方式,即导入和导出的值都指向同一个内存地址,所以导入的值会随着导出值变化;而后者在导出时是指拷贝,就算导出的值变化了,导入的值也不会变化,如果想要更新导入的值,就要重新导入。
前者会编译成require/exports
来执行。
Vue父组件监听子组件值变化
通过this.$emit(‘方法’,监听的值)
vue单页项目常用优化
vue项目是单页应用,项目在第一次加载的时候会将大部分内容都加载进来,故而会导致加载很慢,以下是优化方案:
1.使用cdn加载一些不常变化的文件,比如用到的UI框架,vue脚手架相关的文件 (参见vue项目优化--引入cdn文件)
2.将静态的js,css以及图片放到第三方服务器
3.按需加载路由
4.webpack-parallel-uglify-plugin 可以优化打包js文件
5.在代码层面进行优化:
(1) 将重复的功能分块提取出来,封装成组件,让代码进行有效的复用
(2) 尽量利用computed属性代替for循环来渲染数据
(3) 提取公共方法,减少js代码量
(4) 提取公共的样式,减少页面样式的代码量
(5) 按照功能设置打包的代码块
(6) 尽量用v-if 代替v-show.因为v-show为false只是设置页面的功能模块不可见,它还是会渲染. 而v-if为false,则不会渲染该模块的功能
js是怎样监听数组变化 的
深拷贝一份放在数组原型上(Object.defineProperty)
数据劫持
vue监听数组变化
Object.defineProperty来监听数组变化
rem单位是基于根节点
JavaScript中的null和undefined的区别
1、null
表示没有对象,即该处不应该有值,用法如下:
作为函数的参数,表示该函数的参数不是对象;
作为原型链的终点。
2、undefined
表示缺少值,就是此处应该有一个值,但是还没有定义,情况如下:
变量被声明了,但没有赋值时,就等于undefined;
调用函数时,应该提供的参数没有提供,该参数等于undefined;
对象没有赋值的属性,该属性的值为undefined;
函数没有返回值的时,默认返回undefined;
JS如何判断一个对象为空
LuckDay 编辑
2017年09月29日 17:50 · 阅读 4517
关注
原文链接: blog.csdn.net
昨天面试的时候被问到的问题。只怪自己根基不牢,没有回答好。然后面试官提示用for in,突然恍然大悟,然后又查询了网上的一些方法,对这个问题做一下整理。
1、最常见的思路,for…in…遍历属性,为真则为“非空数组”;否则为“空数组”
var judgeObj = function(obj){ for(var item in obj){ return true; } return false; }复制代码
2.通过JSON自带的.stringify方法来判断
var judgeObj = function(obj){ if(JSON.stringify(obj) == "{}") return true; else return false; }复制代码
3.ES6新增的方法Object.keys()
var judgeObj = function(obj){ if(Object.keys(obj).length == 0) return true; else return false; }
JavaScript 判断对象属性是否存在(对象是否包含某个属性)
一般推荐使用 in 运算符,用法是属性名 in 对象,如果存在返回 true,反之为 false,来看个例子:
var echo = { name:'听风是风', job:undefined }; console.log('name' in echo); //true console.log('job' in echo); //true console.log('age' in echo); //false 1234567
但需要注意的是,in 只能判断对象有没有这个属性,无法判断这个属性是不是自身属性,啥意思?咱们接着说。
判断是否是自身属性
一个最简单的构造函数创建实例的例子:
function Parent(){ this.name = 'echo'; }; Parent.prototype.age = 26; var o = new Parent(); o.name//echo o.age//26 1234567
在这个例子中,对于实例 o 而言,name 就是是自身属性,这是它自己有的,而 age 是原型属性,o 虽然没有这个属性,但在需要时,它还是顺着原型链找到了自己的老父亲 Parent,并借用了这个属性。
所以当我们用 in 判断时可以发现结果均为 true:
'name' in o;//true 'age' in o;//true 12
针对这个问题,如果我们还要判断是否是自身属性,就得借用方法hasOwnProperty()
,不信你看:
o.hasOwnProperty('name');//true o.hasOwnProperty('age');//false 12
说到底hasOwnProperty()
做了两件事,除了判断对象是否包含这个属性,还判断此属性是不是对象的自身属性。
所以我们可以简单总结一下,如果我们只需判断对象有没有某个属性,使用 in
运算符就好了。而如果我们还要关心这个属性是不是自身属性,那么推荐hasOwnProperty()
方法。
说了这么多,这两个判断有什么使用场景呢?
关于 in 运算符判断对象有没有某个属性,最常见的,我们希望给某个对象添加一个方法,直接添加又担心其他同事已经声明过,存在覆盖的可能性,所以使用 in 来判断,没有这个属性,咱们再加。
当然针对这个问题,使用 Symbol 来确保对象 key 的唯一性也是不错的做法,这个就看实际场景了。
而关于hasOwnProperty()
这个方法呢,我目前遇到的就是深拷贝实现时会使用,因为我们知道每个对象都有原型,而深拷贝时我们只是希望拷贝对象的自身属性,使用此方法做个区分就是非常奈斯的做法了
js循环机制
事件循环过程可以简单描述为:
a、函数入栈,当 Stack 中执行到异步任务的时候,就将他丢给 WebAPIs ,接着执行同步任务,直到 Stack 为空;
b、在此期间 WebAPIs 完成这个事件,把回调函数放入 CallbackQueue (任务队列)中等待;
c、当执行栈为空时,Event Loop 把 Callback Queue中的一个任务放入Stack中,回到第1步。
事件循环(Event Loop) 是让 JavaScript 做到既是单线程,又绝对不会阻塞的核心机制,也是 JavaScript 并发模型(Concurrency Model)的基础,是用来协调各种事件、用户交互、脚本执行、UI 渲染、网络请求等的一种机制。在执行和协调各种任务时,Event Loop 会维护自己的事件队列。
事件队列是一个存储着待执行任务的队列,其中的任务严格按照时间先后顺序执行,排在队头的任务将会率先执行,而排在队尾的任务会最后执行。事件队列每次仅执行一个任务,在该任务执行完毕之后,再执行下一个任务,一个任务开始后直至结束,不会被其他任务中断。执行栈则是一个类似于函数调用栈的运行容器,当执行栈为空时,JS 引擎便检查事件队列,如果不为空的话,事件队列便将第一个任务压入执行栈中运行。
任务队列:在JavaScript中,异步任务被分为两种,一种宏任务(MacroTask)也叫Task,一种叫微任务:
宏任务的例子很多,包括创建主文档对象、解析HTML、执行主线(或全局)JavaScript代码,更改当前URL以及各种事件,如页面加载、输入、网络事件和定时器事件。从浏览器的角度来看,宏任务代表一个个离散的、独立工作单元。运行完任务后,浏览器可以继续其他调度,如重新渲染页面的UI或执行垃圾回收。
而微任务是更小的任务。微任务更新应用程序的状态,但必须在浏览器任务继续执行其他任务之前执行,浏览器任务包括重新渲染页面的UI。微任务的案例包括promise回调函数、DOM发生变化等。微任务需要尽可能快地、通过异步方式执行,同时不能产生全新的微任务。微任务使得我们能够在重新渲染UI之前执行指定的行为,避免不必要的UI重绘,UI重绘会使应用程序的状态不连续。
当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务对列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行。当微任务对列中的任务都执行完成后再去判断宏任务对列中的任务。每次宏任务执行完毕,都会去判断微任务队列是否产生新任务,若存在就优先执行微任务,否则按序执行宏任务。
事件循环通常至少需要两个任务队列:宏任务队列和微任务队列。两种队列在同一时刻都只执行一个任务。
vue中data为什么是一个函数
vue中的data
是一个对象类型,对象类型的数据是按引用传值
的,这就会导致所有组件的实例都共享
同一份数据,这是不对的,我们要的是每个组件实例都是独立的
为了解决对象类型数据共享的问题,我们需要将 data
定义成一个函数,每个实例需要通过调用函数生成一个独立的数据对象
js中typeof与instanceof分别是什么?
一、typeof的定义和用法:
返回值是一个字符串,用来说明变量的数据类型。
具体用法细节:
1、typeof 一般只能返回如下几个结果:
'undefined' :这个值未定义。
'boolean':这个值是布尔值。
'string' :这个值是字符串。
'number' :这个值是数值。
'object':这个值是对象或null。
'function' :这个值是函数。 2、typeof 来获取一个变量是否存在,如
`if``(``typeof` `a!=``"undefined"``){alert(``"ok"``)}`
而不要去使用 if(a) 因为如果 a 不存在(未声明)则会出错。
3、对于 Array,Null 等特殊对象使用 typeof 一律返回 object,这正是 typeof 的局限性。 二、Instanceof定义和用法:
Instanceof定义和用法:instanceof 用于判断一个变量是否属于某个对象的实例。也可以用来判断某个构造函数的prototype属性是否存在另外一个要检测对象的原型链上。
前端重绘和重排
有经验的大佬对这个概念一定不会陌生,“浏览器输入URL发生了什么”。估计大家已经烂熟于心了,从计算机网络到JS引擎,一路飞奔到浏览器渲染引擎。 经验越多就能理解的越深。
感兴趣的同学可以看一下这篇文章,深度和广度俱佳: 《从输入 URL 到页面加载的过程?如何由一道题完善自己的前端知识体系!》
切回正题,我们继续探讨何为重排。浏览器下载完页面所有的资源后,就要开始构建DOM树,与此同时还会构建渲染树(Render Tree)。(其实在构建渲染树之前,和DOM树同期会构建Style Tree。DOM树与Style Tree合并为渲染树)
DOM树:表示页面的结构 渲染树:表示页面的节点如何显示
一旦渲染树构建完成,就要开始绘制(paint)页面元素了。
当DOM的变化引发了元素几何属性的变化,比如改变元素的宽高
,元素的位置
,导致浏览器不得不重新计算元素的几何属性,并重新构建渲染树,这个过程称为“重排”。完成重排后,要将重新构建的渲染树渲染到屏幕上,这个过程就是“重绘”。
简单的说,重排负责元素的几何属性更新,重绘负责元素的样式更新。而且,重排必然带来重绘,但是重绘未必带来重排。比如,改变某个元素的背景
,这个就不涉及元素的几何属性,所以只发生重绘。
二、 重排触发机制绘
上面已经提到了,重排发生的根本原理就是元素的几何属性发生了改变,那么我们就从能够改变元素几何属性的角度入手
-
添加或删除可见的DOM元素
-
元素位置改变
-
元素本身的尺寸发生改变
-
内容改变
-
页面渲染器初始化
-
浏览器窗口大小发生改变
Vue.nextTick 的原理和用途
Vue在修改数据后,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。
事件循环:
第一个tick(本次更新循环)
1.首先修改数据,这是同步任务。同一事件循环的所有的同步任务都在主线程上执行,形成一个执行栈,此时还未涉及DOM.
2.Vue开启一个异步队列,并缓冲在此事件循环中发生的所有数据变化。如果同一个watcher被多次触发,只会被推入队列中一次。
第二个tick(‘下次更新循环’)
同步任务执行完毕,开始执行异步watcher队列的任务,更新DOM。Vue在内部尝试对异步队列使用原生的Promise.then和MessageChannel 方法,如果执行环境不支持,会采用 setTimeout(fn, 0) 代替。
第三个tick(下次 DOM 更新循环结束之后)
二、应用场景及原因
1.在Vue生命周期的created()钩子函数进行DOM操作一定要放到Vue.nextTick()的回调函数中。
在created()钩子函数执行的时候DOM 其实并未进行任何渲染,而此时进行DOM操作无异于徒劳,所以此处一定要将DOM操作的js代码放进Vue.nextTick()的回调函数中。与之对应的就是mounted()钩子函数,因为该钩子函数执行时所有的DOM挂载和渲染都已完成,此时在该钩子函数中进行任何DOM操作都不会有问题。
2.在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的DOM结构的时候,这个操作都应该放进Vue.nextTick()的回调函数中。
具体原因在Vue的官方文档中详细解释:
Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部尝试对异步队列使用原生的Promise.then和MessageChannel,如果执行环境不支持,会采用setTimeout(fn, 0)代替。
async awit实现原理
async/await 就是 Generator 的语法糖,使得异步操作变得更加方便 2)async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成await 3)async 是 Generator 的语法糖,这个糖体现在这几个方面: async函数内置执行器,函数调用之后,会自动执行,输出最后结果,而Generator需要调用next或者配合co模块使用 更好的语义,async和await,比起星号和yield,语义更清楚了,async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果 更广的适用性,co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async 函数的 await 命令后面,可以是 Promise 对象和原始类型的值 返回值是Promise,async函数的返回值是 Promise 对象,Generator的返回值是 Iterator,Promise 对象使用起来更加方便 4)async/await 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里
Promise的构造函数方法
Promise是什么: 1、认识Promise: Promise 是异步操作的一种解决方案。 先给大家讲一下什么是异步: 回调函数其实就是异步操作,例:
document.addEventListener( 'click', () => { console.log('这里是异步的'); }, false ); console.log('这里是同步的');
2、什么时候使用 Promise: Promise 一般用来解决层层嵌套的回调函数(回调地狱 callback hell)的问题。
在这里插入代码片
Promise的基本用法: 1、实例化构造函数生成实例对象: Promise 解决的不是回调函数,而是回调地狱,即并不是用了Promise就用不了回调函数,它们两个是可以共存的。
// 实例化构造函数生成实例对象: const p = new Promise(() => {});
2、Promise 的状态: Promise的状态解决了Promise的一系列行为。Promise 有 3 种状态,一开始是 pending(未完成),执行 resolve,变成 fulfilled(resolved),已成功;执行 reject,变成 rejected,已失败;Promise 的状态一旦变化,就不会再改变了。
一开始是 pending(未完成):
const p = new Promise((resolve, reject) => { }); console.log(p); // Promise {<pending>
执行 resolve,变成 fulfilled(resolved),已成功:
const p = new Promise((resolve, reject) => { resolve(); }); console.log(p); // Promise {<fulfilled>: undefined}
执行 reject,变成 rejected,已失败:
const p = new Promise((resolve, reject) => { reject(); }); console.log(p); // Promise {<rejected>: undefined}
但这里请大家注意:Promise里面第一个参数是成功状态的回调函数,第二个参数是失败状态的回调函数,Promise里面的resolve和reject只是形参(名字可以随便写,想起什么名就起什么名),写成resolve和reject的原因是为了语义化更强一些,那么,请问下面代码执行结果是什么:
const p = new Promise((r1,r2) => { r2(); }); console.log(p);
因为r1 和 r2 只是形参,第二个参数是失败状态的回调函数,所以调用 r2 就等同于调用 失败状态的回调函数 所以毫无疑问结果是Promise {<rejected>: undefined}
,在这里只是给大家提个醒,大家不要弄迷糊了。
Promise 的状态一旦变化,就不会再改变了:
const p = new Promise((resolve, reject) => { resolve(); reject(); }); console.log(p); // Promise {<fulfilled>: undefined}
3、then方法(简单说明一下,具体在后面讲): 这里then传两个函数,成功了用第一个函数,失败了用第二个函数。
// 失败: const p = new Promise((resolve, reject) => { reject(); }); p.then( () => { console.log('success'); }, () => { console.log('error'); } ); // error // 成功: const p = new Promise((resolve, reject) => { resolve(); }); p.then( () => { console.log('success'); }, () => { console.log('error'); } ); // success
4、resolve 和 reject 函数的参数: resolve 和 reject 函数的参数都会传给相应的then方法里面函数的形参,即resolve()传它的参数给then里面第一个函数的形参,reject()传它的参数给then里面第二个函数的形参;举例:
// 成功的时候: const p = new Promise((resolve, reject) => { resolve({username:'Alex'}); }); p.then( data => { console.log('success', data); }, err => { console.log('error', err); } ); // success {username: "alex"} // 失败的时候: const p = new Promise((resolve, reject) => { reject(new Error('reason')); }); p.then( data => { console.log('success', data); }, err => { console.log('error', err); } ); // error Error: reason // at 2-2.Promise 的基本用法.html:28 // at new Promise (<anonymous>) // at 2-2.Promise 的基本用法.html:16
then方法: 1、什么时候执行: pending->fulfilled 时,执行 then 的第一个回调函数 pending->rejected 时,执行 then 的第二个回调函数 2、执行后的返回值: then 方法执行后返回一个新的 Promise 对象。
const p = new Promise((resolve, reject) => { resolve(); // reject(); }); const p2 = p .then( () => {}, () => {} ) .then() // 因为then方法返回的是一个promise,所以可以在后面再添加then方法。 .then(); console.log(p, p2, p === p2); // Promise {<fulfilled>: undefined} Promise {<pending>} false
3、then 方法返回的 Promise 对象的状态改变: 在前面讲过第一个new 的promise可以决定紧挨着后面then的执行回调函数,例:
const p = new Promise((resolve, reject) => { reject(); }); p.then( () => { console.log('success'); }, () => { console.log('err'); }); // err
因为在p中执行reject(),所以执行p紧挨着的then的第二个回调函数,因此输出err, 而我们都知道then方法可以继续使用在上一个then方法的后面的,但是大家有没有想过新的then方法是怎么执行的呢?下面进行讲解: 在讲解之前大家先判断一下下面这块代码的打印出来的值是多少?
// 案例1: const p = new Promise((resolve, reject) => { reject(); }); p.then( () => { console.log('success'); }, () => { console.log('err'); }).then( data => { console.log('success2', data); }, err => { console.log('err2', err); } )
这个值是err success2 undefined, 我先给大家分析一下,在p中执行reject(),所以在第一个then中执行第二个回调函数,所以打印出err,然后在第一个then中默认返回的永远都是成功状态的 Promise 对象,所以在第二个then中调用第第一个回调函数,所以接着打印出success2 undefined,由此可见后面的then执行第一个回调函数还是第二个回调函数看的是前面的then,可能大家对 在第一个then中默认返回的永远都是成功状态的 Promise 对象 这句话不太理解 ,下面我通过代码给大家演示: 先简单通过代码看一下:
// 在 then 的回调函数中,return 后面的东西,会用 Promise 包装一下 return undefined; // 等价于 return new Promise(resolve => { resolve(undefined); });
这是详细讲解: 1、在所有函数中都有return值,没有写的时候就默认返回undefined,所以在第一个then里面就默认返回undefined(相当于程序中默认有 return undefined;
这样一行代码)
() => { console.log('err'); // 默认这里有 return undefined;这样一行代码 })
又因为 默认返回的永远都是成功状态的 Promise 对象,即:
() => { console.log('err'); // 默认这里有 return undefined;这样一行代码 // 因为默认返回的永远都是成功状态的 Promise 对象,所以相当于一个新的Promise里面执行resolve(),即: return new Promise((resolve, reject) => { resolve(undefined); // 因为默认返回undefined,所以resolve里面参数是undefined })
因为上一个then执行resolve,所以到第二个then那里就执行第一个回调函数,所以打印出 success2 undefined,打印出undefined是因为在一个then里面的reject传进去的undefined。讲到这里大家可能会问:默认返回的永远都是成功状态的 Promise 对象,那么如何返回失败状态的Promise对象呢?其实是这样的,then里面默认的是返回的永远都是成功状态的 Promise 对象,那么我直接给一个失败状态的Promise,那么不就不会再出现默认值了呢,没错,就是这样。举例:
const p = new Promise((resolve, reject) => { reject(); }); p.then( () => { console.log('success'); }, () => { console.log('err'); / return new Promise((resolve, reject) => { reject('reason'); }); / }).then( data => { console.log('success2', data); }, err => { console.log('err2', err); } )
大家注意横线里面的两行代码,那样写即程序不会再执行那个 默认的正确状态的Promis对象了,而执行自己写的那个错误的Promise对象(即/中间的代码),所以,这个案例的执行结果是:err err2 reason,原理同案例1一样。
那么简单测试一下上面知识是否掌握了,上案例:
const p = new Promise((resolve, reject) => { reject(); }); p.then( () => { console.log('success'); }, () => { // 步骤一: console.log('err'); }).then( data => { // 步骤二: console.log('success2', data); }, err => { console.log('err2', err); } ).then( data => { // 步骤三: console.log('success3', data); }, err => { console.log('err3', err); } );
只要大家将上面知识理解了,那么这个案例就是小菜一碟,答案是:err success2 undefined success3 undefined,执行的分别是步骤一,步骤二,步骤三。 catch方法: 1、有什么用: 在前面的then方法中,我们知道它里面有两个回调函数(第一个处理成功的状态,第二个处理失败的状态),但在实际开发中一般then只处理成功的状态,因此只传一个回调函数,因此catch就是用来专门处理失败的状态的,即catch 本质上是 then 的特例,就是then(null, err => {});
2、基本用法:
new Promise((resolve, reject) => { // resolve(123); reject('reason'); }).then(data => { console.log(data); // then 处理正确的状态 }).catch(err => { console.log(err); // catch 处理错误的状态 }) // reason // 上面的catch其实就相当于 .then(null, err => { console.log(err); });
catch() 可以捕获它前面的错误,一般总是建议,Promise 对象后面要跟 catch 方法,这样可以处理 Promise 内部发生的错误: catch可以捕获前面的错误,如果错误一旦被catc捕获到则错误就消失了。还有就是catch默认也是返回一个成功状态的Promise对象(原因很简单,catch本质是then,因为then默认返回成功状态的Promise对象,所以catch也是返回成功状态的Promise对象),所以catch后面跟的then只会执行成功的回调函数,如果想执行失败状态的回调函数需要自己手动return 失败状态的Promise对象,除了这种方法外还有一种方法,见代码:
new Promise((resolve, reject) => { // resolve(123); reject('reason'); }) .then(data => { console.log(data); }) /// 第一块 .catch(err => { console.log(err); throw new Error('reason'); }) .then(data => { console.log(data); }) 第二块 .catch(err => { console.log(err); }); / // reason // Error: reason
reason是第一块代码执行的结果, Error: reason是第二块代码执行的结果。在第一块代码里扔一个错误,那么就会被第二块代码中的catch捕获到,这样也不会再执行then里面的成功状态的回调函数了。 Promise.resolve() 和 Promise.reject(): 1、Promise.resolve(): 是成功状态 Promise 的一种简写形式。
new Promise(resolve => resolve('foo')); // 这个地方的resolve是一个形参,名字想怎么写就怎么写 // 简写 Promise.resolve('foo'); // 这里的resolve是一个固定的方法名,不能乱写
参数: (1)一般参数(最应该关注):
Promise.resolve('foo').then(data => { console.log(data); }); // foo
(2)特殊参数(简单了解一下即可,不要深究): 1、把Promise当作参数传进去。
const p1 = new Promise(resolve => { setTimeout(resolve, 1000, '我执行了'); }); Promise.resolve(p1).then(data => { console.log(data); }); // 我执行了(注:在一秒后打印)
这是为什么呢?原因是:当 Promise.resolve() 接收的是 Promise 对象时,直接返回这个 Promise 对象,什么都不做。 即上面的代码等同于:
const p1 = new Promise(resolve => { setTimeout(resolve, 1000, '我执行了'); // 上面的setTimeout等同于: // setTimeout(() => { // resolve('我执行了'); // }, 1000); }); p1.then(data => { console.log(data); }); // 我执行了(注:一秒后打印) console.log(Promise.resolve(p1) === p1); // true
还有就是,当 resolve 函数接收的是 Promise 对象时,后面的 then 会根据传递的 Promise 对象的状态变化决定执行哪一个回调。 上代码进行描述:
const p1 = new Promise(resolve => { setTimeout(resolve, 1000, '我执行了'); }); new Promise(resolve => resolve(p1)).then(data => { console.log(data); });
上面那句话的意思就是如果函数接收的是一个Promise对象(即上面代码的p1)时,后面的then受p1支配,因为p1在一秒后执行了resolve('我执行了');
所以就给then里面传了个 ‘我执行了’ ,所以then执行后打印出 我执行了。
2、具有 then 方法的对象: 因这种形式在实际开发中不咋用,这里不再细讲,大家知道有这个东西就行。(要是碰到了直接查文档就完事了)
2、Promise.reject(): 失败状态 Promise 的一种简写形式。
new Promise((resolve, reject) => { reject('reason'); }); // 等价于 Promise.reject('reason');
参数: 不管什么参数,都会原封不动地向后传递,作为后续方法的参数。(跟resolve相比简单多了)
还有一个Promise.resolve() 和 Promise.reject()的用处就是用在then方法的return中,如:
new Promise((resolve, rejcet) => { resolve(123); }) .then(data => { // return data; // return Promise.resolve(data); // 传失败状态的Promise对象的简写 return Promise.reject('reason'); // 传失败状态的Promise对象的简写 }) .then(data => { console.log(data); }) .catch(err => console.log(err)); // reason
Promise.all(): 1、有什么用: Promise.all() 关注多个 Promise 对象的状态变化;可以在Promise对象中传入多个 Promise 实例,包装成一个新的 Promise 实例返回。 2、基本用法: Promise.all() 的状态变化与所有传入的 Promise 实例对象状态有关 所有状态都变成 resolved,最终的状态才会变成 resolved 只要有一个变成 rejected,最终的状态就变成 rejected 见代码:
// 封装一个延迟函数: const delay = ms => { return new Promise(resolve => { setTimeout(resolve, ms); }); }; const p1 = delay(1000).then(() => { console.log('p1 完成了'); return 'p1'; // 默认返回成功状态的Promise对象 }); const p2 = delay(2000).then(() => { console.log('p2 完成了'); return 'p2'; // 默认返回成功状态的Promise对象 }); const p = Promise.all([p1, p2]); p.then( data => { console.log(data); }, err => { console.log(err); } );
上面代码中的执行结果是:p1完成了(一秒后),p2完成了 (2) [“p1”, “p2”] (两秒后) 这块代码是最终的状态是 resolved 再看下面的代码:
const delay = ms => { return new Promise(resolve => { setTimeout(resolve, ms); }); }; const p1 = delay(1000).then(() => { console.log('p1 完成了'); return Promise.reject('reason'); }); const p2 = delay(2000).then(() => { console.log('p2 完成了'); return 'p2'; }); const p = Promise.all([p1, p2]); p.then( data => { console.log(data); }, err => { console.log(err); } );
这块代码的结果是:p1完成了 reason (注:一秒后打印) p2完成了 (注:两秒后打印) 这块代码中有rejected,所以最终状态就是rejected,可能大家疑惑为什么reason在 p1完成了 后面出现,大家可以这样理解:因为系统检测到某个地方出现了rejected,所以直接判定Promise.all的状态是rejected,所以直接执行p后面then方法的第二个回调函数,所以打印出reason,又因为那个rejected的状态是在p1中检测到的,所以reason出现在 p1完成了 的后面。 大家可以通过下面的例子检测字迹是否理解:
const delay = ms => { return new Promise(resolve => { setTimeout(resolve, ms); }); }; const p1 = delay(1000).then(() => { console.log('p1 完成了'); return 'p1'; }); const p2 = delay(2000).then(() => { console.log('p2 完成了'); return Promise.reject('reason'); }); const p = Promise.all([p1, p2]); p.then( data => { console.log(data); }, err => { console.log(err); } );
这里的结果是:p1完成了(注:一秒后打印出来) p2完成了 reason(注:两秒后打印出来) Promise.race() 和 Promise.allSettled(): 先声明一下,这两个方法大家简单了解一下即可。 1、Promise.race(): Promise.race() 的状态取决于第一个完成的 Promise 实例对象,如果第一个完成的成功了,那最终的就成功;如果第一个完成的失败了,那最终的就失败。
const delay = ms => { return new Promise(resolve => { setTimeout(resolve, ms); }); }; const p1 = delay(1000).then(() => { console.log('p1 完成了'); return 'p1'; }); const p2 = delay(2000).then(() => { console.log('p2 完成了'); return 'p2'; }); const racePromise = Promise.race([p1, p2]); racePromise.then( data => { console.log(data); }, err => { console.log(err); } ); const delay = ms => { return new Promise(resolve => { setTimeout(resolve, ms); }); }; const p1 = delay(1000).then(() => { console.log('p1 完成了'); return 'p1'; }); const p2 = delay(2000).then(() => { console.log('p2 完成了'); return Promise.reject('reason'); }); const racePromise = Promise.race([p1, p2]); racePromise.then( data => { console.log(data); }, err => { console.log(err); } );
这里提供两个例子,供大家自己自行下去进行验证学习,这里就不细讲了,大家在自己编译器上一运行就一目了然了。 2、Promise.allSettled(): Promise.allSettled() 的状态与传入的Promise 状态无关,它后面的then方法永远执行成功的回调函数,它只会忠实的记录下各个 Promise 的表现。见代码:
const delay = ms => { return new Promise(resolve => { setTimeout(resolve, ms); }); }; const p1 = delay(1000).then(() => { console.log('p1 完成了'); return 'p1'; }); const p2 = delay(2000).then(() => { console.log('p2 完成了'); return Promise.reject('reason'); }); const allSettledPromise = Promise.allSettled([p1, p2]); allSettledPromise.then(data => { console.log('succ', data); }); const delay = ms => { return new Promise(resolve => { setTimeout(resolve, ms); }); }; const p1 = delay(1000).then(() => { console.log('p1 完成了'); return Promise.reject('reason'); }); const p2 = delay(2000).then(() => { console.log('p2 完成了'); return Promise.reject('reason'); }); const allSettledPromise = Promise.allSettled([p1, p2]); allSettledPromise.then(data => { console.log('succ', data); });
上面两个例子大家自行演示,看到结果大家都会明白的。 Promise的注意事项: 1、resolve 或 reject 函数执行后的代码: 推荐在调用 resolve 或 reject 函数的时候加上 return,不再执行它们后面的代码。 如:
new Promise((resolve, reject) => { // // return resolve(123); return reject('reason'); });
虽然 resolve 或 reject 函数的后面的代码仍可以执行。如:
new Promise((resolve, reject) => { // // return resolve(123); return reject('reason'); console.log('hi'); }); // hi
但是不建议这样写。 2、Promise.all/race/allSettled 的参数问题: 参数如果不是 Promise对象 的数组,系统会默认的将不是 Promise 的数组元素转变成 Promise对象 的数组。
Promise.all([1, 2, 3]).then(datas => { console.log(datas); }); // 等价于 Promise.all([ Promise.resolve(1), Promise.resolve(2), Promise.resolve(3) ]).then(datas => { console.log(datas); });
参数不只是数组,任何可遍历的都可以作为参数: 如:原生可遍历的:数组、字符串、Set、Map、NodeList、arguments 和非原生可遍历的(通过Iterate) 举个栗子:
// Set作为参数 Promise.all(new Set([1, 2, 3])).then(datas => { console.log(datas); });
3、Promise.all/race/allSettled 的错误处理: 单独处理:
// 第一个案例: const delay = ms => { return new Promise(resolve => { setTimeout(resolve, ms); }); }; const p1 = delay(1000).then(() => { console.log('p1 完成了'); return Promise.reject('reason'); }).catch(err => { console.log('p1', err); }); const p2 = delay(2000).then(() => { console.log('p2 完成了'); return Promise.reject('reason'); }).catch(err => { console.log('p2', err); }); const allPromise = Promise.all([p1, p2]); allPromise.then(datas => { console.log(datas); });
统一处理:
// 第二个案例: const delay = ms => { return new Promise(resolve => { setTimeout(resolve, ms); }); }; const p1 = delay(1000).then(() => { console.log('p1 完成了'); return Promise.reject('reason'); }); const p2 = delay(2000).then(() => { console.log('p2 完成了'); return Promise.reject('reason'); }); const allPromise = Promise.all([p1, p2]); allPromise.then(datas => { console.log(datas); }).catch(err => console.log(err)); 错误既可以单独处理,也可以统一处理 一旦被处理,就不会在其他地方再处理一遍(上面第二个案例)
宏任务和微任务
异步队列,当所有的同步任务执行完成之后会执行异步任务。如果异步队列中还有异步,那么就会牵涉到异步轮询机制,会先走微任务在执行宏任务。当前宏任务周期的所有微任务都执行完成之后会执行下一个宏任务。
宏任务:运行环境提供的异步
微任务:语言提供的异步
js事件轮询
事件轮询 (eventloop) 是"一个解决和处理外部事件时将它们转换为回调函数的调用的实体(entity)"
JavaScript 语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。所有任务都需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
任务队列 “ 任务队列 " 是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空," 任务队列 " 上第一位的事件就会自动进入主线程。
所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务
异步任务指的是,不进入主线程、而进入 " 任务队列 "(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
JS如何实现异步操作 JS 的异步是通过回调函数实现的,即通过任务队列,在主线程执行完当前的任务栈(所有的同步操作),主线程空闲后轮询任务队列,并将任务队列中的任务(回调函数)取出来执行。
" 回调函数 "(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
虽然 JS 是单线程的,但是浏览器的内核是多线程的,在浏览器的内核中不同的异步操作由不同的浏览器内核模块调度执行,异步操作会将相关回调添加到任务队列中。而不同的异步操作添加到任务队列的时机也不同,如 onclick, setTimeout,ajax 处理的方式都不同,这些异步操作是由浏览器内核的 webcore 来执行的,webcore 包含上图中的3种 webAPI,分别是 DOM Binding、network、timer模块。
onclick 由浏览器内核的 DOM Binding 模块来处理,当事件触发的时候,回调函数会立即添加到任务队列中。 setTimeout 会由浏览器内核的 timer 模块来进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中。 ajax 则会由浏览器内核的 network 模块来处理,在网络请求完成返回之后,才将回调函数添加到任务队列中。 异步执行机制 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。 主线程之外,还存在一个 " 任务队列 "(task queue)。只要异步任务有了运行结果,就在 " 任务队列 " 之中放置一个事件。 一旦 " 执行栈 " 中的所有同步任务执行完毕,系统就会读取 " 任务队列 ",看看里面有哪些事件。于是那些对应的异步任务结束等待状态,进入执行栈,开始执行。 主线程不断重复上面的第三步(事件轮询) JS中事件队列的优先级 在 JS 的ES6 中新增的任务队列(promise)优先级是在事件循环之上的,事件循环每次 tick 后会查看 ES6 的任务队列中是否有任务要执行,也就是 ES6 的任务队列比事件循环中的任务(事件)队列优先级更高。
先执行同步代码再执行异步代码,先执行微任务再执行宏任务
setTimeout( fn, 0 ),它在 " 任务队列 " 的尾部添加一个事件,因此要等到同步任务和 " 任务队列 " 现有的事件都处理完,才会得到执行。 803
块级作用域
1.在ES6之前JavaScript是没有块级作用域的,所有的变量都通过var关键字去声明,即在控制语句中的变量也可以在外部的作用域去访问。
2.随着ES6的到来JavaScript给我们带来的let 和 const关键字,也让它本身拥有了块级作用域的概念( { }内部都是块级作用域,在测试小demo的时候可以使用{ }来创建一个块级作用域来避免变量名称的冲突 )。我们在控制语句中使用let 和 const定义的
for循环中使用let定义的变量在全局作用域访问
3.用处:在你需要一些临时的变量的时候,块级作用域就可以发挥他的作用。而通过创建块级作用域,我们就不会担心会不会搞乱其他人定义的全局变量,我们就可以根据自己的想法来定义自己的变量了
全局作用域
JavaScript中的全局作用域中的变量,在任何地方都是可以访问的,全局作用域中的变量在定义的时候生成,浏览器关闭的时候销毁。
函数作用域
js中可以通过函数来创建一个独立作用域称为函数作用域,函数可以嵌套,所以作用域也可以嵌套;
自由变量的概念
在当前作用域没有定义的变量,但是却被访问了。向上级作用域一层一层向上查找,直到找到全局作用域,如果没有找到会抛出:xx is not defined 的错误;
作用域链:自由变量的向上级作用域一层一层查找,直到找到为止,最高找到全局作用域,就形成了作用域链
作用域链的理解
对于一个变量,引擎从当前作用域开始查找变量,如果找不到,就会向上一级继续查找。当抵达最外层全局作用域时,无论找到还是没有找到,查找过程中都会停止。
TCP和UDP的区别
连接性
TCP是面向连接的协议,在收发数据前必须和对方建立可靠的连接,建立连接的3次握手、断开连接的4次挥手,为数据传输打下可靠基础;UDP是一个面向无连接的协议,数据传输前,源端和终端不建立连接,发送端尽可能快的将数据扔到网络上,接收端从消息队列中读取消息段。
可靠性
TCP提供可靠交付的服务,传输过程中采用许多方法保证在连接上提供可靠的传输服务,如编号与确认、流量控制、计时器等,确保数据无差错,不丢失,不重复且按序到达;UDP使用尽可能最大努力交付,但不保证可靠交付。
TCP报文结构
TCP协议面向字节流,将应用层报文看成一串无结构的字节流,分解为多个TCP报文段传输后,在目的站重新装配;UDP协议面向报文,不拆分应用层报文,只保留报文边界,一次发送一个报文,接收方去除报文首部后,原封不动将报文交给上层应用。
吞吐量控制
TCP拥塞控制、流量控制、重传机制、滑动窗口等机制保证传输质量;UDP没有。
双工性
TCP只能点对点全双工通信;UDP支持一对一、一对多、多对一和多对多的交互通信
从上面TCP、UDP编程步骤可以看出,UDP 服务器端不需要调用监听(listen)和接收(accept)客户端连接,而客户端也不需要连接服务器端(connect)。UDP协议中,任何一方建立socket后,都可以用sendto发送数据、用recvfrom接收数据,不必关心对方是否存在,是否发送了数据。
TCP和UDP的使用场景
为了实现TCP网络通信的可靠性,增加校验和、序号标识、滑动窗口、确认应答、拥塞控制等复杂的机制,建立了繁琐的握手过程,增加了TCP对系统资源的消耗;TCP的重传机制、顺序控制机制等对数据传输有一定延时影响,降低了传输效率。TCP适合对传输效率要求低,但准确率要求高的应用场景,比如万维网(HTTP)、文件传输(FTP)、电子邮件(SMTP)等。
UDP是无连接的,不可靠传输,尽最大努力交付数据,协议简单、资源要求少、传输速度快、实时性高的特点,适用于对传输效率要求高,但准确率要求低的应用场景,比如域名转换(DNS)、远程文件服务器(NFS)等。
prototype 和 proto 区别是什么
原型是为了实现对象间的联系,解决构造函数无法数据共享而引入的一个属性,而原型链是一个实现对象间联系即继承的主要方法
-
prototype是构造函数的属性
-
__proto__
是每个实例都有的属性,可以访问 [[prototype]] 属性 -
实例的
__proto__
与其构造函数的prototype指向的是同一个对象 -
显式原型
prototype 每一个函数在创建之后,便会拥有一个prototype属性,这个属性指向函数的原型对象,显示原型的作用是用来实现基于原型的继承与属性的共享
隐式原型
*proto__上面说的这个原型是JavaScript中的内置属性prototype,此属性继承自object对象,但Firefox、Safari和Chrome在每个对象上都支持一个属性proto*,隐式原型的作用是用来构成原型链,实现基于原型的继承
作者:漫卷诗书喜欲狂链接:[JS]深入理解原型和原型链以及区别(包含面试题详解) - 掘金来源:稀土掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
react事件
react并不会真正的绑定事件到每一个具体元素上,而是采用事件代理模式
stestate处在同步逻辑,异步更新状态,更新真实dom
stestate处在异步逻辑,同步更新状态,同步更新真实dom
stestate接受第二个参数,第二个参数回调函数,状态和dom更新完就会被触发
VUE SEO 几种方案
1、SSR服务器渲染
Vue.js 是构建客户端应用程序的框架。默认情况下,可以再浏览器中输出Vue组件,进行生成DOM和操作DOM。然而,也可以将同一个组件渲染未服务器端的HTML字符串,将它们直接发送到浏览器,最后将这些静态标记“激活”为客户端上完全可交互的应用程序。
服务端渲染的Vue.js应用程序也可以被认为是“同构”或“通用”,因为应用程序的大部分代码都可以在服务器和客户端上运行。
权衡之处:
-
开发条件所限,浏览器特定的代码,只能在某些生命周期钩子函数(lifecycle hook)中使用;一些外部扩展库(external library)可能需要特殊处理,才能在服务器渲染应用程序中运行。
-
环境和部署要求更高,需要Node.js server运行环境;
-
高流量的情况下,需要准备相应的服务器负载,并明智地采用缓存策略。
优势:
-
更好的SEO,因为搜索引擎爬虫抓取工具可以直接查看完全渲染的页面;
-
更快的内容到达时间(time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。
不足:
-
一套代码两套执行环境,会引起各种问题,比如服务端没有window、document对象,处理方式是增加判断,如果是客户端才执行;
-
涉及构建设置和部署的更多要求,需要处于node server的运行环境
-
更多的服务端负载
2、Nuxt静态化
Nuxt.js框架,官方是这样介绍的: 从头搭建一个服务端渲染的应用是相当复杂的。幸运的是,我们有一个优秀的社区项目Nuxt.js这让一切变得非常简单。Nuxt是一个基于Vue生态的更高层的框架,为开发服务端渲染的Vue应用提供了极其便利的开发体验。更酷的是,你甚至可以用它来做为静态站生成器。
静态化是Nuxt.js打包的另一种方式,算是Nuxt.js的一个创新点,页面加载速度很快。
注意:在Nuxt.js执行 generate静态化打包时,动态路由会被忽略。
优势:
-
纯静态文件,访问速度超快
-
对比SSR,不涉及到服务器负载方面问题;
-
静态网页不宜遭到黑客攻击,安全性更高。
不足:
-
如果动态路由参数多都化不适用。
3、 预渲染 prerender-spa-plugin
如果你只是用来改善少数营销页面(例如: /,/about,/contact等=)的SEO,那么你可能需要预渲染。无需使用Web服务器实时动态编译HTML,而是使用预渲染方式,在构建时(build time)简单地生成针对特定路由等静态HTML文件。优点是设置预渲染更简单,并可以将你的前端作为一个完全静态的站点。
优势:
-
改动过小,引入插件配置即可
不足:
-
无法使用动态路由
-
只使用少量页面的项目,页面多达几百个的情况下,打包会非常慢
4、使用Phantomjs针对爬虫做处理
Phantomjs是一个基于webkit内核的无头浏览器,即没有UI界面,即它就是一个浏览器,只是其内的点击、翻页等人为相关操作需要程序设计实现。
虽然“PhantomJS宣布终止开发”,但是已经满足对Vue 的SEO处理。
这种解决方案其实是一种旁路机制,原理就是通过Nginx配置,判断访问来源UA是否是爬虫访问,如果是则将搜索引擎的爬虫请求转发到一个node server,再通过PhantomJS来解析完整的HTML,返回给爬虫。
优势:
-
完全不用改动项目代码,按原本的SPA开发即可,对比开发SSR成本小的不要太多;
-
对已用SPA开发完成的项目,这是不二之选。
不足:
-
部署需要node 服务器支持。
-
爬虫访问比网页访问要慢一些,因为定时要定时资源加载完成才返回给爬虫;
-
如果呗恶意模拟百度爬虫大量循环爬取,会造成服务器负载方面问题,解决方法是判断访问的IP,是否是百度官方爬虫的IP;
5、总结
如果构建大型网站,如商城类,不要犹豫直接上SSR服务端渲染,当然也有相应的坑等你,社区较成熟,英文好一些, 一切问题都迎刃而解。
如果只是个人博客、公司官网这类,其余三种都可以。
如果对已用SPA开发完成的项目进行SEO优化,而且支持node服务器,请使用Phantomjs。
对于VUE SSR项目已经接手开发过好几个,目前也是维护的状态。 从0~1的项目搭建, 开发使用Vue-SSR中也确实遇到一些坑和问题, 但这些都能得到解决,办法总比困难多的哈哈。
JS判断数组是数组
通过instanceof判断
通过constructor判断
通过Object.prototype.toString.call()判断
通过Array.isArray()判断
js如何合并对象
-
在 JavaScript 中使用 object.assign() 函数合并对象
-
在 JavaScript 中使用扩展运算符合并对象
-
在 JavaScript 中使用 array.reduce() 函数合并对象
-
在 JavaScript 中使用 jquery.extend() 函数合并对象
-
在 JavaScript 中使用用户定义的函数合并对象
vue3 +TS 父子组件之间的传值 通信
-
import { definedProps, definedEmits } from 'vue'//使用语法糖才能使用他们
css绝对定位如何居中?css绝对定位居中的四种实现方法
1、兼容性不错的主流css绝对定位居中的用法:
.conter{ width: 600px; height: 400px; position: absolute; left: 50%; top: 50%; margin-top: -200px; /* 高度的一半 */ margin-left: -300px; /* 宽度的一半 */ }
注意:这种方法有一个很明显的不足,就是需要提前知道元素的尺寸。否则margin负值的调整无法精确。此时,往往要借助JS获得。
2、css3的出现,使得有了更好的解决方法,就是使用transform代替margin. transform中translate偏移的百分比值是相对于自身大小的,可以这样实现css绝对定位居中:
.conter{ width: 600px; height: 400px; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); /* 50%为自身尺寸的一半 */ }
3、margin:auto实现绝对定位元素的居中(上下左右均0位置定位;margin: auto)
.conter{ width: 600px; height: 400px; position: absolute; left: 0; top: 0; right: 0; bottom: 0; margin: auto; /* 有了这个就自动居中了 */ }
4、使用css3盒模型:flex布局实现css绝对定位居中。这种情况是在不考虑低版本浏览器的情况下可以使用。
1.数组常用方法之 push()(==改变原数组,产生新数组==)
-
push
是用来在数组的末尾追加一个元素,返回添加以后的长度var arr = [1, 2, 3] // 使用 push 方法追加一个元素在末尾 arr.push(4) console.log(arr) // [1, 2, 3, 4] var res = arr.push(1,2,3,34); res//8
2.数组常用方法之 pop()(==改变原数组,产生新数组==)
-
pop
是用来删除数组末尾的一个元素,返回删除的元素var arr = [1, 2, 3] // 使用 pop 方法删除末尾的一个元素 var res = arr.pop() console.log(arr) // [1, 2]
3.数组常用方法之 unshift()(==改变原数组,产生新数组==)
-
unshift
是在数组的最前面添加一个元素var arr = [1, 2, 3] // 使用 unshift 方法想数组的最前面添加一个元素 arr.unshift(4) console.log(arr) // [4, 1, 2, 3]
4.数组常用方法之 shift()(==改变原数组,产生新数组==)
-
shift
是删除数组最前面的一个元素var arr = [1, 2, 3] // 使用 shift 方法删除数组最前面的一个元素 arr.shift() console.log(arr) // [2, 3]
5.数组常用方法之 splice(==改变原数组==)
-
splice
是截取数组中的某些内容,按照数组的索引来截取 -
语法:
splice(从哪一个索引位置开始,截取多少个,替换的新元素)
(第三个参数可以不写)var arr = [1, 2, 3, 4, 5] // 使用 splice 方法截取数组 var res = arr.splice(1, 2) console.log(arr) // [1, 4, 5] console.log(res)//[2,3] - `arr.splice(1, 2)` 表示从索引 1 开始截取 2 个内容 - 第三个参数没有写,就是没有新内容替换掉截取位置 ```javascript var arr = [1, 2, 3, 4, 5] // 使用 splice 方法截取数组 arr.splice(1, 2, '我是新内容') console.log(arr) // [1, '我是新内容', 4, 5]
1)、删除元素,并返回删除的元素 可以删除任意数量的项,只需指定 2 个参数:要删除的第一项的位置和要删除的项数。例如, splice(0,2)会删除数组中的前两项。 var arr = [1,3,5,7,9,11]; var arrRemoved = arr.splice(0,2); console.log(arr); //[5, 7, 9, 11] console.log(arrRemoved); //[1, 3] 2)、向指定索引处添加元素 可以向指定位置插入任意数量的项,只需提供 3 个参数:起始位置、 0(要删除的项数)和要插入的项。例如,splice(2,0,4,6)会从当前数组的位置 2 开始插入 4 和 6。 var array1 = [22, 3, 31, 12]; array1.splice(1, 0, 12, 35); //[] console.log(array1); // [22, 12, 35, 3, 31, 12] 3)、替换指定索引位置的元素 可以向指定位置插入任意数量的项,且同时删除任意数量的项,只需指定 3 个参数:起始位置、要删除的项数和要插入的任意数量的项。插入的项数不必与删除的项数相等。例如,splice (2,1,4,6)会删除当前数组位置 2 的项,然后再从位置 2 开始插入 4 和 6。 const array1 = [22, 3, 31, 12]; array1.splice(1, 1, 8); //[3] console.log(array1); // [22, 8, 31, 12] 作者:物理达人Physics侯老师 https://www.bilibili.com/read/cv9715493/ 出处:bilibili
-
arr.splice(1, 2, '我是新内容')
表示从索引 1 开始截取 2 个内容 -
然后用第三个参数把截取完空出来的位置填充
6.数组常用方法之 reverse(==改变原数组,产生新数组==)
-
reverse
是用来反转数组使用的var arr = [1, 2, 3] // 使用 reverse 方法来反转数组 arr.reverse() console.log(arr) // [3, 2, 1]
7.数组常用方法之 sort(==改变原数组,产生新数组==)
-
sort
是用来给数组排序的(默认按照字典排序 先按==照第一位排序-==如果第一位相等就按照第二位)var arr = [2, 3, 1] // 使用 sort 方法给数组排序 arr.sort() console.log(arr) // [1, 2, 3]
-
这个只是一个基本的简单用法
// 升序 arr4.sort(function (a, b) { return a - b }) // 降序 arr4.sort(function (a, b) { return b - a })
let arr5 = [{ username: 'zhangsan', age: 19 }, { username: 'lisi', age: 10 }, ] // 按照对象的年龄 降序 // a ,b 数组的元素 arr5.sort(function (a, b) { return b.age - a.age })
-
8.数组常用方法之 concat(==不====改变原数组==)
-
concat
是把多个数组进行拼接 -
和之前的方法有一些不一样的地方,就是
concat
==不会改变原始数组,而是返回一个新的数组==var arr = [1, 2, 3] // 使用 concat 方法拼接数组 var newArr = arr.concat([4, 5, 6]) console.log(arr) // [1, 2, 3] console.log(newArr) // [1, 2, 3, 4, 5, 6]
-
注意: ==
concat
方法不会改变原始数组==
-
9.数组常用方法之 join==(不会改变原始数组,而是把链接好的字符串返回)==
-
join
是把数组里面的每一项内容链接起来,变成一个字符串 -
可以自己定义每一项之间链接的内容
join(要以什么内容链接)
默认使用逗号作为分隔符 -
不会改变原始数组,而是把链接好的字符串返回
var arr = [1, 2, 3] // 使用 join 链接数组 var str = arr.join('-') console.log(arr) // [1, 2, 3] console.log(str) // 1-2-3
-
注意: join 方法不会改变原始数组,而是返回链接好的字符串
-
10.slice 复制数组的一部分 ==(不改变原始数组,返回一个新的数组)==
var arr9 = [9,8,7,6,5,4,3,2]; // 参数 两个 开始位置 结束位置,不包含结束位置,不改变原数组 var res9 = arr9.slice(1,3) console.log(res9)//[8, 7] // 第二个参数不传入,复制到最后 console.log(arr9.slice(1))//[8, 7, 6, 5, 4, 3, 2] // 如果传入负数,倒着数位置,最后一个是 -1 console.log(arr9.slice(1,-2))// [8, 7, 6, 5, 4]
注:
如果之只传入一个大于数组长度的参数,则返回一个空数组 无论是如何提取数组元素,原数组始终保持不变
ES5 中常见的数组常用方法
11.indexOf
接收两个参数:要查找的项和(可选的)表示查找起点位置的索引。
indexOf():从数组的开头(位置 0)开始向后查找。
lastIndexOf:从数组的末尾开始向前查找。
这两个方法都返回要查找的项在数组中的位置,或者在没找到的情况下返回-1。在比较第一个参数与数组中的每一项时,会使用全等操作符。
-
indexOf
用来找到数组中某一项的索引 -
语法:
indexOf(你要找的数组中的项)
var arr = [1, 2, 3, 4, 5] // 使用 indexOf 超找数组中的某一项 var index = arr.indexOf(3) console.log(index) // 2
-
我们要找的是数组中值为 3 的那一项
-
返回的就是值为 3 的那一项在该数组中的索引
-
-
如果你要找的内容在数组中没有,那么就会返回 -1
var arr = [1, 2, 3, 4, 5] // 使用 indexOf 超找数组中的某一项 var index = arr.indexOf(10) console.log(index) // -1
-
你要找的值在数组中不存在,那么就会返回 -1
-
12.forEach
-
和
for
循环一个作用,就是用来遍历数组的 -
这个方法没有返回值。参数都是 function 类型,默认有传
-
语法:
arr.forEach(function (item, index, arr) {})
var arr = [1, 2, 3] // 使用 forEach 遍历数组 arr.forEach(function (item, index, arr) { // item 就是数组中的每一项 // index 就是数组的索引 // arr 就是原始数组 console.log('数组的第 ' + index + ' 项的值是 ' + item + ',原始数组是', arr) })
-
forEach()
的时候传递的那个函数,会根据数组的长度执行 -
数组的长度是多少,这个函数就会执行多少回
-
13.map ==(不改变原数组,返回新数组)==
-
和
forEach
类似,只不过可以对数组中的每一项进行操作,==返回一个新的数组==var arr = [1, 2, 3] // 使用 map 遍历数组 var newArr = arr.map(function (item, index, arr) { // item 就是数组中的每一项 // index 就是数组的索引 // arr 就是原始数组 return item + 10 }) console.log(newArr) // [11, 12, 13]
14.filter==(不改变原数组,返回新增数组)==
-
和
map
的使用方式类似,按照我们的条件来筛选数组 -
把原始数组中满足条件的筛选出来,组成一个新的数组返回
var arr = [1, 2, 3] // 使用 filter 过滤数组 var newArr = arr.filter(function (item, nindex, arr) { // item 就是数组中的每一项 // index 就是数组的索引 // arr 就是原始数组 return item > 1 }) console.log(newArr) // [2, 3]
-
我们设置的条件就是
> 1
-
返回的新数组就会是原始数组中所有
> 1
的项
15.every 返回布尔值
判断 是否数组所有的元素都满足条件
let flag = arr.every(function(item,index,arr){ return 条件 }) let res4 = arr4.every(function (v) { return v >= 18 }) console.log(res4);
-
16.some 返回布尔值
判断是否数组有一些元素满足条件
let flag = arr.some(function(item,index,arr){ return 条件 }) let res5 = arr4.some(function (v) { return v >= 18 }) console.log(res5);//true
17.find
查找数组中第一个满足条件的元素
var arr6 = [10, 19, 17, 20]; let res6 = arr6.find(function (v, i) { return v === 19 }) console.log(res6); var arr7 = [{ id: 1, name: 'zs' }, { id: 2, name: 'lisi' }]; var obj = arr7.find(function (v) { return v.id == 2 }) console.log(obj); console.log(obj.name);
18.findIndex
查找数组中第一个满足条件的元素的下标
var res7 = arr7.findIndex(function (v) { return v.id == 2 }) console.log(res7);
19.reduce
let res8 = arr8.reduce(function (a, b) { console.log(a, b); //a 第一次是 数组中第一个元素 //10+19 =29 // 然后几次 就是前面 数字的和 //a = 29 // b 17 return a + b; }) console.log(res8); let res9 = arr8.reduce(function (a, b) { console.log(a, b); //a 是累积变量 ,1000就是累积变量的初始值 // b 是数组的元素 return a + b; }, 1000) console.log(res9);
20.fill() es6 新增 ==(会改变原数组)==
fill()方法能使用特定值填充数组中的一个或多个元素。当只是用一个参数时,该方法会用该参数的值填充整个数组。
let arr = [1, 2, 3, cc , 5]; arr.fill(1); console.log(arr);//[1,1,1,1,1];
如果不想改变数组中的所有元素,而只是想改变其中一部分,那么可以使用可选的起始位置参数与结束位置参数(不包括结束位置的那个元素)
3 个参数: 填充数值,起始位置参数,结束位置参数(不包括结束位置的那个元素)
let arr = [1, 2, 3, arr , 5]; arr.fill(1, 2); console.log(arr);//[1,2,1,1,1] arr.fill(0, 1, 3); console.log(arr);//[1,0,0,1,1];
21.includes() es7 新增,返回布尔值
includes() 方法用来判断一个数组是否包含一个指定的值,如果是返回 true,否则 false。
参数有两个,其中第一个是(必填)需要查找的元素值,第二个是(可选)开始查找元素的位置
const array1 = [22, 3, 31, 12, arr ]; const includes = array1.includes(31); console.log(includes); // true const includes1 = array1.includes(31, 3); // 从索引3开始查找31是否存在 console.log(includes1); // false
需要注意的是:includes使用===运算符来进行值比较,仅有一个例外:NaN 被认为与自身相等。
let values = [1, NaN, 2]; console.log(values.indexOf(NaN));//-1 console.log(values.includes(NaN));//true
22.toLocaleString() 和 toString()
将数组转换为字符串
const array1 = [22, 3, 31, 12]; const str = array1.toLocaleString(); const str1 = array1.toString(); console.log(str); // 22,3,31,12 console.log(str1); // 22,3,31,12
23.copyWithin() [es6 新增]==(该方法会改变现有数组)==
copyWithin() 方法用于从数组的指定位置拷贝元素到数组的另一个指定位置中。
//将数组的前两个元素复制到数组的最后两个位置 let arr = [1, 2, 3, arr , 5]; arr.copyWithin(3, 0); console.log(arr);//[1,2,3,1,2]
默认情况下,copyWithin()方法总是会一直复制到数组末尾,不过你还可以提供一个可选参数来限制到底有多少元素会被覆盖。这第三个参数指定了复制停止的位置(不包含该位置本身)。
let arr = [1, 2, 3, arr , 5, 9, 17]; //从索引3的位置开始粘贴 //从索引0的位置开始复制 //遇到索引3时停止复制 arr.copyWithin(3, 0, 3); console.log(arr);//[1,2,3,1,2,3,17]
24.flat() 和 flatMap() es6 新增(==该方法返回一个新数组,不改变原数组。)==
flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。
该方法返回一个新数组,对原数据没有影响。
参数: 指定要提取嵌套数组的结构深度,默认值为 1。
const arr1 = [0, 1, 2, [3, 4]]; console.log(arr1.flat()); // expected output: [0, 1, 2, 3, 4] const arr2 = [0, 1, 2, [[[3, 4]]]]; console.log(arr2.flat(2)); // expected output: [0, 1, 2, [3, 4]] //使用 Infinity,可展开任意深度的嵌套数组 var arr4 = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]]; arr4.flat(Infinity); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] // 扁平化数组空项,如果原数组有空位,flat()方法会跳过空位 var arr4 = [1, 2, , 4, 5]; arr4.flat(); // [1, 2, 4, 5]
flatMap()方法对原数组的每个成员执行一个函数,相当于执行Array.prototype.map(),然后对返回值组成的数组执行flat()方法。
该方法返回一个新数组,不改变原数组。
// 相当于 [[2, 4], [3, 6], [4, 8]].flat() [2, 3, 4].flatMap((x) => [x, x * 2]) // [2, 4, 3, 6, 4, 8]
25、 entries(),keys() 和 values() 【ES6】
entries(),keys()和values() —— 用于遍历数组。它们都返回一个遍历器对象,可以用for...of循环进行遍历
区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历
for (let index of [ a , b ].keys()) { console.log(index); } // 0 // 1 for (let elem of [ a , b ].values()) { console.log(elem); } // a // b for (let [index, elem] of [ a , b ].entries()) { console.log(index, elem); } // 0 "a" // 1 "b"
如果不使用for...of循环,可以手动调用遍历器对象的next方法,进行遍历。
let letter = [ a , b , c ]; let entries = letter.entries(); console.log(entries.next().value); // [0, a ] console.log(entries.next().value); // [1, b ] console.log(entries.next().value); // [2, c ]
JS 如何终止 forEach 循环 break 报错,return 跳不出循环
-
终止 forEach 可以使用 try catch 内部抛出错误,catch 捕获错误。
let arr = [1, 2, 3] try { arr.forEach(item => { if (item === 2) { throw('循环终止') } console.log(item) }) } catch(e) { console.log('e: ', e) } 复制代码
当然我们大可以用其他方法代替
-
Array.prototype.some
当 return true 的时候,会终止遍历
-
Array.prototype.every
当 return false 的时候,会终止遍历
JS的节流、防抖及使用场景
结合应用场景
-
debounce
-
search搜索联想,用户在不断输入值时,用防抖来节约请求资源。
-
window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
-
-
throttle
-
鼠标不断点击触发,mousedown(单位时间内只触发一次)
-
监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断
-
JS 运行机制-EventLoop(事件循环) javascript 是单线程的 JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完 全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
主线程和任务队列 单线程就意味着,所有任务需要排队。所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
异步任务:不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
具体来说:
-
所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
-
主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件
-
一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",根据顺序循环调用异步任务,进入执行栈,开始执行,直到所有任务队列异步任务执行完
-
另外,任务队列中的每一个事件都是一个宏任务,执行栈执行的过程中也会有微任务,他们的执行顺序是:见下面总结
下图就是主线程和任务队列的示意图
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。 只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。
JS中Map和ForEach的区别
区别
forEach()方法不会返回执行结果,而是undefined。也就是说,forEach()会修改原来的数组。而map()方法会得到一个新的数组并返回。
css实现0.5px线
使用scale缩放
<style> .hr.scale-half { height: 1px; transform: scaleY(0.5); } </style> <p>1px + scaleY(0.5)</p> <div class="hr scale-half"></div>
css实现三角形
.triangle { width: 0; height: 0; border: 100px solid; border-color: orangered skyblue gold yellowgreen; }
localStorage、sessionStorage、cookie区别
一、区别
-
localStorage: localStorage 的生命周期是永久的,关闭页面或浏览器之后 localStorage 中的数据也不会消失。localStorage 除非主动删除数据,否则数据永远不会消失
-
sessionStorage: sessionStorage 的生命周期是仅在当前会话下有效。sessionStorage 引入了一个“浏览器窗口”的概念,sessionStorage 是在同源的窗口中始终存在的数据。只要这个浏览器窗口没有关闭,即使刷新页面或者进入同源另一个页面,数据依然存在。但是 sessionStorage 在关闭了浏览器窗口后就会被销毁。同时独立的打开同一个窗口同一个页面,sessionStorage 也是不一样的
-
cookie: cookie生命期为只在设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭。 存放数据大小为4K左右, 有个数限制(各浏览器不同),一般不能超过20个。缺点是不能储存大数据且不易读取
正文 Cookie 数据生命性:一般由服务器生成,可设置失效时间。(也可以由客户端生成) 存放数据大小: 一般大小不能超过4KB 作用域:Cookie的作用域仅仅由domain和path决定,与协议和端口无关 与服务器端通信:浏览器每次向服务器发出请求,就会自动把当前域名下所有未过期的Cookie一同发送到服务器(会带来额外的性能开销) 易用性:缺乏数据操作接口(document.cookie)。 适用场景:只有那些每次请求都需要让服务器知道的信息(保持用户的登录状态),才应该放在 Cookie 里面。 localStorage 数据生命性::存储在 localStorage 的数据可以长期保留; 存放数据大小: 一般为5MB 作用域:localStorage的作用域是限定在文档源级别的。文档源通过协议、主机名以及端口三者来确定。 与服务器端通信:不会自动把数据发给服务器,仅在本地保存。 易用性:有很多易用的数据操作接口,比如setItem、getItem、removeItem 适用场景:常用于长期登录(+判断用户是否已登录),适合长期保存在本地的数据 sessionStorage 数据生命性:sessionStorage 里面的数据在页面会话结束(关闭对应浏览器标签或窗口)时会被清除。 存放数据大小: 一般为5MB 作用域:sessionStorage的作用域也是限定在文档源级别。但需要注意的是,如果相同文档源的页面渲染在不同的标签中,sessionStorage的数据是无法共享的。 与服务器端通信:不会自动把数据发给服务器,仅在本地保存。 易用性:有很多易用的数据操作接口,比如setItem、getItem、removeItem 适用场景:敏感账号一次性登录; Web Storage (localStorage、sessionStorage)的优势 存储空间更大 更节省流量(没有额外性能开销) 获取数据从本地获取会比服务器端获取快得多,所以显示更快 IndexedDB(补充内容) IndexedDB 就是浏览器提供的本地数据库,它可以被网页脚本创建和操作。
IndexedDB 不属于关系型数据库(不支持 SQL 查询语句),更接近 NoSQL 数据库。
IndexedDB储存空间大,一般来说不少于 250MB,甚至没有上限。
IndexedDB不仅可以储存字符串,还可以储存二进制数据(ArrayBuffer 对象和 Blob 对象)。
提供查找接口,还能建立索引
异步操作:IndexedDB 操作时不会锁死浏览器,用户依然可以进行其他操作,这与 LocalStorage 形成对比,后者的操作是同步的。异步设计是为了防止大量数据的读写,拖慢网页的表现。
JS中find和filter的区别
find的用法
通俗的讲find是找到数组中符合条件的第一项,看下面代码
filter的用法
filter的功能也是过滤,不过是在遍历数组后拿到所有符合条件的item项
nuxt上线
.nuxt文件、nuxt.config.js、package.json、static
原生js怎样获取参数
window.location.search
在JavaScript中,可以使用window.location
对象来获取地址栏传递的参数。具体可以通过以下几种方式获取:
-
使用
window.location.search
获取查询字符串参数:
javascriptCopy code// 假设当前页面的URL为:http://example.com?param1=value1¶m2=value2 const queryString = window.location.search; // 获取查询字符串部分,即 "?param1=value1¶m2=value2" // 使用URLSearchParams解析查询字符串 const params = new URLSearchParams(queryString); // 获取特定参数的值 const param1Value = params.get('param1'); // "value1" const param2Value = params.get('param2'); // "value2"
-
使用
window.location.href
获取完整URL,然后通过字符串操作截取参数:
javascriptCopy code// 假设当前页面的URL为:http://example.com?param1=value1¶m2=value2 const url = window.location.href; // 获取完整URL // 截取参数部分 const queryString = url.split('?')[1]; // 使用URLSearchParams解析查询字符串 const params = new URLSearchParams(queryString); // 获取特定参数的值 const param1Value = params.get('param1'); // "value1" const param2Value = params.get('param2'); // "value2"
请注意,URLSearchParams
是ES6引入的新特性,用于处理URL查询字符串。在使用前请确保浏览器的兼容性。
使用以上方法,您可以在JavaScript中获取地址栏传递的参数。首先获取完整URL或查询字符串,然后使用适当的方式解析和提取参数的值。
ts接口定义类型怎样使用里面某一个类型的值
在TypeScript中,可以使用接口定义类型,并且可以通过类型注解将接口中的某个属性的类型应用到其他变量或函数中。
以下是一个示例,展示了如何使用接口中的某个类型的值:
typescriptCopy codeinterface Person { name: string; age: number; } // 定义一个接口类型的变量 const person: Person = { name: 'John', age: 25, }; // 使用接口中的某个类型的值 let personName: string = person.name; let personAge: number = person.age; console.log(personName); // 输出 "John" console.log(personAge); // 输出 25
在这个示例中,我们定义了一个名为Person
的接口,该接口包含name
和age
属性的类型定义。然后,我们创建了一个person
变量,该变量符合Person
接口的定义。
接下来,我们使用person
对象中的name
和age
属性的类型注解,将其应用到personName
和personAge
变量上。这样,personName
的类型就是string
,personAge
的类型就是number
,并且它们分别被赋值为person
对象中对应的属性值。
通过这种方式,我们可以在TypeScript中定义接口并利用接口中的某个类型的值,以便在其他变量或函数中使用相应的类型
js存储数组方式
数组的存储结构 1.基本数据类型变量都存储在栈区域 2.引用变量存储在堆区域,如对象。 3.复杂数据类型是存储在堆区域,赋值时是将对象在堆区域的地址赋值给变量 4.- 因为复杂数据类型,变量存储的是地址,真实内容在 堆空间 内存储
-
所以赋值的时候相当于把 obj 存储的那个地址复制了一份给到了 obj1变量
现在 obj 和 obj1 两个变量存储的地址一样,指向一个内存空间
-
所以使用 obj1 这个变量修改空间内的内容,obj 指向的空间也会跟着改变了
vue2为啥不对数组进行劫持呢
Vue.js 2.x中的响应式系统使用了一种称为"劫持"或"代理"的技术来实现对对象属性的监测和响应。然而,Vue.js 2.x没有直接对数组进行劫持,而是通过重写数组的原型方法来实现对数组的响应式支持。
Vue.js之所以选择不直接对数组进行劫持,是因为数组的变异方法(例如push、pop、splice等)是无法通过普通的对象属性劫持来监听的。如果Vue.js直接对数组进行劫持,那么只有通过Vue提供的特殊方法才能保证响应式更新,而不能直接使用JavaScript原生的数组方法。这样会导致语法和用法上的不一致,增加了学习和使用的复杂性。
相反,Vue.js 2.x通过重写数组的原型方法(例如push、pop、splice等)来捕获数组的变化,并触发响应式更新。这意味着当你使用这些变异方法修改数组时,Vue.js能够监听到这些变化并更新相关的视图。
总结一下,Vue.js 2.x选择不对数组进行直接劫持的原因是为了保持语法和用法上的一致性,避免增加复杂性,并通过重写数组的原型方法来实现对数组的响应式支持。
浏览输入URL地址会进过那些流程
当你在浏览器中输入URL地址并按下回车键时,以下是浏览器的一般流程:
-
URL解析:浏览器会解析输入的URL,提取出协议(如HTTP、HTTPS)、主机名(例如www.example.com)和路径(如/page)等信息。
-
DNS解析:浏览器将主机名发送给域名系统(DNS)服务器,以获取与主机名对应的IP地址。这个过程涉及到查询DNS缓存、本地主机文件和递归查询等步骤,最终得到主机的IP地址。
-
建立TCP连接:使用获取到的IP地址,浏览器会与服务器建立TCP连接。这个过程涉及到TCP的三次握手,确保浏览器和服务器之间建立可靠的连接。
-
发起HTTP请求:建立TCP连接后,浏览器会向服务器发送HTTP请求。这个请求包括请求方法(如GET、POST)、请求头(包含一些额外的信息,如用户代理、Cookie等)以及可能的请求体(对于POST请求)。
-
服务器处理请求:服务器接收到浏览器发送的请求后,根据请求的路径和其他信息,处理请求并生成响应。
-
接收响应:浏览器接收到服务器发送的响应,响应包括状态码(表示请求成功、失败或其他状态)、响应头(包含一些元数据,如内容类型、缓存控制等)和响应体(实际的响应数据)。
-
渲染页面:如果响应的内容是HTML,浏览器会解析HTML、构建DOM树和CSSOM树,然后将它们合并为渲染树,最终绘制到屏幕上。
-
关闭连接:页面渲染完成后,浏览器会关闭与服务器的TCP连接,释放资源。
需要注意的是,上述流程是一个简化的描述,实际上还包括其他步骤和优化。例如,浏览器可能会使用HTTP持久连接来重复使用已经建立的连接,减少TCP握手的开销;还有缓存机制、安全验证等等。但总体来说,上述流程涵盖了从输入URL到页面展示的主要步骤。
js数组方法怎样把二位数组变成一维数组
JavaScript提供了多种方法将二维数组转换为一维数组。以下是几种常见的方法:
-
使用
concat()
方法和扩展运算符(spread operator):
javascriptCopy codeconst twoDimensionalArray = [[1, 2], [3, 4], [5, 6]]; const flattenedArray = [].concat(...twoDimensionalArray); console.log(flattenedArray);
输出:
javascriptCopy code [1, 2, 3, 4, 5, 6]
-
使用
reduce()
方法:
javascriptCopy codeconst twoDimensionalArray = [[1, 2], [3, 4], [5, 6]]; const flattenedArray = twoDimensionalArray.reduce((result, currentArray) => result.concat(currentArray), []); console.log(flattenedArray);
输出:
javascriptCopy code [1, 2, 3, 4, 5, 6]
-
使用
flatMap()
方法(适用于ES2019及以上版本):
javascriptCopy codeconst twoDimensionalArray = [[1, 2], [3, 4], [5, 6]]; const flattenedArray = twoDimensionalArray.flatMap((currentArray) => currentArray); console.log(flattenedArray);
输出:
javascriptCopy code [1, 2, 3, 4, 5, 6]
这些方法都能将二维数组中的所有元素合并到一个新的一维数组中。你可以根据自己的需求选择适合的方法来转换二维数组。
1.setup函数怎么实现
import unittest
class TestMyFunction(unittest.TestCase): def setUp(self): # 在这里设置你的测试环境 # 例如,初始化一个数据库连接,或者创建一个临时文件等 pass
def tearDown(self): # 在这里清理你的测试环境 # 例如,关闭数据库连接,删除临时文件等 pass def test_my_function(self): # 在这里编写你的测试代码 pass
if name == 'main': unittest.main()
2.websocket会不会跨域
WebSocket本身是不存在跨域问题的,因此,可以利用WebSocket来进行非同源之间的通信。这意味着你可以自由地创建WebSocket实例并通过open方法发送数据到后台,同时也可以利用message方法接收后台传来的数据。
然而,在实践中,你可能仍会碰到一些跨域问题。例如,如果你使用socket.io库,它只会使用WebSocket传输协议进行连接,从而避免一些跨域问题。在前端解决io访问跨域问题,可以通过一些配置和选项来实现。
3.怎么封装aixos
要封装axios,首先需要安装axios,可以通过npm install axios --save命令进行安装。然后,在项目中创建一个http.js文件,在这个文件中进行二次封装。
封装的主要目的是为了使axios的使用更加方便和高效。以下是一些常见的封装方法:
基本配置:可以配置默认的请求地址、超时时间等。
请求拦截:可以在发送请求之前做一些处理,例如添加token到header中等。
响应拦截:可以对响应数据进行处理,例如统一处理错误信息等。
封装实例:可以将axios实例化,并在需要的地方引入和使用。
创建API:可以根据项目需求,创建对应的API,如get、post、put、delete等,并保持参数格式一致。
4.pinia怎么用
Pinia是一个Vue的状态管理库,相当于Vuex的替代品,主要用来实现跨组件或页面的状态管理。下面是如何在项目中使用Pinia的步骤:
安装Pinia:可以通过npm install pinia命令进行安装。
引入Pinia:在项目的main.js文件中引入Pinia并创建容器,然后挂载到根实例上。例如:import { createPinia } from 'pinia' const pinia = createPinia()。
创建Store:在项目中创建一个store文件夹,然后在该文件夹中新建一个文件(例如user.ts),用于定义仓库Store。
修改Vue应用:在使用app.use(pinia)后,你的所有Vue组件现在都可以访问到pinia提供的功能
5.怎么首页白屏优化
减少HTTP请求:合并CSS和JavaScript文件,使用CSS Sprites等技术来减少请求的数量。 使用CDN加速:将静态资源部署到CDN上,以加速资源的加载速度。 开启GZIP压缩:启用服务器端的GZIP压缩可以减少文件大小,从而加快加载速度。 优化图片:通过压缩图片大小、选择合适的图片格式等方式来减少图片的加载时间。 服务端渲染:在服务端将渲染逻辑处理好,然后将处理好的HTML直接返回给前端展示,这样既可以解决白屏问题,也可以提高SEO效果。
6.vue2和vue3中有哪些优点和缺点
Vue2的优点主要体现在以下几个方面:首先,Vue2简单易学,上手成本低,使得开发者能够快速构建用户界面。其次,Vue2支持渐进式框架,可以适用于小型到大型的项目。最后,双向数据绑定和模板语法使Vue2在处理复杂场景时渲染效率较高。然而,Vue2也存在一些不足之处,例如在处理大型应用时,其性能和体积控制不如React好。
Vue3则在许多方面进行了优化和改进。首先,Vue3的diff算法的优化使得其在渲染大量静态节点时更加高效。此外,Vue3的速度更快,相比Vue2来说,重写了虚拟 Dom 实现,编译模板的优化,更高效的组件初始化,update性能提高1.3~2倍,SSR速度提高了2~3倍。同时,通过webpack的tree-shaking功能,Vue3可以将无用模块“剪辑”,仅打包需要的模块,这样既可以帮助开发人员实现更多其他的功能,而不必担忧整体体积过大,也对使用者有益,因为打包出来的包体积变小了。另外,