一、h5c3部分
1.css实现垂直水平居中的方式?
- 行内元素,父元素设置:
div{
height:60px
line-height:60px;
text-align: center;
}
- 块级元素,已知宽度
div {
width:100px;
margin:0 auto;
}
- 块级元素,未知宽度-- flex/定位,
flex布局
div{
display: flex;
justify-content: center;
align-items: center;
}
绝对定位
div{
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
}
2.flex怎么改变主轴线方向?
flex-direction
控制主轴方向,flex-wrap
控制子元素是否换行
/* 主轴方向:所有的项目必须沿主轴排列 */
.container {
/* 默认主轴为水平,行的方向 */
flex-direction: row;
/* 项目从右往左排列 */
flex-direction: row-reverse;
/* 项目垂直从小到大排列 */
flex-direction: column;
/* 项目垂直从大到小排列 */
flex-direction: column-reverse;
}
/* 主轴上的项目换行显示 */
.container {
flex-wrap: nowrap;
/* 如果允许换行,当前容纳不下的项目会换行显示,此时创建的容器 */
flex-wrap: wrap;
/* 项目反向排列 */
flex-wrap: wrap-reverse;
}
3.如何解决移动端适配问题?
- 设置
viewport
视口宽度,下面meta标签的作用是让layout viewport的宽度等于visual viewport的宽度,同时不允许用户手动缩放,从而达到理想视口。
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
- 使用rem进行适配;
rem使用方式为,使用js去动态计算根节点html的字体大小,假设设计稿为750px,根节点初始字体设置为100px,则1rem等同于100px;根据屏幕宽度计算根节点大小,就可以自适应,docEl.style.fontSize = 100 * (屏幕大小/ 750) + 'px';
这里会有遇见之前说的移动浏览器因字体过小强制设置了最小字体的bug,所以还要注意差值,设置合适的倍数去兼容; - 用密集媒体查询方式配合rem设置font-size;
- vw/vh单位,视口宽高100份之一,屏幕375px,1vw=3.75px;
4.css样式的层级
(!important>)>id选择器>class选择器(属性选择器/伪类选择器)>标签选择器(伪元素选择器) 同类选择符条件下层级越多的优先级越高。优先级就近原则,同权重情况下样式定义最近者为准。载入样式以最后载入的定位为准。
5. css中的动画实现
@keyframes animation1 {
0% {
width:100px
}
50% {
width:200px
}
to {
width:100px
}
}
div{
/* animation: 动画名称 时间 动画效果 延迟时间 播放次数(1次infinite|3次:3) 迭代(1次:normal|反复:alternate); */
animation: animation1 0.5s linear 2s infinite alternate;
}
二、 js部分
1.如何理解全局变量和局部变量?
这个题和第2题一起回答,简单来说:
- 全局变量:其他任何位置使用var声明的的变量,函数除外,那么这个变量就是全局变量,全局变量可以在页面的任何位置使用。还有一种特殊的声明,隐式全局变量的变量没有var。
- 局部变量:let ,const,定义的一个大括号内写的变量,只能在函数内部起效果和作用;
全局变量,如果页面不关闭,那么变量所占用的内存就不会释放,就会占空间,消耗内存。局部变量通常情况不能被外层使用;
2.什么是闭包?有哪些优缺点?什么是闭包柯理化?z怎么释放闭包产生的内存?
首先想要理解闭包就先要理解js中的全局作用域和块级作用域,js的特点是在函数内部可以取全局变量,但是如果你需要在一个函数内部取局部变量的值,那就需要使用到函数内部签到一个函数来达到需求,那这个被嵌套的函数也就是闭包。
function a(){
let name='1111'
function b(){
console.log(name+'222')//1111222
}
}
以上函数b()就是闭包;闭包特有的特点就是:
- 函数嵌套函数;
- 函数内部可以去到局部变量的值和全局变量;
- 因为一直被引用,所以不会被js的垃圾回收机制回收;
那什么是闭包的柯理化呢?
function a(x) {
return function b(y) {
return function c(z) {
console.log(x + y + z)
}
}
}
a(1)(2)(3)
就像上述闭包,多层函数嵌套的语义化就很混乱了,通俗来讲将具有多个参数的函数转换为单参数函数链的方式,这就是柯理化。,例如es的展开运算符就使用到了柯理化的内核;
3.什么是js高阶函数?
简单就是函数中接受一个函数作为参数或将函数作为输出返回的函数,这种就是高阶函数,在日常中我们常见的map、reduce、filter、sort就是高阶函数;还有中用法常在在封装公共函数中,传入函数返回所得;
https://zhuanlan.zhihu.com/p/49579052
function add(x, y, f) {
return f(x) + f(y)
}
let num = add(2, 3, Math.abs)
console.log(num)
4.reduce()/map()/filiter()/sort()的用法?
es6的高阶函数,返回一个新的数组
- map,
map() 方法将获取回调函数中的每个返回值,并使用这些值创建一个新数组。
let array =[1,2,3,4]
let newArray = array.map((itme,index,array)=> item * 2)
//map((当前项,当前索引,原数组)=>{每一项乘2},callback函数种this指向,默认指向全局)
console.log(newArray)//[2,4,6,8]
- reduce,
对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。接受两个参数arr.refuce(函数,初始值(选填,默认第一个值))
const reduceFun = (prev, curr, index, arr)=> prev + curr;
//求和函数reduce = (累计值, 当前值, 当前索引, 操作数组)=> 累计值+ 循环的当前值;
let arr = [11,66,22,1000,3]
console.log(arr.reduce(reduceFun))//1102
console.log(arr.reduce(reduceFun,20000))//21102
- sort
数组排序,默认按照转译后的字符串,各个字符的Unicode位点进行排序,操作数组本身,返回一个和原数组相同的新数组;
let arr = [11,66,22,1000,3]
let arr2 = arr.sort()
console.log(arr);//[1000, 11, 22, 3, 66]
console.log(arr2 === arr)//true
传参arr.sort(第一个参数,后一个参数)
let data = [{name:'aa',age:28},{name:'cc',age:07},{name:'bb',age:18}]
let newData = data.sort(function (a, b) {
return (a.age - b.age)
});
let newData2 = data.sort( (a, b)=> a.age - b.age);
console.log(newData2)//Array [Object { name: "cc", age: 7 }, Object { name: "bb", age: 18 }, Object { name: "aa", age: 28 }]
console.log(newData2 === newData )/true
5.js如何求和?
- 常规for循环相加
- Math.abs函数相加
function add(x, y, f) {
return f(x) + f(y)
}
let num = add(2, 3, Math.abs)
console.log(num)
- 上述柯理化相加函数
function a(x) {
return function b(y) {
return function c(z) {
console.log(x + y + z)
}
}
}
a(1)(2)(3)
- reduce编程式函数
const reduceFun = (prev, curr, index, arr)=> prev + curr;
//求和函数reduce = (累计值, 当前值, 当前索引, 操作数组)=> 累计值+ 循环的当前值;
let arr = [11,66,22,1000,3]
console.log(arr.reduce(reduceFun))//1102
- eval函数
function sum(arr) {
return eval(arr.join("+"))
}
console.log(sum([1,3,4]))//8
6.import和require导入文件方式有什么区别?
两者都是js为解决模块化提出的方案;
- require导入方式是为common.js模块化服务器端(node.js)开发的,运行时使用。在客户端浏览器上没办法执行,配合model.exports使用,导出的类型不加限制,原理是将导出的对象赋值给model的exports属性,然后用require的方式导出做这个对象的拷贝;可以使用es6的解构赋值导出;
- import导入方式在编译时客户端使用,是es6提出的方案,配合export使用,可以使用解构赋值导入,页面代码的导入顺序不影响,导入的是对象的引用指针;
7.eventLoop事件轮询机制?
https://blog.youkuaiyun.com/weixin_43216105/article/details/99547109
主线程执行完毕,查询任务队列,按顺序取出任务推入主线程处理,并重复此查询-执行过程;
思路:
- js两大特点:单线程+非阻塞
- js把执行事件可以分为:同步+异步
- 异步api种可以分为:宏任务:setTimeout,script事件,i/o,;微任务promise,ajax,nextTick等,执行完毕按先后顺序推入任务队列
- js的执行顺序:同步线程->微任务->宏任务
8.promise是什么?如何使用?如果把settimeout改为promise方式如何改变?
Promise对象,解决异步调用的
let p = new Promise((resolve,reject)=>{
if(/**fulfilled**/){
resolve(value)
}else{
/**rejected*/
reject(error)
}
})
p().then((value)=>{
// success
}).catch(error=>{
// failure
})
function timeout(time){
return new Promise(resolve=>{
setTimeout(resolve,time)
})
}
// 期望:
timeout(100).then(value=>{console.log(value)})
如何中断Promise?
Promise一旦开始就不能结束,所以思路是return 一个pendding的状态,就可以终端后面的.then/.catch;
promise().then(()=>{
return new Promise((resolve,reject)=>{})
}).then(()=>{}).catch(()=>{})
9.如何改变this的指向?之间有什么区别?
this指向的优先级:new绑定 > 显式绑定 > 隐式绑定 > 默认绑定
-
new
构造函数中的this指向调用对象
-
显式绑定
- func.call(thisArg,a,b,c)
- func.apply(thisArg,[a,b,c])
- func.bind(thisArg,a,b,c) :会创建一个函数,函数的第一个参数就是this的指向,之后的参数将依次传入作为它的参数。
- 三者都可以传参,但是apply是数组,而call是参数列表,且apply和call是一次性传入参数,而bind可以分为多次传入
- bind是返回绑定this之后的函数,apply、call 则是立即执行;
var publicAccounts = {
name: '小明',
author: 'koala',
subscribe: function(a,b) {
console.log(a + this.name + b)
}
}
publicAccounts.subscribe('小红') // 输出结果: 小红小明undefined
var obj = { name: '小黄', author: '考拉' }
var subscribe1 = publicAccounts.subscribe.bind(obj, '小刚','小李')
subscribe1() //小刚小黄小李
// 方式一:只在bind中传递函数参数
fn.bind(obj,1,2)()
// 方式二:在bind中传递函数参数,也在返回函数中传递参数
fn.bind(obj,1)(2)
- ()=>{}:它没有自己的this对象,内部的this就是定义时上层作用域中的this
隐式绑定:this的指向通俗理解,就是谁调用它就指向谁。一直往上找,没有调用就指向window。
10.浅拷贝和深拷贝?
浅拷贝:只拷贝的对象本身,对象属性的指针没有改变;
深拷贝:在内存中开一块新内存存放对象,和之前的对象完全不同;
原型链
每个对象的__proto__都是指向它的构造函数的原型对象prototype的;
三、vue部分
你对SPA单页面的理解,它的优缺点分别是什么?如何实现SPA应用呢?如何优化缺点?
SPA是一种网络应用程序或网站的模型,它通过动态重写当前页面来与用户交互,这种方法避免了页面之间切换打断用户体验在单页应用中,所有必要的代码(HTML、JavaScript和CSS)都通过单个页面的加载而检索,或者根据需要(通常是为响应用户操作)动态装载适当的资源并添加到页面页面在任何时间点都不会重新加载,也不会将控制转移到其他页面;
单页应用优缺点
优点:
- 具有桌面应用的即时性、网站的可移植性和可访问性
- 用户体验好、快,内容的改变不需要重新加载整个页面
- 良好的前后端分离,分工更明确
缺点:
- 不利于搜索引擎的抓取seo
- 首次渲染速度相对较慢
原理
hash模式:监听地址栏中hash变化驱动界面变化,用pushsate记录浏览器的历史,驱动界面发送变化;
history模式:借用 HTML5 history api,history.pushState 浏览器历史纪录添加记录;
history.replaceState修改浏览器历史纪录中当前纪录;
history.popState 当 history 发生变化时触发;
优化缺点
一、如何给SPA做SEO
理论上,搜索引擎更喜欢静态页面形式的网页,搜索引擎对静态页面的评分一般要高于动态页面。
- ssr 服务端渲染,将组件或者页面通过服务器生成hml,返回给浏览器访问;
- url静态化
- 通过程序将动态页面抓取为静态页面保存在服务器中;
- URL伪静态化,URL Rewrite即URL重写,就是把传入Web的请求重定向到其他URL的过程。比如http://www.123.com/news/index.asp?id=123 使用UrlRewrite转换后可以显示为http://www.123.com/news/123.html。把外部请求的静态地址转化为实际的动态页面地址更有利于seo优化;
- 使用Phantomjs针对爬虫处理,原理是通过Nginx配置,判断访问来源是否为爬虫,如果是则搜索引擎的爬虫请求会转发到一个node server,再通过PhantomJS来解析完整的HTML,返回给爬虫。
二、解决首屏加载慢
首屏时间(First Contentful Paint),指的是浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间,此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容;
常见的几种优化方式
- 减少入口文件体积,路由懒加载,把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加;
- 静态资源本地缓存,后端资源缓存:采用http缓存,304,采用server worker离线缓存;前端资源缓存:合理利用localStorage,keep-alive缓存静态页面;
- UI框架按需加载,如:element-UI antd;
- 图片资源压缩,使用在线icon,或者雪碧图,减少http压力;
- 组件重复打包,假设A.js文件是一个常用的库,现在有多个路由使用了A.js文件,这就造成了重复下载,解决方案:在webpack的config文件中,修改CommonsChunkPlugin的配置minChunks: 3,会把使用3次及以上的包抽离出来,放进公共依赖文件,避免了重复加载组件;
- webpack配置开启GZip压缩,前端:使用compression-webpack-plugin 配置configureWebpack:CompressionPlugin;服务端配置:若发送请求的浏览器支持gzip,就发送给它gzip格式的文件,服务器是express框架下只要安装compression就能使用;
- SSR服务端渲染,组件或页面通过服务器生成html字符串;
- CDN减少请求次数;
vue2中 new vue()之后发生了什么?
完成数据绑定,又将数据渲染到视图中;
new Vue({
router,
store,
reader:h=>h.app
}).$mount('#app')
- 在new vue后开始this._init
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
- 合并router,store等外部options属性,初始化生命周期,初始化事件监听,初始化渲染方法,初始化依赖注入的内容,触发 beforeCreate 钩子函数,初始化data(new Observer(value):Observer模式,对象的属性键进入getter/setter),props计算属性,监听属性,然后触发Created钩子函数;
initLifecycle(vm)//初始化生命周期位
initEvents(vm)//初始化组件事件监听
initRender(vm)//初始化渲染方法
callHook(vm, 'beforeCreate')//注册函数
//在data/props之前,观察者模式,调用:defineReactive中: Object.defineProperty初始化依赖注入内容,
initInjections(vm)
initState(vm)//初始化props/data/method/watch
initProvide(vm)
callHook(vm, 'created')
// 挂载组件
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
tips:provide和inject可以轻松实现跨级访问父组件的数据
- 然后使用$mount将数据模型挂载到Dom上;
- vue 不允许直接挂载到body或页面文档上;
- 如果存在template模板,解析vue模板文件;
- 如果是dom,通过getOuterHTML选择器获取元素内容;
- 最终都会解析成render函数,调用compileToFunctions,会将template解析成render函数,再次调用$mount
- 调用mountComponent渲染组件,触发生命周期beforeMount,定义updateComponent渲染页面视图的方法;
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
vm._render()
创建虚拟dom;vm._update
主要功能是调用patch,将vnode转换为真实DOM,并且更新到页面中
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm) // 设置当前激活的作用域
vm._vnode = vnode
// 执行具体的挂载逻辑
if (!prevVnode) {
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
}
- 实例化Watcher(观察者)监听组件数据,一旦发生变化,触发beforeUpdate生命钩子,
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
vue怎么自定义指令?自定义指令内层的原理是什么?
- 全局自定义指令
<input type="text" v-focus>
vue.directives('focus',{
inserted: function(el){
el.focus()
}
})
- 注册局部指令
directives(
focus:{
inserted: function(el){
el.focus()
}
}
)
- 内部钩子函数
- bind:初始化,只调用一次,指令第一次绑定到元素时调用;原理:display:none
- inserted:插入父节点的时候调用;原理:如果是下拉框
- update:插入模板更新时调用;
- componentUpdated:所有模板更新完后调用;
- unbind:只调用一次,指令与元素解绑时调用。原理:display:none
- 实践:防抖;图片懒加载; 一键 Copy的功能
为什么data属性是一个函数而不是一个对象?
- 根实例对象data可以是对象也可以是函数(根实例是单例),不会产生数据污染情况;
- 组件实例对象data必须为函数,目的是为了防止多个组件实例对象之间共用一个data,产生数据污染。采用函数的形式,initData时会将其作为工厂函数都会返回全新data对象;
源码位置:/vue-dev/src/core/instance/state.js
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
}
export function getData (data: Function, vm: Component): any {
// #7573 disable dep collection when invoking data getters
pushTarget()
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, `data()`)
return {}
} finally {
popTarget()
}
}
vue如何实现响应式?动态给vue的data添加一个新的属性时会发生什么?怎样解决?
vue2是用过Object.defineProperty实现数据响应式,defineReactive方法
const obj = {}
Object.defineProperty(obj, 'foo', {
get() {
console.log(`get foo:${val}`);
return val
},
set(newVal) {
if (newVal !== val) {
console.log(`set foo:${newVal}`);
val = newVal
}
}
})
}
当我们访问foo属性或者设置foo值的时候都能够触发setter与getter,但是我们为obj添加新属性的时候,却无法触发事件属性的拦截;能打印出来bar,但是页面不更新;
obj.bar = '新属性'
原因是一开始obj的foo属性被设成了响应式数据,而bar是后面新增的属性,并没有通过Object.defineProperty设置成响应式数据;
若想实现数据与视图同步更新,可采取下面三种解决方案:
- Vue.set(target, propertyName/index, value)
function set (target: Array<any> | Object, key: any, val: any): any {
...
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
/**
* 定义对象上的响应性属性
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
// 预定义 getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
- Object.assign()
直接使用Object.assign()添加到对象的新属性不会触发更新,应创建一个新的对象,合并原对象和混入对象的属性
this.someObject = Object.assign({},this.someObject,{newProperty1:1,newProperty2:2 ...})
- $forcecUpdated()
$forceUpdate迫使Vue 实例重新渲染,调用的是update()
Vue.prototype.$forceUpdate = function () {
const vm: Component = this
if (vm._watcher) {
vm._watcher.update()
}
}
vue的双向绑定是怎么实现的?
我们都知道 Vue 是数据双向绑定的框架,双向绑定由三个重要部分构成 M(model):数据层;V(view):视图层;VM(viewModel):业务逻辑层;
理解VM
主要职责是:数据变化后更新视图,视图变化后更新数据(双向绑定);
组成:
- 监听器(Observer):对所有数据的属性进项监听;
- 解析器(compiler):对页面每个元素节点的指令进行扫描和解析,根据指令模板替换数据,绑定相应更新函数;
vue 实现原理:
配置data的响应式 -------> 实例化对应依赖的watcher -------> 触发get -------> 由Dep去收集相关的watcher ------->watcher收集当前的Dep -------> 页面交互 -------> 触发set -------> Dep通知watcher更新 -------> watcher更新依赖项。
- new vue()初始化,调用observe对data()执行了响应式处理;
- 同时调用compiler对模板执行编译,找到其动态绑定的数据,从data中取出初始数据,并初始化视图;
- 定义一个更新函数和观察者Watcher,等将来数据变化时就watcher就会调用更新函数;
- 由于data的某个key在⼀个视图中可能出现多次,在defineProperty的get中触发Dep,所以每个key都需要⼀个管家Dep来管理多个Watcher;
- 将来data中数据⼀旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数;
vue中是如何传值通信的?
- 通过 props 传递,父组件传递数据给子组件
- 通过 $emit 触发自定义事件,子组件传递数据给父组件
- 使用 ref,父组件通过设置子组件ref来获取数据
- EventBus,兄弟组件传值,创建一个中央时间总线EventBus,兄弟组件通过 e m i t 触 发 自 定 义 事 件 , emit触发自定义事件, emit触发自定义事件,emit第二个参数为传递的数值,另一个兄弟组件通过$on监听自定义事件
- p a r e n t 或 parent 或 parent或root:通过共同祖辈 p a r e n t 或 者 parent或者 parent或者root搭建通信;
- attrs 与 listeners,祖先传递数据给子孙
- Provide 与 Inject,在祖先组件定义provide属性,返回传递的值,在后代组件通过inject接收组件传递过来的值
- Vuex;复杂关系的组件数据传递
Vue中的$nextTick有什么作用?
- 把回调函数放入callbacks等待执行
- 将执行函数放到微任务或者宏任务中
- 事件循环到了微任务或者宏任务,执行函数依次执行callbacks中的回调
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
vue如何实现组件化?其原理?
组件化:.vue文件就是一个组件,或者vue.component;组件注册分为全局注册和局部注册,是用来构成业务模块的页面;
// 组件显示的内容
<template id="testComponent">
<div>component!</div>
</template>
Vue.component('componentA',{
template: '#testComponent'
template: `<div>component</div>` // 组件内容少可以通过这种形式
})
vuex如何使用?
vue中的虚拟dom,实现思路?
虚拟dom只是真实dom的抽象化,以js对象作为节点数,用对象的属性描述节点,最终映射到真实环境上;在Javascript对象中,虚拟DOM 表现为一个 Object对象。并且最少包含标签名 (tag)、属性 (attrs) 和子元素对象 (children) 三个属性,不同框架对这三个属性的名命可能会有差别;
举例说明:
// 真实DOM
<div id="app">
<p class="p">节点内容</p>
<h3>{{ foo }}</h3>
</div>
// 实例化Vue
const app = new Vue({
el:"#app",
data:{
foo:"foo"
}
})
虚拟Dom解析:
(function anonymous(
) {
with(this){
return _c('div',{attrs:{"id":"app"}},
[
_c('p',{staticClass:"p"},[_v("节点内容")]),
_v(" "),
_c('h3',[_v(_s(foo))])
]
)
}
}
)
通过VNode,vue可以对这颗抽象树进行创建节点,删除节点以及修改节点的操作, 经过diff算法得出一些需要修改的最小单位,再更新视图,减少了dom操作,提高了性能!
虚拟Dom的意义
- 操作DOM的代价仍旧是昂贵的,频繁操作还是会出现页面卡顿,影响用户的体验,当你在一次操作时,需要更新10个DOM节点,浏览器没这么智能,收到第一个更新DOM请求后,并不知道后续还有9次更新操作,因此会马上执行流程,最终执行10次流程;而通过VNode,同样更新10个DOM节点,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地的一个js对象中,最终将这个js对象一次性attach到DOM树上,避免大量的无谓计算;
- 虚拟 DOM优势是 diff 算法,减少 JavaScript 操作真实 DOM 的带来的性能消耗;
- 虚拟 DOM 最大的优势在于抽象了原本的渲染过程,实现了跨平台的能力,而不仅仅局限于浏览器的 DOM;
原理
vue是通过createElement生成VNode;_createElement(context, tag, data, children, normalizationType)
- context 表示 VNode 的上下文环境,是 Component 类型
- tag 表示标签,它可以是一个字符串,也可以是一个 Component
- data 表示 VNode 的数据,它是一个 VNodeData 类型
- children 表示当前 VNode的子节点,它是任意类型的
- normalizationType 表示子节点规范的类型,类型不同规范的方法也就不一样,主要是参考 render 函数是编译生成的还是用户手写的;
if (normalizationType === ALWAYS_NORMALIZE) {
//包含嵌套数组<template>, <slot>, v-for, 或者书写渲染函数/jsx情况下
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
//simpleNormalizeChildren方法调用场景是 render 函数是编译生成的
children = simpleNormalizeChildren(children)
}
总结:createElement 创建 VNode 的过程,每个 VNode 有 children,children 每个元素也是一个VNode,这样就形成了一个虚拟树结构,用于描述真实的DOM树结构
vue中的key的意义?
key是给每一个vnode的唯一id,也是diff的一种优化策略,可以根据key,更准确, 更快的找到对应的vnode节点;
patchVnode时updateChildren方法中会对新旧vnode进行diff,然后将比对出的结果用来更新真实的DOM;
- 如果使用了key,Vue会根据keys的顺序记录element,曾经拥有了key的element如果不再出现的话,会被直接remove或者destoryed
- 如果不用key,就默认undefined,Vue会采用就地复地原则:最小化element的移动,并且会尝试尽最大程度在同适当的地方对相同类型的element,做patch或者reuse。
function sameVnode (a, b) {
return (
a.key === b.key &&
a.asyncFactory === b.asyncFactory && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
isUndef(b.asyncFactory.error)
)
)
)
}
vue3使用过吗?和vue2对比有什么亮点?
亮点
重构目的:使用es6语法及TS类型,解决架构问题(比如打包事件过长,类型判断不友好 ,缺少更干净的复用逻辑机制,单文件过大,深层响应式等);
优点:更快更小更友好
- 速度更快
- 重写了虚拟DOM实现-diff算法优化
- 编译模板的优化
- 更有小的组件初始化
- SSR优化
- undate性能提高1.3~2倍
- 体积减少
- tree-sharking 摇树,通过webpack的tree-shaking功能,可以将无用模块“剪辑”,仅打包需要的;
- 更易维护
- Composition API 和option API兼容 兼容vue2语法;
- 更好的TS支持,可以享受到自动的类型定义提示,之前是flow;
- 编译器重写
- 更接近原生
- 可以自定义渲染 API
- 更易使用
- 将响应式API暴露出来,使用引用方式;
VUE3优化方案
-
源码- 源码分层(将编译,渲染等方法都单独抽离包,通过引用方式调用), TS支持
-
性能
-
体积优化——相比Vue2,Vue3整体体积变小了,除了移出一些不常用的API,再重要的是Tree shanking,任何一个函数,如ref、reavtived、computed等,仅仅在用到的时候才打包,没用到的模块都被摇掉,打包的整体体积变小原理:编译阶段利用ES6 Module判断哪些模块已经加载,判断那些模块和变量未被使用或者引用,进而删除对应代码;
-
编译优化
vue2中每一个组件实例都对应一个watcher,当一栏发生改变时,触发setter,然后通知watcher,然后渲染页面;比如:组件内部只有一个动态节点,剩余一堆都是静态节点,所以每个节点 diff 和遍历其实都是不需要的,造成性能浪费;
- vue3在diff算法中加入了静态标记,作用是把会发生变化的节点加了标记,之后变化直接比较;
export const enum PatchFlags { TEXT = 1,// 动态的文本节点 CLASS = 1 << 1, // 2 动态的 class STYLE = 1 << 2, // 4 动态的 style PROPS = 1 << 3, // 8 动态属性,不包括类名和样式 FULL_PROPS = 1 << 4, // 16 动态 key,当 key 变化时需要完整的 diff 算法做比较 HYDRATE_EVENTS = 1 << 5, // 32 表示带有事件监听器的节点 STABLE_FRAGMENT = 1 << 6, // 64 一个不会改变子节点顺序的 Fragment KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性的 Fragment UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key 的 Fragment NEED_PATCH = 1 << 9, // 512 DYNAMIC_SLOTS = 1 << 10, // 动态 solt HOISTED = -1, // 特殊标志是负整数表示永远不会用作 diff BAIL = -2 // 一个特殊的标志,指代差异算法 }
- 静态节点提升,根据节点将不参与更新的元素,做静态提升,只被创建一次,在更新渲染时直接复用;
<span>你好</span> <div>{{ message }}</div> //之前 export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock(_Fragment, null, [ _createVNode("span", null, "你好"), _createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */) ], 64 /* STABLE_FRAGMENT */)) } //之后 const _hoisted_1 = /*#__PURE__*/_createVNode("span", null, "你好", -1 /* HOISTED */) export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock(_Fragment, null, [ _hoisted_1, _createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */) ], 64 /* STABLE_FRAGMENT */)) }
- ssr优化——当静态内容大到一定量级时候,会用createStaticVNode方法在客户端去生成一个static node,这些静态node,会被直接innerHtml,就不需要创建对象,然后根据对象渲染
- 数据劫持优化
vue2中的数据劫持使用的是object.dicineProperty,针对对象已有的属性进行监听,新属性添加和删除不能实现响应式,尽快提供的set和delete实例方法,但还是不够优美,并且再嵌套层级比较深的情况下,就可能存在性能问题;
vue3使用es6中的proxy监听整个对象,同时Proxy 并不能监听到内部深层次的对象变化,而 Vue3 的处理方式是在getter 中去递归响应式,这样的好处是真正访问到的内部对象才会变成响应式,而不是无脑递归
-
-
语法–composition API
- 优化了逻辑组织——相同功能的代码编写在一块
- 优化了逻辑复用
vue中我们使用mixin实现功能的混合,多个minxin混合就会出现命名冲突和数据来源不清晰的问题,vue3中我们可以将一些复用的代码抽离成函数,调用函数即可,整个数据来源清晰了,即使去编写更多的hook函数,也不会出现命名冲突的问题;
Vue3.0里为什么要用 Proxy API 替代 defineProperty API 原理?
原文链接:https://blog.youkuaiyun.com/weixin_43216105/article/details/120000999
- Object.defineProperty只能遍历对象属性进行劫持;
- Proxy直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到响应式目的;
- Proxy可以直接监听数组的变化(push、shift、splice);
- Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等,这是Object.defineProperty不具备的;
- Proxy 不兼容IE,也没有 polyfill, defineProperty 能支持到IE9;
- 因为defineProperty自身的缺陷,导致Vue2在实现响应式过程需要实现其他的方法辅助(如重写数组方法、增加额外set、delete方法)
Composition API 和option API兼容 - setup()
- 执行机制在beforeCreate之前,由于在执行setup时尚未创建组件实例,因此在 setup 选项中没有 this。
- ref(0) reactive({})
- setup(props,con)
亮点:对标Mixin更加明确的复用组件和公共代码
自定义hook
vue路由
1.路由使用的是哪种方式?有什么区别?
- hash
原理:通过监听hash变化驱动界面变化,用pushsate记录浏览器历史,驱动界面发生变化;- 监听hash或者pushsate变化
- 以当前的hash为索引加载相应资源
- 等待资源加载完成,隐藏之前的页面,执行回调
- 显示页面
- history
原理:通过html history api
2.项目中大项目中路由如何封装?
3.路由守卫什么情况使用?内层的实现原理?
4.动态路由传参?全局路由传参?
四、小程序部分
你在开发小程序过程中碰到的比较棘手的问题有哪些?
五、webpack
webpack 热更新原理
关于webpack热模块更新的总结如下:
- 通过webpack-dev-server创建两个服务器:提供静态资源的服务(express)和Socket服务
- express server 负责直接提供静态资源的服务(打包后的资源直接被浏览器请求和解析)
- socket server 是一个 websocket 的长连接,双方可以通信
当 socket server 监听到对应的模块发生变化时,会生成两个文件.json(manifest文件)和.js文件(update chunk) - 通过长连接,socket server 可以直接将这两个文件主动发送给客户端(浏览器)
- 浏览器拿到两个新的文件后,通过HMR runtime机制,加载这两个文件,并且针对修改的模块进行更新
说说如何借助webpack来优化前端性能?
资源加载优化 和 页面渲染优化
- JS代码压缩
terser是一个JavaScript的解释、绞肉机、压缩机的工具集,可以帮助我们压缩、丑化我们的代码,让bundle更小;
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
...
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true // 电脑cpu核数-1
})
]
}
}
- CSS代码压缩
CSS压缩通常是去除无用的空格等,因为很难去修改选择器、属性的名称、值等;
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
module.exports = {
// ...
optimization: {
minimize: true,
minimizer: [
new CssMinimizerPlugin({
parallel: true
})
]
}
}
- Html文件代码压缩
使用HtmlWebpackPlugin插件来生成HTML的模板时候,通过配置属性minify进行html优化
module.exports = {
...
plugin:[
new HtmlwebpackPlugin({
...
minify:{
minifyCSS:false, // 是否压缩css
collapseWhitespace:false, // 是否折叠空格
removeComments:true // 是否移除注释
}
})
]
}
- 文件大小压缩
对文件的大小进行压缩,减少http传输过程中宽带的损耗
new ComepressionPlugin({
test:/\.(css|js)$/, // 哪些文件需要压缩
threshold:500, // 设置文件多大开始压缩
minRatio:0.7, // 至少压缩的比例
algorithm:"gzip", // 采用的压缩算法
})
- 图片压缩
一般来说在打包之后,一些图片文件的大小是远远要比 js 或者 css 文件要来的大,所以图片压缩较为重要
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/',
}
},
{
loader: 'image-webpack-loader',
options: {
// 压缩 jpeg 的配置
mozjpeg: {
progressive: true,
quality: 65
},
// 使用 imagemin**-optipng 压缩 png,enable: false 为关闭
optipng: {
enabled: false,
},
// 使用 imagemin-pngquant 压缩 png
pngquant: {
quality: '65-90',
speed: 4
},
// 压缩 gif 的配置
gifsicle: {
interlaced: false,
},
// 开启 webp,会把 jpg 和 png 图片压缩为 webp 格式
webp: {
quality: 75
}
}
}
]
},
]
}
Tree Shaking
- 代码分离
将代码分离到不同的bundle中,之后我们可以按需加载,或者并行加载这些文件,默认情况下,所有的JavaScript代码(业务代码、第三方依赖、暂时没有用到的模块)在首页全部都加载,就会影响首页的加载速度,代码分离可以分出出更小的bundle,以及控制资源加载优先级,提供代码的加载性能,这里通过splitChunksPlugin来实现,该插件webpack已经默认安装和集成,只需要配置即可
默认配置中,chunks仅仅针对于异步(async)请求,我们可以设置为initial或者all
module.exports = {
...
optimization:{
splitChunks:{
chunks:"all"
}
}
}
内联 chunk
六、其他
移动端下拉刷新是怎么实现的?会涉及到js的哪些方法?
当前手势滑动位置与初始位置差值大于零时,提示正在进行下拉刷新操作
下拉到一定值时,显示松手释放后的操作提示
下拉到达设定最大值松手时,执行回调,提示正在进行更新操作