1. vue的生命周期
- beforeCreate :
el
和data
并未初始化。是获取不到props
或者data
中的数据的 - created :已经可以访问到之前不能访问的数据,但是这时候组件还没被挂载,所以是看不到的。
- beforeMount:开始创建虚拟DOM
- mounted:将虚拟DOM渲染为真实DOM,并且渲染数据。组件中如果有子组件的话,会递归加载子组件,只有当所有子组件全部挂载完毕,才会执行根组件的挂载钩子。
- beforeUpdate:当组件或实例的数据更改之后,会立即执行
beforeUpdate
,然后vue
的虚拟dom机制会重新构建虚拟dom
与上一次的虚拟dom
树利用diff算法进行对比之后重新渲染,一般不做什么事儿 - updated:当更新完成后,执行
updated
,数据已经更改完成,dom
也重新render
完成,可以操作更新后的虚拟dom
- beforeDestroy: 移除事件、定时器等,否则可能会引起内存泄漏。
- destroyed:会进行一系列的销毁操作,如果有子组件的话,也会递归销毁子组件,所有子组件都销毁完毕后才会执行根组件的
destroyed
钩子函数
另外还有keep-alive
独有的生命周期,分别为activated
和deactivated
。用keep-alive
包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行deactivated
函数,命中缓存渲染后会执行actived
钩子函数。如果需要在组件切换的时候,保存一些组件的状态防止多次渲染,可以使用keep-alive
组件包裹需要保存的组件。<keep-alive>
是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。
keep-alive
不会在函数式组件中正常工作,因为它们没有缓存实例。keep-aliv
e有三个
Props
:
include
- 字符串或正则表达式。只有名称匹配的组件会被缓存。exclude
- 字符串或正则表达式。任何名称匹配的组件都不会被缓存。max
- 数字。最多可以缓存多少组件实例。
问题:Keep alive
中的数据怎么更新
activated
中,对数据进行更改beforeRouteUpdate
Vue 的父组件和子组件生命周期钩子执行顺序是什么
- 加载渲染过程
父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
- 子组件更新过程
父beforeUpdate->子beforeUpdate->子updated->父updated
- 父组件更新过程
父beforeUpdate->父updated
- 销毁过程
父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
2.组件通信
- 父子组件通信
-
父组件传
props
给子组件,子组件通过emit
发送事件传递数据给父组件 -
通过访问
$parent
或$children
对象来访问组件实例中的方法和数据 -
$listeners
和.sync
-
包含了父作用域中的 (不含
.native
修饰器的)v-on
事件监听器。它可以通过v-on="$listeners"
传入内部组件——在创建更高层次的组件时非常有用 -
.sync
修饰符:它只是作为一个编译时的语法糖存在。它会被扩展为一个自动更新父组件属性的v-on
监听器。当一个子组件改变了一个 prop 的值时,这个变化也会同步到父组件中所绑定
注意带有
.sync
修饰符的v-bind
不能和表达式一起使用 (例如v-bind:title.sync=”doc.title + ‘!’”
是无效的)。取而代之的是,你只能提供你想要绑定的property
名,类似v-model
。当我们用一个对象同时设置多个
prop
的时候,也可以将这个.sync
修饰符和v-bind
配合使用:<text-document v-bind.sync="doc"></text-document>
- 将
v-bind.sync
用在一个字面量的对象上,例如v-bind.sync=”{ title: doc.title }”
,是无法正常工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。
-
-
- 兄弟组件通信:通过查找父组件中的子组件实现,也就是
this.$parent.$children
,在children
中可以通过组件name
查询到需要的组件实例,然后进行通信 - 跨多层级组件通信:
provide/inject
- 任意组件:
vuex
3.computed和watch区别
computed
是计算属性,依赖其他属性计算值,并且computed
的值有缓存,只有当计算值变化才会返回内容。watch
监听到值的变化就会执行回调,在回调中可以进行一些逻辑操作 所以需要依赖别的属性来动态获得值的时候可以使用computed
,对于监听到值的变化需要做一些复杂业务逻辑的情况可以使用watch
。watch
不缓存,可以执行异步方法
关于它的原理可参考:计算属性 VS 侦听属性
4.mixin和mixins区别
mixin
用于全局混入,会影响到每个组件实例Vue.mixin({ beforeCreat(){ #...逻辑 # 这种方式会影响到每个组件的beforeCreat钩子函数 } })
mixins
:如果多个组件中有相同的业务逻辑,就可以将这些逻辑剥离出来,通过mixins
混入代码,比如上拉下拉加载数据这种逻辑等等。PS.mixins
混入的钩子函数会先于组件内的钩子函数执行,并且在遇到同名选项的时候也会有选择行性的合并。
5.AST
vue会通过编译器将模板通过几个阶段最终编译为render
函数,然后通过执行render
函数生成Virtual DOM
最终映射为真实DOM
,分为三个阶段:
- 1.将模版解析为AST:通过各种各样的正则表达式去匹配模板中的内容,然后将内容提取出来做各种逻辑操作,然后生成一个最基本的
AST
对象 - 2.优化AST:将永远不会变动的节点提取出来,实现复用
Virtual DOM
,跳过对比算法的功能 - 3.将AST转换为
render
函数:遍历整个AST
,根据不同的条件生成不同的代码
引申1:组件更新
- 组件更新的过程核心就是新旧
vnode diff
,对新旧节点相同以及不同的情况分别做不同的处理。新旧节点不同的更新流程是创建新节点
->更新父占位符节点
->删除旧节点
;而新旧节点相同的更新流程是去获取它们的children
,根据不同情况做不同的更新逻辑。最复杂的情况是新旧节点相同且它们都存在子节点,那么会执行updateChildren
逻辑
引申2:parse
-
parse
的目标是把template
模板字符串转换成AST
树,它是一种用JavaScript
对象的形式来描述整个模板。那么整个parse
的过程是利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造AST
树的目的。 -
AST
元素节点总共有 3 种类型,type 为 1 表示是普通元素,为 2 表示是表达式,为 3 表示是纯文本。其实这里我觉得源码写的不够友好,这种是典型的魔术数字,如果转换成用常量表达会更利于源码阅读
Diff
算法:
- 根据真实DOM生成
virtual DOM
,当virtual DOM
某个节点的数据改变后会生成一个新的Vnode
,然后Vnode
和oldVnode
作对比,发现有不一样的地方就直接修改在真实的DOM
上,然后使oldVnode
的值为Vnode
virtual DOM
是将真实的DOM的数据抽取出来,以对象的形式模拟树形结构diff
的过程就是调用名为patch
的函数,比较新旧节点,一边比较一边给真实的DOM
打补丁。 在采取diff算法比较新旧节点的时候,比较只会在同层级进行, 不会跨层级比较
diff流程图如下
参考链接:详解vue的diff算法
6.vue的双向绑定原理
dep
类中有两个方法:addSub
(添加依赖)和notify
(触发更新,调用了watcher中的update方法)defineReactive
最开始初始化Dep
对象的实例,对子对象递归调用observe
方法,这样就保证了无论obj
的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改obj
中一个嵌套较深的属性,也能触发getter
和setter
。最后利用Object.defineProperty
去给obj
的属性key
添加getter
(将watcher
添加到订阅,执行了dep
的addSub
方法) 和setter
(执行watcher
的update
方法)
function observe(obj){
//判断类型
if(!obj||typeof obj!=='object'){
return
}
Object.keys(obj).forEach(key=>{
defineReactive(obj,key,obj[key])
})
}
演示:
var data={name:'aa'}
observe(data)//手动触发一次属性的getter来实现依赖收集
function update(value){
document.querySelector('div').innerText=value
}
new Watcher(data,'name',update)//触发属性的getter添加监听
data.name='yyy'//触发属性的setter更新
针对给对象新增属性并不会触发组件的重新渲染问题,可以使用一个set
方法,它主要做了这些:
- 判断是否为数组且下标是否有效---调用
target.splice(key,1,val)
方法触发更新
//验证数组索引是否是一个非无穷大的正整数
function isValidArrayIndex (val) {
var n = parseFloat(String(val));
return n >= 0 && Math.floor(n) === n && isFinite(val)//Math.floor(n) === n验证是否是整数
}
- 判断key是否已经存在,存在的话
target[key]=val
- 如果对象不是响应式对象就赋值返回
target[key]=val
- 进行双向数据绑定
defineReactive(ob.value,key,val)
,手动派发更新op.dep.notify()
针对通过下标修改数组数据并不会触发组件的重新渲染问题,Vue内部重写了一些数组函数实现派发更新
引申:Vue响应式原理-如何监听Array的变化
- 先获取原生
Array
的原型方法,因为拦截后还是需要原生的方法帮我们实现数组的变化。 - 对
Array
的原型方法使用Object.defineProperty
做一些拦截操作。 - 把需要被拦截的
Array
类型的数据原型指向改造后原型。
我们将代码进行下改造,拦截的过程中还是要将开发者的参数传给原生的方法,保证数组按照开发者的想法被改变,然后我们再去做视图的更新等操作。
7.Vue.set、vm.$set作用
-
向响应式对象中添加一个
property
,并确保这个新property
同样是响应式的,且触发视图更新。 -
它必须用于向响应式对象上添加新
property
,因为 Vue 无法探测普通的新增property
(比如this.myObject.newProperty = 'hi'
)。 -
PS.注意对象不能是
Vue
实例,或者Vue
实例的根数据对象 -
用法:
- 对于对象:
- 对于已经创建的实例,Vue 不允许动态添加根级别的响应式
property
。但是,可以使用Vue.set(object, propertyName, value)
方法向嵌套对象添加响应式property
- 有时你可能需要为已有对象赋值多个新
property
,比如使用Object.assign()
或_.extend()
。但是,这样添加到对象上的新property
不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的property
一起创建一个新的对象this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
- 对于已经创建的实例,Vue 不允许动态添加根级别的响应式
- 对于数组:
- 当利用索引直接设置一个数组项时:
Vue.set(vm.items, indexOfItem, newValue)
或者vm.items.splice(indexOfItem, 1, newValue)
- 当修改数组的长度时:用splice---
vm.items.splice(newLength)
- 当利用索引直接设置一个数组项时:
- 对于对象:
-
引申:
set
派发更新- 队列排序
- 组件的更新由父到子;因为父组件的创建过程是先于子的,所以
watcher
的创建也是先父后子,执行顺序也应该保持先父后子。 - 用户的自定义
watcher
要优先于渲染watcher
执行;因为用户自定义watcher
是在渲染watcher
之前创建的。 - 如果一个组件在父组件的
watcher
执行期间被销毁,那么它对应的watcher
执行都可以被跳过,所以父组件的watcher
应该先执行
- 组件的更新由父到子;因为父组件的创建过程是先于子的,所以
- 队列遍历
- 在对
queue
排序后,接着就是要对它做遍历,拿到对应的watcher
,执行watcher.run()
。这里需要注意一个细节,在遍历的时候每次都会对queue.length
求值,因为在watcher.run()
的时候,很可能用户会再次添加新的watcher
,这样会再次执行到queueWatcher
- 在对
- 队列排序
8.前端路由原理?两种实现方式有什么区别?
vue的路由模式hash
依赖于window.onhashchange
Hash模式 | History模式 | |
---|---|---|
只可以更改# 后面的内容 | 可以通过API设置任意的同源URL | |
历史记录 | 只能更改哈希值,也就是字符串 | 可以添加任意类型的数据到历史记录中 |
后端 | 无需后端配置,兼容性好 | 在用户手动输入地址或者刷新页面的时候会发起URL请求,后端需要配置index.html 页面用于匹配不到静态资源的时候 |
HTML5的History API
为浏览器的全局history对象增加的扩展方法。一般用来解决ajax请求无法通过回退按钮回到请求前状态的问题
在HTML5中,window.history
对象得到了扩展,新增的API包括:
history.pushState(data[,title][,url])
//向历史记录中追加一条记录history.replaceState(data[,title][,url])
//替换当前页在历史记录中的信息。history.state
//是一个属性,可以得到当前页的state信息window.onpopstate
//是一个事件,在点击浏览器后退按钮或**js调用forward()、back()、go()
**时触发。监听函数中可传入一个event
对象,event.state
即为通过pushState()
或replaceState()
方法传入的data参数。用history.pushState()或者history.replaceState()不会触发popstate事件
//比如
window.onpopstate = function(event) {
console.log("location: " + document.location + ", state: " + JSON.stringify(event.state));
};
history.pushState({page: 1}, "title 1", "?page=1");
history.pushState({page: 2}, "title 2", "?page=2");
history.replaceState({page: 3}, "title 3", "?page=3");
history.back(); // Logs "location: http://example.com/example.html?page=1, state: {"page":1}"
history.back(); // Logs "location: http://example.com/example.html, state: null
history.go(2); // Logs "location: http://example.com/example.html?page=3, state: {"page":3}
详情请查看:深入理解前端中的 hash 和 history 路由
9.路由
this.$route.params
: 动态路由匹配$route
和$router
$route
表示当前路由信息对象$router
对象:全局的路由实例,是router
构造方法的实例。
- 路由守卫有哪些:守卫是异步解析执行,此时导航在所有守卫
resolve
完之前一直处于 等待中。 - 全局导航
beforeEach((to, from, next) => {// ...})
(全局前置守卫):确保 next 函数在任何给定的导航守卫中都被严格调用一次beforeResolve
(全局解析守卫):在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。afterEach((to, from) => {//...})
(全局后置钩子):不会接受next
函数也不会改变导航本身beforeEnter: (to, from, next) => {// ...}
:可以在路由配置上直接定义beforeEnter
守卫
- 组件内的守卫
beforeRouteEnter((to, from, next) => {// ...})
:守卫执行前,组件实例还没被创建。不过,你可以通过传一个回调给next
来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数(支持给next
传递回调的唯一守卫)beforeRouteUpdate((to, from, next) => {// ...})
:在当前路由改变,但是该组件被复用时调用,可以访问组件实例this
beforeRouteLeave((to, from, next) => {// ...})
:导航离开该组件的对应路由时调用,可以访问组件实例this
。通常用来禁止用户在还未保存修改前突然离开
执行顺序:在失活的组件里调用beforeRouteLeave
>全局调用beforeEach
>在路由配置里调用beforeEnter
>解析异步路由组件>在被激活的组件里调用 beforeRouteEnter
>beforeResolve
>导航被确认>afterEach
>触发 DOM
更新>用创建好的实例调用beforeRouteEnter
守卫中传给 next 的回调函数
引申1:vue-router原理
1.实现一个静态install
方法,因为作为插件都必须有这个方法,给Vue.use()
去调用
2.可以监听路由变化
3.解析配置的路由,即解析router
的配置项routes
,能根据路由匹配到对应组件
4.实现两个全局组件router-link
和router-view
参考链接:Vue-Router核心实现原理
引申2:前端路由的权限控制
可参考这篇文章:Vue 权限控制(路由验证)
10.和react的区别
React | Vue | |
---|---|---|
使用v-model支持双向绑定,开发更加方便 | ||
改变数据方式 | setState | 修改状态要简单许多 |
页面更新渲染 | 需要用户手动去优化 | vue的底层使用了依赖追踪,页面更新渲染已经是最优了 |
使用JSX,可以完全通过JS来控制页面,更加的灵活 | 使用了模版语法,相比于JSX来说没有那么灵活,但是完全可以脱离工具链,通过直接编写render函数就能在浏览器中运行 |
11.什么是Virtual DOM?为什么Virtual DOM比原生DOM快?
首先操作DOM
是很慢的(因为DOM
是属于渲染引擎中的东西,而JS
又是JS
引擎中的东西。当我们通过JS
操作DOM
的时候,其实这个操作涉及到了两个线程之间的通信,那么势必会带来一些性能上的损耗,操作DOM
次数一多,也就等同于一直在进行线程之间的通信,并且操作DOM
可能还会带来重绘回流的情况,所以也就导致了性能上的问题),可以使用JS对象模拟并渲染出对应的DOM
,而且通过比较DOM
新旧节点的变化(DOM
是一个多叉树的结构),去局部更新DOM
,实现性能的最优化。初次之后还有其他优点:
- 将
Virtual DOM
作为一个兼容层,让我们还能对接非Web端的系统,实现跨端开发 - 同样的,通过
Virtual DOM
我们可以渲染到其他平台,比如实现SSR
(服务器端渲染)、同构渲染等等 - 实现组件的高度抽象化
**引申:**网上都说操作真实 DOM 慢,但测试结果却比 React 更快,为什么?
12.MVVM与MVC
- 传统的MVC架构通常是使用控制器更新模型,视图从模型中获取数据去渲染。当用户有输入时,会通过控制器去更新模型,并且通知视图进行更新。
- MVVM:引入了
ViewModel
的概念。ViewModel
只关心数据和业务的处理,不关心View
如何处理数据,在这种情况下,View
和Model
都可以独立出来,任何一方改变了也不一定需要改变另一方,并且可以将一些可复用的逻辑放在一个ViewModel
中,让多个View
复用这个ViewModel
13.vuex
的mutation
和action
用法
mutation
:- 每个
mutation
都有一个字符串的 事件类型 (type
) 和 一个 回调函数 (handler
)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数 mutation
必须是同步函数
- 每个
Action
Action
提交的是mutation
,而不是直接变更状态。Action
可以包含任意异步操作。
14.介绍下nextTick
nextTick
可以让我们在下次DOM
更新循环结束之后执行延迟回调,用于获得更新后的DOM
$nextTick()
返回一个Promise
对象,可以使用新的ES2017 async/await
语法完成相同的事情:
methods: {
updateMessage: async function () {
this.message = '已更新'
console.log(this.$el.textContent) // => '未更新'
await this.$nextTick()
console.log(this.$el.textContent) // => '已更新'
}
}
15.设计模式有哪些,vue单例模式怎么实现
单例模式(Singleton Pattern
)确保一个类只有一个实例,并提供一个访问它的全局访问点
可以通过两种方法来实现:类和闭包
export function install (_Vue) {
// 是否已经执行过了 Vue.use(Vuex),如果在非生产环境多次执行,则提示错误
if (Vue && _Vue === Vue) {
if (process.env.NODE_ENV !== 'production') {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
// 如果是第一次执行 Vue.use(Vuex),则把传入的 _Vue 赋值给定义的变量 Vue
Vue = _Vue
// Vuex 初始化逻辑
applyMixin(Vue)
}
在 Vue.use(Vuex)
的时候,会调用 install
方法,真正的 Vue
会被当做参数传入,如果多次执行 Vue.use(Vuex)
,也只会生效一次,也就是只会执行一次 applyMixin(Vue)
,所以只会有一份唯一的 Store
,这就是 Vuex
中单例模式的实现。
16.Vue组件 v-model
//lovingVue 的值将会传入这个名为 checked 的 prop。同时当 <base-checkbox> 触发一个 change 事件并附带一个新的值的时候,这个 lovingVue 的 property 将会被更新
<base-checkbox v-model="lovingVue"></base-checkbox>
Vue.component('base-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
},
template: `
<input
type="checkbox"
v-bind:checked="checked"
v-on:change="$emit('change', $event.target.checked)"
>
`
})
参考链接:自定义事件
17.源码中有用到object.create(null)
,它的作用是?
- 你需要一个非常干净且高度可定制的对象当作数据字典的时候
- 想节省
hasOwnProperty
带来的一丢丢性能损失并且可以偷懒少些一点代码的时候
用Object.create(null)
吧!其他时候,请用{}
参考:详解Object.create(null)
18.Vue.config.productionTip = false
设置为 false
以阻止vue
在启动时生成生产提示
19.Vue性能优化
- 首先考虑性能优化的价值、成本、指标
- 性能优化的手段,过程---详细步骤,有一个量化的描述,比如减少了多少请求,结果怎样
- Vue 应用运行时性能优化措施:
- 引入生产环境的
Vue
文件 - 使用单文件组件预编译模板
- 提取组件的
CSS
到单独到文件 - 利用
Object.freeze()
提升性能 - 扁平化
Store
数据结构 - 合理使用持久化
Store
数据 - 组件懒加载:juejin.cn/post/684490…
- 延迟埋点接口请求
- 增加sw
- 去除非首屏的js预加载
- Vue全家桶使用
CDN
资源,并用Ngnix Combo
的方式合并资源- 用
nginx_http_concat
,将请求合并,通过这样的方式http://example.com/??style1.css,style2.css,foo/style3.css
访问合并后的资源。(注意是2个问号) - 同目录下
JS
可通过Nginx combo
合并, 例:libs
目录下vue.min.js
、vue-router.min.js
、vuex.min.js
合并成一个请求下载:
- 用
https://cdn.abc.com.cn/libs/vue/vue_2.6.10/vue.min.js,libs/vue-router/router_3.1.3/vue-router.min.js,libs/vuex/vuex_3.1.1/vuex.min.js
- 埋点和分享组件和非首屏的第三方
JS
在页面onload
事件之后加载 - 第三方依赖不参与打包:
config.externals = { vue: 'Vue', vuex: 'Vuex', 'vue-router': 'VueRouter', axios: 'axios' }
- 引入生产环境的
- Vue 应用加载性能优化措施
- 服务端渲染 / 预渲染
- 组件懒加载
- 使用webpack-bundle-analyzer分析js包大小
参考链接:Vue 应用性能优化指南
20.修饰符
- 事件修饰符
.stop
:阻止冒泡行为,不让当前元素的事件继续往外触发,如阻止点击div内部事件,触发div
事件.prevent
:阻止事件本身行为,如阻止超链接的点击跳转,form
表单的点击提交.capture
:是改变js默认的事件机制,默认是冒泡,capture
功能是将冒泡改为倾听模式.self
:只有是自己触发的自己才会执行,如果接受到内部的冒泡事件传递信号触发,会忽略掉这个信号.once
:是将事件设置为只执行一次,如.click.prevent.once
代表只阻止事件的默认行为一次,当第二次触发的时候事件本身的行为会执行。.once
修饰符还能被用到自定义的组件事件上.passive
:滚动事件的默认行为 (即滚动行为) 将会立即触发,而不会等待onScroll
完成。这个.passive
修饰符尤其能够提升移动端的性能。
PS: .passive
和 .prevent
不能一起使用:.prevent
将会被忽略
- 按键修饰符
Vue
允许为v-on
在监听键盘事件时添加按键修饰符:比如<input v-on:keyup.enter="submit">
- 可以直接将
KeyboardEvent.key
暴露的任意有效按键名转换为kebab-case
来作为修饰符。比如:<input v-on:keyup.page-down="onPageDown">
处理函数只会在$event.key
等于PageDown
时被调用
- 系统修饰符
.ctrl
、.alt
、.shift
、.meta
、.exact
、.left
、.right
、.middle
21.vue强制更新
this.$forceUpdate()
:迫使 Vue 实例重新渲染。注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。- 用
Object.assign
对象改变:oldObj = Object.assign({},newObj)
; 原理:对象是引用类型,直接改变oldObj的某属性指向地址没变,vue不一定能监控到,所以当我们新建一个对象并赋值给oldObj字段的话,直接改变了它的指向地址 - Vue.set
22.自定义指令
一个指令定义对象可以提供如下几个钩子函数 (均为可选):
bind
:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。inserted
:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。update
:所在组件的VNode
更新时调用,但是可能发生在其子VNode
更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新componentUpdated
:指令所在组件的VNode
及其子VNode
全部更新后调用。unbind
:只调用一次,指令与元素解绑时调用。
钩子函数的参数有el
、binding
、vnode
和 oldVnode