- 性能优化:
一般工作中,性能优化主要考虑三个方面:
- 代码层面:
- 将css放到代码的顶层,js放到代码的最底端,避免js阻碍页面渲染
- 减少js操作dom和修改样式,减少页面的重绘和回流
- css,html不要嵌套的太深
- 减少使用循环次数,通过条件判断,及时结束循环
- 文件的按需加载
- 减少请求:
- 合理合并和拆分请求。之前我们都说要减少请求,但是在实际开发中,遇到首屏加载过慢的问题,因为我们的小程序首页是装修页面,之前我们都是通过一个接口返回装修数据,会员的一些数据,以及后台的一些配置数据,就导致接口比较慢。我们的优化思路就是,把装修和配置一类的数据作为json存储到oss上,前端直接读取oss, 剩下的一些数据异步请求接口,这样就不会堵塞页面加载了,
- 合理利用浏览器缓存,使用cdn,加快浏览器请求速度
- 静态资源优化:
- 在项目中,我们把将静态资源比如图片,或者json放到oss上
- 对静态资源进行打包和压缩和缓存
- wepack原理:
webpack打包的过程中主要分为初始化实例、编译以及输出三个部门;
- 初始化:启动构建,读取合并参数,加载plugin,实例化编译器
- 编译:从entry入口出发,针对每个moudle调用loader去翻译文件,再找到该moudle依赖的module,递归进行处理
- 输出:将编译好的module组合成代码块,将代码块转成文件
- webpack的loader和plugin的区别:
- loader本质就是一个函数,相当与一个翻译官的角色,因为webpack只能识别js,所以需要loader给翻译一下
- 常用的loader:声明在module.rules里
css-loader,
sass-loader,
less-loader,
html-loader,
svg-loader,
img-loader,
json-loader
- plugin是插件,他可以扩展webpack的功能,可以监听在生命周期中广播的事件
- 常用插件有:声明在plugins是一个数组
webpack-bundle-analyzer-可视化分析文件大小,
DefinePlugin-定义环境变量,
CommonsChunkPlugin-提取第三方库和公公模块,避免首屏加载文件体积过大。
HotModuleReplacementPlugin-热更新插件,提高开发效率;
webpack-merge:提取公共配置文件,减少重复配置代码
- 热更新插件原理:
大概就是在服务器端和客户端维护一个websocket服务,当本地资源发生变化时,服务器端会像客户端推送数据,客户端会与上次的资源进行diff对比,获取到变化的代码块,然后向服务端发起请求获取需要的代码块
- 如何优化打包速度?
- uglifyjs-webpack-plugin实现多线程并行压缩
- image-webpack-loader 对图片进行压缩
- 缩小打包作用域:
- 在使用loader的时候配置include,exclude等,确定使用规则的范围;
- 引用第三方文件时,使用绝对地址
- 浏览器机制:
- 从一个url到加载出来页面都经历了什么?
- dns解析域名,从url解析出来相对应的服务器的ip:
- dns解析规则,现在本地查看host是否对该域名有做ip映射,有的话就映射到相应的ip
- 检查本地dns解析器是否有缓存
- 检查计算机上配置的dns是否有缓存
- 检查dns服务器上是否有缓存,根据域名现查找.com,--baidu.com,--www.baidu.com 一层一层去查找,直至找到相对应的ip为止
- 等待tcp队列,浏览器要求,同一域名下每个最多建立6个tcp连接
- tcp连接:
- 三次握手和四次挥手
- 三次握手:
- 第一次握手:客户端向服务器端发送一个SYN报文
- 第二次握手:服务端接收到报文之后,向客户端回传SYN报文和ack报文。
这一步其实确定了客户端的发送数据能力以及服务端的接收数据的能力,但是还要确定客户端的接收数据能力和服务端的发送数据能力,所以要进行第三次握手
- 第三次握手:客户端接收到服务端的SYN报文和ack报文,给服务器端回传ack报文,服务端接收到ack报文之后确定了客户端接收数据的能力,客户端确定了服务端发送数据的能力,这样就可以建立连接了
- 四次挥手:
- 第一次挥手:客户端向服务端主动请求关闭,发送一个FIN报文和一个序列号
- 第二次挥手:服务端接收到FIN报文和序列号之后向客户端发送一个ack报文和接收到的序列号+1
- 第三次握手:如果服务端也要关闭连接,再次向客户端发送一个FIN报文和另外一个序列号
- 第四次挥手:客户端向服务端发送ack报文和第二次的序列号+1,当服务端接收到信息之后确认关闭连接
- 客户端发送请求
- 服务端接受请求
- 断开连接
- 然后就开始加载渲染页面了
- 解析html生成dom tree,
- 解析css生成css tree,
- 执行js,
- dom tree和css tree一起构建成render tree
- 开始layout 布局,找到每个节点的位置
- 开始paint 绘制页面,为每个节点加载各自的样式
- 浏览器的事件循环机制:
-js是单线程的,js任务是分为同步任务和异步任务的;
- 同步任务按照js顺序由上向下执行
- 异步任务浏览器会单开一个线程用来计时,当时间结束之后将异步任务的回调放到任务队列中。
- 异步任务又分宏任务和微任务
- setTimeout, setInterval,页面渲染和事件监听,这些都是宏任务
- promise.then()是微任务
- 当同步任务执行完,浏览器会先检查微任务队列,清空微任务队列
- 然后检查宏任务,执行队列中第一个宏任务,在执行宏任务过程中遇到微任务再放到微任务队列中
- 执行完第一个宏任务之后再去检查微任务队列并清空,依次循环执行下去,直至宏任务队列和微任务队列都清空为止结束
- 跨域:
- 同源策略:浏览器的安全机制,协议名、域名、端口号,三者必须一致才算同源;
- 上边三者有任一不一致就视为跨域。
- 解决跨域的方法:
-jsonp: 利用script不受限制,来实现跨域,缺点是只支持get方法;
- 服务器端设置支持跨域,但是这种一般不太安全
- websocket也支持跨域:
websocket主要是在服务端和客户端建立一种长连接关系,之前只能是客户端请求服务端,有了socket之后,也可以服务端自动向客户端发送请求。
通过绑定和监听事件,更新数据。
- iframe也支持跨域
- 浏览器缓存:(强缓存,协商缓存):页面第一次加载的时候会做一些缓存,比如dns缓存和一些请求资源的缓存,所以第二次请求会比第一次快。
- 强缓存就是客户端可以根据cache-control设置max-age,自己判断请求是否过期
- 协商缓存就是客户端会先判断一下请求是否过期,如果过期了发送请求给服务器,服务器再判断一下是否过期,缓存是否过期的结果以服务器判断的结果为准
- 作用域:
- 作用域是指变量的一个独立空间,用来对变量进行隔离;作用域分为全局作用域,函数作用域和块级作用域;
- 作用域链是指:自由变量不断向上寻找变量赋值的过程,这时候又涉及到活动变量AO的一个概念;
- 当函数执行的瞬间,就会生成一个活动对象,按照词法执行顺序对活动对象进行赋值;
- 先解析形参,如果AO对象上声明该属性,当实参传入时给该对象赋值;
- 解析变量,如果AO对象上有该属性,则不做任何处理,如果没有该属性,就新增属性;
- 解析函数,如果AO对象上有该属性,则覆盖,如果没有则新增属性
- 这里还涉及到了变量提升的问题,用var声明的变量和function声明的函数都会有变量提升,变量提升提升的时函数的声明而不是赋值
- 闭包:闭包是指在父函数销毁之后,子函数仍然能够访问父函数的变量。这样的函数叫做闭包。
-缺点:变量得不到释放
- this指向:this的指向取决的函数调用的时候,但是箭头函数例外,取决于函数生命的时候;
- new一个对象都发生了什么?
- 先创建一个空对象obj = {};
- obj.__proto__属性赋值给构造函数的prototype属性;
- 改变this指向,将this指向obj这个对象。
- 原型、原型链
- js对象分为普通对象和函数对象,所有的对象都有__proto__属性,只有函数有prototye属性
- _proto_也是一个对象,他有contructor属性和__proto__属性
- js还有一些自带的构造函数比如Object和Function,Array这种,他们都有prototype属性
- prototype的contructor属性指向当前实例的构造函数
- stu.__proto__ = Student.prototype;
- Student.prototype.contructor = Student;
- 优点:统一构造函数生成的实例,原型上的属性指向同一个内存地址,这样大大节约了内存资源
- 缺点:如果一个构造函数的原型上的属性是对象,当某一个实例对其进行修改时,另外一个也会发生变化。所以尽量避免构造函数原型上的属性是对象格式;
class Student {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayhi() {
return `${this.name}${this.age}sayHi`
}
}
class SinorStu extend Student{
constructor(name, age, male) {
super(name,age);
this.male = male;
}
saySinorHi() {
return super.sayhi() + this.male;
}
}
let promiseAll = function(arr) {
return new Promise((resolve,reject) => {
let len = arr.length;
let count = 0;
let result = [];
for(let i = 0; i < len; i++) {
Promise.resolve(arr[i]).then(res => {
count++;
result[i] = res;
if(count == len) {
return resolve(result);
}
}, err => {
reject(err);
})
}
})
}
function debounce (delay, cb) {
let timer = null;
return function(...arg) {
if(timer) {
clearTimeout(time);
}
timer = setTimeout(function() {
cb.apply(this,arg);
}, delay);
}
}
function throttle(delay, cb) {
let pretime = 0;
return function(...arg) {
let now = +new Date();
if(now - pretime > delay) {
pretime = now;
cb.call(this.arg);
}
}
}
function unique(arr) {
if(Array.isArray(arr)) {
console.error('请输入正确的数据格式!');
return;
}
let obj = {};
let result = [];
arr.length && arr.map(item => {
if(!obj[item]) {
obj[item] = 1;
result.push(item);
}
})
return result;
}
function unique2(arr) {
if(Array.isArray(arr)) {
console.error('请输入正确的数据格式!');
return;
}
return [...new Set(arr)];
}
const bubble = (arr) {
let len = arr.length;
for(let i = len - 1; i > 1; i--) {
for(j = 0; j < i - 1; j++) {
if(arr[j+1] > arr[j]) {
let res = arr[j];
arr[j+1] = arr[j];
arr[j] = res;
}
}
}
return arr;
}
const selectSort = (arr) => {
let len = arr.length;
for(let i = 0; i < len; i++) {
for(let j = i; j < len; j ++) {
if(arr[j] < arr[i]) {
[...arr[i], arr[j]] = [...arr[j], arr[i]];
}
}
}
return arr;
}
const quickSort = (arr) => {
if(arr.length < 2) {
return arr;
}
let len = arr;
let left = [], right = [];
let first = arr[0];
for(let i = 1; i < len; i++) {
if(arr[i] > first) {
right.push(arr[i])
} else {
left.push(arr[i])
}
}
return quickSort(left).concat(first, quickSort(right));
}
const curry = (fn) => {
return function judege (...args) {
if(args.length == fn.length) {
fn(...args);
} else {
return function(...arg) {
judege(..args, ...arg);
}
}
}
}
- vue 的生命周期
vue的生命周期主要分为创建阶段,挂载阶段和销毁阶段
- 在beforeCreated阶段,vue初始化vm实例,初始化了生命周期和一些基本事件
- 在created阶段,给vue实例绑定属性,data,methods,props,watch等等属性
- beforeMounted阶段:模版已经编译完成,只是放在缓存中,还没有挂载到页面上
- mounted: 模版已经挂载到浏览器上了,如果想操作dom的话,mounted是最早可以拿到dom的阶段
- beforeUpdated: 数据已经是最新的了,但是页面还没有更新;
已经拿到最新数据,在内存中重新生成一份新的dom树,然后在触发页面的重新渲染;
- updated: 页面和数据都是最新的了;
- beforeDestoryed: 销毁之前的阶段,多有的属性,data,method还处于可用状态
- destoryed: 完成页面的销毁,所有的指令,方法都不可用了
- vue的响应式实现原理:
vue其实就是一个构造函数,主要分为四部分:监听器、编译器、订阅者,发布订阅器;
- 一个observer监听器:vue2.0是通过object.defineProperty的get和set方法对属性进行监听, 如果监听到数据发生变化,则通知dep。
- 一个compiler指令编译器: 对模版语言进行翻译,绑定相对应的变量和方法;
- 一个watcher订阅者,是监听器和编译器之间的桥梁,当数据发生变化时,dep会通知每一个订阅者去更新数据。这时就出发了UI视图的更新;
- 一个mvvm入口
- vnode和diff算法:
- 视图修改:众所周知,vue是通过修改数据来实现的,当数据被修改时调用了set方法,触发了Dep通知所有的订阅者watcher, watcher会执行之前绑定的回调,进行页面的update, 先执行render拿到新的vnode,然后进行patch对比
- patch: patch主要是将新旧vnode进行比较,得到最小的更新单位,而不是整个视图重新渲染,patch的核心就是diff算法。
- diff算法:
- 判断是否samevnode:首先对同层树进行比较,判断出是否是同一节点,如果不是就创建新的dom,或者移除旧的dom;如果是同一节点,就会进行patchVnode;
- 判断same的标准:
key &&
tag &&
isComment(是否为注释节点) &&
是否都有定义data &&
如果是input标签,判断type是否相等
- patchVnode规则:
- 新旧都是静态节点 && key相同 && (新节点是clone || v-once标识的),只替换elm和componentInstance(组建实例)即可;
- 新旧节点都有子节点,则对子节点进行对比,执行updateChildren;
- 新节点没有children,旧节点有children,则移除dom的所有子节点;
- 新节点有children,旧没有children,则先清空旧dom的文本节点,然后将插入新节点的children
- 新旧都没有子节点,则进行文本节点的替换;
- 重头戏来咯:updateChildren
- 对新旧节点的首位节点都进行标记,两两进行对比,
- 如果是头头或者尾尾是sameVnode,则进行上一步patchVnode;
- 如果头和尾是sameVnode,则先将node挪到相应的位置,再进行patchVnode;
- 如果新节点的头尾和对节点的头尾都没有找到sameVnode,那么则遍历所有旧节点,对比是否有相同的节点;
- 如果有相同的节点则移到相对应的头或尾再进行patchVnode,如果没有,则在相应的位置创建新的vnode;
- 这样算是一组首尾指针比较完了,然后首尾指针依次向前移,继续执行上边的操作,直至尾指针大于首指针结束;
- 如果新节点先触发结束,则删除旧节点中首尾指针中间的节点;如果旧节点先结束,则把新节点中首尾节点的中间节点插入到旧点的的后边;
- 这样就算完成了一次对比
- vue SSR: 将标签渲染成html的过程放到服务器上,然后服务器直接把html片段返回给客户端,这个过程叫做ssr
- 优点:
- 更有利于seo检索:
- 可以加快首屏加载速度
- 缺点:
- 对开发有限制,需要在node环境下才可以使用,只支持beforeCreate和created两个钩子
- 一定程度加大了服务器的压力
- vue的nextTick:
- 干什么的?
- 绑定下次dom更新的回调;
- 为什么需要它?
- dom更新是异步的,所以有时候需要拿dom中的数据的时候需要等dom更新之后才能获取到最新的值;
- dom更新不是立即执行的,是数据的执行set方法的时候触发dep通知所有的watcher,把watcher对象放到一个队列里边,如果遇到id相同的wathcer不会重复放入到队列中;
- 当nexttick运行时,watcher才会被遍历取出来执行update
- nextTick是通过promise实现异步回调的,在watcher的所有update都执行完才执行nextTick的回调
- 为什么dom更新是异步的呢?
- 当数据被频繁操作的时候,如果不是异步的那么则每次都需要重新更新视图,但是异步更新视图,相同id的watcher不会重复放入到队列中,所以dom只会更新一次
-vuex原理:
- vuex主要解决的问题:
- 多层嵌套,父子组件之间互相传递数据非常繁琐
- 非父子兄弟关系的组件,或者不同页面的组件引用同一变量时,当其中一个组件对数据发生修改时,另外一个也需要同步修改;
- vuex和全局变量的区别:
- vuex的存储是响应式的,当store发生变化的时候,相对应的视图也会发生相应的变化
- vuex是不能直接修改state的,必须通过mutation来触发
- vuex主要用五部分:
- module: 避免业务过于复杂时,store过于臃肿,所以采用分模块存储数据
- state: 用来存变量
- getters: 用来存计算属性
- mutation: 唯一一个可以修改state的方法
- action: 处理异步请求,拿到请求数据之后通过commit触发state变化
-Vue-router:
- 实现了什么:
- 实现了单页应用,通过切换不同的路由展示不同的组件
- 常用的模式
- Hash模式:主要利用了浏览器的location.hash实现的,主要是监听onhashchange事件,进行页面渲染;
- history模式:主要是利用h5的History Api实现的,
- popState可以对back、forward、go等方式,切换url进行监听, 再更新视图;
- pushState、replaceState不能被popState监听,vue-router对这两个方法进行了二次封装,不仅实现了页面跳转,还实现了页面的更新;
- 通过上边两种方式监听到了路由变化,怎么进行页面渲染的呢?
- 拿到当前的路由,在路由映射表里找到相对应的路由数据, 并且根据parent找到所有相关的路由组件数据matched,赋值给_route
- vue-router通过defineReactive实现_route为响应式数据,并且为了在全局都方便访问该数据,把它绑定到了Vue.prototype.$route上;
- _route的变化触发了router-view组件的重新渲染;
- router-view是一个函数式组件,他的render方法主要是通过查找父组件是否包含router-view来找到当前的depth,根据depth去渲染不同的component, 具体dom的更新是vue的diff算法去判断的。
- $route 是存当前的路由数据,path,components,matched...
- $router是vue-router实例,上边就有一些对应的变量和方法,
- vue-router特性:
- 动态匹配路由:
- createMatcher:扁平化路由,将路由数组转换成好维护的结构, 生成一个路由数组以及路由映射表
- addRoutes动态添加路由:
- createRouteMap(route, oldPathList, oldPathMap); 扁平化路由
- oldPathList = ['/', '/about', '/about/a', '/about/b'];
- oldPathMap = {
'/' : {
path: '/',
component: home,
parent: undefined,
},
'/about' : {
path: '/about',
component: about,
parent: undefined,
},
'/about/a' : {
path: '/about/a',
component: a,
parent: {
path: '/about',
component: about,
parent: undefined,
},
},
'/about/b' : {
path: '/about/b',
component: b,
parent: {
path: '/about',
component: about,
parent: undefined,
}
}
}
- match: 找到当前的记录
- History类
- base类:
- 属性 router:当前路由
- 属性 current: 当前路由对象
{
path: '/about/a',
matched: [about, aboutA];
}
- transitionTo:
transitionTO(location, callback) {
matcher.match(location);
}
- $route和$router是通过Object.definePropert拦截了Vue.prototype的$route和$router属性,对get方法进行改写;
- $route: 当前路由 current
- $router: router的属性,声明时传的参数,以及一些方法;
- 页面更新:
- 发布订阅模式,监听路由发生变化,重新触发页面渲染
- try catch:
- JS 异常是否能被 try catch 到?
- 可以,try catch只能捕获到js线程执行到try catch代码块时,并且在try catch里边的错误;其他的都不能捕获到
- promise的异常能try catch被捕获到吗?
- 不能,promise的所有回调函数外边都包了一层try catch, 只能被内部的catch方法或reject捕获到,不会往上继续抛异常
try {
console.log(a.b)
} catch(e) {
console.error(e);
}
console.log(111);
try{
new Promise(function (resolve, reject) {
a.b;
}).catch(err => {
console.log(err);
})
console.log(111);
}catch(e){
console.log('error',e);
}
console.log(222);
function task(item) {
console.log(item.name[0])
if(item && item.children) {
for(var i = 0; i < item.children.length; i++) {
task(item.children[i])
}
}
console.log('done');
}
setTimeout(() => {
task({
name: '0',
children: [
{
name: '1',
children: [
{
children: [
{ }
]
}
]
}
]
});
})
setTimeout(() => {
console.log('123')
})
- 微信小程序
- 生命周期:
- 小程序的生命周期:
- onLanuch: 监听小程序初始化
- onShow: 监听小程序启动和切换到前台
- onHide: 监听小程序切换到后台
- 页面的生命周期:
- onLoad:页面创建时,可以拿到options处理url传过来的数据
- onShow:页面出现在前台时
- onReady:页面渲染完毕
- onHide:页面切换到后台
- onUnload:页面销毁
- onShareAppMessage: 分享
- onPullDownRefresh:触发下拉刷新的时
- 组件的生命周期:
- created: 组件实例被创建时
- attached:组件实例进入到页面的节点时
- ready:组件实例在页面被布局完成后
- moved:组件被移动到页面节点数的另外一个位置时
- detached:组件被页面移除时
- error
- pageLifetimes
- show:组件所在页面展示时
- hide:组件所在页面隐藏时
- resize:组件所在页面尺寸发生变化时
- 路由跳转方式
wx.navigateTo: 保留当前页面跳转
wx.redirectTo:关闭当前页面,跳转
wx.navigateBack:关闭当前页面,跳转到上一个页面,不能传参
wx.reLanuch:关闭所有页面,跳转
wx.switchTab:关闭所有非tab的页面,切换tab
- bindtap和catchtap的区别?
- bindtap是点击事件
- catchtap阻止事件冒泡
- 下拉刷新?
- 全局配置下拉刷新app.json可以配置window下的enablePullDownRefresh,
- 页面的xxx.json页面配置enablePullDownRefresh, 通过onPullDownRefresh来监听,做相应的处理
- onReachBottom:下来加载下一页
- xx.json都可以配置什么东西?
- pages, 页面路由以及分包
- window,全局的配置,导航颜色,导航文字颜色,
- tabbar: tab 2-5个
- network:网络请求超时时间
- permission: 授权相关配置
等等很多呢
- 怎么优化?
- 启动优化:
- 分包,尽可能减少首页包的大小
- 尽量减少包内的静态资源,我们是把图片或者静态json文件放到oss上了
- 异步数据处理放到onLoad生命周期处理,不要放到onready生命周期里,避免页面多次渲染
- 可以使用骨架屏或者缓存数据处理页面请求还没回来的情况,避免页面白屏时间太长
- 请求优化:
- 避免频繁操作setData,合并setData
- 与页面展示无关的数据尽量不要放到data里
- 页面间怎样传递数据?
- App().globalData,同步的
- Storage:异步的
- 路由传参,通过currentPages进行页面传参
- 页面滚动穿透
- catchtouchmove事件,但是有时候不好使。
- page-meta标签,动态控制页面是否可以滚动
- 微信小程序原理
- 授权流程
最新授权更新:
- wx.login不需要关注公众号可以直接返回code,根据code去跟后端换unionID(unionID用户的唯一标识,openId标识用户当前登陆状态的 )
- 需要获取用户头像,昵称的接口换成wx.getUserProfile,需要用户授权才能获取,如果需要展示头像和名称,可以通过open-data来展示,不需要授权
- 支付怎么实现的
let pages = getCurrentPages();
let len = pages.length;
let pre = pages[len - 2];
let cur = pages[len - 1];
pre.setData({
test: '123'
});
wx.navigateBack(-1);
- 项目的难点:
1、首页加载优化,将接口拆分,之前接口做的事情太多了,既需要拿页面的装修数据,也需要拿页面的一些配置数据,还需要拿一些用户数据,所以接口会比较慢,造成首屏加载事件过长;
- 装修数据、配置数据存成静态json,放到oss上,前端直接从oss拿,这样速度更快;如果这些数据发生修改时,后台会同步修改这些json数据
- 一些需要后台查数据库的数据再用接口返回,再根据具体业务进行拆分
2、点餐页面如果菜品数据过大的时候,比如有2000个菜品的时候,就会引起页面卡顿或者白屏;
- 原因:
- setdata数据量太大,耗时比较多
- dom结构复杂,造成新旧vdom在diff时比较耗时
- dom复杂,占内存高,造成浏览器内存回收
- 解决方式:
- 利用微信提供的recycle-view长列表组件来解决问题
- 主要思路时动态加载数据,只加载显示区域的数据,当滚动或者点击事件触发页面滚动时,再重新渲染要显示的部分,其他没在显示区域的部分用空div占位,这也就需要在初始化时,页面的高度就是固定的,
- 为了避免滚动时频繁操作造成页面白屏,会多渲染当前页面前后各两屏数据;
3、text-area组件也很坑啊
- 层级最高,这是一个原生的组件,如果在有text-area的页面想出现一个弹窗,需要将控制text-area的显示和隐藏,出现弹窗时让text-area显示为文本
- text-area不随页面滚动而滚动