社招前端一面必会 vue 面试题
DIFF 算法的原理
在新老虚拟 DOM 对比时:
首先,对比节点本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换
如果为相同节点,进行 patchVnode,判断如何对该节点的子节点进行处理,先判断一方有子节点一方没有子节点的情况(如果新的 children 没有子节点,将旧的子节点移除)
比较如果都有子节点,则进行 updateChildren,判断如何对这些新老节点的子节点进行操作(diff 核心)。
匹配时,找到相同的子节点,递归比较子节点
在 diff 中,只对同层的子节点进行比较,放弃跨级的节点比较,使得时间复杂从 O(n3)降低值 O(n),也就是说,只有当新旧 children 都为多个子节点时才需要用核心的 Diff 算法进行同层级比较。
vuex 是什么?怎么使用?哪种功能场景使用它?
Vuex
是一个专为Vue.js
应用程序开发的状态管理模式。vuex
就是一个仓库,仓库里放了很多对象。其中state
就是数据源存放地,对应于一般 vue 对象里面的data
里面存放的数据是响应式的,vue
组件从store
读取数据,若是store
中的数据发生改变,依赖这相数据的组件也会发生更新它通过mapState
把全局的state
和getters
映射到当前组件的computed
计算属性
vuex
一般用于中大型web
单页应用中对应用的状态进行管理,对于一些组件间关系较为简单的小型应用,使用vuex
的必要性不是很大,因为完全可以用组件prop
属性或者事件来完成父子组件之间的通信,vuex
更多地用于解决跨组件通信以及作为数据中心集中式存储数据。使用
Vuex
解决非父子组件之间通信问题vuex
是通过将state
作为数据中心、各个组件共享state
实现跨组件通信的,此时的数据完全独立于组件,因此将组件间共享的数据置于State
中能有效解决多层级组件嵌套的跨组件通信问题
vuex
的State
在单页应用的开发中本身具有一个“数据库”的作用,可以将组件中用到的数据存储在State
中,并在Action
中封装数据读写的逻辑。这时候存在一个问题,一般什么样的数据会放在State
中呢? 目前主要有两种数据会使用vuex
进行管理:
组件之间全局共享的数据
通过后端异步请求的数据
包括以下几个模块
state
:Vuex
使用单一状态树,即每个应用将仅仅包含一个store
实例。里面存放的数据是响应式的,vue
组件从store
读取数据,若是store
中的数据发生改变,依赖这相数据的组件也会发生更新。它通过mapState
把全局的state
和getters
映射到当前组件的computed
计算属性mutations
:更改Vuex
的store
中的状态的唯一方法是提交mutation
getters
:getter
可以对state
进行计算操作,它就是store
的计算属性虽然在组件内也可以做计算属性,但是getters
可以在多给件之间复用如果一个状态只在一个组件内使用,是可以不用getters
action
:action
类似于muation
, 不同在于:action
提交的是mutation
,而不是直接变更状态action
可以包含任意异步操作modules
:面对复杂的应用程序,当管理的状态比较多时;我们需要将vuex
的store
对象分割成模块(modules
)
modules
:项目特别复杂的时候,可以让每一个模块拥有自己的state
、mutation
、action
、getters
,使得结构非常清晰,方便管理
回答范例
思路
给定义
必要性阐述
何时使用
拓展:一些个人思考、实践经验等
回答范例
Vuex
是一个专为Vue.js
应用开发的 状态管理模式 + 库 。它采用集中式存储,管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。我们期待以一种简单的“单向数据流”的方式管理应用,即状态 -> 视图 -> 操作单向循环的方式。但当我们的应用遇到多个组件共享状态时,比如:多个视图依赖于同一状态或者来自不同视图的行为需要变更同一状态。此时单向数据流的简洁性很容易被破坏。因此,我们有必要把组件的共享状态抽取出来,以一个全局单例模式管理。通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。这是
vuex
存在的必要性,它和react
生态中的redux
之类是一个概念Vuex
解决状态管理的同时引入了不少概念:例如state
、mutation
、action
等,是否需要引入还需要根据应用的实际情况衡量一下:如果不打算开发大型单页应用,使用Vuex
反而是繁琐冗余的,一个简单的store
模式就足够了。但是,如果要构建一个中大型单页应用,Vuex
基本是标配。我在使用
vuex
过程中感受到一些等
可能的追问
vuex
有什么缺点吗?你在开发过程中有遇到什么问题吗?
刷新浏览器,
vuex
中的state
会重新变为初始状态。解决方案-插件vuex-persistedstate
action
和mutation
的区别是什么?为什么要区分它们?
action
中处理异步,mutation
不可以
mutation
做原子操作
action
可以整合多个mutation
的集合
mutation
是同步更新数据(内部会进行是否为异步方式更新数据的检测)$watch
严格模式下会报错
action
异步操作,可以获取数据后调佣mutation
提交最终数据
流程顺序:“相应视图—>修改 State”拆分成两部分,视图触发
Action
,Action再触发
Mutation`。基于流程顺序,二者扮演不同的角色:
Mutation
:专注于修改State
,理论上是修改State
的唯一途径。Action
:业务代码、异步请求角色不同,二者有不同的限制:
Mutation
:必须同步执行。Action
:可以异步,但不能直接操作State
组件中写 name 属性的好处
可以标识组件的具体名称方便调试和查找对应属性
实现双向绑定
我们还是以Vue
为例,先来看看Vue
中的双向绑定流程是什么的
new Vue()
首先执行初始化,对data
执行响应化处理,这个过程发生Observe
中同时对模板执行编译,找到其中动态绑定的数据,从
data
中获取并初始化视图,这个过程发生在Compile
中同时定义⼀个更新函数和
Watcher
,将来对应数据变化时Watcher
会调用更新函数由于
data
的某个key
在⼀个视图中可能出现多次,所以每个key
都需要⼀个管家Dep
来管理多个Watcher
将来 data 中数据⼀旦发生变化,会首先找到对应的
Dep
,通知所有Watcher
执行更新函数
流程图如下:
先来一个构造函数:执行初始化,对data
执行响应化处理
对data
选项执行响应化具体操作
编译Compile
对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
依赖收集
视图中会用到data
中某key
,这称为依赖。同⼀个key
可能出现多次,每次都需要收集出来用⼀个Watcher
来维护它们,此过程称为依赖收集多个Watcher
需要⼀个Dep
来管理,需要更新时由Dep
统⼀通知
实现思路
defineReactive
时为每⼀个key
创建⼀个Dep
实例初始化视图时读取某个
key
,例如name1
,创建⼀个watcher1
由于触发
name1
的getter
方法,便将watcher1
添加到name1
对应的Dep
中当
name1
更新,setter
触发时,便可通过对应Dep
通知其管理所有Watcher
更新
声明Dep
创建watcher
时触发getter
依赖收集,创建Dep
实例
watch 原理
watch
本质上是为每个监听属性 setter
创建了一个 watcher
,当被监听的属性更新时,调用传入的回调函数。常见的配置选项有 deep
和 immediate
,对应原理如下
deep
:深度监听对象,为对象的每一个属性创建一个watcher
,从而确保对象的每一个属性更新时都会触发传入的回调函数。主要原因在于对象属于引用类型,单个属性的更新并不会触发对象setter
,因此引入deep
能够很好地解决监听对象的问题。同时也会引入判断机制,确保在多个属性更新时回调函数仅触发一次,避免性能浪费。immediate
:在初始化时直接调用回调函数,可以通过在created
阶段手动调用回调函数实现相同的效果
Vue computed 实现
建立与其他属性(如:
data
、Store
)的联系;属性改变后,通知计算属性重新计算
实现时,主要如下
初始化
data
, 使用Object.defineProperty
把这些属性全部转为getter/setter
。初始化
computed
, 遍历computed
里的每个属性,每个computed
属性都是一个watch
实例。每个属性提供的函数作为属性的getter
,使用Object.defineProperty
转化。Object.defineProperty getter
依赖收集。用于依赖发生变化时,触发属性重新计算。若出现当前
computed
计算属性嵌套其他computed
计算属性时,先进行其他的依赖收集
参考 前端进阶面试题详细解答
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 的基本用法
使用includes
和exclude
:
匹配首先检查组件自身的 name
选项,如果 name
选项不可用,则匹配它的局部注册名称 (父组件 components
选项的键值),匿名组件不能被匹配
设置了 keep-alive
缓存的组件,会多出两个生命周期钩子(activated
与deactivated
):
首次进入组件时:
beforeRouteEnter
>beforeCreate
>created
>mounted
>activated
> ... ... >beforeRouteLeave
>deactivated
再次进入组件时:
beforeRouteEnter
>activated
> ... ... >beforeRouteLeave
>deactivated
使用场景
使用原则:当我们在某些场景下不需要让页面重新加载时我们可以使用keepalive
举个栗子:
当我们从首页
–>列表页
–>商详页
–>再返回
,这时候列表页应该是需要keep-alive
从首页
–>列表页
–>商详页
–>返回到列表页(需要缓存)
–>返回到首页(需要缓存)
–>再次进入列表页(不需要缓存)
,这时候可以按需来控制页面的keep-alive
在路由中设置keepAlive
属性判断是否需要缓存
使用<keep-alive>
思考题:缓存后如何获取数据
解决方案可以有以下两种:
beforeRouteEnter
:每次组件渲染的时候,都会执行beforeRouteEnter
actived
:在keep-alive
缓存的组件被激活的时候,都会执行actived
钩子
扩展补充:LRU 算法是什么?
LRU
的核心思想是如果数据最近被访问过,那么将来被访问的几率也更高,所以我们将命中缓存的组件key
重新插入到this.keys
的尾部,这样一来,this.keys
中越往头部的数据即将来被访问几率越低,所以当缓存数量达到最大值时,我们就删除将来被访问几率最低的数据,即this.keys
中第一个缓存的组件
相关代码
keep-alive
是vue
中内置的一个组件
源码位置:src/core/components/keep-alive.js
可以看到该组件没有template
,而是用了render
,在组件渲染的时候会自动执行render
函数
this.cache
是一个对象,用来存储需要缓存的组件,它将以如下形式存储:
在组件销毁的时候执行pruneCacheEntry
函数
在mounted
钩子函数中观测 include
和 exclude
的变化,如下:
如果include
或exclude
发生了变化,即表示定义需要缓存的组件的规则或者不需要缓存的组件的规则发生了变化,那么就执行pruneCache
函数,函数如下
在该函数内对this.cache
对象进行遍历,取出每一项的name
值,用其与新的缓存规则进行匹配,如果匹配不上,则表示在新的缓存规则下该组件已经不需要被缓存,则调用pruneCacheEntry
函数将其从this.cache
对象剔除即可
关于keep-alive
的最强大缓存功能是在render
函数中实现
首先获取组件的key
值:
拿到key
值后去this.cache
对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存,如下:
直接从缓存中拿 vnode
的组件实例,此时重新调整该组件key
的顺序,将其从原来的地方删掉并重新放在this.keys
中最后一个
this.cache
对象中没有该key
值的情况,如下:
表明该组件还没有被缓存过,则以该组件的key
为键,组件vnode
为值,将其存入this.cache
中,并且把key
存入this.keys
中
此时再判断this.keys
中缓存组件的数量是否超过了设置的最大缓存数量值this.max
,如果超过了,则把第一个缓存组件删掉
Vue 组件渲染和更新过程
渲染组件时,会通过
Vue.extend
方法构建子组件的构造函数,并进行实例化。最终手动调用$mount()
进行挂载。更新组件时会进行patchVnode
流程,核心就是diff
算法
Vue 中如何扩展一个组件
此题属于实践题,考察大家对 vue 常用 api 使用熟练度,答题时不仅要列出这些解决方案,同时最好说出他们异同
答题思路:
按照逻辑扩展和内容扩展来列举
逻辑扩展有:
mixins
、extends
、composition api
内容扩展有
slots
;分别说出他们使用方法、场景差异和问题。
作为扩展,还可以说说
vue3
中新引入的composition api
带来的变化
回答范例:
常见的组件扩展方法有:
mixins
,slots
,extends
等混入
mixins
是分发Vue
组件中可复用功能的非常灵活的方式。混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项
插槽主要用于
vue
组件中的内容分发,也可以用于组件扩展
子组件 Child
父组件 Parent
如果要精确分发到不同位置可以使用具名插槽
,如果要使用子组件中的数据可以使用作用域插槽
组件选项中还有一个不太常用的选项
extends
,也可以起到扩展组件的目的
混入的数据和方法不能明确判断来源且可能和当前组件内变量产生命名冲突,
vue3
中引入的composition api
,可以很好解决这些问题,利用独立出来的响应式模块可以很方便的编写独立逻辑并提供响应式的数据,然后在setup
选项中组合使用,增强代码的可读性和维护性。例如
异步组件是什么?使用场景有哪些?
分析
因为异步路由的存在,我们使用异步组件的次数比较少,因此还是有必要两者的不同。
体验
大型应用中,我们需要分割应用为更小的块,并且在需要组件时再加载它们
回答范例
在大型应用中,我们需要分割应用为更小的块,并且在需要组件时再加载它们。
我们不仅可以在路由切换时懒加载组件,还可以在页面组件中继续使用异步组件,从而实现更细的分割粒度。
使用异步组件最简单的方式是直接给
defineAsyncComponent
指定一个loader
函数,结合 ES 模块动态导入函数import
可以快速实现。我们甚至可以指定loadingComponent
和errorComponent
选项从而给用户一个很好的加载反馈。另外Vue3
中还可以结合Suspense
组件使用异步组件。异步组件容易和路由懒加载混淆,实际上不是一个东西。异步组件不能被用于定义懒加载路由上,处理它的是
vue
框架,处理路由组件加载的是vue-router
。但是可以在懒加载的路由组件中使用异步组件
Vue 中修饰符.sync 与 v-model 的区别
sync
的作用
.sync
修饰符可以实现父子组件之间的双向绑定,并且可以实现子组件同步修改父组件的值,相比较与v-model
来说,sync
修饰符就简单很多了一个组件上可以有多个
.sync
修饰符
v-model
的工作原理
相同点
都是语法糖,都可以实现父子组件中的数据的双向通信
区别点
格式不同:
v-model="num"
,:num.sync="num"
v-model
:@input + value
:num.sync
:@update:num
v-model
只能用一次;.sync
可以有多个
函数式组件优势和原理
函数组件的特点
函数式组件需要在声明组件是指定
functional:true
不需要实例化,所以没有
this
,this
通过render
函数的第二个参数context
来代替没有生命周期钩子函数,不能使用计算属性,
watch
不能通过
$emit
对外暴露事件,调用事件只能通过context.listeners.click
的方式调用外部传入的事件因为函数式组件是没有实例化的,所以在外部通过
ref
去引用组件时,实际引用的是HTMLElement
函数式组件的
props
可以不用显示声明,所以没有在props
里面声明的属性都会被自动隐式解析为prop
,而普通组件所有未声明的属性都解析到$attrs
里面,并自动挂载到组件根元素上面(可以通过inheritAttrs
属性禁止)
优点
由于函数式组件不需要实例化,无状态,没有生命周期,所以渲染性能要好于普通组件
函数式组件结构比较简单,代码结构更清晰
使用场景:
一个简单的展示组件,作为容器组件使用 比如
router-view
就是一个函数式组件“高阶组件”——用于接收一个组件作为参数,返回一个被包装过的组件
例子
源码相关
Vue 中 v-html 会导致哪些问题
可能会导致
xss
攻击v-html
会替换掉标签内部的子元素
双向绑定的原理是什么
我们都知道 Vue
是数据双向绑定的框架,双向绑定由三个重要部分构成
数据层(Model):应用的数据及业务逻辑
视图层(View):应用的展示效果,各类 UI 组件
业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来
而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM
这里的控制层的核心功能便是 “数据双向绑定” 。自然,我们只需弄懂它是什么,便可以进一步了解数据绑定的原理
理解 ViewModel
它的主要职责就是:
数据变化后更新视图
视图变化后更新数据
当然,它还有两个主要部分组成
监听器(
Observer
):对所有数据的属性进行监听解析器(
Compiler
):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
Vue 为什么需要虚拟 DOM?优缺点有哪些
由于在浏览器中操作
DOM
是很昂贵的。频繁的操作DOM
,会产生一定的性能问题。这就是虚拟Dom
的产生原因。Vue2
的Virtual DOM
借鉴了开源库snabbdom
的实现。Virtual DOM
本质就是用一个原生的JS
对象去描述一个DOM
节点,是对真实DOM
的一层抽象
优点:
保证性能下限 : 框架的虚拟
DOM
需要适配任何上层API
可能产生的操作,它的一些DOM
操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的DOM
操作性能要好很多,因此框架的虚拟DOM
至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;无需手动操作 DOM : 我们不再需要手动去操作
DOM
,只需要写好View-Model
的代码逻辑,框架会根据虚拟DOM
和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;跨平台 : 虚拟
DOM
本质上是JavaScript
对象,而DOM
与平台强相关,相比之下虚拟DOM
可以进行更方便地跨平台操作,例如服务器渲染、weex
开发等等。
缺点:
无法进行极致优化:虽然虚拟
DOM
+ 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟DOM
无法进行针对性的极致优化。首次渲染大量
DOM
时,由于多了一层虚拟DOM
的计算,会比innerHTML
插入慢。
虚拟 DOM 实现原理?
虚拟 DOM
的实现原理主要包括以下 3
部分:
用
JavaScript
对象模拟真实DOM
树,对真实DOM
进行抽象;diff
算法 — 比较两棵虚拟DOM
树的差异;pach
算法 — 将两个虚拟DOM
对象的差异应用到真正的DOM
树。
说说你对虚拟 DOM 的理解?回答范例
思路
vdom
是什么引入
vdom
的好处vdom
如何生成,又如何成为dom
在后续的
diff
中的作用
回答范例
虚拟
dom
顾名思义就是虚拟的dom
对象,它本身就是一个JavaScript
对象,只不过它是通过不同的属性去描述一个视图结构通过引入
vdom
我们可以获得如下好处:
将真实元素节点抽象成
VNode
,有效减少直接操作dom
次数,从而提高程序性能直接操作
dom
是有限制的,比如:diff
、clone
等操作,一个真实元素上有许多的内容,如果直接对其进行diff
操作,会去额外diff
一些没有必要的内容;同样的,如果需要进行clone
那么需要将其全部内容进行复制,这也是没必要的。但是,如果将这些操作转移到JavaScript
对象上,那么就会变得简单了操作
dom
是比较昂贵的操作,频繁的dom
操作容易引起页面的重绘和回流,但是通过抽象VNode
进行中间处理,可以有效减少直接操作dom
的次数,从而减少页面重绘和回流方便实现跨平台
同一
VNode
节点可以渲染成不同平台上的对应的内容,比如:渲染在浏览器是dom
元素节点,渲染在Native( iOS、Android)
变为对应的控件、可以实现SSR
、渲染到WebGL
中等等Vue3
中允许开发者基于VNode
实现自定义渲染器(renderer
),以便于针对不同平台进行渲染
vdom
如何生成?在 vue 中我们常常会为组件编写模板 -template
, 这个模板会被编译器 -compiler
编译为渲染函数,在接下来的挂载(mount
)过程中会调用render
函数,返回的对象就是虚拟dom
。但它们还不是真正的dom
,所以会在后续的patch
过程中进一步转化为dom
。
挂载过程结束后,
vue
程序进入更新流程。如果某些响应式数据发生变化,将会引起组件重新render
,此时就会生成新的vdom
,和上一次的渲染结果diff
就能得到变化的地方,从而转换为最小量的dom
操作,高效更新视图
为什么要用 vdom?案例解析
现在有一个场景,实现以下需求:
将该数据展示成一个表格,并且随便修改一个信息,表格也跟着修改。 用 jQuery 实现如下:
这样点击按钮,会有相应的视图变化,但是你审查以下元素,每次改动之后,
table
标签都得重新创建,也就是说table
下面的每一个栏目,不管是数据是否和原来一样,都得重新渲染,这并不是理想中的情况,当其中的一栏数据和原来一样,我们希望这一栏不要重新渲染,因为DOM
重绘相当消耗浏览器性能。因此我们采用 JS 对象模拟的方法,将
DOM
的比对操作放在JS
层,减少浏览器不必要的重绘,提高效率。当然有人说虚拟 DOM 并不比真实的
DOM
快,其实也是有道理的。当上述table
中的每一条数据都改变时,显然真实的DOM
操作更快,因为虚拟DOM
还存在js
中diff
算法的比对过程。所以,上述性能优势仅仅适用于大量数据的渲染并且改变的数据只是一小部分的情况。
如下DOM
结构:
映射成虚拟DOM
就是这样:
使用 snabbdom 实现 vdom
这是一个简易的实现
vdom
功能的库,相比vue
、react
,对于vdom
这块更加简易,适合我们学习vdom
。vdom
里面有两个核心的api
,一个是h
函数,一个是patch
函数,前者用来生成vdom
对象,后者的功能在于做虚拟dom
的比对和将vdom
挂载到真实DOM
上
简单介绍一下这两个函数的用法:
现在我们就来用snabbdom
重写一下刚才的例子:
你会发现, 只有改变的栏目才闪烁,也就是进行重绘 ,数据没有改变的栏目还是保持原样,这样就大大节省了浏览器重新渲染的开销
vue 中使用
h函数
生成虚拟DOM
返回
使用 vue 渲染大量数据时应该怎么优化?说下你的思路!
分析
企业级项目中渲染大量数据的情况比较常见,因此这是一道非常好的综合实践题目。
回答
在大型企业级项目中经常需要渲染大量数据,此时很容易出现卡顿的情况。比如大数据量的表格、树
处理时要根据情况做不同处理:
可以采取分页的方式获取,避免渲染大量数据
vue-virtual-scroller (opens new window)等虚拟滚动方案,只渲染视口范围内的数据
如果不需要更新,可以使用 v-once 方式只渲染一次
通过v-memo (opens new window)可以缓存结果,结合
v-for
使用,避免数据变化时不必要的VNode
创建可以采用懒加载方式,在用户需要的时候再加载数据,比如
tree
组件子树的懒加载
还是要看具体需求,首先从设计上避免大数据获取和渲染;实在需要这样做可以采用虚表的方式优化渲染;最后优化更新,如果不需要更新可以
v-once
处理,需要更新可以v-memo
进一步优化大数据更新性能。其他可以采用的是交互方式优化,无线滚动、懒加载等方案
理解 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
的过程以及使用队列来异步更新的策略,这个我们后面再讲。
Virtual DOM
我们知道,
render function
会被转化成VNode
节点。Virtual DOM
其实就是一棵以 JavaScript 对象( VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。
比如说下面这样一个例子:
渲染后可以得到
这只是一个简单的例子,实际上的节点有更多的属性来标志节点,比如 isStatic (代表是否为静态节点)、 isComment (代表是否为注释节点)等。
更新视图
前面我们说到,在修改一个对象值的时候,会通过
setter -> Watcher -> update
的流程来修改对应的视图,那么最终是如何更新视图的呢?当数据变化后,执行 render function 就可以得到一个新的 VNode 节点,我们如果想要得到新的视图,最简单粗暴的方法就是直接解析这个新的
VNode
节点,然后用innerHTML
直接全部渲染到真实DOM
中。但是其实我们只对其中的一小块内容进行了修改,这样做似乎有些「 浪费 」。那么我们为什么不能只修改那些「改变了的地方」呢?这个时候就要介绍我们的「
patch
」了。我们会将新的VNode
与旧的VNode
一起传入patch
进行比较,经过 diff 算法得出它们的「 差异 」。最后我们只需要将这些「 差异 」的对应 DOM 进行修改即可。
再看全局
回过头再来看看这张图,是不是大脑中已经有一个大概的脉络了呢?
怎么缓存当前的组件?缓存后怎么更新
缓存组件使用keep-alive
组件,这是一个非常常见且有用的优化手段,vue3
中keep-alive
有比较大的更新,能说的点比较多
思路
缓存用
keep-alive
,它的作用与用法使用细节,例如缓存指定/排除、结合
router
和transition
组件缓存后更新可以利用
activated
或者beforeRouteEnter
原理阐述
回答范例
开发中缓存组件使用
keep-alive
组件,keep-alive
是vue
内置组件,keep-alive
包裹动态组件component
时,会缓存不活动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,防止重复渲染DOM
结合属性
include
和exclude
可以明确指定缓存哪些组件或排除缓存指定组件。vue3
中结合vue-router
时变化较大,之前是keep-alive
包裹router-view
,现在需要反过来用router-view
包裹keep-alive
缓存后如果要获取数据,解决方案可以有以下两种
beforeRouteEnter
:在有vue-router的
项目,每次进入路由的时候,都会执行beforeRouteEnter
actived
:在keep-alive
缓存的组件被激活的时候,都会执行actived
钩子
keep-alive
是一个通用组件,它内部定义了一个map
,缓存创建过的组件实例,它返回的渲染函数内部会查找内嵌的component
组件对应组件的vnode
,如果该组件在map
中存在就直接返回它。由于component
的is
属性是个响应式数据,因此只要它变化,keep-alive
的render
函数就会重新执行
Vue.extend 作用和原理
官方解释:
Vue.extend
使用基础Vue
构造器,创建一个“子类”。参数是一个包含组件选项的对象。
其实就是一个子类构造器 是 Vue
组件的核心 api
实现思路就是使用原型继承的方法返回了 Vue 的子类 并且利用 mergeOptions
把传入组件的 options
和父类的 options
进行了合并
extend
是构造一个组件的语法器。然后这个组件你可以作用到Vue.component
这个全局注册方法里还可以在任意vue
模板里使用组件。 也可以作用到vue
实例或者某个组件中的components
属性中并在内部使用apple
组件。Vue.component
你可以创建 ,也可以取组件。
相关代码如下
Vue 中的过滤器了解吗?过滤器的应用场景有哪些?
过滤器实质不改变原始数据,只是对数据进行加工处理后返回过滤后的数据再进行调用处理,我们也可以理解其为一个纯函数
Vue 允许你自定义过滤器,可被用于一些常见的文本格式化
ps: Vue3
中已废弃filter
如何用
vue 中的过滤器可以用在两个地方:双花括号插值和 v-bind
表达式,过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符号指示:
定义 filter
在组件的选项中定义本地的过滤器
定义全局过滤器:
注意:当全局过滤器和局部过滤器重名时,会采用局部过滤器
过滤器函数总接收表达式的值 (之前的操作链的结果) 作为第一个参数。在上述例子中,capitalize
过滤器函数将会收到 message
的值作为第一个参数
过滤器可以串联:
在这个例子中,filterA
被定义为接收单个参数的过滤器函数,表达式 message
的值将作为参数传入到函数中。然后继续调用同样被定义为接收单个参数的过滤器函数 filterB
,将 filterA
的结果传递到 filterB
中。
过滤器是 JavaScript
函数,因此可以接收参数:
这里,filterA
被定义为接收三个参数的过滤器函数。
其中 message
的值作为第一个参数,普通字符串 'arg1'
作为第二个参数,表达式 arg2
的值作为第三个参数
举个例子:
小结:
部过滤器优先于全局过滤器被调用
一个表达式可以使用多个过滤器。过滤器之间需要用管道符“|”隔开。其执行顺序从左往右
应用场景
平时开发中,需要用到过滤器的地方有很多,比如单位转换
、数字打点
、文本格式化
、时间格式化
之类的等
比如我们要实现将30000 => 30,000
,这时候我们就需要使用过滤器
原理分析
使用过滤器
在模板编译阶段过滤器表达式将会被编译为过滤器函数,主要是用过parseFilters
,我们放到最后讲
首先分析一下_f
:
_f
函数全名是:resolveFilter
,这个函数的作用是从this.$options.filters
中找出注册的过滤器并返回
关于resolveFilter
内部直接调用resolveAsset
,将option
对象,类型,过滤器id
,以及一个触发警告的标志作为参数传递,如果找到,则返回过滤器;
resolveAsset
的代码如下:
下面再来分析一下_s
:
_s
函数的全称是 toString
,过滤器处理后的结果会当作参数传递给 toString
函数,最终 toString
函数执行后的结果会保存到Vnode
中的 text 属性中,渲染到视图中
最后,在分析下parseFilters
,在模板编译阶段使用该函数阶段将模板过滤器解析为过滤器函数调用表达式
小结:
在编译阶段通过
parseFilters
将过滤器编译成函数调用(串联过滤器则是一个嵌套的函数调用,前一个过滤器执行的结果是后一个过滤器函数的参数)编译后通过调用
resolveFilter
函数找到对应过滤器并返回结果执行结果作为参数传递给
toString
函数,而toString
执行后,其结果会保存在Vnode
的text
属性中,渲染到视图
评论