.什么是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
进行获取节点等操作,可发起后端请求拿到数据。
beforeUpdate
: 数据更新时调用,发生在虚拟 Dom
重新渲染 和 打补丁之前之调用。
updated
: 组件 Dom
已完成更新,可执行依赖的 Dom
操作,不要操作数据会陷入死循环。
beforeDestroy
: 实例销毁之前调用,可进行优化操作,如销毁定时器,解除绑定事件。
destroyed
: 组件已经被销毁,事件监听器和子实例都会被移除销毁。
首次页面加载会触发四个钩子函数: beforeCreate, created, beforeMount, mounted
且 DMO
渲染在 mounted
中就已经完成了。
可以使用 $on('hook:')
或 $once('hook:')
来简化生命周期的注册
.谈谈 MVVM 模式
Model
: 代表数据模型,也可以在 Model 中定义 数据修改 和 操作 的业务逻辑。
View
: 代表 UI 组件,它负责将 数据模型 转化成 UI 展现出来。
ViewModel
: 监听模型数据的改变和控制视图行为、处理用户交互,简单理解就是一个同步 View 和 Model 的对象,连接 Model 和 View。
在 MVVM 架构下,View 和 Model 之间并没有直接的联系,而是通过 ViewMode
进行交互,Model 和 ViewModel 之间的交互是双向自动的, 因此 View 数据的变化会同步到 Model 中,而 Model 数据的变化也会立即反应到 View 上。而开发者只需关注业务逻辑,不需要手动操作 DOM,不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。
MVVM 和 MVC区别?
mvc
和 mvvm
其实区别并不大。都是一种设计思想。主要就是 mvc
中 Controller
演变成 mvvm
中的 viewModel
。mvvm
主要解决了mvc
中大量的 DOM
操作使页面渲染性能降低,加载速度变慢,影响用户体验。和当 Model
频繁发生变化,开发者需要主动更新到 View
。
说下 Vue 实现数据双向绑定的原理
Vue 实现数据双向绑定主要是:采用 数据劫持结合发布者-订阅者模式
的方式,通过 Object.defineProperty()
来劫持各个属性的 setter
,getter
,在数据变动时发布消息给订阅者,触发相应监听回调。当把一个普通 Javascript
对象传给 Vue 实例来作为它的 data
选项时,Vue 将遍历它的属性,用 Object.defineProperty()
将它们转为 getter/setter
。用户看不到 getter/setter
,但是在内部它们让 Vue追踪依赖,在属性被访问和修改时通知变化。
.请说一下 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
立即执行,而是放在一个 queueWatcher
队列里进行过滤,相同的 watcher
只存一个。最后在调用 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
应该是个函数。
.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 中父子组件的调用顺序
组件的调用都是先父后子,渲染完成的过程顺序都是先子后父
组件的销毁操作是先父后子,销毁完成的顺序是先子后父
在页面渲染的时候,先执行父组件的 beforeCreate -> created -> befroreMount
,当父组件实例化完成的时候会调用 rander
方法,判断组件是不是有子组件,如果有子组件则继续渲染子组件以此类推。当子组件实例化完成时候,会把子组件的插入方法先存起来放到 instertedVNodeQueue
队列里, 最后会调用 invokeIntertHook
方法把当前的队列依次执行。
更新也是一样,先父beforeUpdate -> 子beforeUpdate
再到 子 updated -> 父 updated
加载渲染过程
父beforeCreate-> 父created-> 父beforeMount-> 子beforeCreate-> 子created-> 子beforeMount- > 子mounted-> 父mounted
子组件更新过程
父beforeUpdate-> 子beforeUpdate-> 子updated-> 父updated
父组件更新过程
父beforeUpdate -> 父updated
销毁过程
父beforeDestroy-> 子beforeDestroy-> 子destroyed-> 父destroyed
# Vue中父组件能监听到子组件的生命周期吗
父组件通过@hook:
能够监听到子组件的生命周期,举个栗子:
// 这里是父组件
.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
判断已经执行成功,就是去创建组件、初始化组件和渲染组件。
# Vue中的事件修饰符主要有哪些?分别是什么作用
.stop
:阻止事件冒泡 .native
:绑定原生事件
.once
:事件只执行一次
.self
:将事件绑定在自身身上,相当于阻止事件冒泡
.prevent
:阻止默认事件 .caption
:用于事件捕获
# v-for 里面数据层次太多,数据不刷新怎么办
运用 this.$forceUpdate()
迫使 Vue 实例重新渲染。
注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。
.说说对 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有哪几种路由守卫?
- 全局守卫 ( vue-router 全局有三个守卫 )
router.beforeEach
全局前置守卫 进入路由之前
router.beforeResolve
全局解析守卫(2.5.0+) 在beforeRouteEnter调用之后调用
router.afterEach
全局后置钩子 进入路由之后
// main.js 入口文件
import router from './router'; // 引入路由
router.beforeEach((to, from, next) => {
next();
});
router.beforeResolve((to, from, next) => {
next();
});
router.afterEach((to, from) => {
console.log('afterEach 全局后置钩子');
});
- 路由独享守卫
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// 参数用法什么的都一样,调用顺序在全局前置守卫后面,所以不会被全局守卫覆盖
}
}
]
})
- 路由组件内的守卫
beforeRouteEnter
进入路由前, 在路由独享守卫后调用 不能 获取组件实例 this,组件实例还没被创建
beforeRouteUpdate (2.2)
路由复用同一个组件时, 在当前路由改变,但是该组件被复用时调用 可以访问组件实例 this
beforeRouteLeave
离开当前路由时, 导航离开该组件的对应路由时调用,可以访问组件实例 this
.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
提交的是 mutation
,而不是直接变更状态。
如果请求来的数据不是要被其他组件公用,仅仅在请求的组件内使用,就不需要放入 vuex
的 state
里。
如果被其他地方复用,请将请求放入 action
里方便复用,并包装成 promise
返回。
.assets 和 static的区别
相同点: assets
和 static
两个都是存放静态资源文件。项目中所需要的资源文件图片,字体图标,样式文件等都可以放在这两个文件下。
不相同点:
assets
中存放的静态资源文件在项目打包时,会将 assets
中放置的静态资源文件进行打包上传,所谓打包简单点可以理解为压缩体积,代码格式化。而压缩后的静态资源文件最终也都会放置在static文件中跟着index.html
一同上传至服务器。
static
中放置的静态资源文件就不会要走打包压缩格式化等流程,而是直接进入打包好的目录,直接上传至服务器。因为避免了压缩直接进行上传,在打包时会提高一定的效率,但是 static
中的资源文件由于没有进行压缩等操作,所以文件的体积也就相对于 assets
中打包后的文件提交较大点。在服务器中就会占据更大的空间。
建议:将项目中 template
需要的样式文件js文件等都可以放置在 assets
中,走打包这一流程。减少体积。而项目中引入的第三方的资源文件如iconfoont.css等文件可以放置在 static
中,因为这些引入的第三方文件已经经过处理,我们不再需要处理,直接上传。
Vue 中 key 的作用是什么?
需要使用 key 给每一个节点做唯一标识,可让 diff 算法可以正确识别此节点,以更高效的更新虚拟 DOM。
新旧 children 中的节点只有顺序是不同的时候,最佳的操作应该是通过移动元素的位置来达到更新的目的
需要在新旧 children 的节点中保存映射关系,以便能够在旧 children 的节点中找到可复用的节点。key也就是children中节点的唯一标识
.用vnode描述一个DOM结构
虚拟节点就是用一个对象描述真实的dom元素
会将 template
先转换成 ast
树, ast
通过代码生成 codegen
转成 rander
函数, rander
函数内部调用 $createElement
方法简称 _c
, 传入 tag
(创建的元素), data
(元素的属性), children
(子元素) . 会判断 children
是不是一个字符串, 否则会做深度递归, 最后返回的结果就是一个对象,可描述出DOM
结构.
.简述 Vue 中 diff 算法原理
- 先同级比较, 在比较子节点.
- 判断出一方有子节点另一方没有子节点的情况.
如果新的一方有子节点,老的没有,则把子节点直接插入到老节点里即可.
如果老的一方有子节点,新的没有,则把老的子节点直接删除. - 判断出都有子节点的情况, 递归遍历子采用
双指针
(头/尾指针)的方式比对节点.
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} 匹配文件的正则
既然 Vue 通过数据劫持可以精准探测数据变化,为什么还需要虚拟 DOM 进行 diff 检测差异?
现代前端框架有两种方式侦测变化,一种是 pull
一种是push
pull
: 其代表为 React
,通常会用 setStateAPI
显式更新,然后 React
会进行一层层的 Virtual Dom Diff
操作找出差异,然后 Patch
到 DOM
上,React
从一开始就不知道到底是哪发生了变化,只是知道「有变化了」,然后再进行比较暴力的 Diff
操作查找「哪发生变化了」,另外一个代表就是 Angular
的脏检查操作。
push
: Vue
的响应式系统则是 push
的代表,当 Vue
程序初始化的时候就会对数据 data
进行依赖的收集,一但数据发生变化,响应式系统就会立刻得知,因此 Vue
是一开始就知道是「在哪发生变化了」,但是这又会产生一个问题,如果你熟悉 Vue
的响应式系统就知道,通常一个绑定一个数据就需要一个 Watcher
,一但我们的绑定细粒度过高就会产生大量的 Watcher
,这会带来内存以及依赖追踪的开销,而细粒度过低会无法精准侦测变化,因此 Vue
的设计是选择中等细粒度的方案,在组件级别进行 push
侦测的方式,也就是那套响应式系统,通常我们会第一时间侦测到发生变化的组件,然后在组件内部进行 Virtual Dom Diff
获取更加具体的差异,而Virtual Dom Diff
则是 pull
操作,Vue
是 push+pull
结合的方式进行变化侦测的。
Vue 为什么没有类似于 React 中 shouldComponentUpdate 的生命周期?
根本原因是 Vue
与 React
的变化侦测方式有所不同
React
是 pull
的方式侦测变化,当 React
知道发生变化后,会使用 Virtual Dom Diff
进行差异检测,但是很多组件实际上是肯定不会发生变化的,这个时候需要用 shouldComponentUpdate
进行手动操作来减少diff
,从而提高程序整体的性能。
Vue
是 pull+push
的方式侦测变化的,在一开始就知道那个组件发生了变化,因此在 push
的阶段并不需要手动控制 diff
,而组件内部采用的 diff
方式实际上是可以引入类似于 shouldComponentUpdate
相关生命周期的,但是通常合理大小的组件不会有过量的 diff
,手动优化的价值有限,因此目前 Vue
并没有考虑引入shouldComponentUpdate
这种手动优化的生命周期。
.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压缩
.什么是作用域插槽?
- 插槽: 创建组件虚拟节点时,会将组件儿子的虚拟节点先保存起来。初始化组件时,通过插槽属性将儿子进行分类。(作用域为父组件)
渲染组件时会拿对应的slot
属性的节点进行替换操作。 - 作用域插槽: 在解析的时候不会作为组件的孩子节点。会解析成函数,当子组件渲染时,会调用此函数进行渲染。(作用域为子组件)
普通插槽编译时调用 createElement
方法创建组件,并把子节点生成虚拟 dom
做好标识存起来。渲染时调用 randerSlot
方法循环匹配出对应的虚拟节点在父组件替换当前位置。
而作用域插槽在编译时会把子组件编译成函数,函数不调用就不会渲染。也就是说在初始化组件的时候并不会渲染子节点。渲染页面时调用 randerSlot
方法执行子节点的函数并把对应的属性传过来。当节点渲染完成之后在组件内部替换当前位置。
.Vue与Angular以及React的区别?
1.与AngularJS的区别
相同点:
- 都支持指令:内置指令和自定义指令。
- 都支持过滤器:内置过滤器和自定义过滤器。
- 都支持双向数据绑定。
- 都不支持低端浏览器。
不同点:
-
AngularJS
采用 TypeScript 开发, 而 Vue 可以使用 javascript 也可以使用 TypeScript。 - 在性能上,AngularJS依赖对数据做脏检查,所以Watcher越多越慢。
Vue.js使用基于依赖追踪的观察并且使用异步队列更新,所有的数据都是独立触发的。
对于庞大的应用来 说,这个优化差异还是比较明显的。 - AngularJS社区完善, Vue的学习成本较小
2.与React的区别
相同点:
- React采用特殊的JSX语法,Vue.js在组件开发中也推崇编写.vue特殊文件格式,对文件内容都有一些约定,两者都需要编译后使用。
- 中心思想相同:一切都是组件,组件实例之间可以嵌套。
- 都提供合理的钩子函数,可以让开发者定制化地去处理需求。
- 都不内置AJAX,Route等功能核心包,而是以插件的方式加载。
- 在组件开发中都支持mixins的特性。
不同点:
- vue 组件分为全局注册和局部注册,在 react 中都是通过 import 相应组件,然后模版中引用;
- props 是可以动态变化的,子组件也实时更新,在 react 中官方建议props要像纯函数那样,输入输出一致对应,而且不太建议通过 props 来更改视图
- vue 多了指令系统,让模版可以实现更丰富的功能,而 React 只能使用JSX语法
- react 是整体的思路的就是函数式,所以推崇纯组件,数据不可变,单向数据流,当然需要双向的地方也可以做到,比如结合 redux-form,组件的横向拆分一般是通过高阶组件。而 vue 是数据可变的,双向绑定,声明式的写法,vue组件的横向拆分很多情况下用 mixin。
- Vue增加的语法糖computed和watch,而在React中需要自己写一套逻辑来实现。
高精度全局权限处理
权限控制由前端处理时,通常使用 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
.对于 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 优化,提供了更多的内置功能。
.Vue等单页面应用(spa)及其优缺点
优点
: Vue的目标是通过尽可能简单的 API实现响应的数据绑定和组合的视图组件,核心是一个响应的数据绑定系统。MVVM、数据驱动、组件化、轻量、简洁、高效、快速、模块友好;即第一次就将所有的东西都加载完成,因此,不会导致页面卡顿。
缺点
: 不支持低版本的浏览器,最低只支持到IE9;不利于SEO的优化(如果要支持SEO,建议通过服务端来进行渲染组件);第一次加载首页耗时相对长一些;不可以使用浏览器的导航按钮需要自行实现前进、后退。