什么是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 中就已经完成了。
可以使用 once('hook:') 来简化生命周期的注册
谈谈 MVVM 模式
Mode
l: 代表数据模型,也可以在 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
重新定义 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
中, 相当于异步执行了lushCallBacks
。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
方法定义一个计算属性。在用户取值时,调用的是createComputedGette
r 返回函数 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中的 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
: 能够监听到子组件的生命周期,举个栗子:
// 这里是父组件
<template>
<child @hook:mounted="getChildMounted" />
</template>
<script>
method: {
getChildMounted () {
// 这里可以获取到子组件mounted的信息
}
}
</script>
为什么使用异步组件?
可使用异步的方式加载组件,减少打包体积,主要依赖 import() 语法,可实现文件的分割加载
components:{
testCpt: (resove) => import("../components/testCpt") 或
testCpt: r => require(['@/views/assetsInfo/assetsProofList'],r)
}
加载组件的时候,如果组件是个函数会调用resolveAsyncComponent
方法, 并传入组件定义的函数 asyncFactory
, 并让其马上执行。因为是异步的所以执行后并不会马上返回结果。而返回的是一个 promise
,因此没有返回值, 返回的是一个占位符。
加载完成后,会执行factory
函数并传入了成功/失败的回调。在回调 resolve 成功的回调时会调用 forceRander
方法, 内部调用 $forceUpdate
强制刷新。之后resolveAsyncComponent
判断已经执行成功,就是去创建组件、初始化组件和渲染组件。
作者:酷酷的凯先生
链接:https://www.jianshu.com/p/14e9a060af8a
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。