Vue 前端高级面试问题
.什么是vue生命周期
Vue 实例从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、销毁等一系列过程,称之为 Vue 的生命周期。
作用: 生命周期中有多个事件钩子,在控制整个Vue实例的过程时更容易形成好的逻辑。beforeCreate
: 完成实例初始化,this指向被创建的实例,data,computed,watch,mothods
方法和数据都不可以访问,数据观测之前(data observer)
被调用。created
: 实例创建完成,data,computed,watch,methods
可被访问,未挂载dom,可对 data
进行操作,操作 dom
需放到nextTick
中beforeMount
: 有了el
,找到对应的 template
编译成 render
函数mounted
: 完成挂载dom
和渲染,可对dom
进行操作,并获取dom
节点,可发起后端请求拿到数据beforeUpdate
: 数据更新时调用,发生在虚拟Dom重新渲染和打补丁之前之调用updated
: 组件 dom
已完成更新,可执行依赖的 dom
操作,不要操作数据会陷入死循环beforeDestroy
: 实例销毁之前调用,可进行优化操作,如销毁定时器,解除绑定事件destroyed
: 组件已经被销毁,事件监听器和子实例都会被移除销毁
第一次页面加载会触发: beforeCreate, created, beforeMount, mounted
并且`` 渲染在 mounted
中就已经完成了。
可以使用 $on('hook:')
或 $once('hook:')
来简化生命周期的注册
.请说一下 Vue 响应式数据的原理是什么?
在 Vue 初始化数据时, 使用 Object.defineProperty
重新定义 data
中所有属性,增加了数据 获取(getter) / 设置(setter)
的拦截功能。在 获取 / 设置
时可增加一些逻辑,这个逻辑交叫作 依赖收集
。当页面取到对应属性时会进行依赖收集, 如果属性发生变化, 则会通知收集的依赖进行更新,而负责收集的就是 watcher
。
如负责渲染的 watcher
会在页面渲染的时候对数据进行取值,并把当前 watcher
先存起来对应到数据上,当更新数据的时候告诉对应的 watcher
去更新, 从而实现了数据响应式。
data 一般分为两大类: 对象类型 和 数组:
对象:
在 Vue 初始化的时候,会调用 initData
方法初始化 data
,它会拿到当前用户传入的数据。判断如果已经被观测过则不在观测,如果没有观测过则利用 new Observer
创建一个实例用来观测数据。如果数据是对象类型非数组的话会调用 this.walk(value)
方法把数据进行遍历,在内部使用 definReactive
方法重新定义( definReactive
是比较核心的方法: 定义响应式 ),而重新定义采用的就是 Object.defineProperty
。如当前对象的值还是个对象,会自动调用递归观测。当用户取值的时候会调用 get 方法并收集当前的 wacther
。在 set 方法里,数据变化时会调用 notify
方法触发数据对应的依赖进行更新。
数组:
使用函数劫持的方式重写了数组的方法,并进行了原型链重写。使 data 中的数组指向了自己定义的数组原型方法。这样的话,当调用数组 API
时,可以通知依赖更新。如果数组中包含着引用类型,则会对数组中的引用类型进行再次监控。
也就是当创建了 Observer
观测实例后,如果数据是数组的话,判断是否支持自己原型链,如果不支持则调用 protoAugment
方法使目标指向 arrayMethods
方法。arrayMethods
就是重写的数组方法,包括 push
、pop
、shift
、unshift
、splice
、sort
和 reverse
共七个可以改变数组的方法,内部采用函数劫持的方式。在数组调用重写的方法之后,还是会调用原数组方法去更新数组。只不过重写的方法会通知视图更新。如果使用 push
、unshift
和 splice
等方法新增数据,会调用 observeArray
方法对插入的数据再次进行观测。
如果数组中有引用类型,则继续调用 observeArray
方法循环遍历每一项,继续深度观测。前提是每一项必须是对象类型, 否则 observe
方法会直接 return
。
.为何Vue采用异步渲染?
如不采用异步更新, 则每次更新数据都会对当前组件进行重新渲染, 因此为了性能考虑 Vue 在本轮数据更新结束后,再去异步更新视图。
当数据变化之后, 会调用 notify
方法去通知 watcher
进行数据更新。而 watcher
会调用 update
方法进行更新(这里就是发布订阅模式)。更新时并不是让 wathcer
立即执行, 而是放在一个队列里进行过滤,相同的 watcher
只存一个, 这个队列就是 queueWatcher
方法。最后在调用 nextTick
方法通过 flushSchedulerQueue
异步清空 watcher
队列。
.nextTick 实现原理?
nextTick
方法主要是使用了宏任务和微任务定义了一个异步方法。多次调用 nextTick
会将方法存入队列中, 通过这个异步方法清空当前队列。所以 nextTick
方法就是异步方法。
默认在内部调用 nextTick
时会传入 flushSchedulerQueue
方法, 存在一个数组里并让它执行。用户有时也会调用 nextTick
, 调用时把用户传过来的 cb
也放在数组里 , 都是同一个数组 callbacks
。多次调用 nextTick
只会执行一次, 等到代码都执行完毕后,会调用 timerFunc
这个异步方法。在方法里会依次进行判断所支持的类型:
- 如支持
Promise
则把timerFunc
包裹在了Promise
中并把flushCallbacks
放在了then
中, 相当于异步执行了flushCallBacks
。flushCallBacks
函数作用就是让传过来的方法依次执行。 - 如不是
IE
、支持Mutationobserve
并且 是原生的Mutationobserve
。首先声明一个变量并创建一个文本节点。接着创建Mutationobserve
实例并把flushCallBacks
传入, 调用observe
方法去观测每一个节点。如果节点变化会异步执行flushCallBacks
方法。 - 如果支持
setImmediate
, 则调用setImmediate
传入flushCallBacks
异步执行。 - 以上都不支持就只能调用
setTimeout
传入flushCallBacks
。
作用:$nextTick
是在下次 DOM
更新循环结束之后执行延迟回调,在修改数据之后使用 $nextTick
,则可以在回调中获取更新后的 DOM
。
.请说一下 Vue 中 Computed 和 watch ?
默认 computed
和 watch
内部都是用一个 watcher
实现的 。computed
有缓存功能, 不会先执行, 只有当依赖的属性发生变化才会通知视图跟新。watcher
没有缓存,默认会先执行,只要监听的属性发生变化就会更新视图。
computed
调用 initComputed
方法初始化计算属性时,会获取到用户定义的方法,并创建一个 watcher
把用户定义传进去, 这个 watcher
有个标识: lazy = true
,默认不会执行用户定义的函数。还有个标识 dirty = true
默认去求值 。watcher
内部调用 defineComputed
方法将计算属性定义在实例上, 其底层也是用的 Object.defineProperty
。并且传入了 createComputedGetter
方法定义一个计算属性。在用户取值时,调用的是createComputedGetter
返回函数 computedGetter
。判断当前的 watcher.dirty
是否为 true
。如果为 true
则调用 watcher.evaluate
方法求值。在求值时是调用的 this.get()
方法.其实 this.get()
就是用户传入的方法,执行时会把方法里的属性依次取值。而在取值前调用了 pushTarget
方法将 watcher
放在了全局上,当取值时会进行依赖收集,把当前的计算属性的 watcher
收集起来。等数据变化则通知 watcher
重新执行,也就是进入到了 update
方法中。update
并没有直接让 watcher
执行,而是将 dirty = true
。这样的好处就是,如果 dirty = true
,就进行求值,否则就返回上次计算后的值,从而实现了缓存的机制。
watch
调用 initWatch
方法初始化 watch
的时候, 内部传入用户定义的方法调用了 createWatcher
方法。在 createWatcher
方法中比较核心的就是 $watch
方法内部调用了 new Watcher
并传入了 expOrFn
和 回调函数。expOrFn
如果是个字符串的话, 会包装成一个函数并返回这个字符串。这时 lazy = false
了, 则直接调用了 this.get()
方法, 直接取属性的值。同 computed
在取值前也执行 pushTarget
方法将 watcher
放在了全局上, 当用户取值时就收集了 watcher
。 因此当属性值发生改变时, watcher
就会更新。
如果监听的属性值是个对象, 则取对象里的值就不会更新了, 因为默认只能对属性进行依赖收集, 不能对属性值是对象的进行依赖收集。想要不管属性值是否是对象都能求值进行收集依赖, 可设置 deep = true
。如设置了deep = true
,则会调用 traverse
方法进行递归遍历。
.Vue 组件中 data 为什么必须是一个函数?
因为 js 本身的特性带来的,同一个组件被复用多次,会创建多个实例。这些实例是同一个构造函数。如果 data
是一个对象的话,那么所有组件都共享了同一个对象。为了保证组件中数据的独立性要求每个组件必须通过data
函数返回一个对象作为组件的状态。
Vue 通过 extend
创建子类之后,会调用 mergeOptions
方法合并父类和子类的选项,选中就包括 data
。在循环完父类和子类之后调用 mergeField
函数的中的 strat
方法去合并 data
,如果 data
不是函数而是个对象,则会报错提示 data
应该是个函数。
.谈谈MVVM模式
Model
: 代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑。View
: 代表UI 组件,它负责将数据模型转化成UI 展现出来。ViewModel
: 监听模型数据的改变和控制视图行为、处理用户交互,简单理解就是一个同步View 和 Model的对象,连接Model和View。
在MVVM架构下,View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向自动的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会立即反应到View 上。而开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。
MVVM 和 MVC区别?
mvc
和 mvvm
其实区别并不大。都是一种设计思想。主要就是 mvc
中 Controller
演变成 mvvm
中的 viewModel
。mvvm
主要解决了mvc
中大量的 DOM
操作使页面渲染性能降低,加载速度变慢,影响用户体验。和当 Model
频繁发生变化,开发者需要主动更新到 View
。
.Vue 中事件绑定原理
Vue 中事件绑定分为两种:
- 原生事件绑定: 采用的是
addEventListener
实现 - 组件事件绑定: 采用的是
$on
方法实现
以 click
事件为例,普通 dom
元素绑定事件是 @click
,编译出来是 on
和 click
事件,组件绑定事件是 @click
组件自定义事件 和 @click.native
原生事件两种,编译出来分别是 on
和 click
事件, nativeOn
和 click
事件。组件的 nativeOn
等价于普通元素的 on
,而组件的 on
单独处理。
渲染页面时,普通 dom
会掉用 updateDOMListeners
方法,内部先把 data.on
方法拿出来,然后调用 updateListeners
方法来添加一个监听事件,同时会传入一个 add$1
方法。内部调用 addEventListener
方法直接把事件绑定到元素上。
而组件会调用 updateComponentListeners
方法。内部也是调用 updateListeners
方法但传入的是 add
方法。这里的 add
方法与普通元素的 dom
的 add$1
方法略有不同,采用的是自己定义的发布订阅模式 $on
方法,解析的是 on
方法,组件内部通过 $emit
方法触发的。还有 click.native
方法是直接把事件绑在了最外层元素上,用的也是 updateListeners
方法传入 add$1
方法。
.v-model 的实现原理是什么?
通俗讲 v-model
可以看成是 value + input
的语法糖。
组件的 v-model
也确实是这样 。在组件初始化的时候, 如果检测到有 model
属性,就会调用 transformModel
方法转化 model
。如果没有 prop
属性和 event
属性, 则默认会给组件 prop
为 value
属性, 给 event
为 input
事件 。把 prop
的属性赋给了 data.attrs
并把值也给了它, 即 data.attrs.value = '我们所赋的值'
。会给 on
绑定 input
事件, 对应的就是 callback
。
如果在组件内自定义 model
的 prop
和 event
, 这样的话组件初始化的时候, 接受 属性
和 事件
时不再是 value
和 input
了, 而是我们自定义的 属性
和 事件
。
如果是普通的标签, 则在运行时会自动判断标签的类型, 生成不同的属性 domProp
和 事件 on
。还增加了指令 directive
, 针对输入框的输入法加上了一些逻辑并做了校验和处理。
. Vue中的 v-show 和 v-if 是做什么用的, 两者有什么区别?
v-if
:会在 with
方法里进行判断,如果条件为 true
则创建相应的虚拟节点,否则就创建一个空的虚拟节点也就是不会渲染 DOM
。v-show
: 会在 with
方法里创建了一个指令就 v-show
,在运行的时候处理指令,添加了 style: display = none / originalDisplay
。
v-if
才是“真正的”条件渲染, 因为它会确保在切换过程中条件块内的事件监听器和子组件适当的被销毁和重建。v-if
也是惰性的, 如果在初次渲染时条件为假, 则什么也不做,一直到条件第一次变为真时, 才会渲染条件块。
相比之下, v-show
就简单的多,不管初始条件是什么,元素总会被渲染, 并且只是简单的基于 css
进行切换。
一般来说,v-if
有更高的切换开销,v-show
有更高的初始渲染开销。
因此,如需要频繁的切换则使用 v-show
较好,如在运行时条件不大可能改变则使用 v-if
较好。
. v-if 和 v-for 为什么不能连用?
v-for
的优先级会比 v-if
要高, 在 调用 with
方法编译时会先进行循环, 然后再去做 v-if
的条件判断, 因此性能不高。
因此一般会把 v-if
提出来放在 v-for
外层, 或者想要连用把渲染数据放在计算属性里进行过滤。
.Vue 中的 v-html 会导致哪些问题
v-html
其原理就是用 innerHtml
实现的的, 如果不能保证内容是完全可以被依赖的, 则可能会导致 xxs 攻击。
在运行的时候, 调用 updateDOMProps
方法或解析配置的属性, 如果判断属性是 innerHTML
的话, 会清除所有的子元素。
.Vue 中f父子组件的调用顺序
组件的调用都是先父后子,渲染完成的过程顺序都是先子后父
组件的销毁操作是先父后子,销毁完成的顺序是先子后父
在页面渲染的时候,先执行父组件的 beforeCreate -> created->befroreMount
, 当父组件实例化完成的时候会调用 rander
方法, 判断组件是不是有子组件, 如果有子组件则继续渲染子组件以此类推。当子组件实例化完成时候, 会把子组件的插入方法先存起来放到 instertedVNodeQueue
队列里, 最后会调用 invokeIntertHook
方法把当前的队列依次执行。
更新也是一样, 先父beforeUpdate -> 子beforeUpdate
再到 子 updated -> 父 updated
.Vue 中组件怎么通讯?
- 父子通讯: 父 -> 子
props
, 子 -> 父$on / $emit
通过eventsMixin
方法中的$on
方法 维护一个事件的数组,然后将函数名传入$emit
方法,循环遍历出函数并执行。 - 获得父子组件实例的方式:
$parent / $children
在初始化的时候调用initLifecycle
方法初始化$parent
和$children
放在实例上 - 在父组件中提供数据供子组件/孙子组件注入进来:
Provide / Inject
。
通过initProvide
和initInjections
方法分别把provide
和reject
放在$options
上。在调用reject
的时候,调用resolveInject
方法遍历,查看父级是否有此属性,有则就直接return
并把它定义在自己的实例上。 -
Ref
获得实例的方式调用组件的属性或方法ref 被用来给元素或子组件注册引用信息。 引用信息将会注册在父组件的 $refs 对象上。
用在 DOM 上就是 DOM 实例,用在组件上就是组件实例 -
Event bus
实现跨组件通讯
实质上还是基于$on
和$emit
,因为每个实例都有$on
和$emit
并且事件的绑定和触发必须在同一个实例,所以一般会专门定义一个实例去用于通信,如Vue.prototype.$bnts = new Vue
。 -
Vuex
状态管理实现通讯 -
$attrs
和$Listeners
实现数据 和 事件的传递,还有v-bind="$prop"
.为什么使用异步组件?
可使用异步的方式加载组件,减少打包体积,主要依赖 import()
语法,可实现文件的分割加载
components:{
testCpt: (resove) => import("../components/testCpt") 或
testCpt: r => require(['@/views/assetsInfo/assetsProofList'],r)
}
加载组件的时候, 如果组件是个函数会调用 resolveAsyncComponent
方法, 并传入组件定义的函数 asyncFactory
, 并让其马上执行。因为是异步的所以执行后并不会马上返回结果, 而返回的是一个 promise
,因此没有返回值, 返回的是一个占位符。
加载完成后,会执行 factory
函数并传入了成功/失败的回调。在回调 resolve
成功的回调时会调用 forceRander
方法, 内部调用 $forceUpdate
强制刷新。之后 resolveAsyncComponent
判断已经执行成功,就是去创建组件、初始化组件和渲染组件。
.什么是作用域插槽?
- 插槽: 创建组件虚拟节点时,会将组件儿子的虚拟节点先保存起来。初始化组件时,通过插槽属性将儿子进行分类。(作用域为父组件)
渲染组件时会拿对应的slot
属性的节点进行替换操作。 - 作用域插槽: 在解析的时候不会作为组件的孩子节点。会解析成函数,当子组件渲染时,会调用此函数进行渲染。(作用域为子组件)
普通插槽编译时调用 createElement
方法创建组件,并把子节点生成虚拟 dom
做好标识存起来。渲染时调用 randerSlot
方法循环匹配出对应的虚拟节点在父组件替换当前位置。
而作用域插槽在编译时会把子组件编译成函数,函数不调用就不会渲染。也就是说在初始化组件的时候并不会渲染子节点。渲染页面时调用 randerSlot
方法执行子节点的函数并把对应的属性传过来。当节点渲染完成之后在组件内部替换当前位置。
.说说对keep-alive的了解
keep-alive
是一个抽象组件,可实现组件缓存。当组件切换时不会对当前组件进行卸载。
算法: LRU
-->最近最久未使用法
常用的生命周期: activated
和 deactivated
声明 keep-alive
时在函数里设置了几个属性: props
,created
,destroyed
,mounted
和rander
等;
-
props
: 调用keep-alive
组件可设置的属性,共有三个属性如下:
include: 想缓存的组件
exclude:不想缓存的组件
max:最多缓存多少个 -
created
: 创建一个缓存列表 -
destroyed
: 销毁时清空所有缓存列表 -
mounted
: 会监听 include 和 exclude, 动态添加 或 移除缓存. -
rander
: 渲染时拿到第一个组件,拿到第一个组件,判断是不是在缓存里.
.$route
和 $router
的区别是什么?
$router
为 VueRouter
实例,是个全局路由对象,包含路由跳转方法、钩子函数等。$route
是路由信息对象||跳转的路由对象,每一个路由都会有一个route
对象,是一个局部对象,包含path,params,hash,query,fullPath,matched,name
等路由信息参数。
. Vue 路由的钩子函数
首页可以控制导航跳转,beforeEach,afterEach等,一般用于页面title的修改。
一些需要登录才能调整页面的重定向功能。
beforeEach主要有3个参数to,from,next:
to:route即将进入的目标路由对象,
from:route当前导航正要离开的路由
next:function一定要调用该方法resolve这个钩子。执行效果依赖next方法的调用参数。可以控制网页的跳转。
. vue-router有哪几种路由守卫?
全局守卫:beforeEach
后置守卫:afterEach
全局解析守卫:beforeResolve
路由独享守卫:beforeEnter
全局路由勾子(router.beforeEach)
件路由勾子(beforeRouteEnter)
组件路由勾子的next里的回调(beforeRouteEnter)
.Vue 中常见的性能优化
- 编码优化
(1). 不要将所有的数据放在data
里,data
中的数据都会增加gette
r和setter
,收收集对应的watcher
(2). 在v-for时给每项元素绑定事件必须使用时间代理
(3). SPA页面采用keep-alive
缓存组件
(4). 拆分组件(提高复用性,增加代码的可维护性,减少不必要的渲染)
(5).v-if
当值为false
时内部指令不执行具有阻断功能,很多情况下使用v-if
代替v-show
(6). 使用 key 保证唯一性
(7). 使用Object.freeze
冻结数据,冻结后不再有getter
和setter
(8). 合理使用路由懒加载和异步组件
(9). 数据持久化问题如: 防抖、节流 - Vue 加载性能优化
(1). 第三方模块按需导入(babel-plugin-component
)
(2). 滚动可视区域动态加载(vue-virtual-scroll-list
/ 'vue-virtual-scroller') -- 长列表优化
(3). 图片懒加载(vue-lazyload
) - 用户体验
(1).app-skeleton
骨架屏
(2).app-sheapp
壳 - SEO 优化
(1). 预加载插件prerender-spa-plugin
(2). 服务端渲染 ssr - 打包优化
(1). 使用 CDN 的方式加载第三方模块
(2). 多线程打包
(3).splitChunk
抽离公共文件 - 缓存 压缩
(1). 客户端缓存和服务端缓存
(2). 服务端gzip压缩
.Vue等单页面应用(spa)及其优缺点
优点
: Vue的目标是通过尽可能简单的 API实现响应的数据绑定和组合的视图组件,核心是一个响应的数据绑定系统。MVVM、数据驱动、组件化、轻量、简洁、高效、快速、模块友好;即第一次就将所有的东西都加载完成,因此,不会导致页面卡顿。缺点
: 不支持低版本的浏览器,最低只支持到IE9;不利于SEO的优化(如果要支持SEO,建议通过服务端来进行渲染组件);第一次加载首页耗时相对长一些;不可以使用浏览器的导航按钮需要自行实现前进、后退。
.assets 和 static的区别
相同点: assets 和 static两个都是存放静态资源文件。项目中所需要的资源文件图片,字体图标,样式文件等都可以放在这两个文件下。
不相同点:assets
中存放的静态资源文件在项目打包时,会将 assets 中放置的静态资源文件进行打包上传,所谓打包简单点可以理解为压缩体积,代码格式化。而压缩后的静态资源文件最终也都会放置在static文件中跟着index.html一同上传至服务器。static
中放置的静态资源文件就不会要走打包压缩格式化等流程,而是直接进入打包好的目录,直接上传至服务器。因为避免了压缩直接进行上传,在打包时会提高一定的效率,但是static中的资源文件由于没有进行压缩等操作,所以文件的体积也就相对于assets中打包后的文件提交较大点。在服务器中就会占据更大的空间。
建议:将项目中template需要的样式文件js文件等都可以放置在assets中,走打包这一流程。减少体积。而项目中引入的第三方的资源文件如iconfoont.css等文件可以放置在static中,因为这些引入的第三方文件已经经过处理,我们不再需要处理,直接上传。
.hash模式 和 history模式
hash:
在 url 中带有 #,其原理是 onhashchange
事件。
可以在 window
对象上监听这个事件:
window.onhashchange = function(event){
...
}
history
: 没有原 # , 其原理是 popstate 事件,需要后台配置支持。
html5中新增两个操作历史栈的API: pushState()
和 replaceState()
方法。
history.pushState(data[,title][,url]); // 向历史记录中追加一条记录
history.replaceState(data[,title][,url]); // 替换当前页在历史记录中的信息。
这两个方法也可以改变url,页面也不会重新刷新,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会立即向后端发送请求。
.Vuex是什么? 怎么使用它? 哪种功能场景使用?
Vuex
是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex
只能使用在 vue 上,因为其高度依赖于 vue 的双向绑定和插件系统。
调用了 Vue.mixin
,在所有组件的 beforeCreate
生命周期注入了设置 this.$store
这样一个对象。
场景有:单页应用中,组件之间的状态、音乐播放、登录状态、加入购物车state
: Vuex 使用单一状态树,存放的数据状态,不可以直接修改里面的数据。mutations
: 定义的方法动态修改Vuex 的 store 中的状态或数据。getters
: 类似vue的计算属性,主要用来过滤一些数据。actions
: 可以理解为通过将mutations里面处里数据的方法变成可异步的处理数据的方法,简单的说就是异步操作数据。view 层通过 store.dispath 来分发 action。modules
: 项目特别复杂的时候,可以让每一个模块拥有自己的state、mutation、action、getters,使得结构非常清晰,方便管理。
actions 和 mutations的区别
action主要处理的是异步的操作,mutation必须同步执行,而action就不受这样的限制,也就是说action中我们既可以处理同步,也可以处理异步的操作
action改变状态,最后是通过提交mutation
Vue 中 key 的作用是什么?
需要使用 key 给每一个节点做唯一标识
diff 算法可以正确识别此节点,可以更高效的更新虚拟DOM。
.用vnode描述一个DOM结构
虚拟节点就是用一个对象描述真实的dom元素
会将 template
先转换成 ast
树, ast
通过代码生成 codegen
转成 rander
函数, rander
函数内部调用 $createElement
方法简称 _c
, 传入 tag
(创建的元素), data
(元素的属性), children
(子元素) . 会判断 children
是不是一个字符串, 否则会做深度递归, 最后返回的结果就是一个对象,可描述出DOM
结构.
.简述 Vue 中 diff 算法原理
- 先同级比较, 在比较子节点.
- 判断出一方有子节点另一方没有子节点的情况.
如果新的一方有子节点,老的没有,则把子节点直接插入到老节点里即可.
如果老的一方有子节点,新的没有,则把老的子节点直接删除. - 判断出都有子节点的情况, 递归遍历子采用
双指针
(头/尾指针)的方式比对节点.
.Vue与Angular以及React的区别?
1.与AngularJS的区别
相同点:
- 都支持指令:内置指令和自定义指令。
- 都支持过滤器:内置过滤器和自定义过滤器。
- 都支持双向数据绑定。
- 都不支持低端浏览器。
不同点:
-
AngularJS
的学习成本高,比如增加了Dependency Injection
特性,而Vue.js本身提供的API都比较简单、直观。 - 在性能上,AngularJS依赖对数据做脏检查,所以Watcher越多越慢。
Vue.js使用基于依赖追踪的观察并且使用异步队列更新。所有的数据都是独立触发的。 对于庞大的应用来说,这个优化差异还是比较明显的。
2.与React的区别
相同点:
- React采用特殊的JSX语法,Vue.js在组件开发中也推崇编写.vue特殊文件格式,对文件内容都有一些约定,两者都需要编译后使用。
- 中心思想相同:一切都是组件,组件实例之间可以嵌套。
- 都提供合理的钩子函数,可以让开发者定制化地去处理需求。
- 都不内置AJAX,Route等功能核心包,而是以插件的方式加载。
- 在组件开发中都支持mixins的特性。
不同点:
React依赖Virtual DOM,而Vue.js使用的是DOM模板。React采用的Virtual DOM会对渲染出来的结果做脏检查。
Vue.js在模板中提供了指令,过滤器等,可以非常方便,快捷地操作DOM
11. 高精度全局权限处理
权限控制由前端处理时,通常使用 v-if / v-show 控制元素对不同权限的响应效果。这种情况下,就会导致很多不必要的重复代码,不容易维护,因此可以造一个小车轮,挂在全局上对权限进行处理。
// 注册全局自定义指令,对底层原生DOM操作
Vue.directive('permission', {
// inserted → 元素插入的时候
inserted(el, binding){
// 获取到 v-permission 的值
const { value } = binding
if(value) {
// 根据配置的权限,去当前用户的角色权限中校验
const hasPermission = checkPermission(value)
if(!hasPermission){
// 没有权限,则移除DOM元素
el.parentNode && el.parentNode.removeChild(el)
}
} else{
throw new Error(`need key! Like v-permission="['admin','editor']"`)
}
}
})
// --> 在组件中使用 v-permission
<button v-permission="['admin']">权限1</button>
<button v-permission="['admin', 'editor']">权限2</button>
12 Vue.use 与 Vue.component 的区别
都用于注册全局组件/插件的
Vue.component()
每次只能注册一个组件,功能很单一。
Vue.component('draggable', draggable)
Vue.use()
内部调用的仍是 Vue.component()
去注册全局组件/插件,但它可以做更多事情,比如多次调用 Vue.component() 一次性注册多个组件,还可以调用Vue.directive()、Vue.mixins()、Vue.prototype.xxx=xxx 等等,其第二个可选参数又可以传递一些数据
Vue.use({
install:function (Vue, options) {
// 接收传递的参数: { name: 'My-Vue', age: 28 }
console.log(options.name, options.age)
Vue.directive('my-directive',{
inserted(el, binding, vnode) { }
})
Vue.mixin({
mounted() { }
})
Vue.component('draggable', draggable)
Vue.component('Tree', Tree)
}
},
{ name: 'My-Vue', age: 28 })
在main.js 文件里 动态注册全局组件时, 或用到 require.context
require.context():
一个 Webpack 的API,获取一个特定的上下文(创建自己的context),主要用来实现自动化导入模块。
它会遍历文件夹中的指定文件,然后自动化导入,而不需要每次都显式使用 import / require 语句导入模块!
在前端工程中,如果需要一个文件夹引入很多模块,则可以使用 require.context()
require.context(directory, useSubdirectories = false, regExp = /^\.\//)
directory
{String} 读取目录的路径useSubdirectories
{Boolean} 是否递归遍历子目录regExp
{RegExp} 匹配文件的正则
.对于 vue3.0 特性你有什么了解的吗?
(1). 监测机制的改变
3.0 基于代理 Proxy 的 observer 实现,提供全语言覆盖的反应性跟踪。替代了Vue 2采用 defineProperty去定义get 和 set, 意味着彻底放弃了兼容IE, 这也取消除了 Vue 2 当中基于 Object.defineProperty 的实现所存在的很多限制:
=>只能监测属性,不能监测对象:
=>检测属性的添加和删除;
=>检测数组索引和长度的变更;
=>支持 Map、Set、WeakMap 和 WeakSet。
新的 observer 还提供了以下特性:用于创建 observable 的公开 API
。这为中小规模场景提供了简单轻量级的跨组件状态管理解决方案。默认采用惰性观察
。在 2.x 中,不管反应式数据有多大,都会在启动时被观察到。如果数据集很大,这可能会在应用启动时带来明显的开销。在 3.x 中,只观察用于渲染应用程序最初可见部分的数据。更精确的变更通知
。在 2.x 中,通过 Vue.set 强制添加新属性将导致依赖于该对象的 watcher 收到变更通知。在 3.x 中,只有依赖于特定属性的 watcher 才会收到通知。不可变的 observable
:我们可以创建值的“不可变”版本(即使是嵌套属性),除非系统在内部暂时将其“解禁”。这个机制可用于冻结 prop 传递或 Vuex 状态树以外的变化。更好的调试功能
:我们可以使用新的 renderTracked 和 renderTriggered 钩子精确地跟踪组件在什么时候以及为什么重新渲染。
(2). 模板
模板方面没有大的变更,只改了作用域插槽,2.x 的机制导致作用域插槽变了,父组件会重新渲染,而 3.0 把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能。
同时,对于 render 函数的方面,vue3.0 也进行一系列更改来方便习惯直接使用 api 来生成 vdom 。
(3). 对象式的组件声明方式
vue2.x
中的组件是通过声明的方式传入一系列 option,和 TypeScript 的结合需要通过一些装饰器的方式来做,虽然能实现功能,但是比较麻烦。vue3.0
修改了组件的声明方式,改成了类式的写法,这样使得和 TypeScript 的结合变得很容易。
此外,vue 的源码也改用了 TypeScript 来写。其实当代码的功能复杂之后,必须有一个静态类型系统来做一些辅助管理。现在 vue3.0 也全面改用 TypeScript 来重写了,更是使得对外暴露的 api 更容易结合 TypeScript。静态类型系统对于复杂代码的维护确实很有必要。
(4). 其它方面的更改
支持自定义渲染器,从而使得 weex 可以通过自定义渲染器的方式来扩展,而不是直接 fork 源码来改的方式。
支持 Fragment(多个根节点)和 Protal(在 dom 其他部分渲染组建内容)组件,针对一些特殊的场景做了处理。
基于 treeshaking 优化,提供了更多的内置功能。
推荐阅读
-
常见的前端开发面试问题及其答案
-
前端面试问题(十三)
-
前端基础面试问题 - 第 4 部分 - Vue(第 2 部分)
-
高级架构师面试问题
-
Vue3 中的 30 个高频重点面试问题
-
前端vue项目部署中,用户还在访问系统,存在缓存问题,部署后需要ctrlF5强制刷新,解决热更新的几种方案
-
【面试必备】Vue 3 常见问题全解析,让你轻松过关!
-
必知!2022年Vue面试常见问题大揭秘
-
理解Web前端面试常见问题(2):Keeplive、V-show与v-if的差异、同源策略以及跨域
-
SSM三大框架基础面试题-一、Spring篇 什么是Spring框架? Spring是一种轻量级框架,提高开发人员的开发效率以及系统的可维护性。 我们一般说的Spring框架就是Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。这些模块是核心容器、数据访问/集成、Web、AOP(面向切面编程)、工具、消息和测试模块。比如Core Container中的Core组件是Spring所有组件的核心,Beans组件和Context组件是实现IOC和DI的基础,AOP组件用来实现面向切面编程。 Spring的6个特征: 核心技术:依赖注入(DI),AOP,事件(Events),资源,i18n,验证,数据绑定,类型转换,SpEL。 测试:模拟对象,TestContext框架,Spring MVC测试,WebTestClient。 数据访问:事务,DAO支持,JDBC,ORM,编组XML。 Web支持:Spring MVC和Spring WebFlux Web框架。 集成:远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。 语言:Kotlin,Groovy,动态语言。 列举一些重要的Spring模块? Spring Core:核心,可以说Spring其他所有的功能都依赖于该类库。主要提供IOC和DI功能。 Spring Aspects:该模块为与AspectJ的集成提供支持。 Spring AOP:提供面向切面的编程实现。 Spring JDBC:Java数据库连接。 Spring JMS:Java消息服务。 Spring ORM:用于支持Hibernate等ORM工具。 Spring Web:为创建Web应用程序提供支持。 Spring Test:提供了对JUnit和TestNG测试的支持。 谈谈自己对于Spring IOC和AOP的理解 IOC(Inversion Of Controll,控制反转)是一种设计思想: 在程序中手动创建对象的控制权,交由给Spring框架来管理。IOC在其他语言中也有应用,并非Spring特有。IOC容器实际上就是一个Map(key, value),Map中存放的是各种对象。 将对象之间的相互依赖关系交给IOC容器来管理,并由IOC容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。IOC容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。在实际项目中一个Service类可能由几百甚至上千个类作为它的底层,假如我们需要实例化这个Service,可能要每次都搞清楚这个Service所有底层类的构造函数,这可能会把人逼疯。如果利用IOC的话,你只需要配置好,然后在需要的地方引用就行了,大大增加了项目的可维护性且降低了开发难度。 Spring中的bean的作用域有哪些? 1.singleton:该bean实例为单例 2.prototype:每次请求都会创建一个新的bean实例(多例)。 3.request:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。 4.session:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP session内有效。 5.global-session:全局session作用域,仅仅在基于Portlet的Web应用中才有意义,Spring5中已经没有了。Portlet是能够生成语义代码(例如HTML)片段的小型Java Web插件。它们基于Portlet容器,可以像Servlet一样处理HTTP请求。但是与Servlet不同,每个Portlet都有不同的会话。 Spring中的单例bean的线程安全问题了解吗? 概念用于理解:大部分时候我们并没有在系统中使用多线程,所以很少有人会关注这个问题。单例bean存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题。 有两种常见的解决方案(用于回答的点): 1.在bean对象中尽量避免定义可变的成员变量(不太现实)。 2.在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在ThreadLocal(线程本地化对象)中(推荐的一种方式)。 ThreadLocal解决多线程变量共享问题(参考博客):https://segmentfault.com/a/1190000009236777 Spring中Bean的生命周期: 1.Bean容器找到配置文件中Spring Bean的定义。 2.Bean容器利用Java Reflection API创建一个Bean的实例。 3.如果涉及到一些属性值,利用set方法设置一些属性值。 4.如果Bean实现了BeanNameAware接口,调用setBeanName方法,传入Bean的名字。 5.如果Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader方法,传入ClassLoader对象的实例。 6.如果Bean实现了BeanFactoryAware接口,调用setBeanClassFacotory方法,传入ClassLoader对象的实例。 7.与上面的类似,如果实现了其他*Aware接口,就调用相应的方法。 8.如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象,执postProcessBeforeInitialization方法。 9.如果Bean实现了InitializingBean接口,执行afeterPropertiesSet方法。 10.如果Bean在配置文件中的定义包含init-method属性,执行指定的方法。 11.如果有和加载这个Bean的Spring容器相关的BeanPostProcess对象,执行postProcessAfterInitialization方法。 12.当要销毁Bean的时候,如果Bean实现了DisposableBean接口,执行destroy方法。 13.当要销毁Bean的时候,如果Bean在配置文件中的定义包含destroy-method属性,执行指定的方法。 Spring框架中用到了哪些设计模式? 1.工厂设计模式:Spring使用工厂模式通过BeanFactory和ApplicationContext创建bean对象。 2.代理设计模式:Spring AOP功能的实现。 3.单例设计模式:Spring中的bean默认都是单例的。 4.模板方法模式:Spring中的jdbcTemplate、hibernateTemplate等以Template结尾的对数据库操作的类,它们就使用到了模板模式。 5.包装器设计模式:我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。 6.观察者模式:Spring事件驱动模型就是观察者模式很经典的一个应用。 7.适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式、Spring MVC中也是用到了适配器模式适配Controller。 还有很多。。。。。。。 @Component和@Bean的区别是什么 1.作用对象不同。@Component注解作用于类,而@Bean注解作用于方法。 2.@Component注解通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中(我们可以使用@ComponentScan注解定义要扫描的路径)。@Bean注解通常是在标有该注解的方法中定义产生这个bean,告诉Spring这是某个类的实例,当我需要用它的时候还给我。 3.@Bean注解比@Component注解的自定义性更强,而且很多地方只能通过@Bean注解来注册bean。比如当引用第三方库的类需要装配到Spring容器的时候,就只能通过@Bean注解来实现。 @Configuration public class AppConfig { @Bean public TransferService transferService { return new TransferServiceImpl; } } <beans> <bean id="transferService" class="com.kk.TransferServiceImpl"/> </beans> @Bean public OneService getService(status) { case (status) { when 1: return new serviceImpl1; when 2: return new serviceImpl2; when 3: return new serviceImpl3; } } 将一个类声明为Spring的bean的注解有哪些? 声明bean的注解: @Component 组件,没有明确的角色 @Service 在业务逻辑层使用(service层) @Repository 在数据访问层使用(dao层) @Controller 在展现层使用,控制器的声明 注入bean的注解: @Autowired:由Spring提供 @Inject:由JSR-330提供 @Resource:由JSR-250提供 *扩:JSR 是 java 规范标准 Spring事务管理的方式有几种? 1.编程式事务:在代码中硬编码(不推荐使用)。 2.声明式事务:在配置文件中配置(推荐使用),分为基于XML的声明式事务和基于注解的声明式事务。 Spring事务中的隔离级别有哪几种? 在TransactionDefinition接口中定义了五个表示隔离级别的常量:ISOLATION_DEFAULT:使用后端数据库默认的隔离级别,Mysql默认采用的REPEATABLE_READ隔离级别;Oracle默认采用的READ_COMMITTED隔离级别。ISOLATION_READ_UNCOMMITTED:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。ISOLATION_READ_COMMITTED:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生ISOLATION_REPEATABLE_READ:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。ISOLATION_SERIALIZABLE:最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 Spring事务中有哪几种事务传播行为? 在TransactionDefinition接口中定义了八个表示事务传播行为的常量。 支持当前事务的情况:PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)。 不支持当前事务的情况:PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。 其他情况:PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于PROPAGATION_REQUIRED。 二、SpringMVC篇 什么是Spring MVC ?简单介绍下你对springMVC的理解? Spring MVC是一个基于Java的实现了MVC设计模式的请求驱动类型的轻量级Web框架,通过把Model,View,Controller分离,将web层进行职责解耦,把复杂的web应用分成逻辑清晰的几部分,简化开发,减少出错,方便组内开发人员之间的配合。 Spring MVC的工作原理了解嘛? image.png Springmvc的优点: (1)可以支持各种视图技术,而不仅仅局限于JSP; (2)与Spring框架集成(如IoC容器、AOP等); (3)清晰的角色分配:前端控制器(dispatcherServlet) , 请求到处理器映射(handlerMapping), 处理器适配器(HandlerAdapter), 视图解析器(ViewResolver)。 (4) 支持各种请求资源的映射策略。 Spring MVC的主要组件? (1)前端控制器 DispatcherServlet(不需要程序员开发) 作用:接收请求、响应结果,相当于转发器,有了DispatcherServlet 就减少了其它组件之间的耦合度。 (2)处理器映射器HandlerMapping(不需要程序员开发) 作用:根据请求的URL来查找Handler (3)处理器适配器HandlerAdapter 注意:在编写Handler的时候要按照HandlerAdapter要求的规则去编写,这样适配器HandlerAdapter才可以正确的去执行Handler。 (4)处理器Handler(需要程序员开发) (5)视图解析器 ViewResolver(不需要程序员开发) 作用:进行视图的解析,根据视图逻辑名解析成真正的视图(view) (6)视图View(需要程序员开发jsp) View是一个接口, 它的实现类支持不同的视图类型(jsp,freemarker,pdf等等) springMVC和struts2的区别有哪些? (1)springmvc的入口是一个servlet即前端控制器(DispatchServlet),而struts2入口是一个filter过虑器(StrutsPrepareAndExecuteFilter)。 (2)springmvc是基于方法开发(一个url对应一个方法),请求参数传递到方法的形参,可以设计为单例或多例(建议单例),struts2是基于类开发,传递参数是通过类的属性,只能设计为多例。 (3)Struts采用值栈存储请求和响应的数据,通过OGNL存取数据,springmvc通过参数解析器是将request请求内容解析,并给方法形参赋值,将数据和视图封装成ModelAndView对象,最后又将ModelAndView中的模型数据通过reques域传输到页面。Jsp视图解析器默认使用jstl。 SpringMVC怎么样设定重定向和转发的? (1)转发:在返回值前面加"forward:",譬如"forward:user.do?name=method4" (2)重定向:在返回值前面加"redirect:",譬如"redirect:http://www.baidu.com" SpringMvc怎么和AJAX相互调用的? 通过Jackson框架就可以把Java里面的对象直接转化成Js可以识别的Json对象。具体步骤如下 : (1)加入Jackson.jar (2)在配置文件中配置json的映射 (3)在接受Ajax方法里面可以直接返回Object,List等,但方法前面要加上@ResponseBody注解。 如何解决POST请求中文乱码问题,GET的又如何处理呢? (1)解决post请求乱码问题: 在web.xml中配置一个CharacterEncodingFilter过滤器,设置成utf-8; <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> (2)get请求中文参数出现乱码解决方法有两个: ①修改tomcat配置文件添加编码与工程编码一致,如下: <ConnectorURIEncoding="utf-8" connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/> ②另外一种方法对参数进行重新编码: String userName = new String(request.getParamter("userName").getBytes("ISO8859-1"),"utf-8") ISO8859-1是tomcat默认编码,需要将tomcat编码后的内容按utf-8编码。 Spring MVC的异常处理 ? 统一异常处理: Spring MVC处理异常有3种方式: (1)使用Spring MVC提供的简单异常处理器SimpleMappingExceptionResolver; (2)实现Spring的异常处理接口HandlerExceptionResolver 自定义自己的异常处理器; (3)使用@ExceptionHandler注解实现异常处理; 统一异常处理的博客:https://blog.csdn.net/ctwy291314/article/details/81983103 SpringMVC的控制器是不是单例模式,如果是,有什么问题,怎么解决? 是单例模式,所以在多线程访问的时候有线程安全问题,不要用同步,会影响性能的,解决方案是在控制器里面不能写成员变量。(此题目类似于上面Spring 中 第5题 有两种解决方案) SpringMVC常用的注解有哪些? @RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,则表示类中的所有响应请求的方法都是以该地址作为父路径。 @RequestBody:注解实现接收http请求的json数据,将json转换为java对象。 @ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给客户。 SpingMvc中的控制器的注解一般用那个,有没有别的注解可以替代? 一般用@Controller注解,也可以使用@RestController,@RestController注解相当于@ResponseBody + @Controller,表示是表现层,除此之外,一般不用别的注解代替。 如果在拦截请求中,我想拦截get方式提交的方法,怎么配置? 可以在@RequestMapping注解里面加上method=RequestMethod.GET。 怎样在方法里面得到Request,或者Session? 直接在方法的形参中声明request,SpringMVC就自动把request对象传入。 如果想在拦截的方法里面得到从前台传入的参数,怎么得到? 直接在形参里面声明这个参数就可以,但必须名字和传过来的参数一样。 如果前台有很多个参数传入,并且这些参数都是一个对象的,那么怎么样快速得到这个对象? 直接在方法中声明这个对象,SpringMVC就自动会把属性赋值到这个对象里面。 SpringMVC中函数的返回值是什么? 返回值可以有很多类型,有String, ModelAndView。ModelAndView类把视图和数据都合并的一起的。 SpringMVC用什么对象从后台向前台传递数据的? 通过ModelMap对象,可以在这个对象里面调用put方法,把对象加到里面,前台就可以拿到数据。 怎么样把ModelMap里面的数据放入Session里面? 可以在类上面加上@SessionAttributes注解,里面包含的字符串就是要放入session里面的key。 SpringMvc里面拦截器是怎么写的: 有两种写法,一种是实现HandlerInterceptor接口,另外一种是继承适配器类,接着在接口方法当中,实现处理逻辑;然后在SpringMvc的配置文件中配置拦截器即可: <!-- 配置SpringMvc的拦截器 --> <mvc:interceptors> <!-- 配置一个拦截器的Bean就可以了 默认是对所有请求都拦截 --> <bean id="myInterceptor" class="com.zwp.action.MyHandlerInterceptor"></bean> <!-- 只针对部分请求拦截 --> <mvc:interceptor> <mvc:mapping path="/modelMap.do" /> <bean class="com.zwp.action.MyHandlerInterceptorAdapter" /> </mvc:interceptor> </mvc:interceptors> 注解原理: 注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象。通过代理对象调用自定义注解的方法,会最终调用AnnotationInvocationHandler的invoke方法。该方法会从memberValues这个Map中索引出对应的值。而memberValues的来源是Java常量池 三、Mybatis篇 什么是MyBatis? MyBatis是一个可以自定义SQL、存储过程和高级映射的持久层框架。 讲下MyBatis的缓存 MyBatis的缓存分为一级缓存和二级缓存,一级缓存放在session里面,默认就有, 二级缓存放在它的命名空间里,默认是不打开的,使用二级缓存属性类需要实现Serializable序列化接口, 可在它的映射文件中配置<cache/> Mybatis是如何进行分页的?分页插件的原理是什么? 1)Mybatis使用RowBounds对象进行分页,也可以直接编写sql实现分页,也可以使用Mybatis的分页插件。 2)分页插件的原理:实现Mybatis提供的接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql。 举例:select * from student,拦截sql后重写为:select t.* from (select * from student)t limit 0,10 简述Mybatis的插件运行原理,以及如何编写一个插件? 1)Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、 Executor这4种接口的插件,Mybatis通过动态代理, 为需要拦截的接口生成代理对象以实现接口方法拦截功能, 每当执行这4种接口对象的方法时,就会进入拦截方法, 具体就是InvocationHandler的invoke方法,当然, 只会拦截那些你指定需要拦截的方法。 2)实现Mybatis的Interceptor接口并复写intercept方法, 然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可, 记住,别忘了在配置文件中配置你编写的插件。 Mybatis动态sql是做什么的?都有哪些动态sql?能简述一下动态sql的执行原理不? 1)Mybatis动态sql可以让我们在Xml映射文件内, 以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能。 2)Mybatis提供了9种动态sql标签:trim|where|set|foreach|if|choose|when|otherwise|bind。 3)其执行原理为,使用OGNL从sql参数对象中计算表达式的值, 根据表达式的值动态拼接sql,以此来完成动态sql的功能。 #{}和${}的区别是什么? 1)#{}是预编译处理,${}是字符串替换。 2)Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值(有效的防止SQL注入); 3)Mybatis在处理${}时,就是把${}替换成变量的值。 为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里? Hibernate属于全自动ORM映射工具, 使用Hibernate查询关联对象或者关联集合对象时, 可以根据对象关系模型直接获取,所以它是全自动的。 而Mybatis在查询关联对象或关联集合对象时, 需要手动编写sql来完成,所以,称之为半自动ORM映射工具。 Mybatis是否支持延迟加载?如果支持,它的实现原理是什么? 1)Mybatis仅支持association关联对象和collection关联集合对象的延迟加载, association指的就是一对一,collection指的就是一对多查询。 在Mybatis配置文件中, 可以配置是否启用延迟加载lazyLoadingEnabled=true|false。 2)它的原理是,使用CGLIB创建目标对象的代理对象, 当调用目标方法时,进入拦截器方法, 比如调用a.getB.getName, 拦截器invoke方法发现a.getB是null值, 那么就会单独发送事先保存好的查询关联B对象的sql, 把B查询上来,然后调用a.setB(b), 于是a的对象b属性就有值了, 接着完成a.getB.getName方法的调用。 这就是延迟加载的基本原理。 MyBatis与Hibernate有哪些不同? 1)Mybatis和hibernate不同,它不完全是一个ORM框架, 因为MyBatis需要程序员自己编写Sql语句, 不过mybatis可以通过XML或注解方式灵活配置要运行的sql语句, 并将java对象和sql语句映射生成最终执行的sql, 最后将sql执行的结果再映射生成java对象。 2)Mybatis学习门槛低,简单易学,程序员直接编写原生态sql, 可严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发, 例如互联网软件、企业运营类软件等,因为这类软件需求变化频繁, 一但需求变化要求成果输出迅速。但是灵活的前提是mybatis无法做到数据库无关性, 如果需要实现支持多种数据库的软件则需要自定义多套sql映射文件,工作量大。 3)Hibernate对象/关系映射能力强,数据库无关性好, 对于关系模型要求高的软件(例如需求固定的定制化软件) 如果用hibernate开发可以节省很多代码,提高效率。 但是Hibernate的缺点是学习门槛高,要精通门槛更高, 而且怎么设计O/R映射,在性能和对象模型之间如何权衡, 以及怎样用好Hibernate需要具有很强的经验和能力才行。 总之,按照用户的需求在有限的资源环境下只要能做出维护性、 扩展性良好的软件架构都是好架构,所以框架只有适合才是最好。 MyBatis的好处是什么? 1)MyBatis把sql语句从Java源程序中独立出来,放在单独的XML文件中编写, 给程序的维护带来了很大便利。 2)MyBatis封装了底层JDBC API的调用细节,并能自动将结果集转换成Java Bean对象, 大大简化了Java数据库编程的重复工作。 3)因为MyBatis需要程序员自己去编写sql语句, 程序员可以结合数据库自身的特点灵活控制sql语句, 因此能够实现比Hibernate等全自动orm框架更高的查询效率,能够完成复杂查询。 简述Mybatis的Xml映射文件和Mybatis内部数据结构之间的映射关系? Mybatis将所有Xml配置信息都封装到All-In-One重量级对象Configuration内部。 在Xml映射文件中,<parameterMap>标签会被解析为ParameterMap对象, 其每个子元素会被解析为ParameterMapping对象。 <resultMap>标签会被解析为ResultMap对象, 其每个子元素会被解析为ResultMapping对象。 每一个<select>、<insert>、<update>、<delete> 标签均会被解析为MappedStatement对象, 标签内的sql会被解析为BoundSql对象。 什么是MyBatis的接口绑定,有什么好处? 接口映射就是在MyBatis中任意定义接口,然后把接口里面的方法和SQL语句绑定, 我们直接调用接口方法就可以,这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置. 接口绑定有几种实现方式,分别是怎么实现的? 接口绑定有两种实现方式,一种是通过注解绑定,就是在接口的方法上面加 上@Select@Update等注解里面包含Sql语句来绑定, 另外一种就是通过xml里面写SQL来绑定,在这种情况下, 要指定xml映射文件里面的namespace必须为接口的全路径名. 什么情况下用注解绑定,什么情况下用xml绑定? 当Sql语句比较简单时候,用注解绑定;当SQL语句比较复杂时候,用xml绑定,一般用xml绑定的比较多 MyBatis实现一对一有几种方式?具体怎么操作的? 有联合查询和嵌套查询,联合查询是几个表联合查询,只查询一次, 通过在resultMap里面配置association节点配置一对一的类就可以完成; 嵌套查询是先查一个表,根据这个表里面的结果的外键id, 去再另外一个表里面查询数据,也是通过association配置, 但另外一个表的查询通过select属性配置。 Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别? 能,Mybatis不仅可以执行一对一、一对多的关联查询, 还可以执行多对一,多对多的关联查询,多对一查询, 其实就是一对一查询,只需要把selectOne修改为selectList即可; 多对多查询,其实就是一对多查询,只需要把selectOne修改为selectList即可。 关联对象查询,有两种实现方式,一种是单独发送一个sql去查询关联对象, 赋给主对象,然后返回主对象。另一种是使用嵌套查询,嵌套查询的含义为使用join查询, 一部分列是A对象的属性值,另外一部分列是关联对象B的属性值, 好处是只发一个sql查询,就可以把主对象和其关联对象查出来。 MyBatis里面的动态Sql是怎么设定的?用什么语法? MyBatis里面的动态Sql一般是通过if节点来实现,通过OGNL语法来实现, 但是如果要写的完整,必须配合where,trim节点,where节点是判断包含节点有 内容就插入where,否则不插入,trim节点是用来判断如果动态语句是以and 或or 开始,那么会自动把这个and或者or取掉。 Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式? 第一种是使用<resultMap>标签,逐一定义列名和对象属性名之间的映射关系。 第二种是使用sql列的别名功能,将列别名书写为对象属性名, 比如T_NAME AS NAME,对象属性名一般是name,小写, 但是列名不区分大小写,Mybatis会忽略列名大小写,