一、对MVVM的理解
MVVM是Model-View-ViewModel缩写,也就是把MVC中的Controller演变成ViewModel。Model层代表数据模型,View代表UI组件,ViewModel是view和model层的桥梁,数据会绑定到viewmodel层并自动将数据渲染到页面中,视图变化的通知viewmodel层更新数据
二、双向数据绑定原理
采用数据劫持和发布-订阅模式,通过Object.defineProperty()来劫持对象各个属性的getter/setter,在数据变化时,发布消息给订阅者,触发相应监听回调,
发布-订阅模式是什么?
就是一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新
vue2.x中如何监测数组变化?
使用了函数劫持的方式,重写了数组的方法,vue将data中的数组进行了原型链重写,指向了自己定义的数组原型方法。这样当调用API时,可以通知依赖更新。如果数组中包含着引用类型,会对数组中的引用类型再次递归遍历进行监控。这样就实现了监测数组的变化。
const arrayProto = Array.prototype
// arrayMethods 的原型就是数组的原型
export const arrayMethods = Object.create(arrayProto)
// 数组中会修改自身的7个方法
const methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse']
// 如果数组中增加了数据,遍历新增的数据进行响应式处理
const ob = this.__ob__
if (inserted) ob.observeArray(inserted)
// notify change 数组改变后调用 notify方法 通知 Watcher 去更新视图
ob.dep.notify()
这里对__ob__做下解释,__ob__ 会指向一个Observer对象,每个被双向绑定的对象元素(数组也是对象)都会有一个__ob__ ,而且是单例的。
如果 双向绑定的这个obj 是个对象,那么 var childOb 会是一个Observe 对象,否则是一个null。例如当为obj双向绑定的时候,会调用 obj.__ob.__.dep.depend() 添加一个 同样的watcher。
三、vue生命周期
vue的实例从开始创建,初始化数据,模板编译,挂载dom,渲染更新,卸载这一系列过程,会形成一系列事件钩子函数,方便操作
由于vue在初始化实例时对属性执行getter/setter转化,所以属性必须在data对象上才能将vue转换为响应式的,另外提供了$set方法触发视图更新
1.当页面第一次页面加载时
会触发 beforeCreate, created, beforeMount, mounted 这几个钩子函数
2. 更新data中的数据
并不是每次更新data中的数据都会触发beforeUpdate钩子函数,因为只有data中的数据使用在template模板中更新时才会触发beforeUpdate钩子函数。同理,在updated钩子函数修改data中的数据,如果数据未在template模板中使用,不会触发beforeUpdate钩子函数,也不会造成死循环的。
3.哪个生命周期里可以操作data数据?
beforecreated
:el 和 data 并未初始化
created
:完成了 data 数据的初始化,el没有beforeMount
:完成了 el 和 data 初始化 mounted
:完成挂载
四、vue组件的参数传递
父子组件:props和$emit;$parent和ref;provide和inject
非父子组件:new Bus(),vuex
vue动态组件?
一是使用<component>
元素的 is 的特性,二是使用 v-if 。
缓存
上述讲到的方法虽然能够实现了动态组件的切换,但是每次切换都会把上一个组件销毁,然后渲染下一个组件,对于多次切换而言,显然每次的销毁和重新渲染,很大消耗了我们的性能。所以我们可以通过keep-alive对组件进行缓存,对于不显示的组件不是去销毁他,而是使他处理不激活的状态,当需要显示时再去激活。
<keep-alive> <component :is="currentView"></component> </keep-alive>
五、vue路由钩子函数
全局守卫:router.beforeEach和afterEach
路由独享守卫:页面里beforeRouteEnter,beforeRouteUpdate,beforeRouteLeave
六、VUEX是?
全局状态管理器,有state:数据,mutations:只能通过mutations方法修改state数据,coomit调用,actions:异步操作,通过dispath调用,getter:计算,modules:分模块
七、vue组件data为啥是函数
每个组件都是vue的实例;组件共享data属性,当data的值是引用类型,改变一个会影响其他
八、vue与react的区别?
1.vue组件分全局注册和局部注册,而react都是import...然后模板引用;
2.vue有指令系统,而react只能使用jsx语法;
3.vue核心是双向数据绑定,单文件组件,通过模板引擎来处理,而react函数式编程,单项数据流,通过js来生成html;
九、watch和computed区别
computed:具有缓存性,依赖值改变后,下一次调用computed值才会重新调用对应的getter,适用于比较消耗性能的计算场景
watch:类似数据监听回调,无缓存
1.watch如何监听对象?
设置immediate:true,和deep:true
2.深度监听很消耗性能,如何优化?
使用字符串形式来监听,如‘obj.a’:{handler(new,old){}}
3.深度监听很消耗性能,如何关闭监听?
const unwatch = app.$watch('text')
unwatch()//手动注销
十、为何要用虚拟Dom进行diff算法检查差异?
1.什么是虚拟dom?
是对真实dom的抽象,以js对象,即vnode节点作为基础的树,用对象的属性来描述节点,最终通过patch操作,将虚拟节点渲染到页面视图中。
2.为啥要用虚拟dom?
真实dom很庞大,操作dom会带来很大的性能问题
比如如果一次操作,会更新10个dom节点,浏览器就会做10次更新
而用了虚拟dom,发生改变时,虚拟dom不会立即更新视图,而是通过diff算法,找出更新点,然后一次性渲染到视图上,避免大量无谓计算
3.虚拟dom优势?
最大优势是抽象了原本的渲染过程,实现了跨平台的能力,其次是diff算法
包括浏览器,安卓,ios,小程序
例如react,react Native,vue,weex
4.diff算法是个啥?
一种通过同层树节点进行比较的高效算法
1.同级比较,再比较子节点 2.先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除) 3.比较都有子节点的情况(核心diff) 4.递归比较子节点
特点:同层级比较,不会跨级;在diff比较过程中,先首尾,然后循环往中间比较
比如新节点没有那就直接触发旧节点的destory,没有旧节点,就直接creatElm
vue2的核心diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。相比react的diff算法,同样情况下可以减少移动节点次数,减少了不必要的性能损耗,更加的优雅。
vue3.x借鉴了ivi算法和 inferno算法。在创建VNode时就确定其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型,在这个基础上再配合核心的diff算法,使得性能上比较vue2.x有了提升。
小知识?
前端框架有俩种方式侦测变化,一种是pull,一种是push
pull:代表React,先用setState显式更新,然后react再用虚拟dom diff找出差异,最后将差异patch。
也就是一开始不知道哪里有变化,只是知道有变化了,再进行暴力的diff查找
push+pull:代表Vue,程序初始化的时候,对数据data进行依赖收集,一旦发生变化,响应式系统就会立刻知道,
也就是一开始就知道在哪发生了变化,采用组件级别进行push侦测,先侦测到组件,再对组件内部进行虚拟dom diff操作,而虚拟dom diff操作是pull操作,所以vue是通过push+pull侦测变化的
十一、vue中的key
key是vue标记节点的唯一标识,可以使diff操作更加精确,diff算法过程中,会用新节点的key与旧节点进行比对,找出差异
由于在浏览器中操作dom是很昂贵的。频繁的操作dom,会产生一定的性能问题,这就是虚拟dom产生的原因。
Virtual DOM本质就是用一个原生的js对象去描述一个dom节点,是对真实dom的一层抽象。Virtual DOM映射到真实dom要经历VNode的create、diff、patch等阶段。
key的作用是尽可能的复用DOM元素
新旧children中的节点只有顺序是不同的时候,最佳的操作应该是通过移动元素的位置来达到更新的目的。
需要在新旧 children 的节点中保存映射关系,以便能够在旧 children 的节点中找到可复用的节点。key也就是children中节点的唯一标识。
十二、nextTick与宏任务和微任务
在下次DOM更新循环结束之后执行延迟回调。nextTick主要使用了宏任务和微任务。根据执行环境分别尝试采用
- Promise
- MutationObserver
- setImmediate
- 如果以上都不行则采用setTimeout
定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列。
宏任务和微任务?
- 同步任务都在主线程上执行,形成一个执行栈
- 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
- 一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。
我的理解是,vue在更新dom时是异步执行的,当数据发生变化,vue将开启一个异步更新队列,视图需要等队列中所有数据变化完成后,再统一进行更新
十三、你看过VUE的源码吗?
看过一些,不过不是很深入,主要是对vue文件如何运行的比较感兴趣,大概看了一下;
那vue文件如何运行的呢?
vue文件运行过程其实就是vue模板的编译过程,
vue的模板编译原理:
- 第一步是将
模板字符串
转换成element ASTs
(解析器) - 第二步是对
AST
进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器) - 第三步是 使用
element ASTs
生成render
函数代码字符串(代码生成器) - 将render函数挂载到option中
- 执行公共的mount函数
解析器:一部分是 截取
字符串,一部分是对截取之后的字符串做 解析
优化器:标记静态节点,
好处:
- 每次重新渲染的时候不需要为静态节点创建新节点
- 在 Virtual DOM 中 patching 的过程可以被跳过
代码生成器:
<div>
<p>{{name}}</p>
</div>
将上述模板字符串生成 render
函数代码字符串后,得到
{
render: `with(this){return _c('div',[_c('p',[_v(_s(name))])])}`
}
生成后的代码字符串中看到了有几个函数调用 _c
,_v
,_s
。
_c
对应的是 createElement
,它的作用是创建一个元素。
- 第一个参数是一个HTML标签名
- 第二个参数是元素上使用的属性所对应的数据对象,可选项
- 第三个参数是
children
首先解析模板,生成AST语法树(一种用JavaScript对象的形式来描述整个模板)。使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子函数进行相关的处理。
vue的数据响应式的,但真实模板中并不是所有的数据都是响应式的,有一些数据首次渲染后就不会再变化,对应的DOM也不会变化,那么优化过程就是深度遍历AST树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的对比,对运行的模板起到很大的优化作用。
编译的最后一步是将优化后的AST数转换为可执行的代码。
十四、axios的原理?
Axios 是一个基于 promise 的 HTTP 库,支持promise所有的API
1.promise是什么?
promise是一个Javascript异步编程解决方案,它提供then方法,来为异步提供链式回调函数,
可以解决回调地狱问题。
当然在es7时代,也出现了await/async的异步方案
2.Promise怎么使用?
fn = new Promise(function (resolve, reject) {
//resolve
//reject
},2000)
}).then((res)=>{
console.log(res)
},(err)=>{
console.log(err)
})
fn.then((res)=>{
return new Promise((resolve,reject)=>{})
})
var p1 = Promise.resolve(1),
p2 = Promise.reject(2),
Promise.all([p1, p2]).then((res)=>{
//then方法不会被执行
console.log(results);
}).catch((err)=>{
//catch方法将会被执行,输出结果为:2
console.log(err);
});
3.Promise的原理
在Promise的内部,有一个状态管理器的存在,有三种状态:pending、fulfilled、rejected。
(1) promise 对象初始化状态为 pending。
(2) 当调用resolve(成功),会由pending => fulfilled。
(3) 当调用reject(失败),会由pending => rejected。
Promise相关的面试题
const promise = new Promise((resolve, reject) => {
console.log(1);
resolve();
console.log(2);
});
promise.then(() => {
console.log(3);
});
console.log(4);
输出结果为:1,2,4,3。
fetch和axios的异同?
axios 是一个基于Promise 用于浏览器和 nodejs 的 HTTP 客户端,是Promise的实现版本,符合最新的ES规范;
fetch类库实现用的是原生js,对浏览器支持更好;
H5里提供了一个fetch对象,他的诞生,是为了取代ajax的存在而出现,主要目的仅仅只是为了结合ServiceWorkers;
fetch("/users", { method: "POST", headers: { "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ //这里是post请求的请求体 name: "Hubot", login: "hubot", })})
十五、依赖注入provide和inject
主要是为了解决一些循环组件比如tree, menu, list等, 传参困难, 并且难以管理的问题, 主要用于组件封装, 常见于一些ui组件库
通过在父组件中用provide提供的变量,其所有的子组件都可以通过jnject注入去使用
十六,vue指令与自定义指令
指令举例: v-model
v-model本质上就是一个语法糖,可以看成是value+input方法的语法糖。可以通过model属性的prop和event属性进行自定义。原生的v-model会根据标签的不同生成不同的事件和属性。
Vue.directive("test",{
inserted (el,binding,vnode, oldVnode){
console.log(el);
console.log(binding);
console.log(vnode);
},
bind()//只调用一次
});
在初始化过程中,可以通过el.remove()去除,除了el可操作,其余都是只读
应用场景:防抖,图片懒加载
十七,修饰符
1.表单修饰符
lazy:延迟,trim:过滤首空格,number:转数值
2.事件修饰符
顺序从前到后
stop:事件冒泡,相当于原生:e.stopPropagation()
pervent:阻止默认事件,e.preventDefault()
self:自身触发
once:触发一次
capture:从元素顶层往下触发
passive:给passive加了一个lazy
native:监听元素原生事件
click.left/middle/right:鼠标
@onkeyup,@onkeydown.enter/delete...:键盘
v-bind:xxx.sync对props进行双向数据绑定
十八,Keep-alive
缓存不活动组件实例,不会销毁
设置props:include,包含匹配会缓存;exclude,不会缓存
<keep-alive :include="keepAlive" >
<router-view/>
</keep-alive>
多出俩个生命周期钩子,activated与deactiveted
可以在activated激活或者beforeRouteEnter里操作
注意:使用了keep-alive就不会调用beforeDestroy(组件销毁前钩子)和destroyed(组件销毁),因为组件没被销毁,被缓存起来了。
十九,对slot插槽的理解?
分为默认插槽,
具名插槽,<template v-slot:content>content插槽</template>
作用域插槽,<template v-slot:default="slotProps">来自子组件数据{{slotProps}}</template>
原理?
组件要渲染到页面上需要经过template -> render function -> vnode -> Dom
本质上返回Vnode节点的函数
二十,对mixin的理解?
混入mixin是面向对象设计里的类,其他类可以直接使用其方法,一版用于提取公共代码
分为:局部混入 import 然后 mixins:[] 和全局混入 vue.mixin()
混入策略?
替换?当组件与mixin有相同props,methods,inject,computed时,组件的选项会覆盖mixin的选项
合并?data
队列?当有相同生命周期钩子函数和watch时,会合并成一个数组,先执行mixin的钩子,再执行组件的钩子
叠加?component,directives,filters
二十一,vue中组件和插件?
组件:.vue文件
插件:vue-router,添加到vue实例上实现,
写插件:暴露一个install方法,用xue.use()方式进行注册,需要在new Vue()之前注册
二十二,vue中添加新属性,页面不刷新?
是vue2中的defineProperty无法响应
解决方案:
vue.set() 时间是调用defineReactive实现响应
Object.assign({},this.someobj,{a:1,b:2}) 直接使用不会刷新,合并可以
$forceUpdate()
二十三,vue 路由,hash和history的区别,路由切换页面的原理?
形式上:hash模式url里面永远带着#号,开发当中默认使用这个模式。如果用户考虑url的规范那么就需要使用history模式,因为history模式没有#号,是个正常的url,适合推广宣传;
功能上:比如我们在开发app的时候有分享页面,那么这个分享出去的页面就是用vue或是react做的,咱们把这个页面分享到第三方的app里,有的app里面url是不允许带有#号的,所以要将#号去除那么就要使用history模式,但是使用history模式还有一个问题就是,在访问二级页面的时候,做刷新操作,会出现404错误,那么就需要和后端人配合,让他配置一下apache或是nginx的url重定向,重定向到你的首页路由上就ok了
路由的哈希模式其实是利用了window.onhashchange事件,也就是说你的url中的哈希值(#后面的值)如果有变化,就会自动调用hashchange的监听事件,在hashchange的监听事件内可以得到改变后的url,这样能够找到对应页面进行加载
window.addEventListener('hashchange', () => {
// 把改变后的url地址栏的url赋值给data的响应式数据current,调用router-view去加载对应的页面
this.data.current = window.location.hash.substr(1)
})
二十四,如何理解vue是一个渐进式的框架?
vue.js的两个核心是数据驱动和组件系统
对于一个Vue项目来说,vue.js
只提供了vue-cli
生态中最核心的组件系统
和双向数据绑定
。
对于其他的vue-router,vuex插件可以选择不用,也可以全用jquery写代码,都没有问题,
比如说,你要使用React,你必须理解:
- 函数式编程的理念,
- 需要知道什么是副作用,
- 什么是纯函数,
- 如何隔离副作用
- 它的侵入性看似没有Angular那么强,主要因为它是软性侵入。
也可以在新项目启动初期,有限的使用VUE的功能特性,从而降低上手的成本。
二十五,build时webpack如何是运作的?
本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
如上图: 中间的蓝色块就是webpack. 他会将左边各种文件打包成右侧html能够解析的文件
这里涉及两个概念: 打包和模块
1. 什么是模块? 根据依赖关系整合所有用到的模块资源
webpack可以让我们进行模块化开发, 他提供了平台, 并且会帮助我们处理各模块之间的依赖关系.
webpack最终会帮我们将js, css, 图片, json文件等打包为合适的格式, 以供浏览器使用.
在webpack中上述文件类型都可以被当做模块来使用.
2. 什么是打包?
就是将webpack中各种模块资源进行打包合并成一个或多个包. 并且在打包的过程中, 可以对资源进行处理, 如:压缩图片, 将scss转成css, 将ES6语法转成ES5等可以被html识别的文件类型.
webpack 可以使用 loader 来预处理文件。这允许你打包除 JavaScript 之外的任何静态资源。
比如转化css要用到css-loader,style-loader,less-loader等等
- css-loader: 只负责加载css文件, style-loader: 负责将样式加载到Dom中,如果用到less,就要用到less-loader
- 转化图片要用到url-loader
- 将ES6打包成ES5要用到babel-loader
module.exports = {
context: path.resolve(__dirname, '../'),
//用来指定入口, 指定一个路径
entry: {
app: './src/main.js'
},
//用来指定出口. 需要注意的是: 出口是一个对象, 由两部分组成: path和filename
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
//设置项目文件导入路径
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
}
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('media/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
},
node: {
setImmediate: false,
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty'
}
}