一、Vue 基础
1. Vue的基本原理
当一个Vue实例创建时,Vue会遍历data中的属性,用 Object.defineProperty(vue3.0使用 proxy )将它们转为 getter/setter,并且在内部追踪相关依赖,在属性被访问和修改时通知 变化。 每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录 为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而致使它关联的组 件得以更新。
2. 双向数据绑定的原理
Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持 各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要 分为以下几个步骤:
- 需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter 这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化
- compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将 每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到 通知,更新视图
- Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是: ①在自身实 例化时往属性订阅器(dep)里面添加自己 ②自身必须有一个update()方法 ③待属性变动 dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功 成身退。
- MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer 来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher 搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化 (input) -> 数据model变更的双向绑定效果。
3. 使用 Object.defineProperty() 来进行数据劫持有什么缺点?
在对一些属性进行操作时,使用这种方法无法拦截,比如通过下标方式修改数组数据或者 给对象新增属性,这都不能触发组件的重新渲染,因为 Object.defineProperty 不能拦截到 这些操作。更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通 过重写函数的方式解决了这个问题。
在 Vue3.0 中已经不使用这种方式了,而是通过使用 Proxy 对对象进行代理,从而实现数据 劫持。使用Proxy 的好处是它可以完美的监听到任何方式的数据改变,唯一的缺点是兼容性 的问题,因为 Proxy 是 ES6 的语法。
4. MVVM、MVC、MVP的区别
MVC、MVP 和 MVVM 是三种常见的软件架构设计模式,主要通过分离关注点的方式来组 织代码结构,优化开发效率。
在开发单页面应用时,往往一个路由页面对应了一个脚本文件,所有的页面逻辑都在一个 脚本文件里。页面的渲染、数据的获取,对用户事件的响应所有的应用逻辑都混合在一 起,这样在开发简单项目时,可能看不出什么问题,如果项目变得复杂,那么整个文件就 会变得冗长、混乱,这样对项目开发和后期的项目维护是非常不利的。
(1)MVC
MVC 通过分离 Model、View 和 Controller 的方式来组织代码结构。其中 View 负责页面的 显示逻辑,Model 负责存储页面的业务数据,以及对相应数据的操作。并且 View 和 Model 应用了观察者模式,当 Model 层发生改变的时候它会通知有关 View 层更新页面。 Controller 层是 View 层和 Model 层的纽带,它主要负责用户与应用的响应操作,当用户 与页面产生交互的时候,Controller 中的事件触发器就开始工作了,通过调用 Model 层, 来完成对 Model 的修改,然后 Model 层再去通知 View 层更新。
(2)MVVM
MVVM 分为 Model、View、ViewModel:
- Model代表数据模型,数据和业务逻辑都在Model层中定义;
- View代表UI视图,负责数据的展示;
- ViewModel负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作;
Model和View并无直接关联,而是通过ViewModel来进行联系的,Model和ViewModel之 间有着双向数据绑定的联系。因此当Model中的数据改变时会触发View层的刷新,View中 由于用户交互操作而改变的数据也会在Model中同步。
这种模式实现了 Model和View的数据自动同步,因此开发者只需要专注于数据的维护操作 即可,而不需要自己操作DOM。
(3)MVP
MVP 模式与 MVC 唯一不同的在于 Presenter 和 Controller。在 MVC 模式中使用观察者模 式,来实现当 Model 层数据发生变化的时候,通知 View 层的更新。这样 View 层和 Model 层耦合在一起,当项目逻辑变得复杂的时候,可能会造成代码的混乱,并且可能会 对代码的复用性造成一些问题。MVP 的模式通过使用 Presenter 来实现对 View 层和 Model 层的解耦。MVC 中的Controller 只知道 Model 的接口,因此它没有办法控制 View 层的更新,MVP 模式中,View 层的接口暴露给了 Presenter 因此可以在 Presenter 中将 Model 的变化和 View 的变化绑定在一起,以此来实现 View 和 Model 的同步更新。这样 就实现了对 View 和 Model 的解耦,Presenter 还包含了其他的响应逻辑。
5. Computed 和 Watch 的区别
对于Computed:
- 它支持缓存,只有依赖的数据发生了变化,才会重新计算
- 不支持异步,当Computed中有异步操作时,无法监听数据的变化
- computed的值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是 基于data声明过,或者父组件传递过来的props中的数据进行计算的。
- 如果一个属性是由其他属性计算而来的,这个属性依赖其他的属性,一般会使用 computed
- 如果computed属性的属性值是函数,那么默认使用get方法,函数的返回值就是属性 的属性值;在computed中,属性有一个get方法和一个set方法,当数据发生变化时, 会调用set方法。
对于Watch:
- 它不支持缓存,数据变化时,它就会触发相应的操作
- 支持异步监听
- 监听的函数接收两个参数,第一个参数是最新的值,第二个是变化之前的值
- 当一个属性发生变化时,就需要执行相应的操作
- 监听数据必须是data中声明的或者父组件传递过来的props中的数据,当发生变化时, 会触发其他操作,函数有两个的参数:
- immediate:组件加载立即触发回调函数
- deep:深度监听,发现数据内部的变化,在复杂数据类型中使用,例如数组中的 对象发生变化。需要注意的是,deep无法监听到数组和对象内部的变化。
当想要执行异步或者昂贵的操作以响应不断的变化时,就需要使用watch。
总结:
- computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依 赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的 值。
- watch 侦听器 : 更多的是观察的作用,无缓存性,类似于某些数据的监听回调, 每当监听的数据变化时都会执行回调进行后续操作。
运用场景:
- 当需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利 用 computed 的缓存特性,避免每次获取值时都要重新计算。
- 当需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许执行异步操作 ( 访问一个 API ),限制执行该操作的频率,并在得 到最终结果前,设置中间状态。这些都是计算属性无法做到的。
6. Computed 和 Methods 的区别
可以将同一函数定义为一个 method 或者一个计算属性。对于最终的结果,两种方式 是相同的
不同点:
- computed: 计算属性是基于它们的依赖进行缓存的,只有在它的相关依赖发生改 变时才会重新求值;
- method 调用总会执行该函数。
7. slot是什么?有什么作用?原理是什么?
slot又名插槽,是Vue的内容分发机制,组件内部的模板引擎使用slot元素作为承载分发内 容的出口。插槽slot是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么 显示是由父组件决定的。slot又分三类,默认插槽,具名插槽和作用域插槽。
- 默认插槽:又名匿名插槽,当slot没有指定name属性值的时候一个默认显示插槽,一 个组件内只有有一个匿名插槽。
- 具名插槽:带有具体名字的插槽,也就是带有name属性的slot,一个组件可以出现多 个具名插槽。
- 作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插 槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给 父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。
实现原理:当子组件vm实例化时,获取到父组件传入的slot标签的内容,存放在 vm. <math xmlns="http://www.w3.org/1998/Math/MathML"> s l o t 中,默认插槽为 v m . slo t 中,默认插槽为 vm. </math>slot中,默认插槽为vm.slot.default ,具名插槽为 vm. <math xmlns="http://www.w3.org/1998/Math/MathML"> s l o t . x x x , x x x 为插槽名,当组件执行渲染函数时候,遇到 s l o t 标签,使用 slot.xxx ,xxx 为插槽名, 当组件执行渲染函数时候,遇到slot标签,使用 </math>slot.xxx,xxx为插槽名,当组件执行渲染函数时候,遇到slot标签,使用slot 中的内容进行替换,此时可以为插 槽传递数据,若存在数据,则可称该插槽为作用域插槽
8. 过滤器的作用,如何实现一个过滤器
根据过滤器的名称,过滤器是用来过滤数据的,在Vue中使用 filters 来过滤数据, fi lters 不会修改数据,而是过滤数据,改变用户看到的输出(计算属性 computed ,方 法 methods 都是通过修改数据来处理数据格式的输出显示)。 使用场景:
- 需要格式化数据的情况,比如需要处理时间、价格等数据格式的输出 / 显示。
- 比如后端返回一个 年月日的日期字符串,前端需要展示为 多少天前 的数据格式,此 时就可以用 fliters 过滤器来处理数据。
过滤器是一个函数,它会把表达式中的值始终当作函数的第一个参数。过滤器用在插值表 达式 {{ }} 和 v-bind 表达式 中,然后放在操作符" | "后面进行指示。
例如,在显示金额,给商品价格添加单位:
js
<li>商品价格: {fitem.price filterPricer1</li>
filters: (
filterPrice (price) {
return price ? (¥' + price) :'--'
}
}
9. 如何保存页面的当前的状态
既然是要保持页面的状态(其实也就是组件的状态),那么会出现以下两种情况:
- 前组件会被卸载
- 前组件不会被卸载
那么可以按照这两种情况分别得到以下方法:
组件会被卸载:
- 将状态存储在LocalStorage / SessionStorage
只需要在组件即将被销毁的生命周期中在 LocalStorage / SessionStorage 中把当前组件的 state 通过 JSON.stringify() 储存下来就可以了。在这里面需要注意的是组件更新状态的时机。
比如从 B 组件跳转到 A 组件的时候,A 组件需要更新自身的状态。但是如果从别的组件跳 转到 B 组件的时候,实际上是希望 B 组件重新渲染的,也就是不要从 Storage 中读取信 息。所以需要在 Storage 中的状态加入一个 flag 属性,用来控制 A 组件是否读取 Storage 中的状态。
优点
- 兼容性好,不需要额外库或工具。
- 简单快捷,基本可以满足大部分需求。
缺点
- 状态通过 JSON 方法储存(相当于深拷贝),如果状态中有特殊情况(比如 Date 对 象、Regexp 对象等)的时候会得到字符串而不是原来的值。(具体参考用 JSON 深拷 贝的缺点)
- 如果 B 组件后退或者下一页跳转并不是前组件,那么 flag 判断会失效,导致从其他页 面进入 A 组件页面时 A 组件会重新读取 Storage,会造成很奇怪的现象
- 路由传值
通过 react-router 的 Link 组件的 prop ------ to 可以实现路由间传递参数的效果。\ 在这里需要用到 state 参数,在 B 组件中通过 history.location.state 就可以拿到 state 值, 保存它。返回 A 组件时再次携带 state 达到路由状态保持的效果。
优点
- 简单快捷,不会污染 LocalStorage / SessionStorage。
- 可以传递 Date、RegExp 等特殊对象(不用担心 JSON.stringify / parse 的不足)
缺点
- 如果 A 组件可以跳转至多个组件,那么在每一个跳转组件内都要写相同的逻辑
组件不会被卸载:
(1)单页面渲染
要切换的组件作为子组件全屏渲染,父组件中正常储存页面状态。
优点
- 代码量少
- 不需要考虑状态传递过程中的错误
缺点
- 增加 A 组件维护成本
- 需要传入额外的 prop 到 B 组件
- 无法利用路由定位页面
除此之外,在Vue中,还可以是用keep-alive来缓存页面,当组件在keep-alive内被切换时 组件的activated、deactivated这两个生命周期钩子函数会被执行 被包裹在keep-alive中的组件的状态将会被保留:
js
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</kepp-alive>
router.js
js
{
path:'/',
name:'xxx',
component: ()=>import( ../src/views/xxx.vue'),
meta:{
keepAlive: true // 需要被缓存
}
},
10. 常见的事件修饰符及其作用
- .stop :等同于 JavaScript 中的 event.stopPropagation() ,防止事件冒泡;
- .prevent :等同于 JavaScript 中的 event.preventDefault() ,防止执行预设 的行为(如果事件可取消,则取消该事件,而不停止事件的进一步传播);
- .capture :与事件冒泡的方向相反,事件捕获由外到内;
- .self :只会触发自己范围内的事件,不包含子元素;
- .once :只会触发一次。
11. v-if、v-show、v-html 的原理
- v-if会调用addIfCondition方法,生成vnode的时候会忽略对应节点,render的时候就 不会渲染;
- v-show会生成vnode,render的时候也会渲染成真实节点,只是在render过程中会在 节点的属性中修改show属性值,也就是常说的display;
- v-html会先移除节点下的所有节点,调用html方法,通过addProp添加innerHTML属 性,归根结底还是设置innerHTML为v-html的值。
13. v-if和v-show的区别
- 手段:v-if是动态的向DOM树内添加或者删除DOM元素;v-show是通过设置DOM元素 的display样式属性控制显隐;
- 编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部 的事件监听和子组件;v-show只是简单的基于css切换;
- 编译条件:v-if是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为 真时才开始局部编译; v-show是在任何条件下,无论首次条件是否为真,都被编译,然 后被缓存,而且DOM元素保留;
- 性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗;
- 使用场景:v-if适合运营条件不大可能改变;v-show适合频繁切换。
14. v-model 是如何实现的,语法糖实际是什么?
(1)作用在表单元素上
动态绑定了 input 的 value 指向了 messgae 变量,并且在触发 input 事件的时候去动态把 message设置为目标值:
js
<input v-model="sth"
// 等同于
<input
v-bind:value="message
v-on:input="message=$event.target.value"
>
//$event 指代当前触发的事件对象;
//$event.target 指代当前触发的事件对象的dom;
//$event.target.value 就是当前dom的value值:
//在@input方法中,value => sth;
//在:value中,sth => value;
(2)作用在组件上
在自定义组件中,v-model 默认会利用名为 value 的 prop和名为 input 的事件
本质是一个父子组件通信的语法糖,通过prop和$.emit实现。因此父组件 v-model 语法糖 本质上可以修改为:
js
<child :value="message" @input="function(e)(message = e)"></child>
在组件的实现中,可以通过 v-model属性来配置子组件接收的prop名称,以及派发的事件名称。 例子:
js
// 父组件
<aa-inputv-model="aa"></aa-input>
// 等价于
<aa-input v-bind:value="aa" v-on:input="aa-$event.targetvalue"x</aa-i
// 子组件:
<input v-bind:value="aa" v-on:input="onmessage"></aa-input>
props:(value:aa ,
methods:{
onmessage(e){
$emit("input',e.target .value)
}
}
默认情况下,一个组件上的v-model 会把 value 用作 prop且把 input 用作 event。但是一 些输入类型比如单选框和复选框按钮可能想使用 value prop 来达到不同的目的。使用 model 选项可以回避这些情况产生的冲突。js 监听input 输入框输入数据改变,用 oninput,数据改变以后就会立刻出发这个事件。通过input事件把数据 <math xmlns="http://www.w3.org/1998/Math/MathML"> e m i t 出去,在父组件接受。父组件设置 v − m o d e l 的值为 i n p u t emit 出去,在父组 件接受。父组件设置v-model的值为input </math>emit出去,在父组件接受。父组件设置v−model的值为inputemit过来的值。
15. v-model 可以被用在自定义组件上吗?如果可以,如何使用?
可以。v-model 实际上是一个语法糖,如:
js
<input y-model="searchText">
实际上相当于:
js
<input
v-bind:value="searchText"
v-on;input="searchText = $event.target.walue"
>
用在自定义组件上也是同理:
js
<custom-input y-model="searchtext">
相当于:
js
<custom-input
y-bind:value="searchText"
y-on:input="searchText = $event"
></custom-input>
显然,custom-input 与父组件的交互如下:
- 父组件将 searchText 变量传入custom-input组件,使用的 prop 名为 value ;
- custom-input组件向父组件传出名为 input 的事件,组件将接收到的值赋值给 searchText ; 所以,custom-input 组件的实现应该类似于这样:
js
vue.component( custom-input, {
props: ['walue']
template:
input
v-bind:walue="walue"
v-on:input="semit("input", $event.target.value)"
>
})
16. data为什么是一个函数而不是对象
Vue组件可能存在多个实例,如果使用对象形式定义data,则会导致它们共用一个data对 象,那么状态变更将会影响所有组件实例,这是不合理的;采用函数形式定义,在initData 时会将其作为工厂函数返回全新data对象,有效规避多实例之间状态污染问题。而在Vue根 实例创建过程中则不存在该限制,也是因为根实例只能有一个,不需要担心这种情况。
17. 对keep-alive的理解,它是如何实现的,具体缓存的是什么?
如果需要在组件切换的时候,保存一些组件的状态防止多次渲染,就可以使用 keep-alive 组件包裹需要保存的组件。
(1)keep-alive
keep-alive有以下三个属性:
- include 字符串或正则表达式,只有名称匹配的组件会被匹配;
- exclude 字符串或正则表达式,任何名称匹配的组件都不会被缓存;
- max 数字,最多可以缓存多少组件实例。
注意:keep-alive 包裹动态组件时,会缓存不活动的组件实例。
主要流程
- 判断组件 name ,不在 include 或者在 exclude 中,直接返回 vnode,说明该组件不 被缓存。
- 获取组件实例 key ,如果有获取实例的 key,否则重新生成。
- key生成规则,cid +"∶∶"+ tag ,仅靠cid是不够的,因为相同的构造函数可以注册为不 同的本地组件。
- 如果缓存对象内存在,则直接从缓存对象中获取组件实例给 vnode ,不存在则添加到 缓存对象中。
- 最大缓存数量,当缓存组件数量超过 max 值时,清除 keys 数组内第 一个组件。 (2)keep-alive 的实现
js
const patternTypes: Array<Function> = [string,RegExp,Array] // 接: 字符,正则,缴组
export default {
name: "keep-alive",
abstract: true,// 抽象组件,是一个抽象组件:它自身不会染一个 DOM 元素,也不会出现在父组件链中。
props: {
include: patterntypes,// 匹配的组件,缓存
exclude: patterntypes,// 不去匹配的组件,不缓存
max:[string,Mumber], // 存组件的最大实例缴量,由于级存的是组件实例(vnode),量过多的时候,会占用过多的内存,可以用max指定上限
},
created() (
// 用于初始化缓存虚拟DOM效组和vnode的key
this.cache = object.create(nul1)
this.keys = []
},
destroyed() {
// 销毁爆存cache的组件实例
for (const key in this.cache) {
prunecacheEntry(this.cache, key, this.keys)
}
},
mounted() {
// prune 削该精[v.]
// 去监控include和exclude的改变,根据最新的include和exclude的内容,来实时削波漫存的组件的内容
this.$watch("include", (val) => {
prunecache(this, (name) => matches(val, name))
})
this.swatch('exclude", (val) => {
pruneCache(this, (name) => !matches(wal, name))
})
},
}
render函数:
- 会在 keep-alive 组件内部去写自己的内容,所以可以去获取默认 slot 的内容,然后根 据这个去获取组件
- keep-alive 只对第一个组件有效,所以获取第一个子组件。
- 和 keep-alive 搭配使用的一般有:动态组件 和router-view
js
render{
function getFirstcomponentchild (children: ?Array<VNode>): ?vNode {
if (Array.isArray(children)) {
for (let i = 8; i < children.length; i++) {
const c = children[i]
if (isDef(c) 8& (isDef(c.componentoptions) isAsyncPlaceholder(c))) {
return c
}
}
}
}
const slot = this,sslots.default // 获取默认插增const wnode: WNode = getFirstComponentchild(slot)// 苏取第一个子组件const componentoptions: ?VNodecomponentoptions = vnode & vnode.componentoptions // 组件参
if (componentoptions) { //是否有组件
// 参数check pattern
const name: ?string = getComponentName(componentoptions) // 获取组件名
const { include, exclude ] = this
if (
// not included
(include && (!name !matches(include, name)))
// excluded
(exclude && name && matches(exclude, mame))
){
//如果不匹配当前组件的名字和include以及exclude那么直接返回组件的实例
return vnode
}
const { cache, keys } = this
//获取这个组件的key
const key: ?string = wnode key == ul1
// 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
if (cache[key]) {
// LU超存黄路执行
vnode.componentInstance = cache[key].componentInstance // 组件初次染的时候componentinstance为undefined
// make current key freshest
remove( keys , key)
keys.push(ker)
//根据LU级存策略执行,将key从原来的位置移除,然后将这个key值放到最后面
} else {
// 在存列表里面没有的话,则加入,同时判断当前加入之后,是否超过了max所设定的范围,如果是,则去除
// 使用时间间隔最长的一个
cache[key] = vnode
keys.push(key)
// prune oldest entry
if (this.max && keys.length > parseint(this.max)) {
prunecacheEntry(cache, keys[e], keys, this._vnode)
}
}
// 将组件的keeDAlive店性设警为true
vnode,data.keepAlive = true // 作用: 判断是否要执行组件的created、mounted生命同期函缴
}
return vnode l (slot && slot[e])
}
keep-alive 具体是通过 cache 数组缓存所有组件的 vnode 实例。当 cache 内原有组件被使 用时会将该组件 key 从 keys 数组中删除,然后 push 到 keys数组最后,以便清除最不常用 组件。
实现步骤:
- 获取 keep-alive 下第一个子组件的实例对象,通过他去获取这个组件的组件名
- 通过当前组件名去匹配原来 include 和 exclude,判断当前组件是否需要缓存,不需要 缓存,直接返回当前组件的实例vNode
- 需要缓存,判断他当前是否在缓存数组里面: 存在,则将他原来位置上的 key 给移除,同时将这个组件的 key 放到数组最后面 (LRU) 不存在,将组件 key 放入数组,然后判断当前 key数组是否超过 max 所设置的范围, 超过,那么削减未使用时间最长的一个组件的 key
- 最后将这个组件的 keepAlive 设置为 true
(3)keep-alive 本身的创建过程和 patch 过程
缓存渲染的时候,会根据 vnode.componentInstance(首次渲染 vnode.componentInstance 为 undefined) 和 keepAlive 属性判断不会执行组件的 created、mounted 等钩子函数,而是对缓存的组件执行 patch 过程∶ 直接把缓存的 DOM 对象直接插入到目标元素中,完成了数据更新的情况下的渲染过程。
首次渲染
- 组件的首次渲染∶判断组件的 abstract 属性,才往父组件里面挂载 DOM
js
// corerinstance/lifecwcle
function initLifecycle (vm: Component) {
const options = vm.soptions
// locate first non-abstract parent
let parent = options. parent
if (parent && !options.abstract) { // 判断组件的abstract属性,才往父组件里面挂DOMwhile while(parent.soptions.abstract 8& parent.$parent) {
parent = parent.Sparent
parent.$children.push(vm)
vm. Sparent = parent
um.sroot = parent ? parent.$root : vm
vm.$children = []
vm.$refs = f
vm. watcher = null
vm. inactive = null
vm._directInactive = false
vm. ismounted = false
vm. isDestroyed = false
vm. isBeingDestroyed = false
}
- 判断当前 keepAlive 和 componentInstance 是否存在来判断是否要执行组件 prepatch 还是执行创建 componentlnstance
js
// core/vdom/create-component
init (wnode: vNodewithData, hydrating: boolean): ?boolean (
if (
vnode.componentinstance &8.
!vnode. componentinstance. isDestroyed &8
vnode.data.keepAlive
) {// componentinstance在初次是undefined!!!
// kept-alive components, treat as a patch
const mountedNode: any = ynode // work around flow
componentvNodeHooks,prepatch(mountedNode,mountedNode) // prepatch面效执行的是组件更
} else{
const child = wode.componentinstance = createComponentinstanceForVnode(whode .
activeinstance
child.smount(hydrating ? vnode.elm : undefined, hydrating)
}
}
prepatch 操作就不会在执行组件的 mounted 和 created 生命周期函数,而是直接将 DOM 插入
(4)LRU (least recently used)缓存策略
LRU 缓存策略∶ 从内存中找出最久未使用的数据并置换新的数据。
LRU(Least rencently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想 是 "如果数据最近被访问过,那么将来被访问的几率也更高" 最常见的实现是使用一个链 表保存缓存数据,详细算法实现如下∶
- 新数据插入到链表头部
- 每当缓存命中(即缓存数据被访问),则将数据移到链表头部
- 链表满的时候,将链表尾部的数据丢弃。
18. $nextTick 原理及作用
Vue 的 nextTick 其本质是对 JavaScript 执行原理 EventLoop 的一种应用。
nextTick 的核心是利用了如 Promise 、MutationObserver、setImmediate、setTimeout的 原生 JavaScript 方法来模拟对应的微/宏任务的实现,本质是为了利用 JavaScript 的这些异 步回调任务队列来实现 Vue 框架中自己的异步回调队列。
nextTick 不仅是 Vue 内部的异步队列的调用方法,同时也允许开发者在实际项目中使用这 个方法来满足实际应用中对 DOM 更新数据时机的后续逻辑处理nextTick 是典型的将底层 JavaScript 执行原理应用到具体案例中的示例,引入异步更新队 列机制的原因∶
- 如果是同步更新,则多次对一个或多个属性赋值,会频繁触发 UI/DOM 的渲染,可以 减少一些无用渲染
- 同时由于 VirtualDOM 的引入,每一次状态发生变化后,状态变化的信号会发送给组 件,组件内部使用 VirtualDOM 进行计算得出需要更新的具体的 DOM 节点,然后对 DOM 进行更新操作,每次更新状态后的渲染过程需要更多的计算,而这种无用功也将 浪费更多的性能,所以异步渲染变得更加至关重要
Vue采用了数据驱动视图的思想,但是在一些情况下,仍然需要操作DOM。有时候,可能 遇到这样的情况,DOM1的数据发生了变化,而DOM2需要从DOM1中获取数据,那这时就 会发现DOM2的视图并没有更新,这时就需要用到了 nextTick 了。
由于Vue的DOM操作是异步的,所以,在上面的情况中,就要将DOM2获取数据的操作写 在 $nextTick 中。
所以,在以下情况下,会用到nextTick:
- 在数据变化后执行的某个操作,而这个操作需要使用随数据变化而变化的DOM结构的 时候,这个操作就需要方法在 nextTick() 的回调函数中。
- 在vue生命周期中,如果在created()钩子进行DOM操作,也一定要放在 nextTick() 的回调函数中。
因为在created()钩子函数中,页面的DOM还未渲染,这时候也没办法操作DOM,所以,此 时如果想要操作DOM,必须将操作的代码放在 nextTick() 的回调函数中。
19. Vue 中给 data 中的对象属性添加一个新的属性时会发生什么? 如何解决?
js
<template>
<div>
<u1>
<li v-for="value in obj" :key="value"> {{value}} </li>
</u1>
<button aclick="addobjb">aafp obj.be/button>
</div>
</template>
<script>
export default {
data () {
return {
obj: {
a: obj.a
}
}
},
methods: {
addobjB () {
this.obj.b = "obj.b"
console.log(this.obj)
}
}
}
</script>
点击 button 会发现,obj.b 已经成功添加,但是视图并未刷新。这是因为在Vue实例创建 时,obj.b并未声明,因此就没有被Vue转换为响应式的属性,自然就不会触发视图的更 新,这时就需要使用Vue的全局 api $set():
js
addobis () {
this.$set(this.obj,"b ,"obj.b')
console.log(this.obi)
}
$set()方法相当于手动的去把obj.b处理成一个响应式的属性,此时视图也会跟着改变了。
20. Vue中封装的数组方法有哪些,其如何实现页面更新
在Vue中,对响应式处理利用的是Object.defineProperty对数据进行拦截,而这个方 法并不能监听到数组内部变化,数组长度变化,数组的截取变化等,所以需要对这些 操作进行hack,让Vue能监听到其中的变化。
那Vue是如何实现让这些数组方法实现元素的实时更新的呢,下面是Vue中对这些方法的封 装:
js
// 存缴组原型
const arrayProto = Array.prototype;
// 实现 arrayMethods. proto=== Array.prototype
export const arrayMethods = objectcreate(arrayProto);//要进行功能拓展的方法
const methodsToPatch = [
push"
pop
"shift"
"unshift"
"splice"
"sort"
"reverse"
]
/*
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function(method) {
// 爆存原生致组方法
const original = arrayProto[method];
def(arrayethods, method, function mutator(...args) {
// 执行并规存原生缴组功能
const result = original.apply(this , args);
// 响应式处理
const ob = this._ob_;
let inserted;
switch (method) {
// push、unshit会新增索引,所以要手动observer
case "push":
case "unshift":
inserted = args;
break;
// splice方法,如果传入了第三个参效,也会有案引加入,也要手动observere
case "splice":
inserted = argsslice(2);
break
}
if (inserted) ob.observeArray(inserted);
// 获插入的值,并设管响应式监听
//notify change
ob.dep.notify();// 通知依赖更新
//返回原生数组方法的执行结果
return result;
})
})
简单来说就是,重写了数组中的那些原生方法,首先获取到这个数组的__ob__,也就是它 的Observer对象,如果有新的值,就调用observeArray继续对新的值观察变化(也就是通 过 target__proto__ == arrayMethods 来改变了数组实例的型),然后手动调用 notify,通知渲染watcher,执行update。
21. Vue 单页应用与多页应用的区别
概念:
- SPA单页面应用(SinglePage Web Application),指只有一个主页面的应用,一开始 只需要加载一次js、css等相关资源。所有内容都包含在主页面,对每一个功能模块组 件化。单页应用跳转,就是切换相关组件,仅仅刷新局部资源。
- MPA多页面应用 (MultiPage Application),指有多个独立页面的应用,每个页面必 须重复加载js、css等相关资源。多页应用跳转,需要整页资源刷新。
区别:
22. Vue template 到 render 的过程
vue的模版编译过程主要如下:template -> ast -> render函数
vue 在模版编译版本的码中会执行 compileToFunctions 将template转化为render函数:
js
// 将模板编评为render函效
const { render, staticRenderFns } = compileToFunctions(templateoptions//当略》, this)
CompileToFunctions中的主要逻辑如下∶
(1)调用parse方法将template转化为ast(抽象语法树)
js
constast = parse(template.trim(), options)
- parse的目标:把tamplate转换为AST树,它是一种用 JavaScript对象的形式来描述整 个模板。
- 解析过程:利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时 候都会分别执行对应的 回调函数,来达到构造AST树的目的。
AST元素节点总共三种类型:type为1表示普通元素、2为表达式、3为纯文本
(2)对静态节点做优化
js
optimize(ast,options)
这个过程主要分析出哪些是静态节点,给其打一个标记,为后续更新渲染可以直接跳过静 态节点做优化
深度遍历AST,查看每个子树的节点元素是否为静态节点或者静态节点根。如果为静态节 点,他们生成的DOM永远不会改变,这对运行时模板更新起到了极大的优化作用。
(3)生成代码
js
const code = generate(ast, options)
generate将ast抽象语法树编译成 render字符串并将静态部分放到 staticRenderFns 中,最 后通过 new Function( render ) 生成render函数。
23. Vue data 中某一个属性的值发生改变后,视图会立即同步执行 重新渲染吗?
不会立即同步执行重新渲染。Vue 实现响应式并不是数据发生变化之后 DOM 立即变化, 而是按一定的策略进行 DOM 的更新。Vue 在更新 DOM 时是异步执行的。只要侦听到数 据变化, Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对 于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环tick中,Vue 刷新队列并执行实际(已去重的)工作。
24. 简述 mixin、extends 的覆盖逻辑
(1)mixin 和 extends
mixin 和 extends均是用于合并、拓展组件的,两者均通过 mergeOptions 方法实现合并。
- mixins 接收一个混入对象的数组,其中混入对象可以像正常的实例对象一样包含实例 选项,这些选项会被合并到最终的选项中。Mixin 钩子按照传入顺序依次调用,并在调 用组件自身的钩子之前被调用。
- extends 主要是为了便于扩展单文件组件,接收一个对象或构造函数。
(2)mergeOptions 的执行过程
- 规范化选项(normalizeProps、normalizelnject、normalizeDirectives)
- 对未合并的选项,进行判断
js
if(!child._base) {
if(child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if(child.mixins) {
for(let i = 0, l = child.mixins.length; i < l; i++){
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
- 合并处理。根据一个通用 Vue 实例所包含的选项进行分类逐一判断合并,如 props、 data、 methods、watch、computed、生命周期等,将合并结果存储在新定义的 options 对象里。
- 返回合并结果 options。
25. 描述下Vue自定义指令
在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普 通 DOM 元素进行底层操作,这时候就会用到自定义指令。
一般需要对DOM元素进行底层操作时使用,尽量只用来操作 DOM展示,不修改内部的 值。当使用自定义指令直接修改 value 值时绑定v-model的值也不会同步更新;如必须修改 可以在自定义指令中使用keydown事件,在vue组件中使用 change事件,回调中修改vue数 据
(1)自定义指令基本内容
- 全局定义: Vue.directive("focus",{})
- 局部定义: directives:{focus:{}}
- 钩子函数:指令定义对象提供钩子函数
- bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化 设置。
- inSerted:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文 档中)。
- update:所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前调 用。指令的值可能发生了改变,也可能没有。但是可以通过比较更新前后的值来忽略不必 要的模板更新。
- ComponentUpdate:指令所在组件的 VNode及其子VNode全部更新后调用。
- unbind:只调用一次,指令与元素解绑时调用。
- 钩子函数参数
- el:绑定元素
- bing: 指令核心对象,描述指令全部信息属性
- name
- value
- oldValue
- expression
- arg
- modifers
- vnode 虚拟节点
- oldVnode:上一个虚拟节点(更新钩子函数中才有用)
(2)使用场景
- 普通DOM元素进行底层操作的时候,可以使用自定义指令
- 自定义指令是用来操作DOM的。尽管Vue推崇数据驱动视图的理念,但并非所有情况 都适合数据驱动。自定义指令就是一种有效的补充和扩展,不仅可用于定义任何的 DOM操作,并且是可复用的。
(3)使用案例
初级应用:
- 鼠标聚焦
- 下拉菜单
- 相对时间转换
- 滚动动画 高级应用:
- 自定义指令实现图片懒加载
- 自定义指令集成第三方插件
26. 子组件可以直接改变父组件的数据吗?
子组件不可以直接改变父组件的数据。这样做主要是为了维护父子组件的单向数据流。每 次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。如果这样做了,Vue 会在浏览器的控制台中发出警告。
Vue提倡单向数据流,即父级 props 的更新会流向子组件,但是反过来则不行。这是为了 防止意外的改变父组件状态,使得应用的数据流变得难以理解,导致数据流混乱。如果破 坏了单向数据流,当应用复杂时,debug 的成本会非常高。
只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改。
27. Vue是如何收集依赖的?
在初始化 Vue 的每个组件时,会对组件的 data 进行初始化,就会将由普通对象变成响应 式对象,在这个过程中便会进行依赖收集的相关逻辑,如下所示∶
js
function defieneReactive (obj, key, val){
const dep = new Dep();
...
Object.defineProperty(obj, key, {
...
get: function reactiveGetter () {
if(Dep.target){
dep.depend();
...
}
return val
}
...
})
}
以上只保留了关键代码,主要就是 const dep = new Dep() 实例化一个 Dep 的实例, 然后在 get 函数中通过 dep.depend() 进行依赖收集。
(1)Dep
Dep是整个依赖收集的核心,其关键代码如下:
js
class Dep {
static target;
subs;
constructor () {
...
this.subs = [];
}
addSub (sub) {
this.subs.push(sub)
}
removeSub (sub) {
remove(this.sub, sub)
}
depend () {
if(Dep.target){
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subds.slice();
for(let i = 0;i < subs.length; i++){
subs[i].update()
}
}
}
Dep 是一个 class ,其中有一个关 键的静态属性 static,它指向了一个全局唯一 Watcher, 保证了同一时间全局只有一个 watcher 被计算,另一个属性 subs 则是一个 Watcher 的数 组,所以 Dep 实际上就是对 Watcher 的管理,再看看 Watcher 的相关代码∶
(2)Watcher
js
class Watcher {
getter;
...
constructor (vm, expression){
...
this.getter = expression;
this.get();
}
get () {
pushTarget(this);
value = this.getter.call(vm, vm)
...
return value
}
addDep (dep){
...
dep.addSub(this)
}
...
}
function pushTarget (_target) {
Dep.target = _target
}
Watcher 是一个 class,它定义了一些方法,其中和依赖收集相关的主要有 get、addDep 等。
(3)过程
在实例化 Vue 时,依赖收集的相关过程如下∶
- 初 始 化 状 态 initState , 这 中 间 便 会 通 过 defineReactive 将数据变成响应式对象,其 中的 getter 部分便是用来依赖收集的。
- 初始化最终会走 mount 过程,其中会实例化 Watcher ,进入 Watcher 中,便会执行 this.get() 方法,
js
updateComponent = () => {
vm._update(vm._render())
}
new Watcher(vm, updateComponent)
get 方法中的 pushTarget 实际上就是把 Dep.target 赋值为当前的 watcher。
this.getter.call(vm,vm),这里的 getter 会执行 vm._render() 方法,在这个过程中便会 触发数据对象的 getter。那么每个对象值的 getter 都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 方法,也就会执行 Dep.target.addDep(this)。刚才 Dep.target 已经被 赋值为 watcher,于是便会执行 addDep 方法,然后走到 dep.addSub() 方法,便将当前的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目的是为后续数据变化时候能通知到 哪些 subs 做准备。所以在 vm._render() 过程中,会触发所有数据的 getter,这样便已经完 成了一个依赖收集的过程。
28. 对 React 和 Vue 的理解,它们的异同
相似之处:
- 都将注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关的库;
- 都有自己的构建工具,能让你得到一个根据最佳实践设置的项目模板;
- 都使用了Virtual DOM(虚拟DOM)提高重绘性能;
- 都有props的概念,允许组件间的数据传递;
- 都鼓励组件化应用,将应用分拆成一个个功能明确的模块,提高复用性。
不同之处 :
1)数据流 Vue默认支持数据双向绑定,而React一直提倡单向数据流
2)虚拟DOM Vue2.x开始引入"Virtual DOM",消除了和React在这方面的差异,但是在具体的细节还是有 各自的特点。
- Vue宣称可以更快地计算出Virtual DOM的差异,这是由于它在渲染过程中,会跟踪每 一个组件的依赖关系,不需要重新渲染整个组件树。
- 对于React而言,每当应用的状态被改变时,全部子组件都会重新渲染。当然,这可以 通过 PureComponent/shouldComponentUpdate这个生命周期方法来进行控制,但 Vue将此视为默认的优化。
3)组件化 React与Vue最大的不同是模板的编写。
- Vue鼓励写近似常规HTML的模板。写起来很接近标准 HTML元素,只是多了一些属 性。
- React推荐你所有的模板通用JavaScript的语法扩展------JSX书写。
具体来讲:React中render函数是支持闭包特性的,所以import的组件在render中可以直接 调用。但是在Vue中,由于模板中使用的数据都必须挂在 this 上进行一次中转,所以 import 一个组件完了之后,还需要在 components 中再声明下。
4)监听数据变化的实现原理不同
- Vue 通过 getter/setter 以及一些函数的劫持,能精确知道数据变化,不需要特别的优 化就能达到很好的性能
- React 默认是通过比较引用的方式进行的,如果不优化 (PureComponent/shouldComponentUpdate)可能导致大量不必要的vDOM的重新 渲染。这是因为 Vue 使用的是可变数据,而React更强调数据的不可变。
5)高阶组件 react可以通过高阶组件(HOC)来扩展,而Vue需要通过mixins来扩展。
高阶组件就是高阶函数,而React的组件本身就是纯粹的函数,所以高阶函数对React来说 易如反掌。相反Vue.js使用HTML模板创建视图组件,这时模板无法有效的编译,因此Vue 不能采用HOC来实现。
6)构建工具 两者都有自己的构建工具:
- React ==> Create React APP
- Vue ==> vue-cli
7)跨平台
- React ==> React Native
- Vue ==> Weex
29. Vue的优点
- 轻量级框架:只关注视图层,是一个构建数据的视图集合,大小只有几十 kb ;
- 简单易学:国人开发,中文文档,不存在语言障碍 ,易于理解和学习;
- 双向数据绑定:保留了 angular 的特点,在数据操作方面更为简单;
- 组件化:保留了 react 的优点,实现了 html 的封装和重用,在构建单页面应用 方面有着独特的优势;
- 视图,数据,结构分离:使数据的更改更为简单,不需要进行逻辑代码的修改,只需 要操作数据就能完成相关操作;
- 虚拟DOM: dom 操作是非常耗费性能的,不再使用原生的 dom 操作节点,极大解 放 dom 操作,但具体操作的还是 dom 不过是换了另一种方式;
- 运行速度更快:相比较于 react 而言,同样是操作虚拟 dom,就性能而言, vue 存在很大的优势。
30. assets和static的区别
相同点: assets 和 static 两个都是存放静态资源文件。项目中所需要的资源文件 图片,字体图标,样式文件等都可以放在这两个文件下,这是相同点
不相同点: assets 中存放的静态资源文件在项目打包时,也就是运行 npm run buil d 时会将 assets 中放置的静态资源文件进行打包上传,所谓打包简单点可以理解为压 缩体积,代码格式化。而压缩后的静态资源文件最终也都会放置在 static 文件中跟着 index.html 一同上传至服务器。 static 中放置的静态资源文件就不会要走打包压缩 格式化等流程,而是直接进入打包好的目录,直接上传至服务器。因为避免了压缩直接进 行上传,在打包时会提高一定的效率,但是 static 中的资源文件由于没有进行压缩等 操作,所以文件的体积也就相对于 assets 中打包后的文件提交较大点。在服务器中就 会占据更大的空间。
建议: 将项目中 template 需要的样式文件js文件等都可以放置在 assets 中,走打包 这一流程。减少体积。而项目中引入的第三方的资源文件如 iconfoont.css 等文件可以 放置在 static 中,因为这些引入的第三方文件已经经过处理,不再需要处理,直接上 传。
31. delete和Vue.delete删除数组的区别
- delete 只是被删除的元素变成了 empty/undefined 其他的元素的键值还是不 变。
- Vue.delete 直接删除了数组 改变了数组的键值。
32. vue如何监听对象或者数组某个属性的变化
当在项目中直接设置数组的某一项的值,或者直接设置对象的某个属性值,这个时候,你 会发现页面并没有更新。这是因为Object.defineProperty()限制,监听不到变化。
解决方式:
- this.$set(你要改变的数组/对象,你要改变的位置/key,你要改成什么value)
js
this.$set(this.arr, 0, "OBKoro1"); // 改变数组
this.$set(this.obj, "c", "OBKoro1"); // 改变对象
- 调用以下几个数组的方法
js
splice()、 push()、pop()、shift()、unshift()、sort()、reverse()
vue源码里缓存了array的原型链,然后重写了这几个方法,触发这几个方法的时候会 observer数据,意思是使用这些方法不用再进行额外的操作,视图自动进行更新。 推荐使 用splice方法会比较好自定义,因为splice可以在数组的任何位置进行删除/添加操作
vm. $set 的实现原理是:
- 如果目标是数组,直接使用数组的 splice 方法触发相应式;
- 如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进 行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)
33. 什么是 mixin ?
- Mixin 使我们能够为 Vue 组件编写可插拔和可重用的功能。
- 如果希望在多个组件之间重用一组组件选项,例如生命周期 hook、 方法等,则可以 将其编写为 mixin,并在组件中简单的引用它。
- 然后将 mixin 的内容合并到组件中。如果你要在 mixin 中定义生命周期 hook,那么它 在执行时将优化于组件自已的 hook。
34. Vue模版编译原理
vue中的模板template无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的 HTML语法,所有需要将template转化成一个JavaScript函数,这样浏览器就可以执行这一 个函数并渲染出对应的HTML元素,就可以让视图跑起来了,这一个转化的过程,就成为模 板编译。模板编译又分三个阶段,解析parse,优化optimize,生成generate,最终生成可 执行函数render。
- 解析阶段:使用大量的正则表达式对template字符串进行解析,将标签、指令、属性 等转化为抽象语法树AST。
- 优化阶段:遍历AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候 进行diff比较时,直接跳过这一些静态节点,优化runtime的性能。
- 生成阶段:将最终的AST转化为render函数字符串。
35. 对SSR的理解
SSR也就是服务端渲染,也就是将Vue在客户端把标签渲染成HTML的工作放在服务端完成,然后再把html直接返回给客户端
SSR的优势:
- 更好的SEO
- 首屏加载速度更快
SSR的缺点:
- 开发条件会受到限制,服务器端渲染只支持beforeCreate和created两个钩子;
- 当需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于Node.js的运行环境;
- 更多的服务端负载。
36. Vue的性能优化有哪些
待更新。。。