意见箱
恒创运营部门将仔细参阅您的意见和建议,必要时将通过预留邮箱与您保持联络。感谢您的支持!
意见/建议
提交建议

腾讯前端一面常考面试题合集

来源:恒创科技 编辑:恒创科技编辑部
2024-01-27 15:39:59
TCP粘包是怎么回事,如何处理?

默认情况下, TCP 连接会启⽤延迟传送算法 (Nagle 算法), 在数据发送之前缓存他们. 如果短时间有多个数据发送, 会缓冲到⼀起作⼀次发送 (缓冲⼤⼩⻅ socket.bufferSize ), 这样可以减少 IO 消耗提⾼性能.

如果是传输⽂件的话, 那么根本不⽤处理粘包的问题, 来⼀个包拼⼀个包就好了。但是如果是多条消息, 或者是别的⽤途的数据那么就需要处理粘包.

下面看⼀个例⼦, 连续调⽤两次 send 分别发送两段数据 data1 和 data2, 在接收端有以下⼏种常⻅的情况:
A. 先接收到 data1, 然后接收到 data2 .
B. 先接收到 data1 的部分数据, 然后接收到 data1 余下的部分以及 data2 的全部.
C. 先接收到了 data1 的全部数据和 data2 的部分数据, 然后接收到了 data2 的余下的数据.
D. ⼀次性接收到了 data1 和 data2 的全部数据.


腾讯前端一面常考面试题合集

其中的 BCD 就是我们常⻅的粘包的情况. ⽽对于处理粘包的问题, 常⻅的解决⽅案有:

多次发送之前间隔⼀个等待时间:只需要等上⼀段时间再进⾏下⼀次 send 就好, 适⽤于交互频率特别低的场景. 缺点也很明显, 对于⽐较频繁的场景⽽⾔传输效率实在太低,不过⼏乎不⽤做什么处理.关闭 Nagle 算法:关闭 Nagle 算法, 在 Node.js 中你可以通过 socket.setNoDelay() ⽅法来关闭 Nagle 算法, 让每⼀次 send 都不缓冲直接发送。该⽅法⽐较适⽤于每次发送的数据都⽐较⼤ (但不是⽂件那么⼤), 并且频率不是特别⾼的场景。如果是每次发送的数据量⽐较⼩, 并且频率特别⾼的, 关闭 Nagle 纯属⾃废武功。另外, 该⽅法不适⽤于⽹络较差的情况, 因为 Nagle 算法是在服务端进⾏的包合并情况, 但是如果短时间内客户端的⽹络情况不好, 或者应⽤层由于某些原因不能及时将 TCP 的数据 recv, 就会造成多个包在客户端缓冲从⽽粘包的情况。 (如果是在稳定的机房内部通信那么这个概率是⽐较⼩可以选择忽略的)进⾏封包/拆包: 封包/拆包是⽬前业内常⻅的解决⽅案了。即给每个数据包在发送之前, 于其前/后放⼀些有特征的数据, 然后收到数据的时 候根据特征数据分割出来各个数据包。数组去重

实现代码如下:

function uniqueArr(arr) {
  return [...new Set(arr)];
}
手写题:数组扁平化
function flatten(arr) {
  let result = [];

  for (let i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i])) {
      result = result.concat(flatten(arr[i]));
    } else {
      result = result.concat(arr[i]);
    }
  }

  return result;
}

const a = [1, [2, [3, 4]]];
console.log(flatten(a));

偏函数

什么是偏函数?偏函数就是将一个 n 参的函数转换成固定 x 参的函数,剩余参数(n - x)将在下次调用全部传入。举个例子:

function add(a, b, c) {
    return a + b + c
}
let partialAdd = partial(add, 1)
partialAdd(2, 3)

发现没有,其实偏函数和函数柯里化有点像,所以根据函数柯里化的实现,能够能很快写出偏函数的实现:

function partial(fn, ...args) {
    return (...arg) => {
        return fn(...args, ...arg)
    }
}

如上这个功能比较简单,现在我们希望偏函数能和柯里化一样能实现占位功能,比如:

function clg(a, b, c) {
    console.log(a, b, c)
}
let partialClg = partial(clg, '_', 2)
partialClg(1, 3)  // 依次打印:1, 2, 3

_ 占的位其实就是 1 的位置。相当于:partial(clg, 1, 2),然后 partialClg(3)。明白了原理,我们就来写实现:

function partial(fn, ...args) {
    return (...arg) => {
        args[index] = 
        return fn(...args, ...arg)
    }
}
图片懒加载

与普通的图片懒加载不同,如下这个多做了 2 个精心处理:

图片全部加载完成后移除事件监听;加载完的图片,从 imgList 移除;
let imgList = [...document.querySelectorAll('img')]
let length = imgList.length

// 修正错误,需要加上自执行
- const imgLazyLoad = function() {
+ const imgLazyLoad = (function() {
    let count = 0

   return function() {
        let deleteIndexList = []
        imgList.forEach((img, index) => {
            let rect = img.getBoundingClientRect()
            if (rect.top < window.innerHeight) {
                img.src = img.dataset.src
                deleteIndexList.push(index)
                count++
                if (count === length) {
                    document.removeEventListener('scroll', imgLazyLoad)
                }
            }
        })
        imgList = imgList.filter((img, index) => !deleteIndexList.includes(index))
   }
- }
+ })()

// 这里最好加上防抖处理
document.addEventListener('scroll', imgLazyLoad)
继承

原型链继承

function Animal() {
    this.colors = ['black', 'white']
}
Animal.prototype.getColor = function() {
    return this.colors
}
function Dog() {}
Dog.prototype =  new Animal()

let dog1 = new Dog()
dog1.colors.push('brown')
let dog2 = new Dog()
console.log(dog2.colors)  // ['black', 'white', 'brown']

原型链继承存在的问题:

问题1:原型中包含的引用类型属性将被所有实例共享;问题2:子类在实例化的时候不能给父类构造函数传参;

借用构造函数实现继承

function Animal(name) {
    this.name = name
    this.getName = function() {
        return this.name
    }
}
function Dog(name) {
    Animal.call(this, name)
}
Dog.prototype =  new Animal()

借用构造函数实现继承解决了原型链继承的 2 个问题:引用类型共享问题以及传参问题。但是由于方法必须定义在构造函数中,所以会导致每次创建子类实例都会创建一遍方法。

组合继承

组合继承结合了原型链和盗用构造函数,将两者的优点集中了起来。基本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。

function Animal(name) {
    this.name = name
    this.colors = ['black', 'white']
}
Animal.prototype.getName = function() {
    return this.name
}
function Dog(name, age) {
    Animal.call(this, name)
    this.age = age
}
Dog.prototype =  new Animal()
Dog.prototype.constructor = Dog

let dog1 = new Dog('奶昔', 2)
dog1.colors.push('brown')
let dog2 = new Dog('哈赤', 1)
console.log(dog2) 
// { name: "哈赤", colors: ["black", "white"], age: 1 }

寄生式组合继承

组合继承已经相对完善了,但还是存在问题,它的问题就是调用了 2 次父类构造函数,第一次是在 new Animal(),第二次是在 Animal.call() 这里。

所以解决方案就是不直接调用父类构造函数给子类原型赋值,而是通过创建空函数 F 获取父类原型的副本。

寄生式组合继承写法上和组合继承基本类似,区别是如下这里:

- Dog.prototype =  new Animal()
- Dog.prototype.constructor = Dog

+ function F() {}
+ F.prototype = Animal.prototype
+ let f = new F()
+ f.constructor = Dog
+ Dog.prototype = f

稍微封装下上面添加的代码后:

function object(o) {
    function F() {}
    F.prototype = o
    return new F()
}
function inheritPrototype(child, parent) {
    let prototype = object(parent.prototype)
    prototype.constructor = child
    child.prototype = prototype
}
inheritPrototype(Dog, Animal)

如果你嫌弃上面的代码太多了,还可以基于组合继承的代码改成最简单的寄生式组合继承:

- Dog.prototype =  new Animal()
- Dog.prototype.constructor = Dog

+ Dog.prototype =  Object.create(Animal.prototype)
+ Dog.prototype.constructor = Dog

class 实现继承

class Animal {
    constructor(name) {
        this.name = name
    } 
    getName() {
        return this.name
    }
}
class Dog extends Animal {
    constructor(name, age) {
        super(name)
        this.age = age
    }
}
setTimeout、setInterval、requestAnimationFrame 各有什么特点?

异步编程当然少不了定时器了,常见的定时器函数有 setTimeoutsetIntervalrequestAnimationFrame。最常用的是setTimeout,很多人认为 setTimeout 是延时多久,那就应该是多久后执行。

其实这个观点是错误的,因为 JS 是单线程执行的,如果前面的代码影响了性能,就会导致 setTimeout 不会按期执行。当然了,可以通过代码去修正 setTimeout,从而使定时器相对准确:

let period = 60 * 1000 * 60 * 2
let startTime = new Date().getTime()
let count = 0
let end = new Date().getTime() + period
let interval = 1000
let currentInterval = interval
function loop() {
  count++
  // 代码执行所消耗的时间
  let offset = new Date().getTime() - (startTime + count * interval);
  let diff = end - new Date().getTime()
  let h = Math.floor(diff / (60 * 1000 * 60))
  let hdiff = diff % (60 * 1000 * 60)
  let m = Math.floor(hdiff / (60 * 1000))
  let mdiff = hdiff % (60 * 1000)
  let s = mdiff / (1000)
  let sCeil = Math.ceil(s)
  let sFloor = Math.floor(s)
  // 得到下一次循环所消耗的时间
  currentInterval = interval - offset 
  console.log('时:'+h, '分:'+m, '毫秒:'+s, '秒向上取整:'+sCeil, '代码执行时间:'+offset, '下次循环间隔'+currentInterval) 
  setTimeout(loop, currentInterval)
}
setTimeout(loop, currentInterval)

接下来看 setInterval,其实这个函数作用和 setTimeout 基本一致,只是该函数是每隔一段时间执行一次回调函数。

通常来说不建议使用 setInterval。第一,它和 setTimeout 一样,不能保证在预期的时间执行任务。第二,它存在执行累积的问题,请看以下伪代码

function demo() {
  setInterval(function(){
    console.log(2)
  },1000)
  sleep(2000)
}
demo()

以上代码在浏览器环境中,如果定时器执行过程中出现了耗时操作,多个回调函数会在耗时操作结束以后同时执行,这样可能就会带来性能上的问题。

如果有循环定时器的需求,其实完全可以通过 requestAnimationFrame 来实现:

function setInterval(callback, interval) {
  let timer
  const now = Date.now
  let startTime = now()
  let endTime = startTime
  const loop = () => {
    timer = window.requestAnimationFrame(loop)
    endTime = now()
    if (endTime - startTime >= interval) {
      startTime = endTime = now()
      callback(timer)
    }
  }
  timer = window.requestAnimationFrame(loop)
  return timer
}
let a = 0
setInterval(timer => {
  console.log(1)
  a++
  if (a === 3) cancelAnimationFrame(timer)
}, 1000)

首先 requestAnimationFrame 自带函数节流功能,基本可以保证在 16.6 毫秒内只执行一次(不掉帧的情况下),并且该函数的延时效果是精确的,没有其他定时器时间不准的问题,当然你也可以通过该函数来实现 setTimeout

一个 tcp 连接能发几个 http 请求?

如果是 HTTP 1.0 版本协议,一般情况下,不支持长连接,因此在每次请求发送完毕之后,TCP 连接即会断开,因此一个 TCP 发送一个 HTTP 请求,但是有一种情况可以将一条 TCP 连接保持在活跃状态,那就是通过 Connection 和 Keep-Alive 首部,在请求头带上 Connection: Keep-Alive,并且可以通过 Keep-Alive 通用首部中指定的,用逗号分隔的选项调节 keep-alive 的行为,如果客户端和服务端都支持,那么其实也可以发送多条,不过此方式也有限制,可以关注《HTTP 权威指南》4.5.5 节对于 Keep-Alive 连接的限制和规则。

而如果是 HTTP 1.1 版本协议,支持了长连接,因此只要 TCP 连接不断开,便可以一直发送 HTTP 请求,持续不断,没有上限; 同样,如果是 HTTP 2.0 版本协议,支持多用复用,一个 TCP 连接是可以并发多个 HTTP 请求的,同样也是支持长连接,因此只要不断开 TCP 的连接,HTTP 请求数也是可以没有上限地持续发送

HTML5有哪些更新1. 语义化标签header:定义文档的页眉(头部);nav:定义导航链接的部分;footer:定义文档或节的页脚(底部);article:定义文章内容;section:定义文档中的节(section、区段);aside:定义其所处内容之外的内容(侧边);2. 媒体标签

(1) audio:音频

<audio src='' controls autoplay loop='true'></audio>

属性:

controls 控制面板autoplay 自动播放loop=‘true’ 循环播放

(2)video视频

<video src='' poster='imgs/aa.jpg' controls></video>

属性:

poster:指定视频还没有完全下载完毕,或者用户还没有点击播放前显示的封面。默认显示当前视频文件的第一针画面,当然通过poster也可以自己指定。controls 控制面板widthheight

(3)source标签
因为浏览器对视频格式支持程度不一样,为了能够兼容不同的浏览器,可以通过source来指定视频源。

<video>
     <source src='aa.flv' type='video/flv'></source>
     <source src='aa.mp4' type='video/mp4'></source>
</video>
3. 表单

表单类型:

email :能够验证当前输入的邮箱地址是否合法url : 验证URLnumber : 只能输入数字,其他输入不了,而且自带上下增大减小箭头,max属性可以设置为最大值,min可以设置为最小值,value为默认值。search : 输入框后面会给提供一个小叉,可以删除输入的内容,更加人性化。range : 可以提供给一个范围,其中可以设置max和min以及value,其中value属性可以设置为默认值color : 提供了一个颜色拾取器time : 时分秒data : 日期选择年月日datatime : 时间和日期(目前只有Safari支持)datatime-local :日期时间控件week :周控件month:月控件

表单属性:

placeholder :提示信息autofocus :自动获取焦点

autocomplete=“on” 或者 autocomplete=“off” 使用这个属性需要有两个前提:

表单必须提交过必须有name属性。required:要求输入框不能为空,必须有值才能够提交。pattern=" " 里面写入想要的正则模式,例如手机号patte="^(+86)?\d{10}$"multiple:可以选择多个文件或者多个邮箱form=" form表单的ID"

表单事件:

oninput 每当input里的输入框内容发生变化都会触发此事件。oninvalid 当验证不通过时触发此事件。4. 进度条、度量器progress标签:用来表示任务的进度(IE、Safari不支持),max用来表示任务的进度,value表示已完成多少

meter属性:用来显示剩余容量或剩余库存(IE、Safari不支持)

high/low:规定被视作高/低的范围max/min:规定最大/小值value:规定当前度量值

设置规则:min < low < high < max

5.DOM查询操作document.querySelector()document.querySelectorAll()

它们选择的对象可以是标签,可以是类(需要加点),可以是ID(需要加#)

6. Web存储

HTML5 提供了两种在客户端存储数据的新方法:

localStorage - 没有时间限制的数据存储sessionStorage - 针对一个 session 的数据存储7. 其他拖放:拖放是一种常见的特性,即抓取对象以后拖到另一个位置。设置元素可拖放:
<img draggable="true" />
画布(canvas ): canvas 元素使用 JavaScript 在网页上绘制图像。画布是一个矩形区域,可以控制其每一像素。canvas 拥有多种绘制路径、矩形、圆形、字符以及添加图像的方法。
<canvas id="myCanvas" width="200" height="100"></canvas>
SVG:SVG 指可伸缩矢量图形,用于定义用于网络的基于矢量的图形,使用 XML 格式定义图形,图像在放大或改变尺寸的情况下其图形质量不会有损失,它是万维网联盟的标准地理定位:Geolocation(地理定位)用于定位用户的位置。‘

总结: (1)新增语义化标签:nav、header、footer、aside、section、article
(2)音频、视频标签:audio、video
(3)数据存储:localStorage、sessionStorage
(4)canvas(画布)、Geolocation(地理定位)、websocket(通信协议)
(5)input标签新增属性:placeholder、autocomplete、autofocus、required
(6)history API:go、forward、back、pushstate

移除的元素有:

纯表现的元素:basefont,big,center,font, s,strike,tt,u;对可用性产生负面影响的元素:frame,frameset,noframes;代码输出结果
function runAsync(x) {
  const p = new Promise(r =>
    setTimeout(() => r(x, console.log(x)), 1000)
  );
  return p;
}
function runReject(x) {
  const p = new Promise((res, rej) =>
    setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x)
  );
  return p;
}
Promise.race([runReject(0), runAsync(1), runAsync(2), runAsync(3)])
  .then(res => console.log("result: ", res))
  .catch(err => console.log(err));

输出结果如下:

0
Error: 0
1
2
3

可以看到在catch捕获到第一个错误之后,后面的代码还不执行,不过不会再被捕获了。

注意:allrace传入的数组中如果有会抛出异常的异步任务,那么只有最先抛出的错误会被捕获,并且是被then的第二个参数或者后面的catch捕获;但并不会影响数组中其它的异步任务的执行。

对对象与数组的解构的理解

解构是 ES6 提供的一种新的提取数据的模式,这种模式能够从对象或数组里有针对性地拿到想要的数值。 1)数组的解构 在解构数组时,以元素的位置为匹配条件来提取想要的数据的:

const [a, b, c] = [1, 2, 3]

最终,a、b、c分别被赋予了数组第0、1、2个索引位的值:

数组里的0、1、2索引位的元素值,精准地被映射到了左侧的第0、1、2个变量里去,这就是数组解构的工作模式。还可以通过给左侧变量数组设置空占位的方式,实现对数组中某几个元素的精准提取:

const [a,,c] = [1,2,3]

通过把中间位留空,可以顺利地把数组第一位和最后一位的值赋给 a、c 两个变量:

2)对象的解构 对象解构比数组结构稍微复杂一些,也更显强大。在解构对象时,是以属性的名称为匹配条件,来提取想要的数据的。现在定义一个对象:

const stu = {
  name: 'Bob',
  age: 24
}

假如想要解构它的两个自有属性,可以这样:

const { name, age } = stu

这样就得到了 name 和 age 两个和 stu 平级的变量:

注意,对象解构严格以属性名作为定位依据,所以就算调换了 name 和 age 的位置,结果也是一样的:

const { age, name } = stu
HTTP 1.0 和 HTTP 1.1 之间有哪些区别?

HTTP 1.0和 HTTP 1.1 有以下区别

连接方面,http1.0 默认使用非持久连接,而 http1.1 默认使用持久连接。http1.1 通过使用持久连接来使多个 http 请求复用同一个 TCP 连接,以此来避免使用非持久连接时每次需要建立连接的时延。资源请求方面,在 http1.0 中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,http1.1 则在请求头引入了 range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。缓存方面,在 http1.0 中主要使用 header 里的 If-Modified-Since、Expires 来做为缓存判断的标准,http1.1 则引入了更多的缓存控制策略,例如 Etag、If-Unmodified-Since、If-Match、If-None-Match 等更多可供选择的缓存头来控制缓存策略。http1.1 中新增了 host 字段,用来指定服务器的域名。http1.0 中认为每台服务器都绑定一个唯一的 IP 地址,因此,请求消息中的 URL 并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机,并且它们共享一个IP地址。因此有了 host 字段,这样就可以将请求发往到同一台服务器上的不同网站。http1.1 相对于 http1.0 还新增了很多请求方法,如 PUT、HEAD、OPTIONS 等。如何实现浏览器内多个标签页之间的通信?

实现多个标签页之间的通信,本质上都是通过中介者模式来实现的。因为标签页之间没有办法直接通信,因此我们可以找一个中介者,让标签页和中介者进行通信,然后让这个中介者来进行消息的转发。通信方法如下:

使用 websocket 协议,因为 websocket 协议可以实现服务器推送,所以服务器就可以用来当做这个中介者。标签页通过向服务器发送数据,然后由服务器向其他标签页推送转发。使用 ShareWorker 的方式,shareWorker 会在页面存在的生命周期内创建一个唯一的线程,并且开启多个页面也只会使用同一个线程。这个时候共享线程就可以充当中介者的角色。标签页间通过共享一个线程,然后通过这个共享的线程来实现数据的交换。使用 localStorage 的方式,我们可以在一个标签页对 localStorage 的变化事件进行监听,然后当另一个标签页修改数据的时候,我们就可以通过这个监听事件来获取到数据。这个时候 localStorage 对象就是充当的中介者的角色。使用 postMessage 方法,如果我们能够获得对应标签页的引用,就可以使用postMessage 方法,进行通信。事件循环机制 (Event Loop)

事件循环机制从整体上告诉了我们 JavaScript 代码的执行顺序 Event Loop即事件循环,是指浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。

先执行 Script 脚本,然后清空微任务队列,然后开始下一轮事件循环,继续先执行宏任务,再清空微任务队列,如此往复。

宏任务:Script/setTimeout/setInterval/setImmediate/ I/O / UI Rendering微任务:process.nextTick()/Promise

上诉的 setTimeout 和 setInterval 等都是任务源,真正进入任务队列的是他们分发的任务。

优先级

setTimeout = setInterval 一个队列setTimeout > setImmediateprocess.nextTick > Promise
for (const macroTask of macroTaskQueue) {  
  handleMacroTask();    
  for (const microTask of microTaskQueue) {    
      handleMicroTask(microTask);  
  }
}
进程之前的通信方式

(1)管道通信

管道是一种最基本的进程间通信机制。管道就是操作系统在内核中开辟的一段缓冲区,进程1可以将需要交互的数据拷贝到这段缓冲区,进程2就可以读取了。

管道的特点:

只能单向通信只能血缘关系的进程进行通信依赖于文件系统生命周期随进程面向字节流的服务管道内部提供了同步机制

(2)消息队列通信

消息队列就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等。消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。 每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。

使用消息队列进行进程间通信,可能会收到数据块最大长度的限制约束等,这也是这种通信方式的缺点。如果频繁的发生进程间的通信行为,那么进程需要频繁地读取队列中的数据到内存,相当于间接地从一个进程拷贝到另一个进程,这需要花费时间。

(3)信号量通信

共享内存最大的问题就是多进程竞争内存的问题,就像类似于线程安全问题。我们可以使用信号量来解决这个问题。信号量的本质就是一个计数器,用来实现进程之间的互斥与同步。例如信号量的初始值是 1,然后 a 进程来访问内存1的时候,我们就把信号量的值设为 0,然后进程b 也要来访问内存1的时候,看到信号量的值为 0 就知道已经有进程在访问内存1了,这个时候进程 b 就会访问不了内存1。所以说,信号量也是进程之间的一种通信方式。

(4)信号通信

信号(Signals )是Unix系统中使用的最古老的进程间通信的方法之一。操作系统通过信号来通知进程系统中发生了某种预先规定好的事件(一组事件中的一个),它也是用户进程之间通信和同步的一种原始机制。

(5)共享内存通信

共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问(使多个进程可以访问同一块内存空间)。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。

(6)套接字通信

上面说的共享内存、管道、信号量、消息队列,他们都是多个进程在一台主机之间的通信,那两个相隔几千里的进程能够进行通信吗?答是必须的,这个时候 Socket 这家伙就派上用场了,例如我们平时通过浏览器发起一个 http 请求,然后服务器给你返回对应的数据,这种就是采用 Socket 的通信方式了。

说一下购物车的逻辑?
//vue中购物车逻辑的实现
1. 购物车信息用一个数组来存储,数组中保存对象,对象中有id和count属性

2. 在vuex中state中添加一个数据 cartList 用来保存这个数组

3. 由于商品详情页需要用到加入购物车功能,所以我们需要提供一个mutation, 用来将购物车信息加入 cartList中

4. 加入购物车信息的时候,遵照如下规则: 如果购物车中已经有了该商品信息,则数量累加,如果没有该商品信息,则新增一个对象

5. 在商品详情页,点击加入购物车按钮的时候,调用vuex提供的addToCart这个mutation将当前的商品信息 (id count)传给addTocart  this.$store.commit("addToCart", {id:  , count:})

// js中购物车逻辑的实现
1.商品页点击“加入购物车”按钮,触发事件

2.事件调用购物车“增加商品”的Js程序(函数、对象方法)

3.向Js程序传递传递“商品id”、“商品数量”等数据

4.存储“商品id”、“商品数量”到浏览器的localStorage中

**展示购物车中的商品******

1.打开购物车页面

2.从localStorage中取出“商品Id”、“商品数量”等信息。

3.调用服务器端“获得商品详情”的接口得到购物车中的商品信息(参数为商品Id)

4.将获得的商品信息显示在购物车页面。

**完成购物车中商品的购买******

1.用户对购物车中的商品完成购买流程,产生购物订单

2.清除localStorage中存储的已经购买的商品信息

备注1:购物车中商品存储的数据除了“商品id”、“商品数量”之外,根据产品要求还可以有其他的信息,例如完整的商品详情(这样就不用掉服务器接口获得详情了)、购物车商品的过期时间,超过时间的购物车商品在下次打开网站或者购物车页面时被清除。

备注2:购物车商品除了存储在localStorage中,根据产品的需求不同,也可以存储在sessionStorage、cookie、session中,或者直接向服务器接口发起请求存储在服务器上。何种情况使用哪种方式存储、有啥区别请自己分析。
怎么解决白屏问题
1、加loading
2、骨架屏
上一篇: 前端二面必会面试题(附答案) 下一篇: 手机怎么远程登录云服务器?