Vue 的生命周期办法有哪些 个别在哪一步发申请
beforeCreate 在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。在以后阶段 data、methods、computed 以及 watch 上的数据和办法都不能被拜访
created 实例曾经创立实现之后被调用。在这一步,实例已实现以下的配置:数据观测(data observer),属性和办法的运算, watch/event 事件回调。这里没有$el,如果非要想与 Dom 进行交互,能够通过 vm.$nextTick 来拜访 Dom
beforeMount 在挂载开始之前被调用:相干的 render 函数首次被调用。
mounted 在挂载实现后产生,在以后阶段,实在的 Dom 挂载结束,数据实现双向绑定,能够拜访到 Dom 节点
beforeUpdate 数据更新时调用,产生在虚构 DOM 从新渲染和打补丁(patch)之前。能够在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程
updated 产生在更新实现之后,以后阶段组件 Dom 已实现更新。要留神的是防止在此期间更改数据,因为这可能会导致有限循环的更新,该钩子在服务器端渲染期间不被调用。
beforeDestroy 实例销毁之前调用。在这一步,实例依然齐全可用。咱们能够在这时进行善后收尾工作,比方革除计时器。
destroyed Vue 实例销毁后调用。调用后,Vue 实例批示的所有货色都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。
activated keep-alive 专属,组件被激活时调用
deactivated keep-alive 专属,组件被销毁时调用
异步申请在哪一步发动?
能够在钩子函数 created、beforeMount、mounted 中进行异步申请,因为在这三个钩子函数中,data 曾经创立,能够将服务端端返回的数据进行赋值。
如果异步申请不须要依赖 Dom 举荐在 created 钩子函数中调用异步申请,因为在 created 钩子函数中调用异步申请有以下长处:
- 能更快获取到服务端数据,缩小页面 loading 工夫;
- ssr 不反对 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;
Vue中diff算法原理
DOM
操作是十分低廉的,因而咱们须要尽量地缩小DOM
操作。这就须要找出本次DOM
必须更新的节点来更新,其余的不更新,这个找出的过程,就须要利用diff算法
vue
的diff
算法是平级比拟,不思考跨级比拟的状况。外部采纳深度递归的形式+双指针(头尾都加指针)
的形式进行比拟。
简略来说,Diff算法有以下过程
- 同级比拟,再比拟子节点(依据
key
和tag
标签名判断) - 先判断一方有子节点和一方没有子节点的状况(如果新的
children
没有子节点,将旧的子节点移除) - 比拟都有子节点的状况(外围
diff
) - 递归比拟子节点
- 失常
Diff
两个树的工夫复杂度是O(n^3)
,但理论状况下咱们很少会进行跨层级的挪动DOM
,所以Vue
将Diff
进行了优化,从O(n^3) -> O(n)
,只有当新旧children
都为多个子节点时才须要用外围的Diff
算法进行同层级比拟。 Vue2
的外围Diff
算法采纳了双端比拟
的算法,同时从新旧children
的两端开始进行比拟,借助key
值找到可复用的节点,再进行相干操作。相比React
的Diff
算法,同样状况下能够缩小挪动节点次数,缩小不必要的性能损耗,更加的优雅- 在创立
VNode
时就确定其类型,以及在mount/patch
的过程中采纳位运算来判断一个VNode
的类型,在这个根底之上再配合外围的Diff
算法,使得性能上较Vue2.x
有了晋升
vue3中采纳最长递增子序列来实现diff
优化
答复范例
思路
diff
算法是干什么的- 它的必要性
- 它何时执行
- 具体执行形式
- 拔高:说一下
vue3
中的优化
答复范例
Vue
中的diff
算法称为patching
算法,它由Snabbdo
m批改而来,虚构DOM
要想转化为实在DOM
就须要通过patch
办法转换- 最后
Vue1.x
视图中每个依赖均有更新函数对应,能够做到精准更新,因而并不需要虚构DOM
和patching
算法反对,然而这样粒度过细导致Vue1.x
无奈承载较大利用;Vue 2.x
中为了升高Watcher
粒度,每个组件只有一个Watcher
与之对应,此时就须要引入patching
算法能力准确找到发生变化的中央并高效更新 vue
中diff
执行的时刻是组件内响应式数据变更触发实例执行其更新函数时,更新函数会再次执行render
函数取得最新的虚构DOM
,而后执行patc
h函数,并传入新旧两次虚构DOM,通过比对两者找到变动的中央,最初将其转化为对应的DOM
操作patch
过程是一个递归过程,遵循深度优先、同层比拟的策略;以vue3
的patch
为例- 首先判断两个节点是否为雷同同类节点,不同则删除从新创立
- 如果单方都是文本则更新文本内容
- 如果单方都是元素节点则递归更新子元素,同时更新元素属性
更新子节点时又分了几种状况
- 新的子节点是文本,老的子节点是数组则清空,并设置文本;
- 新的子节点是文本,老的子节点是文本则间接更新文本;
- 新的子节点是数组,老的子节点是文本则清空文本,并创立新子节点数组中的子元素;
- 新的子节点是数组,老的子节点也是数组,那么比拟两组子节点,更新细节blabla
vue3
中引入的更新策略:动态节点标记等
vdom中diff算法的繁难实现
以下代码只是帮忙大家了解diff
算法的原理和流程
- 将
vdom
转化为实在dom
:
const createElement = (vnode) => { let tag = vnode.tag; let attrs = vnode.attrs || {}; let children = vnode.children || []; if(!tag) { return null; } //创立元素 let elem = document.createElement(tag); //属性 let attrName; for (attrName in attrs) { if(attrs.hasOwnProperty(attrName)) { elem.setAttribute(attrName, attrs[attrName]); } } //子元素 children.forEach(childVnode => { //给elem增加子元素 elem.appendChild(createElement(childVnode)); }) //返回实在的dom元素 return elem;}
- 用繁难
diff
算法做更新操作
function updateChildren(vnode, newVnode) { let children = vnode.children || []; let newChildren = newVnode.children || []; children.forEach((childVnode, index) => { let newChildVNode = newChildren[index]; if(childVnode.tag === newChildVNode.tag) { //深层次比照, 递归过程 updateChildren(childVnode, newChildVNode); } else { //替换 replaceNode(childVnode, newChildVNode); } })}
</details>
如何定义动静路由?如何获取传过来的动静参数?
(1)param形式
- 配置路由格局:
/router/:id
- 传递的形式:在path前面跟上对应的值
- 传递后造成的门路:
/router/123
1)路由定义
//在APP.vue中<router-link :to="'/user/'+userId" replace>用户</router-link> //在index.js{ path: '/user/:userid', component: User,},
2)路由跳转
// 办法1:<router-link :to="{ name: 'users', params: { uname: wade }}">按钮</router-link// 办法2:this.$router.push({name:'users',params:{uname:wade}})// 办法3:this.$router.push('/user/' + wade)
3)参数获取
通过 $route.params.userid
获取传递的值
(2)query形式
- 配置路由格局:
/router
,也就是一般配置 - 传递的形式:对象中应用query的key作为传递形式
- 传递后造成的门路:
/route?id=123
1)路由定义
//形式1:间接在router-link 标签上以对象的模式<router-link :to="{path:'/profile',query:{name:'why',age:28,height:188}}">档案</router-link>// 形式2:写成按钮以点击事件模式<button @click='profileClick'>我的</button> profileClick(){ this.$router.push({ path: "/profile", query: { name: "kobi", age: "28", height: 198 } });}
2)跳转办法
// 办法1:<router-link :to="{ name: 'users', query: { uname: james }}">按钮</router-link>// 办法2:this.$router.push({ name: 'users', query:{ uname:james }})// 办法3:<router-link :to="{ path: '/user', query: { uname:james }}">按钮</router-link>// 办法4:this.$router.push({ path: '/user', query:{ uname:james }})// 办法5:this.$router.push('/user?uname=' + jsmes)
3)获取参数
通过$route.query 获取传递的值
router-link和router-view是如何起作用的
剖析
vue-router
中两个重要组件router-link
和router-view
,别离起到导航作用和内容渲染作用,然而答复如何失效还真有肯定难度
答复范例
vue-router
中两个重要组件router-link
和router-view
,别离起到路由导航作用和组件内容渲染作用- 应用中
router-link
默认生成一个a
标签,设置to
属性定义跳转path
。实际上也能够通过custom
和插槽自定义最终的展示模式。router-view
是要显示组件的占位组件,能够嵌套,对应路由配置的嵌套关系,配合name
能够显示具名组件,起到更强的布局作用。 router-link
组件外部依据custom
属性判断如何渲染最终生成节点,外部提供导航办法navigate
,用户点击之后理论调用的是该办法,此办法最终会批改响应式的路由变量,而后从新去routes
匹配出数组后果,router-view
则依据其所处深度deep
在匹配数组后果中找到对应的路由并获取组件,最终将其渲染进去。
Watch中的deep:true是如何实现的
当用户指定了watch
中的deep属性为true
时,如果以后监控的值是数组类型。会对对象中的每一项进行求值,此时会将以后watcher
存入到对应属性的依赖中,这样数组中对象发生变化时也会告诉数据更新
源码相干
get () { pushTarget(this) // 先将以后依赖放到 Dep.target上 let value const vm = this.vm try { value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { if (this.deep) { // 如果须要深度监控 traverse(value) // 会对对象中的每一项取值,取值时会执行对应的get办法 }popTarget() }
Vue组件渲染和更新过程
渲染组件时,会通过Vue.extend
办法构建子组件的构造函数,并进行实例化。最终手动调用$mount()
进行挂载。更新组件时会进行patchVnode
流程,外围就是diff
算法
watch 原理
watch
实质上是为每个监听属性 setter
创立了一个 watcher
,当被监听的属性更新时,调用传入的回调函数。常见的配置选项有 deep
和 immediate
,对应原理如下
deep
:深度监听对象,为对象的每一个属性创立一个watcher
,从而确保对象的每一个属性更新时都会触发传入的回调函数。次要起因在于对象属于援用类型,单个属性的更新并不会触发对象setter
,因而引入deep
可能很好地解决监听对象的问题。同时也会引入判断机制,确保在多个属性更新时回调函数仅触发一次,防止性能节约。immediate
:在初始化时间接调用回调函数,能够通过在created
阶段手动调用回调函数实现雷同的成果
Vue 是如何实现数据双向绑定的
Vue
数据双向绑定次要是指:数据变动更新视图,视图变动更新数据,如下图所示:
- 输入框内容变动时,
Data
中的数据同步变动。即View => Data
的变动。 Data
中的数据变动时,文本节点的内容同步变动。即Data => View
的变动
Vue 次要通过以下 4 个步骤来实现数据双向绑定的
- 实现一个监听器 Observer :对数据对象进行遍历,包含子属性对象的属性,利用
Object.defineProperty()
对属性都加上setter
和getter
。这样的话,给这个对象的某个值赋值,就会触发setter
,那么就能监听到了数据变动 - 实现一个解析器 Compile :解析
Vue
模板指令,将模板中的变量都替换成数据,而后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,增加监听数据的订阅者,一旦数据有变动,收到告诉,调用更新函数进行数据更新 - 实现一个订阅者 Watcher :
Watcher
订阅者是Observer
和Compile
之间通信的桥梁 ,次要的工作是订阅Observer
中的属性值变动的音讯,当收到属性值变动的音讯时,触发解析器Compile
中对应的更新函数 - 实现一个订阅器 Dep :订阅器采纳 公布-订阅 设计模式,用来收集订阅者
Watcher
,对监听器Observer
和 订阅者Watcher
进行对立治理
Vue 数据双向绑定原理图
双向绑定的原理是什么
咱们都晓得 Vue
是数据双向绑定的框架,双向绑定由三个重要局部形成
- 数据层(Model):利用的数据及业务逻辑
- 视图层(View):利用的展现成果,各类UI组件
- 业务逻辑层(ViewModel):框架封装的外围,它负责将数据与视图关联起来
而下面的这个分层的架构计划,能够用一个专业术语进行称说:MVVM
这里的管制层的外围性能便是 “数据双向绑定” 。天然,咱们只需弄懂它是什么,便能够进一步理解数据绑定的原理
了解ViewModel
它的主要职责就是:
- 数据变动后更新视图
- 视图变动后更新数据
当然,它还有两个次要局部组成
- 监听器(
Observer
):对所有数据的属性进行监听 - 解析器(
Compiler
):对每个元素节点的指令进行扫描跟解析,依据指令模板替换数据,以及绑定相应的更新函数
参考:前端vue面试题具体解答
Vue的事件绑定原理
原生事件绑定是通过addEventListener
绑定给实在元素的,组件事件绑定是通过Vue
自定义的$on
实现的。如果要在组件上应用原生事件,须要加.native
修饰符,这样就相当于在父组件中把子组件当做一般html
标签,而后加上原生事件。
$on
、$emit
是基于公布订阅模式的,保护一个事件核心,on
的时候将事件按名称存在事件中心里,称之为订阅者,而后 emit
将对应的事件进行公布,去执行事件中心里的对应的监听器
EventEmitter(公布订阅模式--简略版)
// 手写公布订阅模式 EventEmitterclass EventEmitter { constructor() { this.events = {}; } // 实现订阅 on(type, callBack) { if (!this.events) this.events = Object.create(null); if (!this.events[type]) { this.events[type] = [callBack]; } else { this.events[type].push(callBack); } } // 删除订阅 off(type, callBack) { if (!this.events[type]) return; this.events[type] = this.events[type].filter(item => { return item !== callBack; }); } // 只执行一次订阅事件 once(type, callBack) { function fn() { callBack(); this.off(type, fn); } this.on(type, fn); } // 触发事件 emit(type, ...rest) { this.events[type] && this.events[type].forEach(fn => fn.apply(this, rest)); }}// 应用如下const event = new EventEmitter();const handle = (...rest) => { console.log(rest);};event.on("click", handle);event.emit("click", 1, 2, 3, 4);event.off("click", handle);event.emit("click", 1, 2);event.once("dbClick", () => { console.log(123456);});event.emit("dbClick");event.emit("dbClick");
源码剖析
- 原生 dom 的绑定
Vue
在创立真是dom
时会调用createElm
,默认会调用invokeCreateHooks
- 会遍历以后平台下绝对的属性解决代码,其中就有
updateDOMListeners
办法,外部会传入add
办法
function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) { if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) { return } const on = vnode.data.on || {} const oldOn = oldVnode.data.on || {} target = vnode.elm normalizeEvents(on) updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context) target = undefined }function add ( name: string, handler: Function, capture: boolean, passive: boolean ) { target.addEventListener( // 给以后的dom增加事件 name, handler, supportsPassive ? { capture, passive } : capture ) }
vue
中绑定事件是间接绑定给实在dom
元素的
- 组件中绑定事件
export function updateComponentListeners ( vm: Component, listeners: Object, oldListeners: ?Object ) { target = vm updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm) target = undefined }function add (event, fn) { target.$on(event, fn) }
组件绑定事件是通过vue
中自定义的$on
办法来实现的
什么是作用域插槽
插槽
- 创立组件虚构节点时,会将组件儿子的虚构节点保存起来。当初始化组件时,通过插槽属性将儿子进行分类
{a:[vnode],b[vnode]}
- 渲染组件时会拿对应的
slot
属性的节点进行替换操作。(插槽的作用域为父组件)
<app> <div slot="a">xxxx</div> <div slot="b">xxxx</div></app> slot name="a" slot name="b"
作用域插槽
- 作用域插槽在解析的时候不会作为组件的孩子节点。会解析成函数,当子组件渲染时,会调用此函数进行渲染。(插槽的作用域为子组件)
- 一般插槽渲染的作用域是父组件,作用域插槽的渲染作用域是以后子组件。
// 插槽const VueTemplateCompiler = require('vue-template-compiler'); let ele = VueTemplateCompiler.compile(` <my-component> <div slot="header">node</div> <div>react</div> <div slot="footer">vue</div> </my-component> `)// with(this) { // return _c('my-component', [_c('div', { // attrs: { "slot": "header" },// slot: "header" // }, [_v("node")] // _文本及诶点 )// , _v(" "), // _c('div', [_v("react")]), _v(" "), _c('div', { // attrs: { "slot": "footer" },// slot: "footer" }, [_v("vue")])]) // }const VueTemplateCompiler = require('vue-template-compiler');let ele = VueTemplateCompiler.compile(` <div> <slot name="header"></slot> <slot name="footer"></slot> <slot></slot> </div> `);with(this) { return _c('div', [_v("node"), _v(" "), _t(_v("vue")])]), _v(" "), _t("default")], 2) }// _t定义在 core/instance/render-helpers/index.js
// 作用域插槽:let ele = VueTemplateCompiler.compile(` <app> <div slot-scope="msg" slot="footer">{{msg.a}}</div> </app> `);// with(this) { // return _c('app', { scopedSlots: _u([{ // // 作用域插槽的内容会被渲染成一个函数 // key: "footer", // fn: function (msg) { // return _c('div', {}, [_v(_s(msg.a))]) } }]) // })// } // }const VueTemplateCompiler = require('vue-template-compiler');VueTemplateCompiler.compile(` <div><slot name="footer" a="1" b="2"></slot> </div> `);// with(this) { return _c('div', [_t("footer", null, { "a": "1", "b": "2" })], 2) }
什么是递归组件?举个例子阐明下?
剖析
递归组件咱们用的比拟少,然而在Tree
、Menu
这类组件中会被用到。
体验
组件通过组件名称援用它本人,这种状况就是递归组件
<template> <li> <div> {{ model.name }}</div> <ul v-show="isOpen" v-if="isFolder"> <!-- 留神这里:组件递归渲染了它本人 --> <TreeItem class="item" v-for="model in model.children" :model="model"> </TreeItem> </ul> </li><script>export default { name: 'TreeItem', // ...}</script>
答复范例
- 如果某个组件通过组件名称援用它本人,这种状况就是递归组件。
- 理论开发中相似
Tree
、Menu
这类组件,它们的节点往往蕴含子节点,子节点构造和父节点往往是雷同的。这类组件的数据往往也是树形构造,这种都是应用递归组件的典型场景。 - 应用递归组件时,因为咱们并未也不能在组件外部导入它本人,所以设置组件
name
属性,用来查找组件定义,如果应用SFC
,则能够通过SFC
文件名推断。组件外部通常也要有递归完结条件,比方model.children
这样的判断。 - 查看生成渲染函数可知,递归组件查找时会传递一个布尔值给
resolveComponent
,这样理论获取的组件就是以后组件自身
原理
递归组件编译后果中,获取组件时会传递一个标识符 _resolveComponent("Comp", true)
const _component_Comp = _resolveComponent("Comp", true)
就是在传递maybeSelfReference
export function resolveComponent( name: string, maybeSelfReference?: boolean): ConcreteComponent | string { return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name}
resolveAsset
中最终返回的是组件本身:
if (!res && maybeSelfReference) { // fallback to implicit self-reference return Component}
为什么Vue采纳异步渲染
Vue 是组件级更新,如果不采纳异步更新,那么每次更新数据都会对以后组件进行从新渲染,所以为了性能, Vue 会在本轮数据更新后,在异步更新视图。核心思想 nextTick
源码相干
dep.notify()
告诉watcher
进行更新,subs[i].update
顺次调用watcher
的update
,queueWatcher
将watcher
去重放入队列,nextTick
(flushSchedulerQueue
)在下一tick
中刷新watcher
队列(异步)
update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this); // 当数据发生变化时会将watcher放到一个队列中批量更新 }}export function queueWatcher (watcher: Watcher) { const id = watcher.id // 会对雷同的watcher进行过滤 if (has[id] == null) { has[id] = true if (!flushing) { queue.push(watcher) } else { let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // queue the flush if (!waiting) { waiting = true if (process.env.NODE_ENV !== 'production' && !config.async) { flushSchedulerQueue() return } nextTick(flushSchedulerQueue) // 调用nextTick办法 批量的进行更新 } } }
子组件能够间接扭转父组件的数据么,阐明起因
这是一个实际知识点,组件化开发过程中有个单项数据流准则
,不在子组件中批改父组件是个常识问题
思路
- 讲讲单项数据流准则,表明为何不能这么做
- 举几个常见场景的例子说说解决方案
- 联合实际讲讲如果须要批改父组件状态应该如何做
答复范例
- 所有的
prop
都使得其父子之间造成了一个单向上行绑定:父级prop
的更新会向下流动到子组件中,然而反过来则不行。这样会避免从子组件意外变更父级组件的状态,从而导致你的利用的数据流向难以了解。另外,每次父级组件产生变更时,子组件中所有的prop
都将会刷新为最新的值。这意味着你不应该在一个子组件外部扭转prop
。如果你这样做了,Vue
会在浏览器控制台中收回正告
const props = defineProps(['foo'])// ❌ 上面行为会被正告, props是只读的!props.foo = 'bar'
- 理论开发过程中有两个场景会想要批改一个属性:
这个 prop 用来传递一个初始值;这个子组件接下来心愿将其作为一个本地的 prop 数据来应用。 在这种状况下,最好定义一个本地的 data
,并将这个 prop
用作其初始值:
const props = defineProps(['initialCounter'])const counter = ref(props.initialCounter)
这个 prop 以一种原始的值传入且须要进行转换。 在这种状况下,最好应用这个 prop
的值来定义一个计算属性:
const props = defineProps(['size'])// prop变动,计算属性自动更新const normalizedSize = computed(() => props.size.trim().toLowerCase())
- 实际中如果的确想要扭转父组件属性应该
emit
一个事件让父组件去做这个变更。留神尽管咱们不能间接批改一个传入的对象或者数组类型的prop
,然而咱们还是可能间接改内嵌的对象或属性
keep-alive 应用场景和原理
keep-alive
是Vue
内置的一个组件, 能够实现组件缓存 ,当组件切换时不会对以后组件进行卸载。 个别联合路由和动静组件一起应用 ,用于缓存组件- 提供
include
和exclude
属性, 容许组件有条件的进行缓存 。两者都反对字符串或正则表达式,include
示意只有名称匹配的组件会被缓存,exclude
示意任何名称匹配的组件都不会被缓存 ,其中exclude
的优先级比include
高 - 对应两个钩子函数
activated
和deactivated
,当组件被激活时,触发钩子函数activated
,当组件被移除时,触发钩子函数deactivated
keep-alive
的中还使用了LRU
(最近起码应用) 算法,抉择最近最久未应用的组件予以淘汰
<keep-alive></keep-alive>
包裹动静组件时,会缓存不流动的组件实例,次要用于保留组件状态或防止从新渲染- 比方有一个列表和一个详情,那么用户就会常常执行关上详情=>返回列表=>关上详情…这样的话列表和详情都是一个频率很高的页面,那么就能够对列表组件应用
<keep-alive></keep-alive>
进行缓存,这样用户每次返回列表的时候,都能从缓存中疾速渲染,而不是从新渲染
对于keep-alive的根本用法
<keep-alive> <component :is="view"></component></keep-alive>
应用includes
和exclude
:
<keep-alive include="a,b"> <component :is="view"></component></keep-alive><!-- 正则表达式 (应用 `v-bind`) --><keep-alive :include="/a|b/"> <component :is="view"></component></keep-alive><!-- 数组 (应用 `v-bind`) --><keep-alive :include="['a', 'b']"> <component :is="view"></component></keep-alive>
匹配首先查看组件本身的 name
选项,如果 name
选项不可用,则匹配它的部分注册名称 (父组件 components
选项的键值),匿名组件不能被匹配
设置了 keep-alive
缓存的组件,会多出两个生命周期钩子(activated
与deactivated
):
- 首次进入组件时:
beforeRouteEnter
>beforeCreate
>created
>mounted
>activated
> ... ... >beforeRouteLeave
>deactivated
- 再次进入组件时:
beforeRouteEnter
>activated
> ... ... >beforeRouteLeave
>deactivated
应用场景
应用准则:当咱们在某些场景下不须要让页面从新加载时咱们能够应用keepalive
举个栗子:
当咱们从首页
–>列表页
–>商详页
–>再返回
,这时候列表页应该是须要keep-alive
从首页
–>列表页
–>商详页
–>返回到列表页(须要缓存)
–>返回到首页(须要缓存)
–>再次进入列表页(不须要缓存)
,这时候能够按需来管制页面的keep-alive
在路由中设置keepAlive
属性判断是否须要缓存
{ path: 'list', name: 'itemList', // 列表页 component (resolve) { require(['@/pages/item/list'], resolve) }, meta: { keepAlive: true, title: '列表页' }}
应用<keep-alive>
<div id="app" class='wrapper'> <keep-alive> <!-- 须要缓存的视图组件 --> <router-view v-if="$route.meta.keepAlive"></router-view> </keep-alive> <!-- 不须要缓存的视图组件 --> <router-view v-if="!$route.meta.keepAlive"></router-view></div>
思考题:缓存后如何获取数据
解决方案能够有以下两种:
beforeRouteEnter
:每次组件渲染的时候,都会执行beforeRouteEnter
beforeRouteEnter(to, from, next){ next(vm=>{ console.log(vm) // 每次进入路由执行 vm.getData() // 获取数据 })},
actived
:在keep-alive
缓存的组件被激活的时候,都会执行actived
钩子
// 留神:服务器端渲染期间avtived不被调用activated(){ this.getData() // 获取数据},
扩大补充:LRU 算法是什么?
LRU
的核心思想是如果数据最近被拜访过,那么未来被拜访的几率也更高,所以咱们将命中缓存的组件key
从新插入到this.keys
的尾部,这样一来,this.keys
中越往头部的数据行将来被拜访几率越低,所以当缓存数量达到最大值时,咱们就删除未来被拜访几率最低的数据,即this.keys
中第一个缓存的组件
相干代码
keep-alive
是vue
中内置的一个组件
源码地位:src/core/components/keep-alive.js
export default { name: "keep-alive", abstract: true, //形象组件 props: { include: patternTypes, //要缓存的组件 exclude: patternTypes, //要排除的组件 max: [String, Number], //最大缓存数 }, created() { this.cache = Object.create(null); //缓存对象 {a:vNode,b:vNode} this.keys = []; //缓存组件的key汇合 [a,b] }, destroyed() { for (const key in this.cache) { pruneCacheEntry(this.cache, key, this.keys); } }, mounted() { //动静监听include exclude this.$watch("include", (val) => { pruneCache(this, (name) => matches(val, name)); }); this.$watch("exclude", (val) => { pruneCache(this, (name) => !matches(val, name)); }); }, render() { const slot = this.$slots.default; //获取包裹的插槽默认值 获取默认插槽中的第一个组件节点 const vnode: VNode = getFirstComponentChild(slot); //获取第一个子组件 // 获取该组件节点的componentOptions const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions; if (componentOptions) { // 获取该组件节点的名称,优先获取组件的name字段,如果name不存在则获取组件的tag const name: ?string = getComponentName(componentOptions); const { include, exclude } = this; // 不走缓存 如果name不在inlcude中或者存在于exlude中则示意不缓存,间接返回vnode if ( // not included 不蕴含 (include && (!name || !matches(include, name))) || // excluded 排除外面 (exclude && name && matches(exclude, name)) ) { //返回虚构节点 return vnode; } const { cache, keys } = this; // 获取组件的key值 const key: ?string = vnode.key == null ? // same constructor may get registered as different local components // so cid alone is not enough (#3269) componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : "") : vnode.key; // 拿到key值后去this.cache对象中去寻找是否有该值,如果有则示意该组件有缓存,即命中缓存 if (cache[key]) { //通过key 找到缓存 获取实例 vnode.componentInstance = cache[key].componentInstance; // make current key freshest remove(keys, key); //通过LRU算法把数组外面的key删掉 keys.push(key); //把它放在数组开端 } else { cache[key] = vnode; //没找到就换存下来 keys.push(key); //把它放在数组开端 // prune oldest entry //如果超过最大值就把数组第0项删掉 if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode); } } vnode.data.keepAlive = true; //标记虚构节点曾经被缓存 } // 返回虚构节点 return vnode || (slot && slot[0]); },};
能够看到该组件没有template
,而是用了render
,在组件渲染的时候会主动执行render
函数
this.cache
是一个对象,用来存储须要缓存的组件,它将以如下模式存储:
this.cache = { 'key1':'组件1', 'key2':'组件2', // ...}
在组件销毁的时候执行pruneCacheEntry
函数
function pruneCacheEntry ( cache: VNodeCache, key: string, keys: Array<string>, current?: VNode) { const cached = cache[key] /* 判断以后没有处于被渲染状态的组件,将其销毁*/ if (cached && (!current || cached.tag !== current.tag)) { cached.componentInstance.$destroy() } cache[key] = null remove(keys, key)}
在mounted
钩子函数中观测 include
和 exclude
的变动,如下:
mounted () { this.$watch('include', val => { pruneCache(this, name => matches(val, name)) }) this.$watch('exclude', val => { pruneCache(this, name => !matches(val, name)) })}
如果include
或exclude
产生了变动,即示意定义须要缓存的组件的规定或者不须要缓存的组件的规定产生了变动,那么就执行pruneCache
函数,函数如下
function pruneCache (keepAliveInstance, filter) { const { cache, keys, _vnode } = keepAliveInstance for (const key in cache) { const cachedNode = cache[key] if (cachedNode) { const name = getComponentName(cachedNode.componentOptions) if (name && !filter(name)) { pruneCacheEntry(cache, key, keys, _vnode) } } }}
在该函数内对this.cache
对象进行遍历,取出每一项的name
值,用其与新的缓存规定进行匹配,如果匹配不上,则示意在新的缓存规定下该组件曾经不须要被缓存,则调用pruneCacheEntry
函数将其从this.cache
对象剔除即可
对于keep-alive
的最弱小缓存性能是在render
函数中实现
首先获取组件的key
值:
const key = vnode.key == null? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : ''): vnode.key
拿到key
值后去this.cache
对象中去寻找是否有该值,如果有则示意该组件有缓存,即命中缓存,如下:
/* 如果命中缓存,则间接从缓存中拿 vnode 的组件实例 */if (cache[key]) { vnode.componentInstance = cache[key].componentInstance /* 调整该组件key的程序,将其从原来的中央删掉并从新放在最初一个 */ remove(keys, key) keys.push(key)}
间接从缓存中拿 vnode
的组件实例,此时从新调整该组件key
的程序,将其从原来的中央删掉并从新放在this.keys
中最初一个
this.cache
对象中没有该key
值的状况,如下:
/* 如果没有命中缓存,则将其设置进缓存 */else { cache[key] = vnode keys.push(key) /* 如果配置了max并且缓存的长度超过了this.max,则从缓存中删除第一个 */ if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode) }}
表明该组件还没有被缓存过,则以该组件的key
为键,组件vnode
为值,将其存入this.cache
中,并且把key
存入this.keys
中
此时再判断this.keys
中缓存组件的数量是否超过了设置的最大缓存数量值this.max
,如果超过了,则把第一个缓存组件删掉
虚构DOM实现原理?
- 虚构DOM实质上是JavaScript对象,是对实在DOM的形象
- 状态变更时,记录新树和旧树的差别
- 最初把差别更新到真正的dom中
Vue的diff算法详细分析
1. 是什么
diff
算法是一种通过同层的树节点进行比拟的高效算法
其有两个特点:
- 比拟只会在同层级进行, 不会跨层级比拟
- 在diff比拟的过程中,循环从两边向两头比拟
diff
算法在很多场景下都有利用,在 vue
中,作用于虚构 dom
渲染成实在 dom
的新旧 VNode
节点比拟
2. 比拟形式
diff
整体策略为:深度优先,同层比拟
- 比拟只会在同层级进行, 不会跨层级比拟
- 比拟的过程中,循环从两边向两头收拢
上面举个vue
通过diff
算法更新的例子:
新旧VNode
节点如下图所示:
第一次循环后,发现旧节点D与新节点D雷同,间接复用旧节点D作为diff
后的第一个实在节点,同时旧节点endIndex
挪动到C,新节点的 startIndex
挪动到了 C
第二次循环后,同样是旧节点的开端和新节点的结尾(都是 C)雷同,同理,diff
后创立了 C 的实在节点插入到第一次创立的 D 节点前面。同时旧节点的 endIndex
挪动到了 B,新节点的 startIndex
挪动到了 E
第三次循环中,发现E没有找到,这时候只能间接创立新的实在节点 E,插入到第二次创立的 C 节点之后。同时新节点的 startIndex
挪动到了 A。旧节点的 startIndex
和 endIndex
都放弃不动
第四次循环中,发现了新旧节点的结尾(都是 A)雷同,于是 diff
后创立了 A 的实在节点,插入到前一次创立的 E 节点前面。同时旧节点的 startIndex
挪动到了 B,新节点的startIndex
挪动到了 B
第五次循环中,情景同第四次循环一样,因而 diff
后创立了 B 实在节点 插入到前一次创立的 A 节点前面。同时旧节点的 startIndex
挪动到了 C,新节点的 startIndex 挪动到了 F
新节点的 startIndex
曾经大于 endIndex
了,须要创立 newStartIdx
和 newEndIdx
之间的所有节点,也就是节点F,间接创立 F 节点对应的实在节点放到 B 节点前面
3. 原理剖析
当数据产生扭转时,set
办法会调用Dep.notify
告诉所有订阅者Watcher
,订阅者就会调用patch
给实在的DOM
打补丁,更新相应的视图
源码地位:src/core/vdom/patch.js
function patch(oldVnode, vnode, hydrating, removeOnly) { if (isUndef(vnode)) { // 没有新节点,间接执行destory钩子函数 if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] if (isUndef(oldVnode)) { isInitialPatch = true createElm(vnode, insertedVnodeQueue) // 没有旧节点,间接用新节点生成dom元素 } else { const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { // 判断旧节点和新节点本身一样,统一执行patchVnode patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) } else { // 否则间接销毁及旧节点,依据新节点生成dom元素 if (isRealElement) { if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { oldVnode.removeAttribute(SSR_ATTR) hydrating = true } if (isTrue(hydrating)) { if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { invokeInsertHook(vnode, insertedVnodeQueue, true) return oldVnode } } oldVnode = emptyNodeAt(oldVnode) } return vnode.elm } }}
patch
函数前两个参数位为oldVnode
和 Vnode
,别离代表新的节点和之前的旧节点,次要做了四个判断:
- 没有新节点,间接触发旧节点的
destory
钩子 - 没有旧节点,阐明是页面刚开始初始化的时候,此时,基本不须要比拟了,间接全是新建,所以只调用
createElm
- 旧节点和新节点本身一样,通过
sameVnode
判断节点是否一样,一样时,间接调用patchVnode
去解决这两个节点 - 旧节点和新节点本身不一样,当两个节点不一样的时候,间接创立新节点,删除旧节点
上面次要讲的是patchVnode
局部
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) { // 如果新旧节点统一,什么都不做 if (oldVnode === vnode) { return } // 让vnode.el援用到当初的实在dom,当el批改时,vnode.el会同步变动 const elm = vnode.elm = oldVnode.elm // 异步占位符 if (isTrue(oldVnode.isAsyncPlaceholder)) { if (isDef(vnode.asyncFactory.resolved)) { hydrate(oldVnode.elm, vnode, insertedVnodeQueue) } else { vnode.isAsyncPlaceholder = true } return } // 如果新旧都是动态节点,并且具备雷同的key // 当vnode是克隆节点或是v-once指令管制的节点时,只须要把oldVnode.elm和oldVnode.child都复制到vnode上 // 也不必再有其余操作 if (isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) ) { vnode.componentInstance = oldVnode.componentInstance return } let i const data = vnode.data if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode) } const oldCh = oldVnode.children const ch = vnode.children if (isDef(data) && isPatchable(vnode)) { for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode) } // 如果vnode不是文本节点或者正文节点 if (isUndef(vnode.text)) { // 并且都有子节点 if (isDef(oldCh) && isDef(ch)) { // 并且子节点不完全一致,则调用updateChildren if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) // 如果只有新的vnode有子节点 } else if (isDef(ch)) { if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') // elm曾经援用了老的dom节点,在老的dom节点上增加子节点 addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) // 如果新vnode没有子节点,而vnode有子节点,间接删除老的oldCh } else if (isDef(oldCh)) { removeVnodes(elm, oldCh, 0, oldCh.length - 1) // 如果老节点是文本节点 } else if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, '') } // 如果新vnode和老vnode是文本节点或正文节点 // 然而vnode.text != oldVnode.text时,只须要更新vnode.elm的文本内容就能够 } else if (oldVnode.text !== vnode.text) { nodeOps.setTextContent(elm, vnode.text) } if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode) } }
patchVnode
次要做了几个判断:
- 新节点是否是文本节点,如果是,则间接更新
dom
的文本内容为新节点的文本内容 - 新节点和旧节点如果都有子节点,则解决比拟更新子节点
- 只有新节点有子节点,旧节点没有,那么不必比拟了,所有节点都是全新的,所以间接全副新建就好了,新建是指创立出所有新
DOM
,并且增加进父节点 - 只有旧节点有子节点而新节点没有,阐明更新后的页面,旧节点全副都不见了,那么要做的,就是把所有的旧节点删除,也就是间接把
DOM
删除
子节点不完全一致,则调用updateChildren
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { let oldStartIdx = 0 // 旧头索引 let newStartIdx = 0 // 新头索引 let oldEndIdx = oldCh.length - 1 // 旧尾索引 let newEndIdx = newCh.length - 1 // 新尾索引 let oldStartVnode = oldCh[0] // oldVnode的第一个child let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最初一个child let newStartVnode = newCh[0] // newVnode的第一个child let newEndVnode = newCh[newEndIdx] // newVnode的最初一个child let oldKeyToIdx, idxInOld, vnodeToMove, refElm // removeOnly is a special flag used only by <transition-group> // to ensure removed elements stay in correct relative positions // during leaving transitions const canMove = !removeOnly // 如果oldStartVnode和oldEndVnode重合,并且新的也都重合了,证实diff完了,循环完结 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { // 如果oldVnode的第一个child不存在 if (isUndef(oldStartVnode)) { // oldStart索引右移 oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left // 如果oldVnode的最初一个child不存在 } else if (isUndef(oldEndVnode)) { // oldEnd索引左移 oldEndVnode = oldCh[--oldEndIdx] // oldStartVnode和newStartVnode是同一个节点 } else if (sameVnode(oldStartVnode, newStartVnode)) { // patch oldStartVnode和newStartVnode, 索引左移,持续循环 patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] // oldEndVnode和newEndVnode是同一个节点 } else if (sameVnode(oldEndVnode, newEndVnode)) { // patch oldEndVnode和newEndVnode,索引右移,持续循环 patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] // oldStartVnode和newEndVnode是同一个节点 } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right // patch oldStartVnode和newEndVnode patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) // 如果removeOnly是false,则将oldStartVnode.eml挪动到oldEndVnode.elm之后 canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) // oldStart索引右移,newEnd索引左移 oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] // 如果oldEndVnode和newStartVnode是同一个节点 } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left // patch oldEndVnode和newStartVnode patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) // 如果removeOnly是false,则将oldEndVnode.elm挪动到oldStartVnode.elm之前 canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) // oldEnd索引左移,newStart索引右移 oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] // 如果都不匹配 } else { if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 尝试在oldChildren中寻找和newStartVnode的具备雷同的key的Vnode idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) // 如果未找到,阐明newStartVnode是一个新的节点 if (isUndef(idxInOld)) { // New element // 创立一个新Vnode createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) // 如果找到了和newStartVnodej具备雷同的key的Vnode,叫vnodeToMove } else { vnodeToMove = oldCh[idxInOld] /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && !vnodeToMove) { warn( 'It seems there are duplicate keys that is causing an update error. ' + 'Make sure each v-for item has a unique key.' ) } // 比拟两个具备雷同的key的新节点是否是同一个节点 //不设key,newCh和oldCh只会进行头尾两端的互相比拟,设key后,除了头尾两端的比拟外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key能够更高效的利用dom。 if (sameVnode(vnodeToMove, newStartVnode)) { // patch vnodeToMove和newStartVnode patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue) // 革除 oldCh[idxInOld] = undefined // 如果removeOnly是false,则将找到的和newStartVnodej具备雷同的key的Vnode,叫vnodeToMove.elm // 挪动到oldStartVnode.elm之前 canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) // 如果key雷同,然而节点不雷同,则创立一个新的节点 } else { // same key but different element. treat as new element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) } } // 右移 newStartVnode = newCh[++newStartIdx] } }
while
循环次要解决了以下五种情景:
- 当新老
VNode
节点的start
雷同时,间接patchVnode
,同时新老VNode
节点的开始索引都加 1 - 当新老
VNode
节点的end
雷同时,同样间接patchVnode
,同时新老VNode
节点的完结索引都减 1 - 当老
VNode
节点的start
和新VNode
节点的end
雷同时,这时候在patchVnode
后,还须要将以后实在dom
节点挪动到oldEndVnode
的前面,同时老VNode
节点开始索引加 1,新VNode
节点的完结索引减 1 - 当老
VNode
节点的end
和新VNode
节点的start
雷同时,这时候在patchVnode
后,还须要将以后实在dom
节点挪动到oldStartVnode
的后面,同时老VNode
节点完结索引减 1,新VNode
节点的开始索引加 1 如果都不满足以上四种情景,那阐明没有雷同的节点能够复用,则会分为以下两种状况:
- 从旧的
VNode
为key
值,对应index
序列为value
值的哈希表中找到与newStartVnode
统一key
的旧的VNode
节点,再进行patchVnode
,同时将这个实在dom
挪动到oldStartVnode
对应的实在dom
的后面 - 调用
createElm
创立一个新的dom
节点放到以后newStartIdx
的地位
- 从旧的
小结
- 当数据产生扭转时,订阅者
watcher
就会调用patch
给实在的DOM
打补丁 - 通过
isSameVnode
进行判断,雷同则调用patchVnode
办法 patchVnode
做了以下操作:- 找到对应的实在
dom
,称为el
- 如果都有都有文本节点且不相等,将
el
文本节点设置为Vnode
的文本节点 - 如果
oldVnode
有子节点而VNode
没有,则删除el
子节点 - 如果
oldVnode
没有子节点而VNode
有,则将VNode
的子节点实在化后增加到el
- 如果两者都有子节点,则执行
updateChildren
函数比拟子节点
- 找到对应的实在
updateChildren
次要做了以下操作:- 设置新旧
VNode
的头尾指针 - 新旧头尾指针进行比拟,循环向两头聚拢,依据状况调用
patchVnode
进行patch
反复流程、调用createElem
创立一个新节点,从哈希表寻找key
统一的VNode
节点再分状况操作
- 设置新旧
v-if 和 v-show 的区别
v-if 在编译过程中会被转化成三元表达式,条件不满足时不渲染此节点。
v-show 会被编译成指令,条件不满足时管制款式将对应节点暗藏 (display:none)
vue-router 动静路由是什么
咱们常常须要把某种模式匹配到的所有路由,全都映射到同个组件。例如,咱们有一个User
组件,对于所有ID
各不相同的用户,都要应用这个组件来渲染。那么,咱们能够在vue-router
的路由门路中应用“动静门路参数”(dynamic segment) 来达到这个成果
const User = { template: "<div>User</div>",};const router = new VueRouter({ routes: [ // 动静门路参数 以冒号结尾 { path: "/user/:id", component: User }, ],});
问题: vue-router
组件复用导致路由参数生效怎么办?
解决办法:
- 通过
watch
监听路由参数再发申请
watch: { //通过watch来监听路由变动 "$route": function(){ this.getData(this.$route.params.xxx); }}
- 用
:key
来阻止“复用”
<router-view :key="$route.fullPath" />
答复范例
- 很多时候,咱们须要将给定匹配模式的路由映射到同一个组件,这种状况就须要定义动静路由
- 例如,咱们可能有一个
User
组件,它应该对所有用户进行渲染,但用户ID
不同。在Vue Router
中,咱们能够在门路中应用一个动静字段来实现,例如:{ path: '/users/:id', component: User }
,其中:id
就是门路参数 - 门路参数 用冒号
:
示意。当一个路由被匹配时,它的params
的值将在每个组件中以this.$route.params
的模式裸露进去。 - 参数还能够有多个,例如/
users/:username/posts/:postId
;除了$route.params
之外,$route
对象还公开了其余有用的信息,如$route.query
、$route.hash
等
什么是 mixin ?
- Mixin 使咱们可能为 Vue 组件编写可插拔和可重用的性能。
- 如果心愿在多个组件之间重用一组组件选项,例如生命周期 hook、 办法等,则能够将其编写为 mixin,并在组件中简略的援用它。
- 而后将 mixin 的内容合并到组件中。如果你要在 mixin 中定义生命周期 hook,那么它在执行时将优化于组件自已的 hook。