keep-alive 是 Vue 内置的一个组件,可以实现组件缓存,当组件切换时不会对当前组件进行卸载。
首先我们来看一下笔者画的内部流程图。
大家第一次看到这个图一定是一头雾水的,没有关系,我们来逐个讲一下这些模块的作用以及调用关系。相信讲完之后大家对Vue.js
内部运行机制会有一个大概的认识。
在
new Vue()
之后。 Vue 会调用_init
函数进行初始化,也就是这里的init
过程,它会初始化生命周期、事件、 props、 methods、 data、 computed 与 watch 等。其中最重要的是通过Object.defineProperty
设置setter
与getter
函数,用来实现「 响应式 」以及「 依赖收集 」,后面会详细讲到,这里只要有一个印象即可。初始化之后调用
$mount
会挂载组件,如果是运行时编译,即不存在 render function 但是存在 template 的情况,需要进行「 编译 」步骤。
compile编译可以分成 parse
、optimize
与 generate
三个阶段,最终需要得到 render function。
1. parse
parse
会用正则等方式解析 template 模板中的指令、class、style等数据,形成AST。
2. optimize
optimize
的主要作用是标记 static 静态节点,这是 Vue 在编译过程中的一处优化,后面当update
更新界面时,会有一个patch
的过程, diff 算法会直接跳过静态节点,从而减少了比较的过程,优化了patch
的性能。
3. generate
generate
是将 AST 转化成render function
字符串的过程,得到结果是render
的字符串以及 staticRenderFns 字符串。
parse
、optimize
与 generate
这三个阶段以后,组件中就会存在渲染 VNode
所需的 render function
了。接下来也就是 Vue.js 响应式核心部分。
这里的
getter
跟setter
已经在之前介绍过了,在init
的时候通过Object.defineProperty
进行了绑定,它使得当被设置的对象被读取的时候会执行getter
函数,而在当被赋值的时候会执行setter
函数。
render function
被渲染的时候,因为会读取所需对象的值,所以会触发 getter
函数进行「 依赖收集 」,「 依赖收集 」的目的是将观察者 Watcher
对象存放到当前闭包中的订阅者 Dep
的 subs
中。形成如下所示的这样一个关系。在修改对象的值的时候,会触发对应的
setter
,setter
通知之前「 依赖收集 」得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher 就会开始调用update
来更新视图,当然这中间还有一个patch
的过程以及使用队列来异步更新的策略,这个我们后面再讲。
我们知道,
render function
会被转化成VNode
节点。Virtual DOM
其实就是一棵以 JavaScript 对象( VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。
比如说下面这样一个例子:
{
tag: 'div', /*说明这是一个div标签*/
children: [ /*存放该标签的子节点*/
{
tag: 'a', /*说明这是一个a标签*/
text: 'click me' /*标签的内容*/
}
]
}
渲染后可以得到
<div>
<a>click me</a>
</div>
这只是一个简单的例子,实际上的节点有更多的属性来标志节点,比如 isStatic (代表是否为静态节点)、 isComment (代表是否为注释节点)等。
setter -> Watcher -> update
的流程来修改对应的视图,那么最终是如何更新视图的呢?VNode
节点,然后用 innerHTML
直接全部渲染到真实 DOM
中。但是其实我们只对其中的一小块内容进行了修改,这样做似乎有些「 浪费 」。patch
」了。我们会将新的 VNode
与旧的 VNode
一起传入 patch
进行比较,经过 diff 算法得出它们的「 差异 」。最后我们只需要将这些「 差异 」的对应 DOM 进行修改即可。回过头再来看看这张图,是不是大脑中已经有一个大概的脉络了呢?
虚拟节点就是用一个对象来描述一个真实的DOM元素。首先将template
(真实DOM)先转成ast
,ast
树通过codegen
生成render
函数,render
函数里的_c
方法将它转为虚拟dom
vue 中 key 值的作用可以分为两种情况来考虑:
key 是为 Vue 中 vnode 的唯一标记,通过这个 key,diff 操作可以更准确、更快速
参考:前端vue面试题详细解答
computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;
运用场景:
官方解释:Vue.extend 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。
其实就是一个子类构造器 是 Vue 组件的核心 api 实现思路就是使用原型继承的方法返回了 Vue 的子类 并且利用 mergeOptions 把传入组件的 options 和父类的 options 进行了合并
出现该问题是因为在 Vue 代码尚未被解析之前,尚无法控制页面中 DOM 的显示,所以会看见模板字符串等代码。
解决方案是,在 css 代码中添加 v-cloak 规则,同时在待编译的标签上添加 v-cloak 属性:
[v-cloak] { display: none; }
<div v-cloak>
{{ message }}
</div>
涉及到Vue中的模板编译原理,主要过程:
ast
树,ast
用对象来描述真实的JS语法(将真实DOM转换成虚拟DOM)ast
树生成代码const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = createStore({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
store.getters.c // -> moduleA里的getters
store.commit('d') // -> 能同时触发子模块中同名mutation
store.dispatch('e') // -> 能同时触发子模块中同名action
module
,项目规模变大之后,单独一个store
对象会过于庞大臃肿,通过模块方式可以拆分开来便于维护modules
选项组织起来:reateStore({modules:{...}})
store.state.a.xxx
,但同时getters
、mutations
和actions
又在全局空间中,使用方式和之前一样。如果要做到完全拆分,需要在子块加上namespace
选项,此时再访问它们就要加上命名空间前缀。pinia
显然在这方面有了很大改进,是时候切换过去了dep
属性watcher
去更新splice
去更新数组(数组的响应式原理就是重新了splice
等方法,调用splice
就会触发视图更新)基本使用
以下方法调用会改变原始数组:
push()
,pop()
,shift()
,unshift()
,splice()
,sort()
,reverse()
,Vue.set( target, key, value )
Vue.set(target, key, value )
target
:要更改的数据源(可以是对象或者数组)key
:要更改的具体数据value
:重新赋的值<div id="app">{{user.name}} {{user.age}}</div>
<div id="app"></div>
<script>
// 1. 依赖收集的特点:给每个属性都增加一个dep属性,dep属性会进行收集,收集的是watcher
// 2. vue会给每个对象也增加一个dep属性
const vm = new Vue({
el: '#app',
data: { // vm._data
user: {name:'poetry'}
}
});
// 对象的话:调用defineReactive在user对象上定义一个age属性,增加到响应式数据中,触发对象本身的watcher,ob.dep.notify()更新
// 如果是数组 通过调用 splice方法,触发视图更新
vm.$set(vm.user, 'age', 20); // 不能给根属性添加,因为给根添加属性 性能消耗太大,需要做很多处理
// 修改肯定是同步的 -> 更新都是一步的 queuewatcher
</script>
相关源码
// src/core/observer/index.js 44
export class Observer { // new Observer(value)
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep() // 给所有对象类型增加dep属性
}
}
// src/core/observer/index.js 201
export function set (target: Array<any> | Object, key: any, val: any): any {
// 1.是开发环境 target 没定义或者是基础类型则报错
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
// 2.如果是数组 Vue.set(array,1,100); 调用我们重写的splice方法 (这样可以更新视图)
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
// 利用数组的splice变异方法触发响应式
target.splice(key, 1, val)
return val
}
// 3.如果是对象本身的属性,则直接添加即可
if (key in target && !(key in Object.prototype)) {
target[key] = val // 直接修改属性值
return val
}
// 4.如果是Vue实例 或 根数据data时 报错,(更新_data 无意义)
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
// 5.如果不是响应式的也不需要将其定义成响应式属性
if (!ob) {
target[key] = val
return val
}
// 6.将属性定义成响应式的
defineReactive(ob.value, key, val)
// 通知视图更新
ob.dep.notify()
return val
}
我们阅读以上源码可知,vm.$set 的实现原理是:
splice
方法触发相应式;defineReactive
方法进行响应式处理( defineReactive
方法就是 Vue
在初始化对象时,给对象属性采用 Object.defineProperty
动态添加 getter
和 setter
的功能所调用的方法)可以标识组件的具体名称方便调试和查找对应属性
// 源码位置 src/core/global-api/extend.js
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub // 记录自己 在组件中递归自己 -> jsx
}
Vue 的编译过程就是将 template 转化为 render 函数的过程 分为以下三步
第一步是将 模板字符串 转换成 element ASTs(解析器)
第二步是对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器)
第三步是 使用 element ASTs 生成 render 函数代码字符串(代码生成器)
相关代码如下
export function compileToFunctions(template) {
// 我们需要把html字符串变成render函数
// 1.把html代码转成ast语法树 ast用来描述代码本身形成树结构 不仅可以描述html 也能描述css以及js语法
// 很多库都运用到了ast 比如 webpack babel eslint等等
let ast = parse(template);
// 2.优化静态节点
// 这个有兴趣的可以去看源码 不影响核心功能就不实现了
// if (options.optimize !== false) {
// optimize(ast, options);
// }
// 3.通过ast 重新生成代码
// 我们最后生成的代码需要和render函数一样
// 类似_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))
// _c代表创建元素 _v代表创建文本 _s代表文Json.stringify--把对象解析成文本
let code = generate(ast);
// 使用with语法改变作用域为this 之后调用render函数可以使用call改变this 方便code里面的变量取值
let renderFn = new Function(`with(this){return ${code}}`);
return renderFn;
}
我们在 vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:
以 input 表单元素为例:
<input v-model='something'>
相当于
<input v-bind:value="something" v-on:input="something = $event.target.value">
如果在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件,如下所示:
父组件:
<ModelChild v-model="message"></ModelChild>
子组件:
<div>{{value}}</div>
props:{
value: String
},
methods: {
test1(){
this.$emit('input', '小红')
},
},
时间复杂度: 个树的完全diff
算法是一个时间复杂度为O(n*3)
,vue进行优化转化成O(n)
。
理解:
key
很重要。这个可以是这个节点的唯一标识,告诉diff
算法,在更改前后它们是同一个DOM节点v-for
为什么要有key
,没有key
会暴力复用,举例子的话随便说一个比如移动节点或者增加节点(修改DOM),加key
只会移动减少操作DOM。diff算法的优化策略:四种命中查找,四个指针
此题属于实践题,考察大家对vue常用api使用熟练度,答题时不仅要列出这些解决方案,同时最好说出他们异同
答题思路:
mixins
、extends
、composition api
slots
;vue3
中新引入的composition api
带来的变化回答范例:
mixins
,slots
,extends
等mixins
是分发 Vue
组件中可复用功能的非常灵活的方式。混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项// 复用代码:它是一个配置对象,选项和组件里面一样
const mymixin = {
methods: {
dosomething(){}
}
}
// 全局混入:将混入对象传入
Vue.mixin(mymixin)
// 局部混入:做数组项设置到mixins选项,仅作用于当前组件
const Comp = {
mixins: [mymixin]
}
vue
组件中的内容分发,也可以用于组件扩展子组件Child
<div>
<slot>这个内容会被父组件传递的内容替换</slot>
</div>
父组件Parent
<div>
<Child>来自父组件内容</Child>
</div>
如果要精确分发到不同位置可以使用具名插槽
,如果要使用子组件中的数据可以使用作用域插槽
extends
,也可以起到扩展组件的目的// 扩展对象
const myextends = {
methods: {
dosomething(){}
}
}
// 组件扩展:做数组项设置到extends选项,仅作用于当前组件
// 跟混入的不同是它只能扩展单个对象
// 另外如果和混入发生冲突,该选项优先级较高,优先起作用
const Comp = {
extends: myextends
}
vue3
中引入的composition api
,可以很好解决这些问题,利用独立出来的响应式模块可以很方便的编写独立逻辑并提供响应式的数据,然后在setup
选项中组合使用,增强代码的可读性和维护性。例如// 复用逻辑1
function useXX() {}
// 复用逻辑2
function useYY() {}
// 逻辑组合
const Comp = {
setup() {
const {xx} = useXX()
const {yy} = useYY()
return {xx, yy}
}
}
sync
的作用
.sync
修饰符可以实现父子组件之间的双向绑定,并且可以实现子组件同步修改父组件的值,相比较与v-model
来说,sync
修饰符就简单很多了.sync
修饰符<!-- 正常父传子 -->
<Son :a="num" :b="num2" />
<!-- 加上sync之后的父传子 -->
<Son :a.sync="num" :b.sync="num2" />
<!-- 它等价于 -->
<Son
:a="num"
:b="num2"
@update:a="val=>num=val"
@update:b="val=>num2=val"
/>
<!-- 相当于多了一个事件监听,事件名是update:a, -->
<!-- 回调函数中,会把接收到的值赋值给属性绑定的数据项中。 -->
v-model
的工作原理
<com1 v-model="num"></com1>
<!-- 等价于 -->
<com1 :value="num" @input="(val)=>num=val"></com1>
v-model="num"
, :num.sync="num"
v-model
: @input + value
:num.sync
: @update:num
v-model
只能用一次;.sync
可以有多个1.工厂模式 - 传入参数即可创建实例
虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode
2.单例模式 - 整个程序有且仅有一个实例
vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉
3.发布-订阅模式 (vue 事件机制)
4.观察者模式 (响应式数据原理)
5.装饰模式: (@装饰器的用法)
6.策略模式 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案-比如选项的合并策略
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 钩子函数中调用异步请求有以下优点:
keep-alive可以实现组件的缓存,当组件切换时不会对当前组件进行卸载。常用的2个属性
include/exclude,2个生命周期
activated,
deactivated
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。