自我介绍
面试官您好,我的名字叫,我应聘是web前端开发工程师,
从上一家公司,主要做外包服务,互联网+,专注于为企业或政府提供全面互联网软件技术解决方案
包括APP研发、小程序研发、微信公众号研发、网站研发、。
目前团队人数约为50人,其中85%员工为技术人员。在广州,技术人员人数约10人,专注负责孵化器内的高新技术项目。
从事前端这行工作有三年工作经验,主要做过该项目是基于vue-cli创建的一个综合性的电商购物网站,
类似于淘宝,京东商城。
登录后用户可以在该网站浏览商品,加入购物车,下订单以及各种各样的活动。
未登录的用户可以不能进行下单,他们可以通过手机号注册或者qq登录的方式进行登录完成购物。
还有做实店的后台官理系统,主要做商品管理,商品分类,管理员登录,权限管理。
商品图片上传图片。
面试题
var let const声明的区别?
var let,const 三者都可以声明变量
var存在变量提升,而let与const 不存在变量提升
var 定义的变量可以声明多次,而let、const定义的变量只能声明
var 定义多次变量时,后面定义的内容会覆盖前面的,但不会报错。
var, let 声明的变量可以再次赋值,而const声明的变量不能再次赋值。
const 定义的是常量,常量是不能修改的。
但常量里面所包含的内容还是可以改的。
var声明的变量没有自身的作用域,而let,const 声明的变量有自身的作用域。
在js中是没有块级作用域的,列如在函数内var声明的变量,在函数外也可以调用,但let、const声明的变量有自身的作用域,在函数内定义的变量只能在函数内使用。
var 是有声明,
父子组件的生命周期?
什么是组件的生命周期?
一个组件从创建到销毁的整个过程就叫生命周期。
生命周期函数(钩子函数)
生命周期是vue框架的内置函数,随着组件的生命周期,自动按序执行。
作用:
特定的时间点,执行某些特定的操作。
生命周期的四个阶段
初始化阶段
该阶段是为了创建组件,里面又有两个钩子函数,
beforeCreate,created beforeCreate ==> 创建前
挂载阶段
该阶段是为了修改变量刷新视图,里面又有两个钩子函数,
beforeUpdate,updated
beforeUpdate ==>更新前
updated ==>更新后
销毁阶段
该阶段是为了把组件对象从内存删除,里面又有两个钩子函数,
boforeDestory(销毁前),destroyed (销毁后,用于清除定时器,移出绑定的js事件)
父子组件生命周期执行顺序
挂载阶段
父组件实例初始化前
父组件实例初始化后
父组件vue的虚拟DOM,挂载到真实的网页之前。
实例初始化前
实例初始化后
vue的虚拟DOM,挂载到真实的网页上
父组件vue的虚拟DOM,挂载到真实的网页上。
父子组件的生命周期的执行顺序是从外到内,再从内到外。
非父子组件之间的三种传值办法?
1.兄弟组件的传值:buS总线 --发布订阅---观察者模式
Vue.prototype.属性 = new Vue(),使Vue的原型上得属性等于Vue实例。
2.利用vuex状态管理进行共享达到传值的目的。
3.以及当兄弟节点有共同的父节点时可以利用两次父子传值达到兄弟传值的目的。
路由传参的几种方式 ?
传参方式可划分为params传参和query传参,而params传参又可分为在url中显示参数和不显示参数两种方式。
1.params 传参(显示参数) 又可分为 声明式 和 编程式 两种方式。
<router-link :to="/child/1">跳转到子路由 </router-link>
{
path:'/child/:id',
component:Child
}
编程式this.$router.push:同样需要子路由提前配置好参数。
{
path:'/child/:id',
component:Child
}
this.$router.push({
path:'/child/${id}',
})
接收:this.$route.params.id
2 params传参(不显示参数) 也可分为声明式和编程式两种方式,与显示参数不同的是,这里是通过路由的别名name进行传值
<router-link :to="{name:'Child',params:{id:1}}">跳转到子路由</router-link>
{
path: '/child,
name: 'Child',
component: Child
}
this.$router.push({
name:'Child',
params:{
id:1
}
})
接收 this.$route.params.id
3.query 传参(显示参数)也可分为声明式和编程式 两种方式
声明式router-link:该方式是通过 router-link 组件的 to 属性实现,不过使用该方式传值的时候,需要子路由提前配置好路由别名
{
path: '/child,
name: 'Child',
component: Child
}
<router-link :to="{name:'Child',query:{id:1}}">跳转到子路由</router-link>
编程式 this.$router.push:使用该方式传值的时候,同样需要子路由提前配置好路由别名(name 属性)
{
path: '/child,
name: 'Child',
component: Child
}
this.$router.push({
name:'Child',
query:{
id:1
}
})
接收: this.$route.query.id
跨域
商城小程序,负责哪些模块?* **
购物车功能实现
应用场景 : 适用于商城,秒杀,商品购买等类型的小程序,负责将顾客浏览的商品保存下来,方便客户集中比较商品与购买商品。
思路分析:
实现购物车功能前思考以下问题:
1.小程序如何布局?使用什么布局能提升页面开发效率??
2.将购物车功能分为四个小功能:
1.一键全选/取消商品
2.动态添加商品可手动输入
3.计算结算商品金额
4.左滑动删除商品
答:1.在小程序中主要兼容安卓和ios两种型号手机,在页面开发中可使用flex布局,能极大的提高页面开发效率。
2.请仔细阅读代码分析,
JS中的对象?
js中对象分为三种:自定义对象,内置对象,浏览器对象。
内置对象就是指js语言自带的一些对象,这些对象供开发者使用,并提供了一些常用的或是最基本而必要的功能(属性和方法)
内置对象最大的优点就是帮助我们快速开发。
多查MDN的内置对象文档。
1.学习对象中的方法
2.查阅该方法的功能
3.查看里面参数的意义和类型
4.查看返回值的意义和类型
5.通过demo进行测试
常用的四种内置对象
1.Math对象
2.Date对象
3.Array对象
4.String对象
Math 数学对象
Math 对象不是构造函数,它具有数学常数和函数的数学和方法。
跟数学相关的运算(求绝对值,取整,最大值等) 可以使用Math中
Math.PI //圆周率
Math.floor() //向下取整
Math.cell() //向上取整
Math.round() //四舍五入,就近取整,注意:-3.5结果是-3
Math.abs() //绝对值
Math.max()/min() //求最大和最小值
浏览器缓存机制?
cookie 前后台都可以设置时间, 大小4k ,每一次请求都会自动携带在请求里面
localStorage 数据持久化,除非清理掉,否则永久在 5M 需要手动设置到请求体内。
sessionStorage 浏览器窗口关闭就清理 5M 需要手动设置到请求体中
indexDB 除非清理掉,否则永久在 根据本地硬件大小 基本是用不完
携带token(有销毁时间的)
一些用户的习惯类的信息,
跨域问题?
源(origin),由协议、ip地址、端口 组成。
如果非同源,共有以下三种行为受到限制
DOM无法操作
Cookie无法操作
Ajax请求无效
CORS全称Cross Resource Sharing,意为跨域资讯共享。
当一个资源去访问另一个不同域名或者同域名不同端口的资源时,就会发出跨域请求。
如果此时另一个资源不允许其进行跨域资源访问,那么访问就会遇到跨域问题,
跨域指的是浏览器不能执行其它网站的脚本。
是由浏览器的同源策略造成的,是浏览器对JavaScript施加的安全限制。
跨域方案之CORS
通过服务器设置一系列响应头来实现跨域。
第三方模块 cors 来解决跨域问题。
下载安装cors: npm install cors
加载cors: const cors = require('cors')
注册为中间件:app.use(cors())
注意:这个中间件要放到其他中间件之前
跨域方案之JSONP
JSONP方案只支持GET请求
JSONP没有浏览器兼容问题,任何浏览器都支持
HTML标签的src属性,不受跨域访问的影响,没有跨域问题。
JSONP的实现
客户端先准备一个函数,比如abc,回到深圳。
====
JSONP方法只支持GET请求。
JSONP没有浏览器兼容问题,任何浏览器都支持。
HTML标签的src属性,不受跨域访问的影响,没有跨域问题。
JSONP的实现
客户端先准备一个函数,比如abc,并且设置好形参,准备接收响应数据客户端通过script标签的src属性,向接口发生请求,并传递callBack= abc 参数服务端响应一个字符串响应一个字符串abc({"status":0,"message":"登录成功"})客户端将服务器端返回的字符串当做jS代码解释,从而实现跨域。
jSONP原理
请求发送到服务器 获取callback参数(abc)
响应abc("hello world")
运行abc("hello world")
有多少工作经验?
上一家有两年工作经验,主要做企业内部管理系统,实体店的商品信息管理系,做PC端比较多,还有uniapp前端框架做微信小程很小,网页端网站,使用技术栈vue全家桶,使用element-ui组件,使用地图echart来做数据可视化,做前端性能优化。
做前端性能优化?
图片懒加载的实现方式?
在页面放置img标签
给img图片加上alt,width,height和data-src
通过js判断页面是否滚动到某张图片需要显示的位置,这时将
src赋值为data-src
offsetTOP方法
图片出现在视窗内的情况:offsetTop<clientHeight + scrollTop
getBoundingClientRect方法
图片出现在视窗内的情况:
element.getBoundingClientRect().top < clientHeight
h5的IntersectionObserver方式。
intersectionRatio:目标元素可见比例,即intersectionRect占boundingClientRect的比例,完全可见为1,完全不可见时小于0。
优化
通过以下css可以提高性能
之所以使用visibility而不是display是因为
Visiblity不会触发重绘(repaint)和重排(reflow)
img{
visibility:hidden;
}
img[src]{
visibility:vi
因为scroll事件的触发频率很高,频繁操作dom结点会造成很大的性能问题,所以需要做节流和防抖设计,减少scroll事件的触发频率sible;
}
因为scroll事件的触发频率很高,频繁操作dom结点会造成很大的性能问题,所以需要做节流和防抖设计,减少scroll事件的触发频率
vue3+ts做项目? Jquery做前端项目?
js和ts的区别?
首先,它们都是脚本语言.JavaScript是轻量级的解释性脚本语言,可嵌入到HTMLz页面中,在浏览器端执行。
而TypeScript是JavaScript的超集(ts是微软开发的开源编程语言),即包含JavaScript的所有元素,能运行JavaScript的代码,并扩展了JavaScript的语法。
(ts包含了js的库和函数,ts上可以写任何的js,)
登录业务实现?
移动端适配?
transform: scale(0.5) 方案
div {
height:1px;
background:#000;
-webkit-transform: scaleY(0.5);
-webkit-transform-origin:0 0;
overflow: hidden;
}
css根据设备像素比媒体查询后的解决方案
/* 2倍屏 */
@media only screen and (-webkit-min-device-pixel-ratio: 2.0) {
.border-bottom::after {
-webkit-transform: scaleY(0.5);
transform: scaleY(0.5);
}
}
/* 3倍屏 */
@media only screen and (-webkit-min-device-pixel-ratio: 3.0) {
.border-bottom::after {
-webkit-transform: scaleY(0.33);
transform: scaleY(0.33);
}
}
如此,完美的解决一像素看着粗的问题
如何适配
viewport
视口(viewport)代表当前可见的计算机图形区域。在Web浏览器术语中,通常与浏览器窗口相同,但不包括浏览器的UI, 菜单栏等——即指你正在浏览的文档的那一部分。
那么在移动端如何配置视口呢? 简单的一个meta标签即可!
<meta name="viewport" content="width=device-width; initial-scale=1; maximum-scale=1; minimum-scale=1; user-scalable=no;">
我们在移动端视口要想视觉效果和体验好,那么我们的视口宽度必去无限接近理想视口
理想视口:一般来讲,这个视口其实不是真是存在的,它对设备来说是一个最理想布局视口尺寸,在用户不进行手动缩放的情况下,可以将页面理想地展示。那么所谓的理想宽度就是浏览器(屏幕)的宽度了。
其中user-scalable设置为no 可以解决移动端点击事件延迟问题(拓展)
解决适配方法
1.rem适配方法
rem是CSS3新增的一个相对单位,这个单位引起了广泛关注。
这个单位与em有什么区别呢?
区别在于使用rem为元素设定字体大小时,仍然是相对大小,但相对的只是HTML根元素。
这个单位可谓集相对大小和绝对大小的优点于一身,通过它既可以做到只修改根元素就成比例地调整所有字体大小,又可以避免字体大小逐层复合的连锁反应。
目前,除了它既可以做到只修改根元素就成比例地调整所有字体大小,又可以避免字体大小逐层字体大小逐层复合的连锁反应。目前,除了IE8及更早版本外,所有浏览器均已支持rem. 对于不支持它的浏览器,应对方法也很简单,就是多写一个绝对单位的声明。这些浏览器会忽略用rem设定的字体大小。
//假设我给根元素的大小设置为14px
html{
font-size:14px
}
//那么我底下的p标签如果想要也是14像素
p{
font-size:1rem
}
//如此即可
rem的布局 不得不提flexible,flexible方案是阿里早期开源的一个移动端适配解决方案,引用flexible后,我们在页面上统一使用rem来布局。
他的原理非常简单
// set 1rem = viewWidth / 10
function setRemUnit () {
var rem = docEl.clientWidth / 10
docEl.style.fontSize = rem + 'px'
}
setRemUnit();
rem 是相对于html节点的font-size来做计算的。
所以在页面初始话的时候给根元素设置一个font-size,接下来的元素就根据rem来布局,这样就可以保证在页面大小变化时,布局可以自适应,
如此我们只需要给设计稿的px转换成对应的rem单位即可
当然,这个方案只是个过渡方案,为什么说是过渡方案
因为当年viewport在低版本安卓设备上还有兼容问题,而vw,vh还没能实现所有浏览器兼容,所以flexible方案用rem来模拟vmin来实现在不同设备等比缩放的“通用”方案,之所以说是通用方案,是因为他这个方案是根据设备大小去判断页面的展示空间大小即屏幕大小,然后根据屏幕大小去百分百还原设计稿,从而让人看到的效果(展示范围)是一样的,这样一来,苹果5 和苹果6p屏幕如果你按照设计稿还原的话,字体大小实际上不一样,而人们在一样的距离上希望看到的大小其实是一样的,本质上,用户使用更大的屏幕,是想看到更多的内容,而不是更大的字。
so,这个用缩放来解决问题的方案是个过渡方案,注定时代所淘汰
vw,vh布局
vh,vw方案即将视觉视口宽度 window.innerWidth和视觉视口高度 window.innerHeight等分为100份。
vh和vw方案和rem类似也是相当麻烦需要做单位转化,而且px转换成vw不一定能完全整除,因此有一定的像素差。
不过在工程化的今天,webpack解析css 的时候用postcss-loader 有个postcss-px-to-viewport能自动实现px到vw的转化
{
loader: 'postcss-loader',
options: {
plugins: ()=>[
require('autoprefixer')({
browsers: ['last 5 versions']
}),
require('postcss-px-to-viewport')({
viewportWidth: 375, //视口宽度(数字)
viewportHeight: 1334, //视口高度(数字)
unitPrecision: 3, //设置的保留小数位数(数字)
viewportUnit: 'vw', //设置要转换的单位(字符串)
selectorBlackList: ['.ignore', '.hairlines'], //不需要进行转换的类名(数组)
minPixelValue: 1, //设置要替换的最小像素值(数字)
mediaQuery: false //允许在媒体查询中转换px(true/false)
})
]
}
3、px为主,vx和vxxx(vw/vh/vmax/vmin)为辅,搭配一些flex(推荐)
之所以推荐使用此种方案,是由于我们要去考虑用户的需求,用户之所以去买大屏手机,不是为了看到更大的字,而是为了看到更多的内容,这样直接使用px是最明智的方案,使用vw,rem等布局手段无可厚非,但是,flex这种弹性布局大行其道的今天,如果如果还用这种传统的思维去想问题显然是有两个原因(个人认为px是最好的,可能有大佬,能用vw,或者rem写出精妙的布局,也说不准):
1、为了偷懒,不愿意去做每个手机的适
2、不愿意去学习新的布局方式,让flex等先进的布局和你擦肩而过
移动端适配流程
1.在head设置width=device-width的viewport
2.在css中使用px
3.在适当的场景使用flex布局,或者配合vw进行自适应。
4.在跨设备类型的时候(pc <-> 手机 <-> 平板)使用媒体查询。
5.在跨设备类型如果交互差异太大的情况,考虑分开项目开发。
完美的响应式布局vw+vh+rem视口布局?
vm和vh是视口(viewport units)单位
何谓视口,就是根据你浏览器窗口的大小的单位,不受显示器分辨率的影响,是不是很神奇,这就代表了,我们不需要顾虑到现在那么多不同电脑有关分辨率的自适应问题。
vw是可视窗口的宽度单位,和百分比有点一样,1vw = 可视窗口的宽度的百分之一。比如窗口宽度大小是1800px,那么1vw = 18px。和百分比不一样的是,vw始终相对于可视窗口的宽度,而百分比和其父元素的宽度有关。
vh就是可视窗口的高度了。
vw是可视窗口的宽度单位,和百分比有点一样,1vw = 可视窗口的宽度的百分之一。比如窗口宽度大小是1800px,那么1vw = 18px。和百分比不一样的是,vw始终相对于可视窗口的宽度,而百分比和其父元素的宽度有关。
vh就是可视窗口的高度了。
VM、VH 与rem的使用
1、页面布局
宽度和高度全部自动适应!再加上rem布局的字体适应,可以完美解决各种屏幕适配问题! 故这个meta元素的视口必须声明。
2、响应垂直居中
可以使用vw,vh来实现在页面中响应垂直居中,只需要以下代码
#box {
width: 50vw;
height: 50vh;
margin: 25vh auto;
}
只要设置margin的上下间距,使之heigit + margin-top +margin-bottom = 100 ,width + margin-left + margin-right = 100 ,就能够响应垂直居中
3、模仿bootstrap的栅栏布局
bootstrap的栅栏布局,用vw,vh能够轻松实现。
.col-2 {
float: left;
width: 50vw;
}
.col-4 {
float: left;
width: 25vw;
}
.col-5 {
float: left;
width: 20vw;
}
.col-8 {
float: left;
width: 12.5vw;
}
上面是column实例只要在一行中所有的列加起来等于100vw就实现响应式布局
4、rem布局-解决字体适配(此布局在weex中无法识别)
rem布局原理:根据CSS的媒体查询功能,更改html根字体大小,实现字体大小随屏幕尺寸变化。
@media only screen and (max-width: 1600px) and (min-width: 1280px){
html{
font-size: 14px;
}
}
@media only screen and (max-width: 1280px) and (min-width: 960px){
html{
font-size: 12px;
}
}
@media only screen and (max-width: 960px){
html{
font-size: 10px;
}
}
5、vw、vh、rem的使用
<template>
<div class="box">
</div>
</template>
<style>
.box{
width:50vw;
height: 20vh;
line-height: 20vh;
font-size: 1.5rem;
margin:0 auto;
font-weight: bold;
background-color: rgba(255,255,255,0.8);
}
</style>
上面代码中的50vw代表了 此div占据视口宽度的50%、高度占据视口高度的20%,并且会随着视口的变化,进行自适应;
字体则是1.5倍的html根字体大小。并且根据媒体查询进行字号变化。
总结:使用vw+vh+rem的布局是个不错的选择,因为视口单位有个缺点就是它没有最小或者最大限制,这就达不到我们都时候所希望的一个限制(比如窗口太小了,字都看不清)。所以我们可以在根元素上设置vw和vh,然后在根元素上限制最大最小值,然后配合body设置最大最小宽度。但要注意浏览器的兼容性问题!
什么是闭包,应用场景?
一个函数的作用域可以访问另一个函数的局部变量,这个变量所在的函数就是闭包函数
闭包使得内部函数可以访问外部函数的属性(变量或方法)
在 JavaScript 中, 每当创建一个函数, 闭包就会在函数创建的同时被创建出来
闭包本身就是 javascript 的重要知识点
5.1变量的作用域复习
变量根据作用域的不同分为两种:全局变量和局部变量。
函数内部可以使用全局变量。
函数外部不可以使用局部变量。
当函数执行完毕,本作用域内的局部变量会销毁。
5.2什么是闭包
闭包(closure)指有权访问另一个函数作用域中变量的函数。
简单理解就是 ,一个作用域可以访问另外一个函数内部的局部变量。
<scritp>
function fn1(){ // fn1 就是闭包函数
var num = 10;
function fn2(){
console.log(num); //10
}
}
</script>
5.3闭包的作用
作用:延伸变量的作用范围。
变量不会销毁(核心作用)
变量什么时候不会销毁:变量被引用并且有内存
闭包的缺点:内存泄漏(栈溢出)
闭包的场景:以下代码
var liList = ul.getElementsByTagName('li')
for(var i=0; i<6; i++){
!function(ii){ //立即执行函数
liList[ii].onclick = function(){
alert(ii) // 0、1、2、3、4、5
}
}(i)
}
//注:如果不加立即执行函数全局变量i==6 ,点击任何一个li都是6(因为只有在for循环结束才会触发事件)
闭包形成的条件
1,函数嵌套
2.将内部函数作为返回值返回
3.内部函数必须使用到外部的变量
function fn() {
var num = 10;
function fun() {
console.log(num);
}
return fun;
}
var f = fn();// return后面的返回值和fn()相等 即fn()===fun
f();
5.4闭包的案例
立即执行函数也成为小闭包因为立即执行函数里面的任何一个函数都可以使用它的i这个变量
利用闭包的方式得到当前li 的索引号
for (var i = 0; i < lis.length; i++) {
// 利用for循环创建了4个立即执行函数
// 立即执行函数也成为小闭包因为立即执行函数里面的任何一个函数都可以使用它的i这个变量
(function(i) {
lis[i].onclick = function() {
console.log(i);
}
})(i);
}
1.闭包应用-3秒钟之后,打印所有li元素的内容
for (var i = 0; i < lis.length; i++) {
(function(i) {
setTimeout(function() {
console.log(lis[i].innerHTML);
}, 3000)
})(i);
}
闭包应用-计算打车价格
/*需求分析
打车起步价13(3公里内), 之后每多一公里增加 5块钱. 用户输入公里数就可以计算打车价格
如果有拥堵情况,总价格多收取10块钱拥堵费*/
var car = (function() {
var start = 13; // 起步价 局部变量
var total = 0; // 总价 局部变量
return {
// 正常的总价
price: function(n) {
if (n <= 3) {
total = start;
} else {
//起步价加上超出的公里数的价格
total = start + (n - 3) * 5
}
return total;
},
// 拥堵之后的费用
yd: function(flag) {
return flag ? total + 10 : total;
}
}
})();
console.log(car.price(5)); // 23
console.log(car.yd(true)); // 33
5.5案例
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function() {
return function() {
return this.name;
};
}
};
console.log(object.getNameFunc()())
-----------------------------------------------------------------------------------
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function() {
var that = this;
return function() {
return that.name;
};
}
};
console.log(object.getNameFunc()())
宏任务和微任务?
宏任务:常见的定时器,用户交互事件等等。(宏任务就是特定的这些个任务,没什么特殊含义)
微任务:Promise相关任务,MutationObserver等(一样,只是一种称呼而已!!!)
每个层级的宏任务,都对应了他们的微任务队列,微任务队列遵循先进先出的原则,当全局同步代码执行完毕后,就开始执行第一层的任务。同层级的微任务永远先于宏任务执行,并且会在当前层级宏任务结束前全部执行完毕 就按照我的套路,从全局上下文退出前(全局的同步代码执行完毕后),开始收集当前层级的微任务和宏任务,然后先清空微任务队列,再执行宏任务.如果这期间遇到宏任务/微任务,就像我这样画个图,把他们塞进对应的层级里即可
1.遇到setTimout,异步宏任务,放入宏任务队列中
2.遇到new Promise,new Promise在实例化的过程中所执行的代码都是同步进行的,所以输出2
3.而Promise.then中注册的回调才是异步执行的,将其放入微任务队列中
4.遇到同步任务console.log(‘5’);输出5;主线程中同步任务执行完
5.从微任务队列中取出任务到主线程中,输出3、 4,微任务队列为空
6.从宏任务队列中取出任务到主线程中,输出1,宏任务队列为空
s
事件循环,底层?
js是一门单线程的编程语言,也就是说js在处理任务的时候,所有任务只能在一个线程上排队被执行,那如果某一个任务耗时比较长呢?总不能等到它执行结束再去执行下一个。
所以在线程之内,又被分为了两个队列:
同步任务队列
异步任务队列
举个例子来说:比如你去银行办理业务,都需要领号排队。银行柜员一个个办理业务,这时这个柜员就相当于一个js线程,客户排的队就相当于同步任务队列,每个人对于柜员相当于一个个的任务。
但这个时候,你的电话突然响了,你去接电话接了半小时。这时候人家柜员一看你这情况,直接叫了下一个,而你领的号就作废了,只能重新零号排队。这时候你就是被分发到了异步任务队列。
等你前边的人都完事了,柜员把你叫过去办了你的业务,这时候就是同步队列中的任务执行完了,主线程会处理异步队列中的任务。
JavaScript是一种单线程的编程语言,同一时间只能做一件事,所有任务都需排队完成。
答:作为浏览器脚本语言,JavaScript主要用途是与用户互动,以及操作DOMl.
这决定了它只能是单线程,否则会带来很复杂的同步问题。
比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准。
为了避免这种复杂性,因此JS只能是单线程。
事件循环机制(Event Loop)
含义:事件循环分为两种,分别是浏览器事件循环和node.js事件循环,JavaScript是一门单线程语言,指主线程只有一个。
Event Loop事件循环,其实就是JS引擎管理事件执行的一个流程,具体由运行环境确定。目前JS的主要运行环境有两个,浏览器和Node.js。
• 事件循环机制告诉了我们JS代码的执行顺序,是指浏览器或Node的一种解决JS单线程运行时不会阻塞的一种机制。
浏览器的事件循环又分为同步任务和异步任务
1、同步任务与异步任务
同步任务
含义:在主线程上排队执行的任务,只有一个任务执行完毕,才能执行后一个任务
异步任务
含义:不进入主线程,而进入“任务队列(task queue)”的任务,只有“任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
分类:异步任务又分为宏任务和微任务。所有同步任务都在主线程上执行,形成一个函数调用栈(执行栈),而异步则先放到任务队列(task queue)里,任务队列又分为宏任务(macro-task)与微任务(micro-task)。
宏任务
宏任务包括:script(整体代码)、setTimout、setInterval、setImmediate(node.js环境)、I/O、UI交互事件
微任务
微任务包括:new promise().then(回调)、MutationObserver(html5新特新)、Object.observe(已废弃)、process.nextTick(node环境)
• 若同时存在promise和nextTick,则先执行nextTick
2、执行过程
所有同步任务都在主线程上执行,形成一个执行栈(调用栈);
主线程之外,还存在一个‘任务队列’(task queue),浏览器中的各种 Web API 为异步的代码提供了一个单独的运行空间,当异步的代码运行完毕以后,会将代码中的回调送入到 任务队列中(队列遵循先进先出得原则)
一旦主线程的栈中的所有同步任务执行完毕后,调用栈为空时系统就会将队列中的回调函数依次压入调用栈中执行,当调用栈为空时,仍然会不断循环检测任务队列中是否有代码需要执行;
3、执行顺序
先执行同步代码,
遇到异步宏任务则将异步宏任务放入宏任务队列中,
遇到异步微任务则将异步微任务放入微任务队列中,
当所有同步代码执行完毕后,再将异步微任务从队列中调入主线程执行,
微任务执行完毕后再将异步宏任务从队列中调入主线程执行,
一直循环直至所有任务执行完毕。
注意:当宏任务和微任务都处于 任务队列(Task Queue) 中时,微任务的优先级大于宏任务,即先将微任务执行完,再执行宏任务;
字符串去重?
1、比较下标
特点:保持原有字符串顺序
思路:遍历字符串元素,元素的下标应该要等于第一次出现的索引的下标
2、使用hashset
特点:不重复+无序
3、使用treeset
特点:不重复+排序
// 字符串去重复
var str = 'assssdfasddddfaaaasdf'
//1.将字符串转成数组去重后,再将所有元素拼接成字符串
// 将字符串转成数组
var arr = str.split('')
console.log(arr);
// 数组去重
for(var i=0;i<arr.length-1;i++){
for(var j=i+1;j<arr.length;j++){
if(arr[i] === arr[j]){
arr.splice(j, 1)
j--
}
}
}
console.log(arr);
// 将去重后的数组拼接成字符串
str = arr.join('')
console.log(str);
// 2.定义一个空字符串用来接收所有不重复的字符
var str = 'assssdfasddddfaaaasdf'
var str1 = str[0]
for(var i=0;i<str.length;i++){
for(var j=0;j<str1.length;j++){
if(str[i] === str1[j]){
flag = false
break
}
}
if(j === str1.length){
str1 += str[i]
}
}
console.log(str1);
// 3.利用对象的键唯一,将字符串所有字符都作为对象的键,将对象的键拼接成字符串
var str = 'assssdfasddddfaaaaaasdf'
var obj = {}
// 遍历字符串,将字符串中每个字符都作为对象的键
for(var i=0;i<str.length;i++){
obj[ str[i] ] = 0
}
// 遍历对象,将所有键拼接成字符串
var str1 = ''
for(var key in obj){
str1 += key
}
console.log(str1);
vue2和vue3的区别?
1. vue2和vue3双向数据绑定原理发生了改变
vue2的双向数据绑定是利用了es5 的一个API Object.definepropert() 对数据进行劫持 结合发布订阅模式来实现的。vue3中使用了es6的proxyAPI对数据进行处理。
相比与vue2,使用proxy API 优势有:defineProperty只能监听某个属性,不能对全对象进行监听;可以省去for in 、闭包等内容来提升效率(直接绑定整个对象即可);可以监听数组,不用再去单独的对数组做特异性操作,vue3可以检测到数组内部数据的变化。
2.Vue3支持碎片(Fragments)
就是说可以拥有多个跟节点。
3. Composition API
Vue2 与vue3 最大的区别是vue2使用选项类型api,对比vue3合成型api。旧得选项型api在代码里分割了不同得属
6. 父子传参不同,setup()函数特性
:data,computed,methods等;新得合成型api能让我们使用方法来分割,相比于旧的API使用属性来分组,这样代码会更加简便和整洁。
4. 建立数据data
vue2是把数据放入data中,vue3就需要使用一个新的setup()方法,此方法在组件初始化构造得时候触发。使用一下三个步骤来简=建立反应性数据: 1. 从vue引入reactive;使用reactive() 方法来声明数据为响应性数据;3. 使用setup()方法来返回我们得响应性数据,从而template可以获取这些响应性数据。
5. 生命周期
vue2 --------------- vue3
beforeCreate -> setup()
Created -> setup()
beforeMount -> onBeforeMount
mounted -> onMounted
beforeUpdate -> onBeforeUpdate
updated -> onUpdated
beforeDestroyed -> onBeforeUnmount
destroyed -> onUnmounted
activated -> onActivated
deactivated -> onDeactivated
6. 父子传参不同,setup()函数特性
setup()函数接收两个参数:props、context(包含attrs、slots、emit)
setup函数是处于生命周期beforeCreated和created俩个钩子函数之前
执行setup时,组件实例尚未被创建(在setup()内部,this不会是该活跃实例得引用,即不指向vue实例,Vue为了避免我们错误得使用,直接将setup函数中得this修改成了undefined)
与模板一起使用时,需要返回一个对象
因为setup函数中,props是响应式得,当传入新的prop时,它将会被更新,所以不能使用es6解构,因为它会消除prop得响应性,如需解构prop,可以通过使用setup函数中得toRefs来完成此操作。
父传子,用props,子传父用事件 Emitting Events。在vue2中,会调用this$emit然后传入事件名和对象;在vue3中得setup()中得第二个参数content对象中就有emit,那么我们只要在setup()接收第二个参数中使用分解对象法取出emit就可以在setup方法中随意使用了。
在setup()内使用响应式数据时,需要通过 .value 获取
import { ref } from 'vue'
const count = ref(0)
console.log(count.value)
从setup() 中返回得对象上得property 返回并可以在模板中被访问时,它将自动展开为内部值。不需要在模板中追加.value。
setup函数只能是同步的不能是异步的。
1.webpack和vite
① vue2使用的是webpack形式去构建项目
webpack是一开始是入口文件,然后分析路由,然后模块,最后进行打包,然后告诉你,服务器准备好了可以开始干了
②vue3使用vite构建项目
先告诉你服务器准备完成,然后等你发送HTTP请求,然后是入口文件,Dynamic import(动态导入)code split point(代码分割)
最大的好处和区别就是为了让项目中一些代码文件多了以后去保存更新数据时更快能够看到实际效果,也就是所谓的(热更新)
2.main.js文件
vue2中我们可以使用pototype(原型)的形式去进行操作,引入的是构造函数
vue3中需要使用结构的形式进行操作,引入的是工厂函数
vue3中app组件中可以没有根标签
3.setup函数
setup函数必须要return 返回出去
<script>
export default {
name: 'appName',
setup(){
//变量
let name = '打工仔'
let age = 18
//方法
function say(){
console.log(`我只是一个${name},今年${age}岁`)
}
//返回一个对象
return {
name,
age,
say
}
}
}
</script>
你会发现当前的${name}中name不需要使用this去进行操作,this的作用只不过是取到某个作用域中变量而已,而setup函数提供了当前只在这个作用域中
这时候就很不爽了,那岂不是每次我定义的变量和方法都需要return,vue3中给我们提供了
在script标签上添加setup 如:<script setup></script>,相当在编译运行时放到了setup中
注:setup比beforeCreate、created生命周期更早,也就是说在当前直接用this去获取data中的数据打出来的还是undefined
setup语法中课接收2个参数setup(props,context)
都知vue2中this.$attrs,this.$slots,this.$emit等同context中attrs,slots,emit
注:当我们只接受一个参数时,页面会提示警告,但是不影响使用
4.指令与插槽
vue2中使用slot可以直接使用slot,而vue3中必须使用v-slot的形式
v-for与v-if在vue2中优先级高的是v-for指令,而且不建议一起使用
vue3中v-for与v-if,只会把当前v-if当做v-for中的一个判断语句,不会相互冲突
vue3中移除keyCode作为v-on的修饰符,当然也不支持config.keyCodes
vue3中移除v-on.native修饰符
vue3中移除过滤器filter
5.ref与reactive
ref
把数据变为响应式数据,ref把它们变成了对象,如果我们直接去操作代码是修改不了的,你会发现当前name和age还是通过get和set修改页面,这时你需要使用.value,并且ref还是Refimpl
<template>
<div class="home">
<h1>姓名:{{name}}</h1>
<h1>年龄:{{age}}</h1>
<button @click="say">修改</button>
</div>
</template>
<script>
import {ref} from 'vue'
export default {
name: 'Home',
setup(){
let name = ref('中介')
let age = ref(18)
console.log(name)
console.log(age)
//方法
function say(){
name='波妞'
age=18
}
return {
name,
age,
say
}
}
}
</script>
这样的话那我们在页面上不是得{{name.value}}来做显示,实际不用这样的,在我们vue3中会检测到你的ref是对象,自动会给你加上.value,如果我们自己定义的ref对象,实例会变为refimpl,这个时候用XX.value.XX进行修改
<template>
<div class="home">
<h1>姓名:{{name}}</h1>
<h1>年龄:{{age}}</h1>
<h2>职业:{{job.occupation}}</h2>
<h2>薪资:{{job.salary}}</h2>
<button @click="say">修改</button>
</div>
</template>
<script>
import {ref} from 'vue'
export default {
name: 'Home',
setup(){
let name = ref('中介')
let age = ref(18)
let job=ref({
occupation:'程序员',
salary:'10k'
})
console.log(name)
console.log(age)
//方法
function say(){
job.value.salary='12k'
}
return {
name,
age,
job,
say
}
}
}
</script>
这时我们打印obj.value,他已经不再是refimpl对象,变成了proxy对象,因为vue3底层是proxy对象,基本数据类型都是按Object.defineProperty里面get和set进行数据劫持,vue3已经把reactive封装进去了,相当于我们在调用ref时,会自动调用reactive
reactive
上面我们说ref里面的对象会调用reactive,把Object转换为Proxy,现在我们直接通过reactive变成Proxy,它进行了一个深层次的响应式
<template>
<div class="home">
<h1>姓名:{{name}}</h1>
<h1>年龄:{{age}}</h1>
<h2>职业:{{job.occupation}}<br>薪资:{{job.salary}}</h2>
<h3>爱好:{{hobby[0]}},{{hobby[1]}},{{ hobby[2] }}</h3>
<button @click="say">修改</button>
</div>
</template>
<script>
import {ref,reactive} from 'vue'
export default {
name: 'Home',
setup(){
let name = ref('波妞')
let age = ref(18)
let job=reactive({
occupation:'程序员',
salary:'10k'
})
let hobby=reactive(['吃饭','睡觉','打豆豆'])
console.log(name)
console.log(age)
//方法
function say(){
job.salary='12k'
hobby[0]='学习'
}
return {
name,
age,
job,
say,
hobby
}
}
}
</script>
这时你肯定会觉得方法太多了,还不如使用ref提供的.value,是不是感觉爽爽爽,但是有一个问题,如果有一堆数据那不是要一直去.value,点到冒烟,这个时候你可以用模拟vue2中data的形式,就会感觉更香
<template>
<div class="home">
<h1>姓名:{{data.name}}</h1>
<h1>年龄:{{data.age}}</h1>
<h2>职业:{{data.job.occupation}}<br>薪资:{{data.job.salary}}</h2>
<h3>爱好:{{data.hobby[0]}},{{data.hobby[1]}},{{ data.hobby[2] }}</h3>
<button @click="say">修改</button>
</div>
</template>
<script>
import {reactive} from 'vue'
export default {
name: 'Home',
setup(){
let data=reactive({
name:'波妞',
age:18,
job:{
occupation:'程序员',
salary:'10k'
},
hobby:['吃饭','睡觉','打豆豆']
})
//方法
function say(){
data.job.salary='12k'
data.hobby[0]='学习'
}
return {
data,
say,
}
}
}
</script>
ref与reactive区别
ref定义的是基本数据类型
ref通过Object.defineProperty()的get和set实现数据劫持
ref操作数据.value,读取时不需要。value
reactive定义对象或数组数据类型
reactive通过Proxy实现数据劫持
reactive操作和读取数据不需要.value
6.vue3的响应式原理
vue2的响应式原理用Object.defineProperty的get和set进行数据劫持,从而实现响应式
vue2中只有get和set方法去进行属性的读取和修改操作,当我们进行新增,删除时,页面不会实时更新
直接通过下标改数组,页面也不会实时更新
vue3中响应式原理使用Proxy进行代理,使用window内置对象Reflect反射,学了Es6的语法的就知道我们在使用Proxy进行代理,好比甲方公司给出需要什么技术的前端攻城狮,让乙方去干招聘、面试等环节
Proxy可以拦截对象中任意的属性变化,当然包括读写,添加,删除等
Reflect对源对象属性进行操作
6.vue3的响应式原理
vue2的响应式原理用Object.defineProperty的get和set进行数据劫持,从而实现响应式
vue2中只有get和set方法去进行属性的读取和修改操作,当我们进行新增,删除时,页面不会实时更新
直接通过下标改数组,页面也不会实时更新
vue3中响应式原理使用Proxy进行代理,使用window内置对象Reflect反射,学了Es6的语法的就知道我们在使用Proxy进行代理,好比甲方公司给出需要什么技术的前端攻城狮,让乙方去干招聘、面试等环节
Proxy可以拦截对象中任意的属性变化,当然包括读写,添加,删除等
Reflect对源对象属性进行操作
const p=new Proxy(data, {
// 读取属性时调用
get (target, propName) {
return Reflect.get(target, propName)
},
//修改属性或添加属性时调用
set (target, propName, value) {
return Reflect.set(target, propName, value)
},
//删除属性时调用
deleteProperty (target, propName) {
return Reflect.deleteProperty(target, propName)
}
})
7.computed和watch与watchEffct区别
computed
vue2中computed方法直接去写上当前方法去进行调用完事
computed:{ //计算属性
_suming(){
return parseInt(this.one)+parseInt(this.two)
},
dataTimeing(){
console.log("计算属性方法");
// return "计算属性方法"+new Date()
return "普通方法"+this.time
}
},
vue3中computed变为组合式Api,那么意味着需要引入,当前如果需要去修改,就需要去终结computed
<template>
<div class="home">
姓:<input type="text" v-model="names.familyName"><br>
名:<input type="text" v-model="names.lastName"><br>
姓名:{{fullName}}<br>
</div>
</template>
<script>
import {reactive,computed} from 'vue'
export default {
name: 'Home',
setup(){
let names=reactive({
familyName:'波',
lastName:'妞'
})
fullName=computed(()=>{
return names.familyName+'.'+names.lastName
})
//修改写法
names.fullName=computed({
get(){
return names.familyName+'.'+names.lastName
},
set(value){
let nameList=value.split('.')
names.familyName=nameList[0]
names.lastName=nameList[1]
}
})
return {
names,
fullName
}
}
}
</script>
<template>
<div class="home">
姓:<input type="text" v-model="names.familyName"><br>
名:<input type="text" v-model="names.lastName"><br>
姓名:{{fullName}}<br>
</div>
</template>
<script>
import {reactive,computed} from 'vue'
export default {
name: 'Home',
setup(){
let names=reactive({
familyName:'波',
lastName:'妞'
})
fullName=computed(()=>{
return names.familyName+'.'+names.lastName
})
//修改写法
names.fullName=computed({
get(){
return names.familyName+'.'+names.lastName
},
set(value){
let nameList=value.split('.')
names.familyName=nameList[0]
names.lastName=nameList[1]
}
})
return {
names,
fullName
}
}
}
</script>
<template>
<div class="home">
姓:<input type="text" v-model="names.familyName"><br>
名:<input type="text" v-model="names.lastName"><br>
姓名:{{fullName}}<br>
</div>
</template>
<script>
import {reactive,computed} from 'vue'
export default {
name: 'Home',
setup(){
let names=reactive({
familyName:'波',
lastName:'妞'
})
fullName=computed(()=>{
return names.familyName+'.'+names.lastName
})
//修改写法
names.fullName=computed({
get(){
return names.familyName+'.'+names.lastName
},
set(value){
let nameList=value.split('.')
names.familyName=nameList[0]
names.lastName=nameList[1]
}
})
return {
names,
fullName
}
}
}
</script>
watch
vue2中watch通过对象的形式去直接监听
watch: {
userName: {
handler(val,res){
console.log(val);
console.log(res);
},
immediate:true,
deep:true
},
}
vue3中watch是不是跟computed都是组合APi呢?它就是
<template>
<div class="home">
<h1>当前数字为:{{num}}</h1>
<button @click="num++">点击数字加一</button>
</div>
</template>
<script>
import {ref,watch} from 'vue'
export default {
name: 'Home',
setup(){
let num=ref('0')
//监听单个
watch(num,(newValue,oldValue)=>{
console.log(`当前数字增加了,${newValue},${oldValue}`)
})
//监听多个响应数据
//watch([num,msg],(newValue,oldValue)=>{
// console.log('当前改变了',newValue,oldValue)
//})
return {
num
}
}
}
</script>
为什么newValue与oldValue一样呢,就很尴尬,都是新的数据,就算你使用ref来定义,还是没有办法监听到oldValue(他喵的,都给你说了ref定义的对象会自动调用reactive),所以在监视reactive定义的响应式数据时,oldValue无法正确获取,并且你会发现,它是强制开启深度监视(deep:true),并且无法关闭。
reactive监听的是响应式数据只是监听其中一个,我们都知道vue3会监听reactive或者ref定义,并不能监听,那需要监听多个属性怎么办呢,可以只能是写成下面这种
watch([()=>names.age,()=>names.familyName],(newValue,oldValue)=>{
console.log('names改变了',newValue,oldValue)
})
如果需要监听深度属性怎么办呢,我们都知道reactive是响应式数据属性,如果这个属性是对象,那么我们就可以开启深度监听
//第一种
watch(()=> names.job.salary,(newValue,oldValue)=>{
console.log('names改变了',newValue,oldValue)
})
//第二种
watch(()=> names.job,(newValue,oldValue)=>{
console.log('names改变了',newValue,oldValue)
},{deep:true})
watchEffect
watchEffect是vue3中新增的函数,看字面意思就知道他也有watch,实际上他跟watch功能一样
优势
默认开启 immediate:true
需要用哪个就监听哪个
值发生改变就调用一次,且不需要返回值
watchEffect(()=>{
const one = num.value
const tow = person.age
console.log('watchEffect执行了')
})
8.生命周期
vue2中我们是通过new Vue(),在执行beforeCreate与created接着问你有没有vm.$mount(el)
vue3中是先准备好所有后再执行
区别:beforeCreate与created并没有组合式API中,setup就相当于这两个生命周期函数
setup中
beforeCreate===>Not needed*
created=======>Not needed*
beforeMount ===>onBeforeMount
mounted=======>onMounted
beforeUpdate===>onBeforeUpdate
updated =======>onUpdatedupdated
beforeUnmount ==>onBeforeUnmount
unmounted =====>onUnmounted
9.hooks函数
//一般都是建一个hooks文件夹,都写在里面
import {reactive,onMounted,onBeforeUnmount} from 'vue'
export default function (){
//鼠标点击坐标
let point = reactive({
x:0,
y:0
})
//实现鼠标点击获取坐标的方法
function savePoint(event){
point.x = event.pageX
point.y = event.pageY
console.log(event.pageX,event.pageY)
}
//实现鼠标点击获取坐标的方法的生命周期钩子
onMounted(()=>{
window.addEventListener('click',savePoint)
})
onBeforeUnmount(()=>{
window.removeEventListener('click',savePoint)
})
return point
}
//在其他地方调用
import useMousePosition from './hooks/useMousePosition'
let point = useMousePosition()
10.toRef与toRefs
toRef相当于ref类型数据
<template>
<div class="home">
<h1>当前姓名:{{names.name}}</h1>
<h1>当前年龄:{{names.age}}</h1>
<h1>当前薪水:{{names.job.salary}}K</h1>
<button @click="names.name+='!'">点击加!</button>
<button @click="names.age++">点击加一</button>
<button @click="names.job.salary++">点击薪水加一</button>
</div>
</template>
<script>
import {reactive} from 'vue'
export default {
name: 'Home',
setup(){
let names=reactive({
name:'老谭',
age:23,
job:{
salary:10
}
})
return {
names
}
}
}
</script>
这时我们只是操作变量,如果我们需要去操作页面字符串可以达到响应式吗?这个时候我们需要把name.XX变为toRef去进行操作name中的数据,
那这时你肯定会去想说句妈卖批,我为何不使用ref去改变呢,ref也能达到响应式数据效果,当前的names里面的数据并不是源数据,而是新定义出的数据,自然操作修改的也不是源数据的names
return {
name:names.name,
age:names.age,
salary:names.job.salary
}
return {
name:toRef(names,'name'),
age:toRef(names,'age'),
salary:toRef(names.job,'salary')
}
return {
name:ref(names.name),
age:ref(names.age),
salary:ref(names.job.salary),
}
toRefs
toRefs与toRef有什么不同呢,字面意思也能看出来s肯定是更多的意思,(这时你又在猜想,是这样的)自信一些,特喵的,当前...是结构了一次,不懂的可以去看看Es6,这就不过多的谈
<h1>当前姓名:{{name}}</h1>
<h1>当前薪水:{{job.salary}}K</h1>
return {
...toRefs(names)
}
11.router
vue2中还是使用this.r o u t e r . p u s h 来 进 行 路 由 跳 转 , 在 v u e 3 中 没 有 这 些 , 而 是 定 义 了 一 个 v u e − r o u t e r , 直 接 引 入 u s e R o u t e , u s e R o u t e r , 相 当 于 v u e 2 中 提 供 的 ‘ t h i s . router.push来进行路由跳转,在vue3中没有这些,而是定义了一个vue-router,直接引入useRoute,useRouter,相当于vue2中提供的`this.router.push来进行路由跳转,在vue3中没有这些,而是定义了一个vue−router,直接引入useRoute,useRouter,相当于vue2中提供的‘this.route,this.r o u t e r ‘ , 别 问 这 ‘ t h i s . router`,别问这`this.router‘,别问这‘this.route,this.$router`有什么区别,这都没看过我建议你去先看看vue2
import {useRouter,useRoute} from "vue-router";
setup(){
const router= useRouter()
const route= useRoute()
function fn(){
this.$route.push('/about')
}
onMounted(()=>{
console.log(route.query.code)
})
return{
fn
}
}
12.全局Api的转移(很重要)
2.x 全局 API( Vue) 3.x 实例 API(app)
Vue.config.xxxx app.config.xxxx
Vue.config.productionTip 移除
Vue.component app.component
Vue.directive app.directive
Vue.mixin app.mixin
Vue.use app.use
Vue.prototype app.config.globalProperties
vue2中可以通过Vue.prototype去操作原型,在vue3中只能通过app.config.globalProperties,当时玩的时候还以为自己写错了,修改的这些你会发现改动的东西挺多
vue2中可以通过Vue.prototype去操作原型,在vue3中只能通过app.config.globalProperties,当时玩的时候还以为自己写错了,修改的这些你会发现改动的东西挺多
其他APi(了解)
13.shallowReactive与shallowRef
shallowReactive浅层次的响应式,它就是只把第一层的数据变为响应式,深层的数据不会变为响应式,shallowRef如果定义的是基本类型的数据,那么它和ref是一样的不会有什么改变,但是要是定义的是对象类型的数据,那么它就不会进行响应式,之前我们说过如果ref定义的是对象,那么它会自动调用reactive变为Proxy,但是要是用到的是shallowRef那么就不会调用reactive去进行响应式。
shallowReactive:只处理对象最外层属性的响应式(浅响应式)。
shallowRef:只处理基本数据类型的响应式, 不进行对象的响应式处理
let person = shallowReactive({
name:'大理段氏',
age:10,
job:{
salary:20
}
})
let x = shallowRef({
y:0
})
14.readonly与shallowReadonly
readonly是接收了一个响应式数据然后重新赋值,返回的数据就不允许修改(深层只读)
shallowReadonly却只是浅层只读(第一层只读,其余层可以进行修改)
names=readonly(names)
names=shallowReadonly(names)
15.toRaw与markRaw
toRaw其实就是将一个由reactive生成的响应式对象转为普通对象。如果是ref定义的话,是没有效果的(包括ref定义的对象)如果在后续操作中对数据进行了添加的话,添加的数据为响应式数据,当然要是将数据进行markRaw操作后就不会变为响应式,可能大家会说,不就是和readonly一样吗?那肯定不一样咯,readonly是根本没办法改,但markRaw是不转化为响应式,但是数据还会发生改变
<template>
<div class="home">
<h1>当前姓名:{{names.name}}</h1>
<h1>当前年龄:{{names.age}}</h1>
<h1>当前薪水:{{names.job.salary}}K</h1>
<h1 v-if="names.girlFriend">女朋友:{{names.girlFriend}}</h1>
<button @click="names.name+='!'">点击加!</button>
<button @click="addAges">点击加一</button>
<button @click="addSalary">点击薪水加一</button>
<button @click="add">添加女朋友</button>
<button @click="addAge">添加女朋友年龄</button>
</div>
</template>
<script>
import {reactive,toRaw,markRaw} from 'vue'
export default {
name: 'Home',
setup(){
let names=reactive({
name:'老伍',
age:23,
job:{
salary:10
}
})
function addAges(){
names.age++
console.log(names)
}
function addSalary(){
let fullName=toRaw(names)
fullName.job.salary++
console.log(fullName)
}
function add(){
let girlFriend={sex:'女',age:40}
names.girlFriend=markRaw(girlFriend)
}
function addAge(){
names.girlFriend.age++
console.log(names.girlFriend.age)
}
return {
names,
add,
addAge,
addAges,
addSalary
}
}
}
</script>
16.customRef
customRef创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制
可以使用其特性做防抖节流,原生的方法就不具体写,说一下概念,后续出一个节流防抖,递归,深浅拷贝等
防抖:规定时间内触发同一时间,只执行一次(执行的这一次可以在最开始也可以在最后)
节流:在规定一定间隔时间内触发同一事件,在当前时间内只执行一次,下一个时间内也只执行一次(当前执行的可以在最开始也可以最后)
<template>
<input type="text" v-model="keyWord">
<h3>{{keyWord}}</h3>
</template>
<script>
import {customRef} from 'vue'
export default {
name: 'App',
setup() {
//自定义一个ref——名为:myRef
function myRef(value,times){
let time
return customRef((track,trigger)=>{
return {
get(){
console.log(`有人从myRef中读取数据了,我把${value}给他了`)
track() //通知Vue追踪value的变化(必须要有,并且必须要在return之前)
return value
},
set(newValue){
console.log(`有人把myRef中数据改为了:${newValue}`)
clearTimeout(time)
time = setTimeout(()=>{
value = newValue
trigger() //通知Vue去重新解析模板(必须要有)
},times)
},
}
})
}
let keyWord = myRef('HelloWorld',1000) //使用自定义的ref
return {keyWord}
}
}
</script>
17.provide与inject
都知道组件传值吧,在vue2中,如果要在后代组件中使用父组件的数据,那么要一层一层的父子组件传值或者用到vuex,但是现在,无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。这个特性有两个部分:父组件有一个 provide 选项来提供数据,子组件有一个 inject 选项来开始使用这些数据
//父
import { provide } from 'vue'
setup(){
let fullname = reactive({name:'阿月',salary:'15k'})
provide('fullname',fullname) //给自己的后代组件传递数据
return {...toRefs(fullname)}
}
//后代
import {inject} from 'vue'
setup(){
let fullname = inject('fullname')
return {fullname}
}
18.响应式判断
isRef: 检查值是否为一个 ref 对象。
isReactive:检查对象是否是由 reactive 创建的响应式代理。
isReadonly: 检查对象是否是由 readonly 创建的只读代理。
isProxy:检查对象是否是由 reactive 或 readonly 创建的 proxy。
import {ref, reactive,readonly,isRef,isReactive,isReadonly,isProxy } from 'vue'
export default {
name:'App',
setup(){
let fullName = reactive({name:'小唐',price:'20k'})
let num = ref(0)
let fullNames = readonly(fullName)
console.log(isRef(num))
console.log(isReactive(fullName))
console.log(isReadonly(fullNames))
console.log(isProxy(fullName))
console.log(isProxy(fullNames))
console.log(isProxy(num))
return {}
}
}
import {ref, reactive,readonly,isRef,isReactive,isReadonly,isProxy } from 'vue'
export default {
name:'App',
setup(){
let fullName = reactive({name:'小唐',price:'20k'})
let num = ref(0)
let fullNames = readonly(fullName)
console.log(isRef(num))
console.log(isReactive(fullName))
console.log(isReadonly(fullNames))
console.log(isProxy(fullName))
console.log(isProxy(fullNames))
console.log(isProxy(num))
return {}
}
}
选项式和组合式api区别?
Vue3和Vue2中有一个很大的区别,就是API的使用方式,在Vue2中使用的是选项API(Options API)在Vue3中使用的组合API(Composition API)
1、什么是选项API写法:
(1)代码风格:data选项写数据,methods选项写函数。。。一个功能逻辑的代码分散
(2)优点:易于学习和使用,写代码的位置已经预定好
(3)缺点:代码组织性差,相似的逻辑(功能)代码不便于复用,逻辑负责代码多了不好阅读
2、什么是组合API写法:
以功能为单位组织代码结构,后续重用功能更加方便
总结:组合API的最大好处就是以功能为单位组织代码结构,有利于代码的复用
选项式和组合式api 注意什么问题?
选项式API的坏处
代码碎片化
通常在维护和开发一个组件时,分为 data、methods、computed、props 等。假如有一些业务在选项 API 的 data、methods、computed 中进行操作。把要关注的相同视角分别用不同颜色的框子框起来,发现我们的关注点被拆分成了这样:
先不说大组件的关注点会被拆分成怎么样,就单纯这一个很简单的演示就能够让各位同志体会到选项式 API 的坏处了。当我们的组件开始变得更大时,逻辑关注点的列表也会增长。如果你是一个军队的指挥官,你会选择把战线拉得很长吗?费时费力,后勤补给也跟不上。
这种碎片化使得理解和维护复杂组件变得困难,选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块。
逻辑不复用
上一小节中说到选项式API使代码变得碎片化,曾经我想过把一段多次使用到的业务代码抽离到一个单独的 js 文件里。我也遇到过一些困扰的小问题,导致我不能完成理想。
首先,在 data 选项声明的变量才是响应式数据。其次,在 methods 选项声明的函数才可以操控组件里的响应式数据。这是下文分析选项式API逻辑不复用时的重要前提条件。
在 demo.js 文件里,我定义了一个 a 变量,calc 函数改变 a 的值:
export let a = 'foo'
export function calc() { a = a + 'bar' }
对于这样的抽离方式,在使用的时候,依旧是需要把变量导入到 data 选项,使其成为响应式数据;函数导入到 methods 选项。
import { a, calc } from './demo.js'
export default {
data() { return { a } },
methods: { _calc() { calc() } }
}
由于 demo.js 文件里的函数操作的是 demo.js 文件里的变量 a ,而不是组件中响应式数据 a。导致我点击了按钮之后页面未作出任何反应。
解决方法是,calc 函数接受一个形参,然后再返回。当然可以!这只是一个简单的字符拼接而已。但是,这不是非常直接的办法,要经历一些曲折,同样会导致代码难读懂的问题。
官方提供的解决方案是混入,而混入还是在写选项API。不再选项API!不再选项API!不再选项API!,因为不符合期望。读到这里,想必知道了选项式API的坏处了。
一句话总结选项式API的坏处就是:代码碎片化、逻辑不复用
组合式API的好处
代码集中化
为什么组合式API就可以让代码不碎片化呢?因为组合式API的组合就在于它把变量、函数集中在一起,减少分离掩盖的潜在的逻辑问题,不在“跳转”相关代码的选项块。
所以,是如何集中化的呢?因为 Vue3 要实现代码集中化,所以,Vue3 的许多选项都抽离成为了一个个模块(这是我的猜想,因为每次用到什么必须从 vue 模块导入什么)。
组合式API就是一个 setup 函数,我们的所有代码全部都要写在这里面。
import { ref } from 'vue'
export default {
setup() {
let a = ref('foo')
function calcA() { a.value = a.value + 'bar' }
let b = ref('bar')
function calcB() { b.value = b.value + 'foo' }
let c = ref('hello')
function calcC() { c.value = c.value + 'world' }
let d = ref('world')
function calcD() { d.value = d.value + 'hello' }
}
}
现在,我们的关注点会被集中化,用图表示就是:
不知道各位同志是否有感受到这种变化,笔者已经感受到组合式API的强大了。
逻辑高复用
还记得第一章第二小节说到的问题吗?Vue3 的许多选项都抽离成为了一个个模块,所以我们可以在一个单独的 js 声明响应式数据了,就是利用ref函数来操作的。
我们新建一个 demo2.js 文件,首先引入 vue 模块中的 ref 函数,用于声明一个响应式数据:
import { ref } from 'vue'
export let a = ref('foo')
export function calc() { a.value = a.value + 'bar' }
然后我们在其他组件中使用:
import { a, calc } from './demo2.js'
export default {
setup() {
return { a, calc } // 这里需要导出a和calc
}
}
由于在 demo2.js 的 calc 函数操作的已经是一个响应式数据了,所以,组件一旦用到了这个模块中的东西,它都会反映到页面中。
大功告成,这得益于 Vue3 的一大进步啊!现在我们写代码可以写得非常舒服了。再加上 Vue3 是 TS 重构的,按道理来说是非常支持 TS 的写法的,前提是你的项目支持 TS。请问各位同志,Vue3 是不是可以适合开发大型应用了呢?
总结
将同一个逻辑关注点相关代码收集在一起,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块。使得开发人员更容易阅读和理解这些代码,这正是组合式 API 要解决的问题。
组合式 API 也导致以往的写法不相同,所以各位同志还是需要花时间去适应一段时间。
vue2和vue3的感受? vue2维护功能,很难维护?
js浮点数的精度不准确的解决方案。
- 最近在做项目的时候,涉及到商品价格的计算,经常会出现计算出现精度问题。刚开始草草了事,直接用toFixed就解决了问题,并没有好好的思考一下这个问题。后来慢慢的,问题越来越多,连toFixed也出现了(允悲),后来经过搜索网上的各种博客和论坛,整理总结了一下。
- 总结了一下,一共有以下两种问题
做购物车计算?
购物车相当于现实中超市的购物车,不同的是一个是实体车,一个是虚拟车而已。用户可以在购物网站的不同页面之间跳转,以选购自己喜爱的商品,点击购买时,该商品就自动保存到你的购物车中,重复选购后,最后将选中的所有商品放在购物车中统一到付款台结账,这也是尽量让客户体验到现实生活中购物的感觉。服务器通过追踪每个用户的行动,以保证在结账时每件商品都物有其主。
购物车的功能包括以下几项:
把商品添加到购物车,即订购
删除购物车中已定购的商品
修改购物车中某一本图书的订购数量
清空购物车
显示购物车中商品清单及数量、价格
实现购物车的关键在于服务器识别每一个用户并维持与他们的联系。但是HTTP协议是一种“无状态(Stateless)”的协议,因而服务器不能记住是谁在购买商品,当把商品加入购物车时,服务器也不知道购物车里原先有些什么,使得用户在不同页面间跳转时购物车无法“随身携带”,这都给购物车的实现造成了一定的困难。
目前购物车的实现主要是通过cookie、session或结合数据库的方式。下面分析一下它们的机制及作用。
1. cookie
cookie是由服务器产生,存储在客户端的一段信息。它定义了一种Web服务器在客户端存储和返回信息的机制,cookie文件它包含域、路径、生存期、和由服务器设置的变量值等内容。当用户以后访问同一个Web服务器时,浏览器会把cookie原样发送给服务器。通过让服务器读取原先保存到客户端的信息,网站能够为浏览者提供一系列的方便,例如在线交易过程中标识用户身份、安全要求不高的场合避免用户重复输入名字和密码、门户网站的主页定制、有针对性地投放广告等等。利用cookie的特性,大大扩展了WEB应用程序的功能,不仅可以建立服务器与客户机的联系,因为cookie可以由服务器定制,因此还可以将购物信息生成cookie值存放在客户端,从而实现购物车的功能。用基于cookie的方式实现服务器与浏览器之间的会话或购物车,有以下特点:
cookie存储在客户端,且占用很少的资源,浏览器允许存放300个cookie,每个cookie的大小为4KB,足以满足购物车的要求,同时也减轻了服务器的负荷;
cookie为浏览器所内置,使用方便。即使用户不小心关闭了浏览器窗口,只要在cookie定义的有效期内,购物车中的信息也不会丢失;
cookie不是可执行文件,所以不会以任何方式执行,因此也不会带来病毒或攻击用户的系统;
基于cookie的购物车要求用户浏览器必须支持并设置为启用cookie,否则购物车则失效;
存在着关于cookie侵犯访问者隐私权的争论,因此有些用户会禁止本机的cookie功能。
2. session
session是实现购物车的另一种方法。session提供了可以保存和跟踪用户的状态信息的功能,使当前用户在session中定义的变量和对象能在页面之间共享,但是不能为应用中其他用户所访问,它与cookie最重大的区别是,session将用户在会话期间的私有信息存储在服务器端,提高了安全性。在服务器生成session后,客户端会生成一个sessionid识别号保存在客户端,以保持和服务器的同步。这个sessionid是只读的,如果客户端禁止cookie功能,session会通过在URL中附加参数,或隐含在表单中提交等其他方式在页面间传送。因此利用session实施对用户的管理则更为安全、有效。
同样,利用session也能实现购物车,这种方式的特点是:
session用新的机制保持与客户端的同步,不依赖于客户端设置;
与cookie相比,session是存储在服务器端的信息,因此显得更为安全,因此可将身份标示,购物等信息存储在session中;
session会占用服务器资源,加大服务器端的负载,尤其当并发用户很多时,会生成大量的session,影响服务器的性能;
因为session存储的信息更敏感,而且是以文件形式保存在服务器中,因此仍然存在着安全隐患。
3. 结合数据库的方式
这也是目前较普遍的模式,在这种方式中,数据库承担着存储购物信息的作用,session或cookie则用来跟踪用户。这种方式具有以下特点:
数据库与cookie分别负责记录数据和维持会话,能发挥各自的优势,使安全性和服务器性能都得到了提高;
每一个购物的行为,都要直接建立与数据库的连接,直至对表的操作完成后,连接才释放。当并发用户很多时,会影响数据库的性能,因此,这对数据库的性能提出了更高的要求;
使cookie维持会话有赖客户端的支持。
各种方式的选择:
虽然cookie可用来实现购物车,但必须获得浏览器的支持,再加上它是存储在客户端的信息,极易被获取,所以这也限制了它存储更多,更重要的信息。所以一般cookie只用来维持与服务器的会话,例如国内最大的当当网络书店就是用cookie保持与客户的联系,但是这种方式最大的缺点是如果客户端不支持cookie就会使购物车失效。
Session 能很好地与交易双方保持会话,可以忽视客户端的设置。在购物车技术中得到了广泛的应用。但session的文件属性使其仍然留有安全隐患。
结合数据库的方式虽然在一定程度上解决了上述的问题,但从上面的例子可以看出:在这种购物流程中涉及到对数据库表的频繁操作,尤其是用户每选购一次商品,都要与数据库进行连接,当用户很多的时候就加大了服务器与数据库的负荷。
购物车原理以及实现
重绘和重排?
下面是我的个人理解,
重排也就是当你改变宽高的时候,浏览器需要重新计算现在的宽高,同样其他元素的位置也因为现在的宽高收到影响,浏览器渲染的时候会有部分失效,然后重新渲染,这个过程计算重排。
重绘:
完成重排之后,浏览器重新渲染后展示新的页面到屏幕上,这个过程我们称为重绘
当我们改变大小的时候,会产生重排,给dom设置颜色的时候,会导致重绘,重排一定会重绘,单重绘不会重排,重排会影响性能,所以尽可能减少重排的操作。
如何中断Promise?
promise 可以通过在流程中使用 throw 来终端流程触发 catch 操作,也可以在某一个节点进行 reject 来中断操作他的链式调用的.then 函数并不代表每一次的对象都是原始的 promise 对象。所以在链式调用的过程中是完全可以实现中断操作的。
原型和原型对象?
什么是原型?什么是原型链?
原型
1.所有函数都有一个prototype(原型),属性值是一个普通的对象
2.所有引用类型都有一个——proto——隐式原型,属性值是一个普通的对象
3.所有引用类型——proto——属性指向 它构造函数的prototype
原型链
每一个实例对象都有一个proto属性,指向的构造函数的原型对象,构造函数的原型对象也是一个对象,也有一个proto属性,这样一层一层往上找就形成了原型链。
什么是原型对象?有什么优点?
简单的来说,无论何时,我们创建的每一个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,这个对象包含了通过调用该构造函数所创建的对象共享的属性和方法。其实我们平常的叫法就是指:prototype就是通过该构造函数创建的某个实例的原型对象,但是其实prototype是每个构造函数的属性而已,只能说万物皆对象罢了。
原型对象的优点是:所有的对象实例都可以共享它包含的属性和方法。这一点可以在构造函数里就可以看出来,因为构造函数在函数里面就定义了对象的实例信息,而原型对象可以在任何地方定义属性和方法。
原型和原型链用来做继承?
所谓继承: 就是子对象自动拥有父对象的属性和方法, 继承可以提 高代码的复用性。 JS里的继承主要依靠是的原型链。让原型对象(每一个构造函数都有一个原型对象porotype)的值,等于另一个类型的实例,即实现了继 承;另外一个类型的原型再指向第三个类型的实例,以此类推,也 就形成了一个原型链。
作用:实现实例共享方法和属性的继承,原型继承。
优点:不用将每个实例一样的属性和方法放在构造函数中,每次new都会创造内存,浪费内存,prototype减少内存的浪费
子组件的生命周期?
vue 父子组件的生命周期顺序
父beforeCrate--> 父created-->父beforeMount->子beforeCrate->子created-> 子beforeMount->子mounted->父mounted
二、子组件更新过程
父beforeUpdate->子beforeUpdate->子updated->父updated
三、父组件更新过程
父beforeUpdate->父updated
四、销毁过程
父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
NextTick是什么
官方对其的定义
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM
什么意思呢?
我们可以理解成,Vue 在更新 DOM 时是异步执行的。当数据发生变化,Vue将开启一个异步更新队列,视图需要等队列中所有数据变化完成之后,再统一进行更新
举例一下
<div id="app"> {{ message }} </div>
const vm = new Vue({
el: '#app',
data: {
message: '原始值'
}
})
this.message = '修改后的值1'
this.message = '修改后的值2'
this.message = '修改后的值3'
console.log(vm.$el.textContent) // 原始值
Layout组件实现
vue-router 嵌套路由。
组件拆分:vue组件,复杂功能拆分都个功能模块,
第二种:样式布局,拆分组件,代码复用。
npm i vue-router@3.3.
父组件如何监听子组件的生命周期? 前端之amd模块化规范? 前端模块规范有三种:CommonJs,AMD和CMD。? 贵公司有多少个前端?7个 围绕商城做的,查账,支付类? web vue2 vue3? 自己的产品? 20多个,30个? 离职前7k?
h5和小程序的区别?
关于小程序与H5,在之前实际上是存在很多争议的,很多人觉得它们是一样的应用,单其实并不是这样的,差别还是非常大的,因为一个网页,一个应用。
从这里就可以区分出来,包括打开的方式也是不一样的,尽呈现的形式有时候很相似,但还是不一样的东西。
1、运行环境
两个的环境是不一样的。
H5是网页,主要依赖浏览器运行(靠浏览器进行解码来呈现内容),如手机内置浏览器,APP,微信。
小程序(通过开发而来的,过程较为复杂,而且呈现的方式是在微信里面,依赖的是APP) 只能依赖微信打开。
二、能力限制
H5(可以通过朋友圈进行分享,形式多种多样,可以通过文字,链接,海报的形式来呈现,H5可以通过短信进行分享,也就是链接或者文字的形式,点进去就可以进行浏览器浏览,然后进行跳转页面)在微信里可以直接分享朋友圈。
小程序(小程序在之前是不支持分享到朋友圈的,只可以通过文字或者图片加小程序码的形式来呈现)只能转发好友或好友群,要想发到朋友圈只能生成有小程序码的图片发到朋友圈。(小程序不可以进行短信分享,只能做相应的引导策略。)
再如支付能力,小程序只支持微信支付,H5里可以选择多种支付方式。
其它:
小程序可以进行卡片分享界面,这个是非常吸引人点击的,对于H5来说,他就不可以这样高大尚,这对于做营销策略方面而言,小程序更高一筹。
不管怎样,两者都有自己的好处,用处都很广,选择合适的才是最重要的,分享裂变能力都很强,这一点是两者都具备的特质。
三、用户体验
小程序基于微信客户端,一旦打开小程序,可以缓存很多资源,数据及使用记录会自动保存,不会因为操作中断丢失操作数据,H5,每次使用都是重新加载,操作中断再回来无法继续操作,都要重新开始。
四、访问入口
H5能在微信外使用,不依赖微信,而小程序是有微信提供的场景入口,并且在聊天界面顶部的“最近使用”和“我的小程序”这个入口相对H5来说有绝对优势。
用户关闭后,H5如果想继续访问,可通过收藏或转发给“文件传输助手”等聊天界面保存,总之是要保存方便下次访问。
微信内可以搜索H5和小程序,根据名字内容来搜索。
数据的缓存?
做外包? 职业方向?
购物车流程, 加入购物的为什么要缓存?
购物车存本地
当选中某些商品下单购买时,会从缓存删除改数据,更新缓存。
当用户购买全部产品时,清空所有缓存。
在做一个项目,一个电商移动端项目,在这个项目中我需要实现一个点击产品详情页面,在商品规格弹出层中点击加入购物车的这样一个效果(不使用ui框架)
商品规格弹出层中,每种商品都有每种
vue传参页面刷新数据丢失问题(全面)
在做vue的时候,经常会遇到组件之间数据的传递问题,通过params或者query传参,但是,当页面刷新的时候,数据会丢失,找不到数据。今天经过总结,解决了这个问题。通过了一下几种情况进行传值:
通过路由params传参
通过路由query传参
通过vuex
1.通过params传参
先在路由path里那个组件需要传递参数,定义一个参数,用于组件传递,params刷新页面数据会丢失。
path: "/chatView/:user"
//这里值用:加参数的写法,user即为参数,注意一定要用/隔开
今天来开始写一下关于购物车的东西, 这里首先抛出四个问题:
1)用户没登陆用户名和密码,添加商品, 关闭浏览器再打开后 不登录用户名和密码** ** 问:购物车商品还在吗?
2)用户登陆了用户名密码,添加商品,关闭浏览器再打开后 不登录用户名和密码** ** 问:购物车商品还在吗?
3)用户登陆了用户名密码,添加商品, 关闭浏览器,然后再打开,登陆用户名和密码 问:购物车商品还在吗?
4)用户登陆了用户名密码,添加商品, 关闭浏览器 外地老家打开浏览器 登陆用户名和密码 问:购物车商品还在吗?
上面四个问题都是以京东为模板, 那么大家猜猜结果是什么呢? 1)在 2)不在了 3)在 4)在
如果你能够猜到答案, 那么说明你真的很棒, 那么关于这四点是怎么实现的呢? (如果有不认可的小伙伴可以用京东实验一下) 下面我们就来讲解下购物车的原理,最后再来说下具体的code实现. 1)用户没有登录, 添加商品, 此时的商品是被添加到了浏览器的Cookie中, 所以当再次访问时(不登录),商品仍然在Cookie中, 所以购物车中的商品还是存在的. 2)用户登录了,添加商品, 此时会将Cookie中和用户选择的商品都添加到购物车中, 然后删除Cookie中的商品. 所以当用户再次访问(不登录),此时Cookie中的购物车商品已经被删除了, 所以此时购物车中的商品不在了. 3)用户登录, 添加商品,此时商品被添加到数据库做了持久化存储, 再次打开登录用户名和密码, 该用户选择的商品肯定还是存在的, 所以购物车中的商品还是存在的. 4)理由3)
这里再说下 没登录 保存商品到Cookie的优点以及保存到Session和数据库的对比:
1:Cookie: 优点: 保存用户浏览器(不用浪费我们公司的服务器) 缺点:Cookie禁用,不提供保存 2:Session:(Redis : 浪费大量服务器内存:实现、禁用Cookie) 速度很快 3:数据库(Mysql、Redis、SOlr) 能持久化的就数据库 速度太慢
那么我今天要讲的就是:
用户没登陆:购物车添加到Cookie中 用户登陆: 保存购物车到Redis中 (不用数据库)
整体的思路图解:
**接下来就是代码实例来实现 购物车的功能了: 首先我们看下购物车和购物项两个JavaBean的设计: **购物车: buyerCart.java
1 public class BuyerCart implements Serializable{
2
3 /**
4 * 购物车
5 */
6 private static final long serialVersionUID = 1L;
7
8 //商品结果集
9 private List<BuyerItem> items = new ArrayList<BuyerItem>();
10
11 //添加购物项到购物车
12 public void addItem(BuyerItem item){
13 //判断是否包含同款
14 if (items.contains(item)) {
15 //追加数量
16 for (BuyerItem buyerItem : items) {
17 if (buyerItem.equals(item)) {
18 buyerItem.setAmount(item.getAmount() + buyerItem.getAmount());
19 }
20 }
21 }else {
22 items.add(item);
23 }
24
25 }
26
27 public List<BuyerItem> getItems() {
28 return items;
29 }
30
31 public void setItems(List<BuyerItem> items) {
32 this.items = items;
33 }
34
35
36 //小计
37 //商品数量
38 @JsonIgnore
39 public Integer getProductAmount(){
40 Integer result = 0;
41 //计算
42 for (BuyerItem buyerItem : items) {
43 result += buyerItem.getAmount();
44 }
45 return result;
46 }
47
48 //商品金额
49 @JsonIgnore
50 public Float getProductPrice(){
51 Float result = 0f;
52 //计算
53 for (BuyerItem buyerItem : items) {
54 result += buyerItem.getAmount()*buyerItem.getSku().getPrice();
55 }
56 return result;
57 }
58
59 //运费
60 @JsonIgnore
61 public Float getFee(){
62 Float result = 0f;
63 //计算
64 if (getProductPrice() < 79) {
65 result = 5f;
66 }
67
68 return result;
69 }
70
71 //总价
72 @JsonIgnore
73 public Float getTotalPrice(){
74 return getProductPrice() + getFee();
75 }
76
77 }
这里使用了@JsonIgonre注解是因为下面需要将BuyerCart 转换成Json格式, 而这几个字段只有get 方法, 所以不能转换, 需要使用忽略Json.
下面是购物项: buyerItem.java
1 public class BuyerItem implements Serializable{
2
3 private static final long serialVersionUID = 1L;
4
5 //SKu对象
6 private Sku sku;
7
8 //是否有货
9 private Boolean isHave = true;
10
11 //购买的数量
12 private Integer amount = 1;
13
14 public Sku getSku() {
15 return sku;
16 }
17
18 public void setSku(Sku sku) {
19 this.sku = sku;
20 }
21
22 public Boolean getIsHave() {
23 return isHave;
24 }
25
26 public void setIsHave(Boolean isHave) {
27 this.isHave = isHave;
28 }
29
30 public Integer getAmount() {
31 return amount;
32 }
33
34 public void setAmount(Integer amount) {
35 this.amount = amount;
36 }
37
38 @Override
39 public int hashCode() {
40 final int prime = 31;
41 int result = 1;
42 result = prime * result + ((sku == null) ? 0 : sku.hashCode());
43 return result;
44 }
45
46 @Override
47 public boolean equals(Object obj) {
48 if (this == obj) //比较地址
49 return true;
50 if (obj == null)
51 return false;
52 if (getClass() != obj.getClass())
53 return false;
54 BuyerItem other = (BuyerItem) obj;
55 if (sku == null) {
56 if (other.sku != null)
57 return false;
58 } else if (!sku.getId().equals(other.sku.getId()))
59 return false;
60 return true;
61 }
62 }
1,将商品加入购物车中
1 //加入购物车
2 function addCart(){
3 // + skuId
4 window.location.href="/shopping/buyerCart?skuId="+skuId+"&amount="+$("#buy-num").val();
5 }
这里传入的参数是skuId(库存表的主键, 库存表保存的商品id,颜色,尺码,库存等信息), 购买数量amount.
接着我们来看Controller是如何来处理的:
1 //加入购物车
2 @RequestMapping(value="/shopping/buyerCart")
3 public <T> String buyerCart(Long skuId, Integer amount, HttpServletRequest request,
4 HttpServletResponse response) throws JsonParseException, JsonMappingException, IOException{
5 //将对象转换成json字符串/json字符串转成对象
6 ObjectMapper om = new ObjectMapper();
7 om.setSerializationInclusion(Include.NON_NULL);
8 BuyerCart buyerCart = null;
9 //1,获取Cookie中的购物车
10 Cookie[] cookies = request.getCookies();
11 if (null != cookies && cookies.length > 0) {
12 for (Cookie cookie : cookies) {
13 //
14 if (Constants.BUYER_CART.equals(cookie.getName())) {
15 //购物车 对象 与json字符串互转
16 buyerCart = om.readValue(cookie.getValue(), BuyerCart.class);
17 break;
18 }
19 }
20 }
21
22 //2,Cookie中没有购物车, 创建购物车对象
23 if (null == buyerCart) {
24 buyerCart = new BuyerCart();
25 }
26
27 //3, 将当前款商品追加到购物车
28 if (null != skuId && null != amount) {
29 Sku sku = new Sku();
30 sku.setId(skuId);
31 BuyerItem buyerItem = new BuyerItem();
32 buyerItem.setSku(sku);
33 //设置数量
34 buyerItem.setAmount(amount);
35 //添加购物项到购物车
36 buyerCart.addItem(buyerItem);
37 }
38
39 //排序 倒序
40 List<BuyerItem> items = buyerCart.getItems();
41 Collections.sort(items, new Comparator<BuyerItem>() {
42
43 @Override
44 public int compare(BuyerItem o1, BuyerItem o2) {
45 return -1;
46 }
47
48 });
49
50 //前三点 登录和非登录做的是一样的操作, 在第四点需要判断
51 String username = sessionProviderService.getAttributterForUsername(RequestUtils.getCSessionId(request, response));
52 if (null != username) {
53 //登录了
54 //4, 将购物车追加到Redis中
55 cartService.insertBuyerCartToRedis(buyerCart, username);
56 //5, 清空Cookie 设置存活时间为0, 立马销毁
57 Cookie cookie = new Cookie(Constants.BUYER_CART, null);
58 cookie.setPath("/");
59 cookie.setMaxAge(-0);
60 response.addCookie(cookie);
61 }else {
62 //未登录
63 //4, 保存购物车到Cookie中
64 //将对象转换成json格式
65 Writer w = new StringWriter();
66 om.writeValue(w, buyerCart);
67 Cookie cookie = new Cookie(Constants.BUYER_CART, w.toString());
68 //设置path是可以共享cookie
69 cookie.setPath("/");
70 //设置Cookie过期时间: -1 表示关闭浏览器失效 0: 立即失效 >0: 单位是秒, 多少秒后失效
71 cookie.setMaxAge(24*60*60);
72 //5,Cookie写会浏览器
73 response.addCookie(cookie);
74 }
75
76 //6, 重定向
77 return "redirect:/shopping/toCart";
78 }
这里设计一个知识点: 将对象转换成json字符串/json字符串转成对象 我们在这里先写一个小的Demo来演示json和对象之间的互转, 这里使用到了springmvc中的ObjectMapper类.
1 public class TestJson {
2
3 @Test
4 public void testAdd() throws Exception {
5 TestTb testTb = new TestTb();
6 testTb.setName("范冰冰");
7 ObjectMapper om = new ObjectMapper();
8 om.setSerializationInclusion(Include.NON_NULL);
9 //将对象转换成json字符串
10 Writer wr = new StringWriter();
11 om.writeValue(wr, testTb);
12 System.out.println(wr.toString());
13
14 //转回对象
15 TestTb r = om.readValue(wr.toString(), TestTb.class);
16 System.out.println(r.toString());
17 }
18
19 }
执行结果: 这里我们使用了Include.NON_NULL, 如果TestTb 中属性为null 的就不给转换成Json, 从对象-->Json字符串 用的是 objectMapper.writeValue(). 从Json字符串-->对象使用的是objectMapper.readValue(). 回归上面我们项目中的代码, 只有未登录 添加商品时才会将此商品添加到Cookie中.
1 //未登录
2 //4, 保存购物车到Cookie中
3 //将对象转换成json格式
4 Writer w = new StringWriter();
5 om.writeValue(w, buyerCart);
6 Cookie cookie = new Cookie(Constants.BUYER_CART, w.toString());
7 //设置path是可以共享cookie
8 cookie.setPath("/");
9 //设置Cookie过期时间: -1 表示关闭浏览器失效 0: 立即失效 >0: 单位是秒, 多少秒后失效
10 cookie.setMaxAge(24*60*60);
11 //5,Cookie写会浏览器
12 response.addCookie(cookie);
我们debug 可以看到: 这里已经将对象购物车对象buyerCart转换成了Json格式. 将商品添加到购物车, 不管是登录还是未登录, 都要先取出Cookie中的购物车, 然后将当前选择的商品追加到购物车中. 然后登录的话 就把Cookie中的购物车清空, 并将购物车的内容添加到Redis中做持久化保存. 如果未登录, 将选择的商品追加到Cookie中.
将购物车追加到Redis中的代码:insertBuyerCartToRedis(这里面包含了判断添加的是否是同款)
View Code
判断用户是否登录: String username = sessionProviderService.getAttributterForUsername(RequestUtils.getCSessionId(request, response));
View Code
sessionProviderService
==========================================2,购物车展示页面 最后 重定向到购物车展示页: return "redirect:/shopping/toCart"; 这里进入结算页有两种方式: 1) 在商品详情页 点击加入购物车. 2) 直接点击购物车按钮 进入购物车结算页.
下面来看下结算页的代码:
1 @Autowired
2 private CartService cartService;
3 //去购物车结算, 这里有两个地方可以直达: 1,在商品详情页 中点击加入购物车按钮 2, 直接点击购物车按钮
4 @RequestMapping(value="/shopping/toCart")
5 public String toCart(Model model, HttpServletRequest request,
6 HttpServletResponse response) throws JsonParseException, JsonMappingException, IOException{
7 //将对象转换成json字符串/json字符串转成对象
8 ObjectMapper om = new ObjectMapper();
9 om.setSerializationInclusion(Include.NON_NULL);
10 BuyerCart buyerCart = null;
11 //1,获取Cookie中的购物车
12 Cookie[] cookies = request.getCookies();
13 if (null != cookies && cookies.length > 0) {
14 for (Cookie cookie : cookies) {
15 //
16 if (Constants.BUYER_CART.equals(cookie.getName())) {
17 //购物车 对象 与json字符串互转
18 buyerCart = om.readValue(cookie.getValue(), BuyerCart.class);
19 break;
20 }
21 }
22 }
23
24 //判断是否登录
25 String username = sessionProviderService.getAttributterForUsername(RequestUtils.getCSessionId(request, response));
26 if (null != username) {
27 //登录了
28 //2, 购物车 有东西, 则将购物车的东西保存到Redis中
29 if (null == buyerCart) {
30 cartService.insertBuyerCartToRedis(buyerCart, username);
31 //清空Cookie 设置存活时间为0, 立马销毁
32 Cookie cookie = new Cookie(Constants.BUYER_CART, null);
33 cookie.setPath("/");
34 cookie.setMaxAge(-0);
35 response.addCookie(cookie);
36 }
37 //3, 取出Redis中的购物车
38 buyerCart = cartService.selectBuyerCartFromRedis(username);
39 }
40
41
42 //4, 没有 则创建购物车
43 if (null == buyerCart) {
44 buyerCart = new BuyerCart();
45 }
46
47 //5, 将购物车装满, 前面只是将skuId装进购物车, 这里还需要查出sku详情
48 List<BuyerItem> items = buyerCart.getItems();
49 if(items.size() > 0){
50 //只有购物车中有购物项, 才可以将sku相关信息加入到购物项中
51 for (BuyerItem buyerItem : items) {
52 buyerItem.setSku(cartService.selectSkuById(buyerItem.getSku().getId()));
53 }
54 }
55
56 //5,上面已经将购物车装满了, 这里直接回显页面
57 model.addAttribute("buyerCart", buyerCart);
58
59 //跳转购物页面
60 return "cart";
61 }
这里 就是 购物车详情展示页面, 这里需要注意, 如果是同一件商品连续添加, 是需要合并的. 购物车详情展示页面就包括两大块, 1) 商品详情 2)总计(商品总额,运费) 其中1)商品详情又包括 商品尺码,商品颜色, 商品购买数量, 是否有货. 取出Redis中的购物车: buyerCart = cartService.selectBuyerCartFromRedis(username);
View Code
将购物车装满, 前面只是将skuId装进购物车, 这里还需要查出sku详情: List items = buyerCart.getItems(); buyerItem.setSku(cartService.selectSkuById(buyerItem.getSku().getId()));
View Code
接着就返回"cart.jsp", 这个就是购物车详情展示页面了.
================================================3, 去结算页面 到了这里就说明用户必须要 登录, 而且购物车中必须要有商品. 所以这里我么你需要利用springmvc的过滤功能, 用户点击结算的时候必须要先登录, 如果没有登录的话就提示用户需要登录.
1 //去结算
2 @RequestMapping(value="/buyer/trueBuy")
3 public String trueBuy(String[] skuIds, Model model, HttpServletRequest request, HttpServletResponse response){
4 //1, 购物车必须有商品,
5 //取出用户名 再取出购物车
6 String username = sessionProviderService.getAttributterForUsername(RequestUtils.getCSessionId(request, response));
7 //取出所有购物车
8 BuyerCart buyerCart = cartService.selectBuyerCartFromRedisBySkuIds(skuIds, username);
9 List<BuyerItem> items = buyerCart.getItems();
10 if (items.size() > 0) {
11 //购物车中有商品
12 //判断所勾选的商品是否都有货, 如果有一件无货, 那么就刷新页面.
13 Boolean flag = true;
14 //2, 购物车中商品必须有库存 且购买大于库存数量时视为无货. 提示: 购物车原页面不动. 有货改为无货, 加红提醒.
15 for (BuyerItem buyerItem : items) {
16 //装满购物车的购物项, 当前购物项只有skuId这一个东西, 我们还需要购物项的数量去判断是否有货
17 buyerItem.setSku(cartService.selectSkuById(buyerItem.getSku().getId()));
18 //校验库存
19 if (buyerItem.getAmount() > buyerItem.getSku().getStock()) {
20 //无货
21 buyerItem.setIsHave(false);
22 flag = false;
23 }
24 if (!flag) {
25 //无货, 原页面不动, 有货改成无货, 刷新页面.
26 model.addAttribute("buyerCart", buyerCart);
27 return "cart";
28 }
29 }
30 }else {
31 //购物车没有商品
32 //没有商品: 1>原购物车页面刷新(购物车页面提示没有商品)
33 return "redirect:/shopping/toCart";
34 }
35
36
37 //3, 正常进入下一个页面
38 return "order";
39 }
取出 所指定的购物车, 因为我们结算之前在购物车详情页面会勾选 我们 需要购买的商品, 所以这里是根据所勾选的商品去结算的. BuyerCart buyerCart = cartService.selectBuyerCartFromRedisBySkuIds(skuIds, username); 从购物车中取出指定商品:
View Code
- 当我们购买的商品只要有一件是无货的状态, 那么刷新购物车详情页面, 回显无货的商品状态. 2)当购物车中午商品时, 刷新当前页面.
购物车就这么多东西, 可能讲解有不到或者错误的地方, 欢迎大家能够指出来.
如果问后端,做全栈?
加购支付模块、
商品加购:
当用户点击前端加入购物车按钮时,获取商品的Id、名称、价格、数量、图片等信息,作为ajax的data参数发送到后端的加购方法。在后端获取商品信息后我们就可以将商品保存在购物车中,购物车我们是在Redis中维护的,使用的是Hash结构,我们使用"car_"+用户Id作为购物车的key,使用"car_"+商品Id作为购物车的field,将前端传过来的数据转换为JSON字符串作为value,通过用户Id和商品Id就可以唯一标识购物车中的一件商品。
那么用户信息时怎么获取到的呢?在之前我做的项目没有做前后端分离,所以用户信息保存在session中,但是项目前后端分离后,session会失效,所以在这里使用的是基于token的登录,用户登录时将用户信息加密,将秘钥返回到前端,前端接受用户请求后保存在sessionStorage中,当前端发送请求时,从sessionStorage中取出秘钥并作为参数发送到后端,这里可以使用ajax全局配置减少重复代码,后端接受参数,解密后就可以得到用户信息。
我的加购逻辑是这样的:在每次加购商品时,都会判断商品是否存在购物车中,如果存在的话就将商品信息取出来转换为商品对象cart,从cart中取出商品的个数,修该前端传过来的商品数据,其实就是前端参数的商品个数上加上cart的商品个数。否则的话不用修改。完成判断后我们就可以尝试在Redis中保存商品数据,首先通过商品Id查询商品的库存,如果库存小于商品个数,将商品个数修改为库存数,否则不用修改,判断逻辑完成后将商品信息保存在购物车也就是Redis中,然后返回一个相应信息给前端。
草率解决JS浮点数运算结果不精确的问题
JavaScript 内部只有一种数字类型Number,也就是说,JavaScript 语言的底层根本没有整数,所有数字都是以IEEE-754标准格式64位浮点数形式储存,1与1.0是相同的。因为有些小数以二进制表示位数是无穷的。JavaScript会把超出53位之后的二进制舍弃,所以涉及小数的比较和运算要特别小心。
二、IEEE算术标准(IEEE 754)
IEEE二进制浮点数算术标准(IEEE 754)是20世纪80年代以来最广泛使用的浮点数运算标准,为许多CPU与浮点运算器所采用。这个标准定义了表示浮点数的格式(包括负零-0)与反常值(denormal number)),一些特殊数值(无穷(Inf)与非数值(NaN)),以及这些数值的“浮点数运算符”;它也指明了四种数值舍入规则和五种例外状况(包括例外发生的时机与处理方式)。
在js中的计算步骤
(1).会将十进制的数字转换成二进制的,但是由于浮点数用二进制表示是无穷的
//如:
0.1——>0.0001 1001 1001 1001 ...(1001循环)
0.2——>0.0011 0011 0011 0011 ...(0011循环)
(2).IEEE754标准的64位双精度浮点数的小数部分最多支持53位二进制,多余的二进制数字被截断,所以两者相加之后的二进制之和是
0.0100110011001100110011001100110011001100110011001101
(3).将截断之后的二进制数字再转换为十进制,0.30000000000000004,所以在计算时产生了误差
四、解决方案:
(1).使用一些大佬写好的库:
Math.js
decimal.js
big.js
…
(2).推荐使用bignumber.js该js压缩版仅18kb
相关示例:
// 计算传入的参数和,参数类型可以是 String,Number
// 两数之和
let x = BigNumber.sum('11', 23)
x.toNumber() // 34
// 多个参数
arr = [2, new BigNumber(14), '15.9999', 12]
let y = BigNumber.sum(...arr)
y.toString() // '43.9999'
// maximum,minimum
// 求最大值,简写 max,min
let x = [2222, 3333, '4444']
BigNumber.max(...x).toNumber() // 4444
BigNumber.min(...x).toNumber() // 2222
decimalPlaces(dp)
// 确定小数位数
let x = new BigNumber(1234.5678912345)
let y = new BigNumber(1234.56)
x.dp(2).toNumber() // 1234.56
y.dp(10).toNumber() // 1234.56
// plus
// 加法运算
0.1 + 0.2 // 0.30000000000000004
let x = new BigNumber(0.1)
x.plus(0.2).toNumber() // 0.3
// minus
// 减法运算
0.3 - 0.1 // 0.19999999999999998
let x = new BigNumber(0.3)
x.minus(0.1) // 0.2
multipliedBy(times)
// 乘法运算
0.6 * 3 // 1.7999999999999998
let x = new BigNumber(0.6)
x.times(3) // 1.8
dividedBy(div)
// 除法运算
let x = new BigNumber(300)
x.div(3).toNumber() // 100
x.div(7).dp(3).toNumber() // 42.857
dividedToIntegerBy(idiv)
// 除法运算,返回整数
let x = new BigNumber(5)
x.idiv(3).toNumber() // 1
x.idiv(0.7).toNumber() // 7
modulo(mod)
// 取余
1 % 0.9 // 0.09999999999999998
let x = new BigNumber(1)
x.mod(0.9).toNumber() // 0.1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
(3).将所有小数转化为整数进行计算后再将计算结果转化为对应的小数
//问题难点 :我也不知道传来的数字会有哪些?(可以通过字符串截取小数点后的数字再通过一些方法获取到最长的那个数然后将传过来的数据乘以相应的倍数。进行数学操作之后将其除以倍数并进行返回);
//本篇主旨草率解决(上面说的操作太麻烦了,又要截取呀又要…操作)
//草率解决思路:直接将传过来的参数乘以指定倍数如:10000倍
相关代码:
// 加
function numAdd2(...nums) {
let sum = 0;
nums.forEach(item => {
sum += item;
});
return sum;
}
function numAdd(...nums) {
let sum = 0;
nums.forEach(item => {
item *= Math.pow(10, 10);
sum += parseInt(item); //parseInt主要是为了 如果小数超过了我们所乘的倍数那么就舍去一般不会超过
});
return sum / 1e10;
}
console.log('处理前:', numAdd2(199, 2.333, 3, 4.064849646, 5.08498489));
console.log('处理后:', numAdd(199, 2.333, 3, 4.064849646, 5.08498489));
console.log('处理前:', numAdd2(0.1, 0.2));
console.log('处理后:', numAdd(0.1, 0.2));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
效果:
//减:
function numReducer2(...nums) {
let sum = nums[0] * 2;
nums.forEach(item => {
sum -= item;
});
return sum;
}
function numReducer(...nums) {
let sum = nums[0] * Math.pow(10, 10) * 2;
nums.forEach(item => {
item *= Math.pow(10, 10);
sum -= parseInt(item); //parseInt主要是为了 如果小数超过了我们所乘的倍数那么就舍去一般不会超过
});
return sum / 1e10;
}
console.log('处理前:', numReducer2(199, 2.333, 3, 4.064849646, 5.08498489));
console.log('处理后:', numReducer(199, 2.333, 3, 4.064849646, 5.08498489));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
效果:
团队开发,一起开发。 pc端, 用vue2 ,小程序用原生,uniapp, 负责 那个难点,element的树形页面,弹窗页面,遇到,点到,树形控制去重。点击树形结构,id父存储data 传给子组件, 项目的图片懒加载。自己写
面试官问题
js函数 实现 和 instanceof 一样的功能
<script>
</script>
vue和原生js的优点分析
很多多年经验的程序,习惯了使用原生html和js来开发前端页面,对于很多没用过vue的程序员来说,觉得vue没有必要。
我们的这篇文章,帮助大家分析vue相对原生js的优缺点,vue的一些好处如下:
一、控件跟数据自动绑定,可以直接使用data里面的数据值来提交表单,而不需要再使用$("#myid").val()那一套方法来获取控件的值,对控件赋值也方便很多,只需要改变data的值,控件就会自动改变值。将复杂的界面操作,转化为对数据进行操作。
比如下面的一段代码就可以很简单的实现了select控件的里面的列表的动态管理:
html代码:
<el-select v-model="mType" style="flex: 1;">
<el-option v-for="(item,index) in enums" :label="item.label" :value="item.value" :key="index"></el-option>
</el-select>
js代码:
data(){
return{
mType:'',
enums:[{value:0,label:'正常'},{value:1,label:'拉黑'}]
}
}
二、页面参数传递和页面状态管理。
页面传值对于vue来说,可供选择的方法非常多。比如使用子组件实现,通过对props属性传值;也可以使用页面url参数的方法传值;或使用vuex全局状态管理的方法页面传值等等。
而原生开发的时候,在页面有多个参数的时候,页面传值和初始化,要复杂很多。而vue直接将参数保存在对象里面,直接给子组件的属性或vuex存储一个对象就行了,比如 , 这样就可以将userinfo传到自定义组件。
三、模块化开发、无刷新保留场景参数更新
比如一个列表页面里面有添加功能,有修改功能,这时候我们可以通过引用子组件的形式,当子组件内容更新的时候,修改主组件的数据,比如修改了一条数据后,我们需要列表页同时刷新,但我们不希望改变原来列表页的页码和搜索条件。假如你用原生开发来实现这个,需要写很多业务逻辑保存上一个页面的搜索条件和页码这些参数,但假如你用vue开发,将变得非常简单。 四、代码的可阅读性
vue天生具有组件化开发的能力,因此不同的功能基本都是写在不同的模块里面,因此代码的可阅读性非常高。当一个新手接手一个旧项目的时候,基本上可以做到一天就能定位到要修改的代码,进行修改,快速接手项目。
五、基于强大的nodejs,添加新的组件库,基本一句npm命令就能安装,比如当我需要使用axios组件的时候,直接npm install axios安装一下,就可以使用axios这个组件。熟悉maven的同学估计很容易就能理解npm工具。
六、主路由、子路由、主页面、子组件的方式,可以让我们彻底抛弃iframe。写过前端的同学都知道,因为iframe的滚动条、和子页面跟其他页面的交互性这些原因、用户体验还是远远没有单页面架构友好。而且使用vue非常简单方便的实现系统菜单、导航等固定布局。
七、各子组件样式不冲突:各个组件之间,可以使用相同的样式名,但有不同的样式属性。比如组件A和组件B的button都绑定了class="btn", 但在两个组件里,我们可以实现两个不同的btn样式属性,互不影响。
vue的不足:
当然,vue也有不足,不足的地方如下:
一、vue是单页面页面,对于搜索引擎不友好,影响seo.因此不适合做公司官网。比如两个vue路由(页面),它的路径是这样的:index.html#aaa 和 index.html#bbb,但对于搜索引擎来说,都是同一个页面,就是index.html。这样搜索引擎就无法收录你的页面。
二、vue门槛较高,使用vue,需要先学习和摸索vue大概3天左右的时候,建议使用vue的时候,不需要看node.js自动帮你生成的js文件。你只需要编写你自己页面的代码就行了。具体nodejs帮你生成的框架代码,其实是不用看的。
vue2和vue3的响应式原理?
Vue2.0实现MVVM(双向数据绑定)的原理是通过 Object.defineProperty 来劫持各个属性的setter、getter,在数据变动时发布消息给订阅者,触发相应的监听回调。Vue 3.0实现响应式基于ES6的Proxy。两者的差异如下:
- Vue2.0
- 基于Object.defineProperty,不具备监听数组的能力,需要重新定义数组的原型来达到响应式。
- Object.defineProperty 无法检测到对象属性的添加和删除 。
- 由于Vue会在初始化实例时对属性执行getter/setter转化,所有属性必须在data对象上存在才能让Vue将它转换为响应式。
- 深度监听需要一次性递归,对性能影响比较大。
- Vue3.0
-
基于Proxy和Reflect,可以原生监听数组,可以监听对象属性的添加和删除。
-
不需要一次性遍历data的属性,可以显著提高性能。
-
因为Proxy是ES6新增的属性,有些浏览器还不支持,只能兼容到IE11 。
如何判断是pc端还是移动端
有时候会被别人问起pc端和手机端有什么区别。一般来说都会去说,兼容性啊,适配啊,网页布局这方面的事情,但是我觉得这个问题如果想要拿一个满分应该从区别出发,从如何判断收尾。毕竟你只知道区别没有用,他可能更想让你说出来你是怎么判断的。
像是面试的时候如果你自己主动说出来扯一堆王八犊子也很能拖延时间。说不定会给面试加分。
从区别入手
Pc端需要考虑的是浏览器的兼容性,不能局限于我们常用的谷歌浏览器,要为客户那边考虑,而手机端需要为不同的型号做考虑,安卓ios华为。同时也要更多考虑手机分辨率的适配。不同操作系统的细微差异。
在布局上,手机端因为要去做布局的自适应,所以可以围绕rem去说一说
关于接口上,手机端会多出很多常见功能,比如微信的支付接口,微信的朋友圈转发,分享文章什么的,支付宝的支付接口,包括高德地图百度地图的这些接口,都是做app会常用的,如果你是一个经验丰富的前端。这里会巩固你的项目经验。
Pc端和手机端在事件处理上区别不大,pc端没有触屏,手机端没有悬停事件,同时手机端多了一个键盘的弹出。
动画效果处理上,pc常用js去做动画,手机端去做动画特效会更倾向使用css3
同时相较用户来说,因为触屏的操作尺度非常大,所以在图标和按钮的处理上会更倾向于放大一些让用户有一个更好的视觉体验。
关于如何辨别是pc端还是手机端
这里参照了阮一峰老师9月份的文章,我上网找资料的时候发现很多人不会标注原作者直接转载。只能说阮一峰老师一个人养活了很多博客主。
目前来说比较常用的是通过js去拿navigator.userAgent这个属性,这是一个字符串,如果里面包含mobi,andorid,iphone等关键词就可以判断为移动设备。
if (/Mobi|Android|iPhone/i.test(navigator.userAgent)) {
// 当前设备是移动设备
}
// 另一种写法
if (
navigator.userAgent.match(/Mobi/i) ||
navigator.userAgent.match(/Android/i) ||
navigator.userAgent.match(/iPhone/i)
) {
// 当前设备是移动设备
}
然后有一个更简单的方法就是通过屏幕宽度判断是否为手机。直接获取window.screen。由于手机端屏幕普遍干不过电脑端,所以可以很简单的去识别是否为手机。
Window还提供了一种方法为window.orientation,这个属性用于判断手机是否是横屏,如果不是移动设备的话你获取这个属性会返回undefined。
第四种是触发touch事件,通过trycatch去为手机端的dom元素执行touch事件,如果捕获异常则证明是pc端。
其他的可以通过各种工具包轮子去判断。常用方法大概就这几种。
数据可视化,个人经验总结(Echarts相关)
数据可视化
数据可视化旨在借助于图形化手段,清晰有效地传达与沟通信息(来源于bd).在我们生活中最常见的,就有各种统计数据做成图表、股票k线图、能力雷达图这些(上面那张个人能力分析图,图片数据纯属虚构);而对于前端开发者来说,就是用一些大神开发好的可视化图表组件将后端传过来的数据用一种直观,清晰的方式呈现在浏览器中,常用的可视化图表图库包括(排名不分先后),后面文章中都是围绕Echarts库的运用:
- D3
- Echarts
- three.js
- HighCharts
- Charts
- G2
色彩的应用
色彩的应用作为数据可视化重要的部分,同样的数据,同样的图表类型,如果不同的人或者不同的公司做出来,有可能呈现的效果会截然不同,这其中的重要区别可能就是色彩的应用。Echarts不同的图表,都提供了一套默认的主题色,所以尽管我们不设置颜色,其呈现的效果也还是不错的。Echarts图表中可进行颜色设置的地方很多,包括但不仅限于下图(官方demo)所示的内容:
关于上面图提到的每个部分在option怎么设置,Echarts的配置手册都有详细描述,这里主要说一些工作中不常用到但又很关键的部分。从实现层面上来讲,颜色的设置分两种,option属性设置和css样式设置,至于为什么,可以从上图的dom结构得到答案,每个Echarts实例大体都包含两个元素:canvas 和 div(方框标注部分),div负责图表tooltip的展示(黄色框圈起部分),而canvas负责除黄色框以外的所有部分。如果是简单的颜色设置,如上面的展示的那张标注图,option属性设置color就足够了,但如果要做出下图所示的强调色,option属性设置color就显得捉襟见肘了,在标题和tooltip的数据显示上,应用了混合色用以加强数据的表现:
对于tooltip中的强调色,由于其根本是dom元素的操作,所以要做出上图所示的效果很简单,控制div元素及其子元素的样式就可以了,like:
option = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0,0,0,.8)',
textStyle: {
color: '#b4d1e6'
},
/*formatter属性的应用,直接行内css样式操作*/
formatter: function (val) {
return val[0].name + ':<span style="color:#ffbf00;font-weight: bold;font-size:14px;padding: 0 5px;" >' + addSeparator(val[0].data) + '个</span>'
}
}
...其他设置
}
复制代码
而对于标题或则图表其他部分要进行混合色的设置,就不是那么简单了,因为其不接受tooltip那种dom元素的直接样式操作,但Echart还是留了足够多的入口来解决这样的需求:富文本标签(rich),官方讲解,比如上面那一段混合色的标题,可以这样来实现,代码拷贝到官方demo,即可查看效果,更多用法可查看官方示例:
title:{
show:true,
left:'center',
top:15,
text:'{a|2017年全省应聘人员总数统计:}{b|165,338}{c|人}',
textStyle :{
rich: {
a: {
color: '#8bb8e8',
fontSize: 14,
fontWeight: 'bold'
},
b: {
color: '#ffcf2a',
fontSize: 16,
fontWeight: 'bold'
},
c: {
color: '#8bb8e8',
fontSize: 14,
fontWeight: 'bold'
}
}
}
}
复制代码
自动轮播(AutoToolTip)的应用
Echarts中的normal与emphasis,以及tooltip的加入,通过hover与unhover状态的切换,让图表多了一些交互。特别是上面提到的tooltip的自定义样式,让展示效果提升了一个档次。但作为前端可视化,很多时候显示在一个大屏幕上,用于参观展示用,所以参观展示的人是不大可能用鼠标一个一个hover来查看具体的数据,这就要求我们需要用自动轮播来代替hover触发tooltip。为此,官方提供了dispatchAction方法与官方demo,也可参考网上一篇文章提供的思路和源码,封装一个图表通用的自动轮播工具,我自己也封装了一个,欢迎参阅,下面是map使用轮播时的效果图。
使用思路(Vue框架下使用),首先将autoShowTip对象添加为Echarts的一个方法;然后在Echarts实例实例化之后,调用this.$echarts.AutoShowTip方法,并传入实例对象,option对象,轮播时间等参数:
这里需要提醒两点:
-
Echarts的dispatchAction方法对常用图表中的Radar图还不支持,个人知道支持的图表类型有:pie,bar,line,map,scatter系列;为解决Radar图的单轴hover与自动轮播,自己写了个方法,并将其也封装到autoShowTip对象中,具体的实现可参考个人博客的文章:从0开始撸一个支持单轴轮播的雷达图之末篇;
-
toolTip使用时,其显示的位置也是大有学问,比如下图左边所示,会弄巧成拙,所以控制tooltip的位置也很重要,Echarts也为此提供了相应的方法,比如加入下面这段代码,就可以达到右图所示的效果:
position: function (pos, params, dom, rect, size) { var obj = {top: '10%'}; //y轴方向,其位置固定 // obj[['left', 'right'][+(pos[0] < size.viewSize[0] - 20)]] = 5; if (pos[0] > (size.viewSize[0] - 100)) { obj['right'] = 0; } else { obj['left'] = pos[0]; } return obj; } 复制代码
-
另外,自动轮播还可以更好的展示数据,当我们数据过长,而展示空间有限时,我们可以把数据切为两端甚至多段,通过自动轮播切换,这样就可以在有限的空间里,展现最好的效果,下面是我做的一个Demo,源码及效果图:
myChart = this.$echarts.init(target); let step =0; option.xAxis.data = labelData.slice(0,length); option.series[0].data = realData.slice(0,length); option.series[1].data = symbolData.slice(0,length); myChart.setOption(option); this.$echarts.AutoShowTip(myChart, option, 3000,{ refreshOption: function () { step = ((step + length)>labelData.length)?0:(step+length); let endStep = ((step + length)>labelData.length)?8:(step+length); option.xAxis.data = labelData.slice(step,endStep); option.series[0].data = realData.slice(step,endStep); option.series[1].data = symbolData.slice(step,endStep); }, isRefresh: labelData.length>length }); 复制代码
坐标系的优化
在做bar或则line,或则基于这两者的扩展系列时,除了上面提到的,其实设置或数值(value)轴的刻度,也是一件需要注意的事情。比如下面两张图片这样,如果单看,感觉没啥,但如果在一个大屏里有多个这种柱状或曲线图,有些间隔线三四根,有些八九根,还有些没有均分,这种可视化展示,就会给人一种七零八落的感觉,根据个人经验,将间隔线控制在6根以下,体验较好。
除了上面提到的这一种,还有就是坐标轴范围过大,数值过大,造成不美观及可读性差。比如下面这种,简直就是失败的炫富,除去y轴数字重叠的问题,还有就是根本无法一眼知道他总收入究竟在那个段位,只知道很多,需要专心的数,才能知道,哦,,,嗦嘎,572.67亿元,抢了他,立刻,马上,现在就去:
上面分享了两种表达不友好的数据展示图表,那怎么才能更好的优化呢。下面我提供一下自己在做公司一个项目时的思路,关于具体实现可以自己摸索,但如果你们家的后端都为你计算好了单位,处理了前两步,那你就只需要做最后一步了。关于为什么不直接设置yaxis的min,max,spiktLine来控制间隔线及间隔,Echarts官方文档有这样的回答:坐标轴的分割段数,需要注意的是这个分割段数只是个预估值,最后实际显示的段数会在这个基础上根据分割后坐标轴刻度显示的易读程度作调整,关于操作interval来控制间隔,也有这样一句提示:因为 splitNumber 是预估的值,实际根据策略计算出来的刻度可能无法达到想要的效果,这时候可以使用 interval 配合 min、max 强制设定刻度划分,一般不建议使用:
地图的应用
Echarts地图,其可以设置为geo地图引擎或百度地图引擎,好像其他地图也支持,只要你知道坐标系的转换关系。geo地图由于部分数据不符合国家《测绘法》规定,目前暂时已经停止下载服务,不过你想找,还是能找到,比如Echarts github账号下。地图表面上在充当一个图表的背景,实际上其更多的作用是作为一个坐标系-经纬度坐标系。关于Echarts geo地图的使用,个人有几点经验分享一下:
- 不同版本的js或则json地图数据,呈现出来的效果差别很大,大到测试给你提bug的地步,比如下面两幅图所示,左边图的甘孜州名称已经把自己的爪牙完全伸到雅安去了,而绵阳则像已经吞并了德阳,自己对比了一下数据,左图的地图json数据是压缩过的,右图是未压缩的,但讲道理的话,应该是这两个json不是同一个版本:
- geo的设置的必要性,前面说过,geo其实存在的重要的作用是作为图表坐标系。所以当你的series存在多个系列需要在同一个坐标系能设定数据,那设置geo是非常有必要的。但值得注意的是,一旦系列中设置了全局geo为参考坐标系,即指定了geoIndex,那么series-map.map 属性,以及 series-map.itemStyle 等样式配置不再起作用,而是采用 geo 中的相应属性;另外要强调的就是谨慎使用roam:true,最好设置一个缩放区间;
- 如果涉及到政府项目,对边界区域很敏感,则最好的选择就是使用Bmap作为地图坐标系;
自定义系列
当时学了Echarts不久,看到Gallery上面一些炫酷的实例,自己也是想动手做了一个,可一看Echarts源码,一脸懵逼,后面又看了一下zRender及羡辙老师的水球图,感觉入了点门,但离做出炫酷效果还是有些差距。直到最近发现4.0版本介绍了自定义系列,捣鼓半天,自己做了个伪3d填充,2d坐标系的柱状图效果(加载动画化花了点时间),感兴趣的可以自己去研究一下:
打个总结
感觉这个经验分享帖,写的越来越像Echarts宣传贴了。掐指一算,自己做数据可视化已经快一年了,但感觉就像入了个门,作为前端的一个分支,水一样深。如果你对可视化还没有概念,推荐看一下蚂蚁金服去年推出的G2,其作者也是前Echarts的作者,里面讲了更多可视化图表理论性的东西,G2可视化基础文档。为了有一个直观的表现,自己用Vue搭了一个可视化Demo,感兴趣的可以留下你的邮箱。 顺便打个卖身广告:本人现在处于离职,如果哪位大神的公司需要找个搬砖的,请收下我的膝盖,在线简历,坐标成都。
关于屏幕适配的方案总结
概念的东西就不说了, 直接看方案以及相关的库,谁让我们是 调库大师。 实现我不会,调库我还行。手动狗头😃
1. media
css3 媒体查询:通过媒体查询的方式,编写适应不同分辨率设备的的css样式。 通常需要 UI
出不同大小的 设计稿,然后进行对应开发。该方案往往在响应试网页中使用。
@media screen and (max-width: 320px){
....适配iphone4的css样式
}
@media screen and (max-width: 375px){
....适配iphone6/7/8的css样式
}
@media screen and (max-width: 414px){
....适配iphone6/7/8 plus的css样式
}
复制代码
缺点:
- 工作量大 ,后期维护也大。
pc
h5
都能实现效果。
优点:
- 只需要修改
css
, 就能达到效果。 - 会自动根据屏幕进行对应的调整
常用库:
Bootsrtap
, 以及各种 UI
框架的栅格化系统这里列举 antd 的栅格化。当然还有 element
等。
2. rem
rem
就是根据浏览器 HTML
元素来 font-size
进行相对的大小设置。通过改变 HTML
的 font-size
来实现适配。它们通常情况下,还要搭配 viewport
来动态设置视口。
常用库:
hotcss
手淘宝解决方案flexible.js
, postcss-pxtorem
主要使用在工程化的项目中,将你写的 px
转成 rem
,px2rem
优点:
- 适配性比较好,浏览器均支持。
缺点:
- 需要进行计算,现在很多可以直接导出
rem
为单位的设计稿。 - 设置的值若出现小数点的情况不好理解,且有一定的误差, 不直观。
4. vw/vh
/ 百分比
与 rem
不同的点在于,这时的单位是 vw/vh
, 它相对于 viewport
来决定大小。 百分比 相比较与 vw/vh
和 rem
, 它是基于父级元素, 并不是根元素。实际使用中需要自己进行对应的布局计算百分比。
常用库:
postcss-px-to-viewport
与 postcss-pxtorem
一样, 它是将单位转换成 vw/vh
。
优点:
- 不需要人为通过
js
操作获取改变根元素的大小,直接用css
新单位,自动生效。
缺点:
- 直接进行单位换算时百分比可能出现小数,计算不方便且有误差.也不够直观。
- 兼容性没有
rem
好。
5. css3 transform
/ zoom
这应该是目前来说新的一个解决方案。利用 css3
的 transform
在需要的容器进行缩放比例控制。需要注意的是 zoom
属性在 firfox 是不支持的。这种方案在 pc
大屏上比较常用。
常用库: 需自己实现, 实现也比较简单,获取网页可见区域宽度高度,进去计算缩放比例,最后追加到需要缩放的容器上
优点:
- 完全可控
缺点:
- 需要浏览器支持
css3
- 使用在
fixed
定位的元素上存在问题
vue项目优化
.keep-alive
- 使用keep-alive缓存不活动的组件 keep-alive是一个抽象组件:它自身不会渲染一个DOM元素,也不会出现在父组件链中;使用keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
- 使用路由懒加载 Vue 是单页面应用,可能会有很多的路由引入 ,这样使用 webpcak 打包后的文件很大,当进入首页时,加载的资源过多,页面会出现白屏的情况,不利于用户体验。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度,但是可能其他的页面的速度就会降下来。
- 图片懒加载 对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。这样对于页面加载性能上会有很大的提升,也提高了用户体验。我们在项目中使用 Vue 的 vue-lazyload 插件:
- 使用节流防抖函数(性能优化) debounce我们加入了防抖以后,当你在频繁的输入时,并不会发送请求,只有当你在指定间隔内没有输入时,才会执行函数。如果停止输入但是在指定间隔内又输入,会重新触发计时。 throttle在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
5.v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
-
v-if 和 v-show 区分使用场景 所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景; v-show则适用于需要非常频繁切换条件的场景。
-
computed 和 watch 区分使用场景
-
长列表性能优化
-
事件的销毁
-
第三方插件的按需引入
vue的一些优化方式
title: vue简单优化 order: 15
Vue 项目中仍然存在项目首屏优化、Webpack 编译配置优化等问题,所以我们仍然需要去关注 Vue 项目性能方面的优化,使项目具有更高效的性能、更好的用户体验。
三部分:
Vue 代码层面的优化; webpack 配置层面的优化; 基础的 Web 技术层面的优化。 一、代码层面的优化 v-if 和 v-show 区分使用场景
computed 和 watch 区分使用场景
v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
🧡🧡🧡长列表性能优化
Vue 会通过 Object.defineProperty 对数据进行劫持,来实现视图响应数据的变化,然而有些时候我们的组件就是纯粹的数据展示,不会有任何改变,我们就不需要 Vue 来劫持我们的数据,在大量数据展示的情况下,这能够很明显的减少组件初始化的时间,那如何禁止 Vue 劫持我们的数据呢?可以通过 Object.freeze 方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了。
export default { data: () => ({ users: {} }), async created() { const users = await axios.get("/api/users"); this.users = Object.freeze(users); } }; 1 2 3 4 5 6 7 8 9 事件的销毁
Vue 组件销毁时,会自动清理它与其它实例的连接,解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。如果在 js 内使用 addEventListene 等方式是不会自动销毁的,我们需要在组件销毁时手动移除这些事件的监听,以免造成内存泄露,如
created() { addEventListener('click', this.click, false) }, beforeDestroy() { removeEventListener('click', this.click, false) } 1 2 3 4 5 6 图片资源懒加载
对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。这样对于页面加载性能上会有很大的提升,也提高了用户体验。我们在项目中使用 Vue 的 vue-lazyload 插件:
//安装插件 npm install vue-lazyload --save-dev //在入口文件 man.js 中引入并使用 import VueLazyload from 'vue-lazyload' Vue.use(VueLazyload) //或者添加自定义选项 Vue.use(VueLazyload, { preLoad: 1.3, error: 'dist/error.png', loading: 'dist/loading.gif', attempt: 1 }) //🤣在 vue 文件中将 img 标签的 src 属性直接改为 v-lazy ,从而将图片显示方式更改为懒加载显示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 路由懒加载
Vue 是单页面应用,可能会有很多的路由引入 ,这样使用 webpcak 打包后的文件很大,当进入首页时,加载的资源过多,页面会出现白屏的情况,不利于用户体验。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度,但是可能其他的页面的速度就会降下来。
const Foo = () => import('./Foo.vue') const router = new VueRouter({ routes: [ { path: '/foo', component: Foo } ] }) 1 2 3 4 5 6 第三方插件的按需引入
我们在项目中经常会需要引入第三方插件,如果我们直接引入整个插件,会导致项目的体积太大,我们可以借助 babel-plugin-component ,然后可以只引入需要的组件,以达到减小项目体积的目的。以下为项目中引入 element-ui 组件库为例:
//首先,安装 babel-plugin-component : npm install babel-plugin-component -D //然后,将 .babelrc 修改为: { "presets": [["es2015", { "modules": false }]], "plugins": [ [ "component", { "libraryName": "element-ui", "styleLibraryName": "theme-chalk" } ] ] } //在 main.js 中引入部分组件: import Vue from 'vue'; import { Button, Select } from 'element-ui';
Vue.use(Button) Vue.use(Select)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 优化无限列表性能
暂未了解
服务端渲染 SSR or 预渲染
服务端渲染是指 Vue 在客户端将标签渲染成的整个 html 片段的工作在服务端完成,服务端形成的 html 片段直接返回给客户端这个过程就叫做服务端渲染。
二、Webpack 层面的优化 😰Webpack 对图片进行压缩
在 vue 项目中除了可以在 webpack.base.conf.js 中 url-loader 中设置 limit 大小来对图片处理,对小于 limit 的图片转化为 base64 格式,其余的不做操作。所以对有些较大的图片资源,在请求资源的时候,加载会很慢,我们可以用 image-webpack-loader来压缩图片:
//首先,安装 image-webpack-loader : npm install image-webpack-loader --save-dev //然后,在 webpack.base.conf.js 中进行配置: { test: /.(png|jpe?g|gif|svg)(?.*)?$/, use:[ { loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('img/[name].[hash:7].[ext]') } }, { loader: 'image-webpack-loader', options: { bypassOnDebug: true, } } ] }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 😰减少 ES6 转为 ES5 的冗余代码
Babel 插件会在将ES6代码转换成 ES5 代码时会注入一些辅助函数,例如下面的 ES6 代码:
class HelloWebpack extends Component{...} 1 这段代码再被转换成能正常运行的 ES5 代码时需要以下两个辅助函数:
babel-runtime/helpers/createClass // 用于实现 class 语法 babel-runtime/helpers/inherits // 用于实现 extends 语法 1 2 在默认情况下, Babel 会在每个输出文件中内嵌这些依赖的辅助函数代码,如果多个源代码文件都依赖这些辅助函数,那么这些辅助函数的代码将会出现很多次,造成代码冗余。为了不让这些辅助函数的代码重复出现,可以在依赖它们时通过 require('babel-runtime/helpers/createClass') 的方式导入,这样就能做到只让它们出现一次。babel-plugin-transform-runtime 插件就是用来实现这个作用的,将相关辅助函数进行替换成导入语句,从而减小 babel 编译出来的代码的文件大小。
//首先,安装 babel-plugin-transform-runtime : npm install babel-plugin-transform-runtime --save-dev //然后,修改 .babelrc 配置文件为: "plugins": [ "transform-runtime" ] 1 2 3 4 5 6 插件的更多详细内容,可以查看官网babel-plugin-transform-runtime 的 详细介绍
😰提取公共代码
如果项目中没有去将每个页面的第三方库和公共模块提取出来,则项目会存在以下问题:
相同的资源被重复加载,浪费用户的流量和服务器的成本。 每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验。 所以我们需要将多个页面的公共代码抽离成单独的文件,来优化以上问题 。Webpack 内置了专门用于提取多个Chunk 中的公共部分的插件 CommonsChunkPlugin,我们在项目中 CommonsChunkPlugin 的配置如下:
// 所有在 package.json 里面依赖的包,都会被打包进 vendor.js 这个文件中。 new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: function(module, count) { return ( module.resource && /.js$/.test(module.resource) && module.resource.indexOf( path.join(__dirname, '../node_modules') ) === 0 ); } }), // 抽取出代码模块的映射关系 new webpack.optimize.CommonsChunkPlugin({ name: 'manifest', chunks: ['vendor'] })
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 要看插件的更多详细内容,可以查看 官网CommonsChunkPlugin 的 详细介绍
😰模板预编译
当使用 DOM 内模板或 JavaScript 内的字符串模板时,模板会在运行时被编译为渲染函数。通常情况下这个过程已经足够快了,但对性能敏感的应用还是最好避免这种用法。
预编译模板最简单的方式就是使用单文件组件——相关的构建设置会自动把预编译处理好,所以构建好的代码已经包含了编译出来的渲染函数而不是原始的模板字符串。
如果你使用 webpack,并且喜欢分离 JavaScript 和模板文件,你可以使用 vue-template-loader,它也可以在构建过程中把模板文件转换成为 JavaScript 渲染函数。
😰提取组件的 CSS
当使用单文件组件时,组件内的 CSS 会以 style 标签的方式通过 JavaScript 动态注入。这有一些小小的运行时开销,如果你使用服务端渲染,这会导致一段 “无样式内容闪烁 (fouc) ” 。将所有组件的 CSS 提取到同一个文件可以避免这个问题,也会让 CSS 更好地进行压缩和缓存。
查阅这个构建工具各自的文档来了解更多:
webpack + vue-loader ( vue-cli 的 webpack 模板已经预先配置好) Browserify + vueify Rollup + rollup-plugin-vue 😰优化 SourceMap
我们在项目进行打包后,会将开发中的多个文件代码打包到一个文件中,并且经过压缩、去掉多余的空格、babel编译化后,最终将编译得到的代码会用于线上环境,那么这样处理后的代码和源代码会有很大的差别,当有 bug的时候,我们只能定位到压缩处理后的代码位置,无法定位到开发环境中的代码,对于开发来说不好调式定位问题,因此 sourceMap 出现了,它就是为了解决不好调式代码问题的。
SourceMap 的可选值如下(+ 号越多,代表速度越快,- 号越多,代表速度越慢, o 代表中等速度 )
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UyT7liGC-1637477843551)(./images/1.png)]
开发环境推荐:cheap-module-eval-source-map
生产环境推荐:cheap-module-source-map
原因如下:
cheap:源代码中的列信息是没有任何作用,因此我们打包后的文件不希望包含列相关信息,只有行信息能建立打包前后的依赖关系。因此不管是开发环境或生产环境,我们都希望添加 cheap 的基本类型来忽略打包前后的列信息; module :不管是开发环境还是正式环境,我们都希望能定位到bug的源代码具体的位置,比如说某个 Vue 文件报错了,我们希望能定位到具体的 Vue 文件,因此我们也需要 module 配置; soure-map :source-map 会为每一个打包后的模块生成独立的 soucemap 文件 ,因此我们需要增加source-map 属性; eval-source-map:eval 打包代码的速度非常快,因为它不生成 map 文件,但是可以对 eval 组合使用 eval-source-map 使用会将 map 文件以 DataURL 的形式存在打包后的 js 文件中。在正式环境中不要使用 eval-source-map, 因为它会增加文件的大小,但是在开发环境中,可以试用下,因为他们打包的速度很快。 😰构建结果输出分析
Webpack 输出的代码可读性非常差而且文件非常大,让我们非常头疼。为了更简单、直观地分析输出结果,社区中出现了许多可视化分析工具。这些工具以图形的方式将结果更直观地展示出来,让我们快速了解问题所在。接下来讲解我们在 Vue 项目中用到的分析工具:webpack-bundle-analyzer 。
我们在项目中 webpack.prod.conf.js 进行配置:
if (config.build.bundleAnalyzerReport) { var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; webpackConfig.plugins.push(new BundleAnalyzerPlugin()); } 1 2 3 4 执行 $ npm run build --report 后生成分析报告如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v93BiJMl-1637477843554)(./images/2.png)]
Vue 项目的编译优化
如果你的 Vue 项目使用 Webpack 编译,需要你喝一杯咖啡的时间,那么也许你需要对项目的 Webpack 配置进行优化,提高 Webpack 的构建效率。具体如何进行 Vue 项目的 Webpack 构建优化,暂未学习
三、基础的 Web 技术优化 🤣开启 gzip 压缩
gzip 是 GNUzip 的缩写,最早用于 UNIX 系统的文件压缩。HTTP 协议上的 gzip 编码是一种用来改进 web 应用程序性能的技术,web 服务器和客户端(浏览器)必须共同支持 gzip。目前主流的浏览器,Chrome,firefox,IE等都支持该协议。常见的服务器如 Apache,Nginx,IIS 同样支持,gzip 压缩效率非常高,通常可以达到 70% 的压缩率,也就是说,如果你的网页有 30K,压缩之后就变成了 9K 左右
以下我们以服务端使用我们熟悉的 express 为例,开启 gzip 非常简单,相关步骤如下:
//安装: npm install compression --save //添加代码逻辑: var compression = require('compression'); var app = express(); app.use(compression()) 1 2 3 4 5 6 重启服务,观察网络面板里面的 response header,如果看到如下红圈里的字段则表明 gzip 开启成功 :
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oOEAmyzu-1637477843556)(./images/3.png)]
🤣浏览器缓存
为了提高用户加载页面的速度,对静态资源进行缓存是非常必要的,根据是否需要重新向服务器发起请求来分类,将 HTTP 缓存规则分为两大类(强制缓存,对比缓存)
🤣CDN 的使用
浏览器从服务器上下载 CSS、js 和图片等文件时都要和服务器连接,而大部分服务器的带宽有限,如果超过限制,网页就半天反应不过来。而 CDN 可以通过不同的域名来加载文件,从而使下载文件的并发连接数大大增加,且CDN 具有更好的可用性,更低的网络延迟和丢包率 。
vue-lazyload库懒加载
如何实现双向绑定?底层原理是什么?
1、如何实现双向绑定? 以用户提交表单为例,其原理是我们对input进行value的属性绑定(v-bind:value="…"),将Model中的变量绑定到View上(Model -> View),以及当用户对input进行操作时,进行事件监听(v-on: input =" … "),从而实现双向数据绑定。v-model实际上是语法糖,结合了上述两个操作。
2、底层原理是什么? 参考:Vue的MVVM是如何实现的 如何追踪数据变化? Vue将遍历传入Vue实例data选项中的js对象的所有property,并使用Object.defineProperty 把这些 property 全部转为getter/setter。这些getter/setter可使property在被访问和修改时通知变更。每个组件实例都对应一个watcher实例,它会在组件渲染的过程中把“接触”过的数据property记为依赖,之后当依赖项的setter触发时,会通知watcher,从而使它关联的组件重新渲染。
vue实现数据双向绑定主要是采用数据劫持,配合发布-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter和getter,在数据变动时发布消息给订阅者,触发响应监听回调。 vue数据双向绑定整合Observer Compile 和 Watcher三者,
(1) 通过Observer来监听自己Model的数据变化: Obeject.defineProperty()来监听属性变动,将需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter,同时创建一个消息订阅器Dep用来收集订阅者,数据变动之后触发notify,再调用订阅者的update方法 (2) 通过Complie来解析编译模板指令: 对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。(每次找到一个数据替换,都要重新渲染一遍,可能会造成页面的回流和重绘,那么我们最好的办法就是把以上的元素放在内存中,在内存中操作完成之后,再替换掉.) (3) 通过Watcher搭起Observer 和Compile 之间的通信桥梁: 能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图,达到 数据变化 -> 视图更新,视图交互变化 -> 数据model变更。什么时候添加绑定watcher? 当订阅数据变化时,来绑定更新函数,从而让watcher更新视图
(1)观察者模式和发布订阅者模式的区别
观察者模式: 在观察者模式里,被观察者subject只需要维护一套观察者observer的集合,这些observer实现相同的接口,subject只需要知道,通知observer时,需要调用哪个方法就好了。观察者模式是由具体目标调度的,其订阅者与发布者之间存在依赖。 发布订阅者模式: :发布订阅模式里,不仅仅只有发布者和订阅者两个角色,还存在一个调度中心,当事件触发时,发布者发布该事件到调度中心(顺带上下文),由调度中心统一调度订阅者注册到调度中心的处理代码。发布/订阅模式是统一由调度中心调的,其订阅者与发布者之间不存在依赖。
(2)Object.defineProperty可以对数组对象数据进行劫持吗?数组对象如何实现数据的响应更新? 参考:深入理解 Object.defineProperty 及实现数据双向绑定
Object.defineProperty(obj, prop, descriptor); 1 descriptor由两部分组成:数据描述符(configurable,enumerable,value 及 writable 配置项)和访问器描述符(configurable,enumerable,get以及set),即使用访问器描述符中 getter或 setter方法的话,不允许使用 writable 和 value 这两个配置项。
ans:
当我们使用 Object.defineProperty 对数组赋值有一个新对象的时候,会执行set方法,但是当我们改变数组中的某一项值的时候,或者使用数组中的push等其他的方法,或者改变数组的长度,都不会执行set方法 通过重写 Array.property.push方法,并且生成一个新的数组赋值给数据,这样数据双向绑定就触发了 重新编写数组的方法: const arrPush = {};
// 如下是 数组的常用方法 const arrayMethods = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]; // 对数组的方法进行重写 arrayMethods.forEach((method) => {
const original = Array.prototype[method]; arrPush[method] = function() { console.log(this); return original.apply(this, arguments); } });
const testPush = []; // 对 testPush 的原型 指向 arrPush,因此testPush也有重写后的方法 testPush.proto = arrPush;
testPush.push(1); // 打印 [], this指向了 testPush
testPush.push(2); // 打印 [1], this指向了 testPush
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 (3)Object.defineProperty和Proxy的区别 Object.definePropery是对对象的属性的劫持,而Proxy是对对象的劫持,因此对于新增的属性不用重新定义getter,setter特性,Proxy也可以实现劫持,同时对于复杂对象也不必进行深度遍历。Vue3中将使用Proxy来实现数据劫持. 参考:Object.defineProperty与Proxy理解整理
let p = new Proxy(target, handler) 1 Object.defineProperty()的主要问题 不能监听数组的变化 必须遍历对象的每个属性 必须深层遍历嵌套的对象 Proxy 针对对象:针对整个对象,而不是对象的某个属性。相比于Object.defineProperty(),省了一个 Object.keys() 的遍历 支持数组:不需要对数组的方法进行重载 嵌套支持:和 Object.defineProperty() 是一样的,也需要通过逐层遍历来解决。Proxy 的写法是在 get 里面递归调用 Proxy 并返回 Proxy的优劣势 优势:Proxy 的第二个参数可以有 13 种拦截方法,比 Object.defineProperty() 要更加丰富 劣势:Proxy 的兼容性不如 Object.defineProperty(), 不能使用 polyfill 来处理兼容性
双向数据绑定原理(三种实现方式)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>双向数据绑定原理(三种实现方式)</title>
</head>
<body>
<input type="text" id="a" />
<span id="b"></span>
<!--
//脏检查
我们说Angularjs(这里特指AngularJS 1.x.x版本,不代表AngularJS 2.x.x版本)双向数据绑定的技术实现是脏检查,大致的原理就是,
Angularjs内部会维护一个序列,将所有需要监控的属性放在这个序列中,当发生某些特定事件时(注意,
这里并不是定时的而是由某些特殊事件触发的),Angularjs会调用 $digest 方法,这个方法内部做的逻辑就是遍历所有的watcher,
对被监控的属性做对比,对比其在方法调用前后属性值有没有发生变化,如果发生变化,则调用对应的handler。
网上有许多剖析Angularjs双向数据绑定实现原理的文章,比如 这篇 ,再比如 这篇 ,等等。
这种方式的缺点很明显,遍历轮训watcher是非常消耗性能的,特别是当单页的监控数量达到一个数量级的时候。
//观察机制
博主之前有一篇转载翻译的文章, Object.observe()带来的数据绑定变革 ,说的就是使用ECMAScript7中的 Object.observe 方法对对象
(或者其属性)进行监控观察,一旦其发生变化时,将会执行相应的handler。
这是目前监控属性数据变更最完美的一种方法,语言(浏览器)原生支持,没有什么比这个更好了。唯一的遗憾就是目前支持广度还不行,有待全面推广。
//封装属性访问器
国产mvvm框架vue.js实现数据双向绑定的原理就是属性访问器。
它使用了ECMAScript5.1(ECMA-262)中定义的标准属性 Object.defineProperty 方法。针对国内行情,
部分还不支持 Object.defineProperty 低级浏览器采用VBScript作了完美兼容,不像其他的mvvm框架已经逐渐放弃对低端浏览器的支持。
-->
<script>
//封装属性访问器
//Object.defineProperty(obj, prop, descriptor)
//obj ,待修改的对象
//prop ,带修改的属性名称
//descriptor ,待修改属性的相关描述
var obj = {};
Object.defineProperty(obj,'a',{
set:function(newVal){
document.getElementById('a').value = newVal;
document.getElementById('b').innerHTML = newVal;
}
});
document.addEventListener('keyup',function(e){
obj.a = e.target.value;
});
</script>
</body>
</html>