Vue 修饰符有哪些
vue 中修饰符分为以下五种
- 表单修饰符
- 事件修饰符
- 鼠标按键修饰符
- 键值修饰符
v-bind
修饰符
1. 表单修饰符
在咱们填写表单的时候用得最多的是 input
标签,指令用得最多的是v-model
对于表单的修饰符有如下:
lazy
在咱们填完信息,光标来到标签的时候,才会将值赋予给 value
,也就是在change
事件之后再进行信息同步
<input type="text" v-model.lazy="value">
<p>{{value}}</p>
trim
主动过滤用户输出的首空格字符,而两头的空格不会过滤
<input type="text" v-model.trim="value">
number
主动将用户的输出值转为数值类型,但如果这个值无奈被 parseFloat
解析,则会返回原来的值
<input v-model.number="age" type="number">
2. 事件修饰符
事件修饰符是对事件捕捉以及指标进行了解决,有如下修饰符
.stop
阻止了事件冒泡,相当于调用了event.stopPropagation
办法
<div @click="shout(2)">
<button @click.stop="shout(1)">ok</button>
</div>
// 只输入 1
.prevent
阻止了事件的默认行为,相当于调用了event.preventDefault
办法
<form v-on:submit.prevent="onSubmit"></form>
.capture
应用事件捕捉模式,使事件触发从蕴含这个元素的顶层开始往下触发
<div @click.capture="shout(1)">
obj1
<div @click.capture="shout(2)">
obj2
<div @click="shout(3)">
obj3
<div @click="shout(4)">
obj4
</div>
</div>
</div>
</div>
// 输入构造: 1 2 4 3
.self
只当在event.target
是以后元素本身时触发处理函数
<div v-on:click.self="doThat">...</div>
应用修饰符时,程序很重要;相应的代码会以同样的程序产生。因而,用
v-on:click.prevent.self
会阻止所有的点击,而v-on:click.self.prevent
只会阻止对元素本身的点击
.once
绑定了事件当前只能触发一次,第二次就不会触发
<button @click.once="shout(1)">ok</button>
.passive
通知浏览器你不想阻止事件的默认行为
在挪动端,当咱们在监听元素滚动事件的时候,会始终触发 onscroll
事件会让咱们的网页变卡,因而咱们应用这个修饰符的时候,相当于给 onscroll
事件整了一个 .lazy
修饰符
<!-- 滚动事件的默认行为 (即滚动行为) 将会立刻触发 -->
<!-- 而不会期待 `onScroll` 实现 -->
<!-- 这其中蕴含 `event.preventDefault()` 的状况 -->
<div v-on:scroll.passive="onScroll">...</div>
- 不要把
.passive
和.prevent
一起应用, 因为.prevent
将会被疏忽,同时浏览器可能会向你展现一个正告。passive
会通知浏览器你不想阻止事件的默认行为
native
让组件变成像html
内置标签那样监听根元素的原生事件,否则组件上应用v-on
只会监听自定义事件
<my-component v-on:click.native="doSomething"></my-component>
<!-- 应用.native 修饰符来操作一般 HTML 标签是会令事件生效的 -->
3. 鼠标按钮修饰符
鼠标按钮修饰符针对的就是左键、右键、中键点击,有如下:
.left
左键点击.right
右键点击.middle
中键点击
<button @click.left="shout(1)">ok</button>
<button @click.right="shout(1)">ok</button>
<button @click.middle="shout(1)">ok</button>
4. 键盘事件的修饰符
键盘修饰符是用来润饰键盘事件(onkeyup
,onkeydown
)的,有如下:
keyCode
存在很多,但 vue 为咱们提供了别名,分为以下两种:
- 一般键(
enter
、tab
、delete
、space
、esc
、up
、down
、left
、right
…) - 零碎润饰键(
ctrl
、alt
、meta
、shift
…)
<!-- 只有按键为 keyCode 的时候才触发 -->
<input type="text" @keyup.keyCode="shout()">
还能够通过以下形式自定义一些全局的键盘码别名
Vue.config.keyCodes.f2 = 113
5. v-bind 修饰符
v-bind
修饰符次要是为属性进行操作,用来别离有如下:
- async 能对
props
进行一个双向绑定
// 父组件
<comp :myMessage.sync="bar"></comp>
// 子组件
this.$emit('update:myMessage',params);
以上这种办法相当于以下的简写
// 父亲组件
<comp :myMessage="bar" @update:myMessage="func"></comp>
func(e){this.bar = e;}
// 子组件 js
func2(){this.$emit('update:myMessage',params);
}
应用 async
须要留神以下两点:
- 应用
sync
的时候,子组件传递的事件名格局必须为update:value
,其中value
必须与子组件中props
中申明的名称完全一致 - 留神带有
.sync
修饰符的v-bind
不能和表达式一起应用 - prop 设置自定义标签属性,防止裸露数据,避免净化 HTML 构造
<input id="uid" title="title1" value="1" :index.prop="index">
- camel 将命名变为驼峰命名法,如将
view-Box
属性名转换为viewBox
<svg :viewBox="viewBox"></svg>
利用场景
依据每一个修饰符的性能,咱们能够失去以下修饰符的利用场景:
.stop
:阻止事件冒泡.native
:绑定原生事件.once
:事件只执行一次.self
:将事件绑定在本身身上,相当于阻止事件冒泡.prevent
:阻止默认事件.caption
:用于事件捕捉.once
:只触发一次.keyCode
:监听特定键盘按下.right
:右键
生命周期钩子是如何实现的
Vue 的生命周期钩子外围实现是利用公布订阅模式先把用户传入的的生命周期钩子订阅好(外部采纳数组的形式存储)而后在创立组件实例的过程中会一次执行对应的钩子办法(公布)
相干代码如下
export function callHook(vm, hook) {
// 顺次执行生命周期对应的办法
const handlers = vm.$options[hook];
if (handlers) {for (let i = 0; i < handlers.length; i++) {handlers[i].call(vm); // 生命周期外面的 this 指向以后实例
}
}
}
// 调用的时候
Vue.prototype._init = function (options) {
const vm = this;
vm.$options = mergeOptions(vm.constructor.options, options);
callHook(vm, "beforeCreate"); // 初始化数据之前
// 初始化状态
initState(vm);
callHook(vm, "created"); // 初始化数据之后
if (vm.$options.el) {vm.$mount(vm.$options.el);
}
};
说一下 Vue 的生命周期
Vue 实例有⼀个残缺的⽣命周期,也就是从开始创立、初始化数据、编译模版、挂载 Dom -> 渲染、更新 -> 渲染、卸载 等⼀系列过程,称这是 Vue 的⽣命周期。
- beforeCreate(创立前):数据观测和初始化事件还未开始,此时 data 的响应式追踪、event/watcher 都还没有被设置,也就是说不能拜访到 data、computed、watch、methods 上的办法和数据。
- created(创立后):实例创立实现,实例上配置的 options 包含 data、computed、watch、methods 等都配置实现,然而此时渲染得节点还未挂载到 DOM,所以不能拜访到
$el
属性。 - beforeMount(挂载前):在挂载开始之前被调用,相干的 render 函数首次被调用。实例已实现以下的配置:编译模板,把 data 外面的数据和模板生成 html。此时还没有挂载 html 到页面上。
- mounted(挂载后):在 el 被新创建的 vm.$el 替换,并挂载到实例下来之后调用。实例已实现以下的配置:用下面编译好的 html 内容替换 el 属性指向的 DOM 对象。实现模板中的 html 渲染到 html 页面中。此过程中进行 ajax 交互。
- beforeUpdate(更新前):响应式数据更新时调用,此时尽管响应式数据更新了,然而对应的实在 DOM 还没有被渲染。
- updated(更新后):在因为数据更改导致的虚构 DOM 从新渲染和打补丁之后调用。此时 DOM 曾经依据响应式数据的变动更新了。调用时,组件 DOM 曾经更新,所以能够执行依赖于 DOM 的操作。然而在大多数状况下,应该防止在此期间更改状态,因为这可能会导致更新有限循环。该钩子在服务器端渲染期间不被调用。
- beforeDestroy(销毁前):实例销毁之前调用。这一步,实例依然齐全可用,
this
仍能获取到实例。 - destroyed(销毁后):实例销毁后调用,调用后,Vue 实例批示的所有货色都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务端渲染期间不被调用。
另外还有 keep-alive
独有的生命周期,别离为 activated
和 deactivated
。用 keep-alive
包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated
钩子函数,命中缓存渲染后会执行 activated
钩子函数。
Vue 组件通信有哪几种形式
- props 和 $emit 父组件向子组件传递数据是通过 prop 传递的,子组件传递数据给父组件是通过 $emit 触发事件来做到的
- $parent,$children 获取以后组件的父组件和以后组件的子组件
- $attrs 和 $listeners A->B->C。Vue 2.4 开始提供了 $attrs 和 $listeners 来解决这个问题
- 父组件中通过 provide 来提供变量,而后在子组件中通过 inject 来注入变量。(官网不举荐在理论业务中应用,然而写组件库时很罕用)
- $refs 获取组件实例
- envetBus 兄弟组件数据传递 这种状况下能够应用事件总线的形式
- vuex 状态治理
什么是 mixin?
- Mixin 使咱们可能为 Vue 组件编写可插拔和可重用的性能。
- 如果心愿在多个组件之间重用一组组件选项,例如生命周期 hook、办法等,则能够将其编写为 mixin,并在组件中简略的援用它。
- 而后将 mixin 的内容合并到组件中。如果你要在 mixin 中定义生命周期 hook,那么它在执行时将优化于组件自已的 hook。
理解 nextTick 吗?
异步办法,异步渲染最初一步,与 JS 事件循环分割严密。次要应用了宏工作微工作(setTimeout
、promise
那些),定义了一个异步办法,屡次调用 nextTick
会将办法存入队列,通过异步办法清空以后队列。
Vue 生命周期钩子是如何实现的
vue
的生命周期钩子就是回调函数而已,当创立组件实例的过程中会调用对应的钩子办法- 外部会对钩子函数进行解决,将钩子函数保护成数组的模式
Vue
的生命周期钩子外围实现是利用公布订阅模式先把用户传入的的生命周期钩子订阅好(外部采纳数组的形式存储)而后在创立组件实例的过程中会一次执行对应的钩子办法(公布)
<script>
// Vue.options 中会寄存所有全局属性
// 会用本身的 + Vue.options 中的属性进行合并
// Vue.mixin({// beforeCreate() {// console.log('before 0')
// },
// })
debugger;
const vm = new Vue({
el: '#app',
beforeCreate: [function() {console.log('before 1')
},
function() {console.log('before 2')
}
]
});
console.log(vm);
</script>
相干代码如下
export function callHook(vm, hook) {
// 顺次执行生命周期对应的办法
const handlers = vm.$options[hook];
if (handlers) {for (let i = 0; i < handlers.length; i++) {handlers[i].call(vm); // 生命周期外面的 this 指向以后实例
}
}
}
// 调用的时候
Vue.prototype._init = function (options) {
const vm = this;
vm.$options = mergeOptions(vm.constructor.options, options);
callHook(vm, "beforeCreate"); // 初始化数据之前
// 初始化状态
initState(vm);
callHook(vm, "created"); // 初始化数据之后
if (vm.$options.el) {vm.$mount(vm.$options.el);
}
};
// 销毁实例实现
Vue.prototype.$destory = function() {
// 触发钩子
callHook(vm, 'beforeDestory')
// 本身及子节点
remove()
// 删除依赖
watcher.teardown()
// 删除监听
vm.$off()
// 触发钩子
callHook(vm, 'destoryed')
}
原理流程图
v-for 为什么要加 key
如果不应用 key,Vue 会应用一种最大限度缩小动静元素并且尽可能的尝试就地批改 / 复用雷同类型元素的算法。key 是为 Vue 中 vnode 的惟一标记,通过这个 key,咱们的 diff 操作能够更精确、更疾速
更精确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 比照中能够防止就地复用的状况。所以会更加精确。
更疾速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历形式更快
Vue.js 的 template 编译
简而言之,就是先转化成 AST 树,再失去的 render 函数返回 VNode(Vue 的虚构 DOM 节点),具体步骤如下:
首先,通过 compile 编译器把 template 编译成 AST 语法树(abstract syntax tree 即 源代码的形象语法结构的树状表现形式),compile 是 createCompiler 的返回值,createCompiler 是用以创立编译器的。另外 compile 还负责合并 option。
而后,AST 会通过 generate(将 AST 语法树转化成 render funtion 字符串的过程)失去 render 函数,render 的返回值是 VNode,VNode 是 Vue 的虚构 DOM 节点,外面有(标签名、子节点、文本等等)
参考:前端 vue 面试题具体解答
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
有一组默认指令,比方v-model
或v-for
,同时Vue
也容许用户注册自定义指令来扩大 Vue 能力- 自定义指令次要实现一些可复用低层级
DOM
操作 - 应用自定义指令分为定义、注册和应用三步:
- 定义自定义指令有两种形式:对象和函数模式,前者相似组件定义,有各种生命周期;后者只会在
mounte
d 和updated
时执行
- 注册自定义指令相似组件,能够应用
app.directive()
全局注册,应用{directives:{xxx}}
部分注册 - 应用时在注册名称前加上
v-
即可,比方v-focus
- 我在我的项目中罕用到一些自定义指令,例如:
- 复制粘贴
v-copy
- 长按
v-longpress
- 防抖
v-debounce
- 图片懒加载
v-lazy
- 按钮权限
v-premission
- 页面水印
v-waterMarker
- 拖拽指令
v-draggable
vue3
中指令定义产生了比拟大的变动,次要是钩子的名称放弃和组件统一,这样开发人员容易记忆,不易犯错。另外在v3.2
之后,能够在setup
中以一个小写v
结尾不便的定义自定义指令,更简略了
根本应用
当 Vue 中的外围内置指令不可能满足咱们的需要时,咱们能够定制自定义的指令用来满足开发的需要
咱们看到的 v-
结尾的行内属性,都是指令,不同的指令能够实现或实现不同的性能,对一般 DOM 元素进行底层操作,这时候就会用到自定义指令。除了外围性能默认内置的指令 (v-model
和 v-show
),Vue
也容许注册自定义指令
// 指令应用的几种形式:// 会实例化一个指令,但这个指令没有参数
`v-xxx`
// -- 将值传到指令中
`v-xxx="value"`
// -- 将字符串传入到指令中,如 `v-html="'<p> 内容 </p>'"`
`v-xxx="'string'"`
// -- 传参数(`arg`),如 `v-bind:class="className"`
`v-xxx:arg="value"`
// -- 应用修饰符(`modifier`)`v-xxx:arg.modifier="value"`
注册一个自定义指令有全局注册与部分注册
// 全局注册注册次要是用过 Vue.directive 办法进行注册
// Vue.directive 第一个参数是指令的名字(不须要写上 v - 前缀),第二个参数能够是对象数据,也能够是一个指令函数
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus() // 页面加载实现之后主动让输入框获取到焦点的小性能}
})
// 部分注册通过在组件 options 选项中设置 directive 属性
directives: {
focus: {
// 指令的定义
inserted: function (el) {el.focus() // 页面加载实现之后主动让输入框获取到焦点的小性能
}
}
}
// 而后你能够在模板中任何元素上应用新的 v-focus property,如下:<input v-focus />
钩子函数
bind
:只调用一次,指令第一次绑定到元素时调用。在这里能够进行一次性的初始化设置。inserted
:被绑定元素插入父节点时调用 (仅保障父节点存在,但不肯定已被插入文档中)。update
:被绑定于元素所在的模板更新时调用,而无论绑定值是否变动。通过比拟更新前后的绑定值,能够疏忽不必要的模板更新。componentUpdated
:被绑定元素所在模板实现一次更新周期时调用。unbind
:只调用一次,指令与元素解绑时调用。
所有的钩子函数的参数都有以下:
el
:指令所绑定的元素,能够用来间接操作 DOM-
binding
:一个对象,蕴含以下property
:name
:指令名,不包含v-
前缀。value
:指令的绑定值,例如:v-my-directive="1 + 1"
中,绑定值为2
。oldValue
:指令绑定的前一个值,仅在update
和componentUpdated
钩子中可用。无论值是否扭转都可用。expression
:字符串模式的指令表达式。例如v-my-directive="1 + 1"
中,表达式为"1 + 1"
。arg
:传给指令的参数,可选。例如v-my-directive:foo
中,参数为"foo"
。modifiers
:一个蕴含修饰符的对象。例如:v-my-directive.foo.bar
中,修饰符对象为{foo: true, bar: true}
vnode
:Vue
编译生成的虚构节点oldVnode
:上一个虚构节点,仅在update
和componentUpdated
钩子中可用
除了 el
之外,其它参数都应该是只读的,切勿进行批改。如果须要在钩子之间共享数据,倡议通过元素的 dataset
来进行
<div v-demo="{color:'white', text:'hello!'}"></div>
<script>
Vue.directive('demo', function (el, binding) {console.log(binding.value.color) // "white"
console.log(binding.value.text) // "hello!"
})
</script>
利用场景
应用自定义组件组件能够满足咱们日常一些场景,这里给出几个自定义组件的案例:
- 防抖
// 1. 设置 v -throttle 自定义指令
Vue.directive('throttle', {bind: (el, binding) => {
let throttleTime = binding.value; // 防抖工夫
if (!throttleTime) { // 用户若不设置防抖工夫,则默认 2s
throttleTime = 2000;
}
let cbFun;
el.addEventListener('click', event => {if (!cbFun) { // 第一次执行
cbFun = setTimeout(() => {cbFun = null;}, throttleTime);
} else {event && event.stopImmediatePropagation();
}
}, true);
},
});
// 2. 为 button 标签设置 v -throttle 自定义指令
<button @click="sayHello" v-throttle> 提交 </button>
- 图片懒加载
设置一个 v-lazy
自定义组件实现图片懒加载
const LazyLoad = {
// install 办法
install(Vue,options){
// 代替图片的 loading 图
let defaultSrc = options.default;
Vue.directive('lazy',{bind(el,binding){LazyLoad.init(el,binding.value,defaultSrc);
},
inserted(el){
// 兼容解决
if('InterpObserver' in window){LazyLoad.observe(el);
}else{LazyLoad.listenerScroll(el);
}
},
})
},
// 初始化
init(el,val,def){
// src 贮存实在 src
el.setAttribute('src',val);
// 设置 src 为 loading 图
el.setAttribute('src',def);
},
// 利用 InterpObserver 监听 el
observe(el){
let io = new InterpObserver(entries => {
let realSrc = el.dataset.src;
if(entries[0].isIntersecting){if(realSrc){
el.src = realSrc;
el.removeAttribute('src');
}
}
});
io.observe(el);
},
// 监听 scroll 事件
listenerScroll(el){let handler = LazyLoad.throttle(LazyLoad.load,300);
LazyLoad.load(el);
window.addEventListener('scroll',() => {handler(el);
});
},
// 加载实在图片
load(el){
let windowHeight = document.documentElement.clientHeight
let elTop = el.getBoundingClientRect().top;
let elBtm = el.getBoundingClientRect().bottom;
let realSrc = el.dataset.src;
if(elTop - windowHeight<0&&elBtm > 0){if(realSrc){
el.src = realSrc;
el.removeAttribute('src');
}
}
},
// 节流
throttle(fn,delay){
let timer;
let prevTime;
return function(...args){let currTime = Date.now();
let context = this;
if(!prevTime) prevTime = currTime;
clearTimeout(timer);
if(currTime - prevTime > delay){
prevTime = currTime;
fn.apply(context,args);
clearTimeout(timer);
return;
}
timer = setTimeout(function(){prevTime = Date.now();
timer = null;
fn.apply(context,args);
},delay);
}
}
}
export default LazyLoad;
- 一键 Copy 的性能
import {Message} from 'ant-design-vue';
const vCopy = { //
/*
bind 钩子函数,第一次绑定时调用,能够在这里做初始化设置
el: 作用的 dom 对象
value: 传给指令的值,也就是咱们要 copy 的值
*/
bind(el, { value}) {
el.$value = value; // 用一个全局属性来存传进来的值,因为这个值在别的钩子函数里还会用到
el.handler = () => {if (!el.$value) {
// 值为空的时候,给出提醒,我这里的提醒是用的 ant-design-vue 的提醒,你们随便
Message.warning('无复制内容');
return;
}
// 动态创建 textarea 标签
const textarea = document.createElement('textarea');
// 将该 textarea 设为 readonly 避免 iOS 下主动唤起键盘,同时将 textarea 移出可视区域
textarea.readOnly = 'readonly';
textarea.style.position = 'absolute';
textarea.style.left = '-9999px';
// 将要 copy 的值赋给 textarea 标签的 value 属性
textarea.value = el.$value;
// 将 textarea 插入到 body 中
document.body.appendChild(textarea);
// 选中值并复制
textarea.select();
// textarea.setSelectionRange(0, textarea.value.length);
const result = document.execCommand('Copy');
if (result) {Message.success('复制胜利');
}
document.body.removeChild(textarea);
};
// 绑定点击事件,就是所谓的一键 copy 啦
el.addEventListener('click', el.handler);
},
// 当传进来的值更新的时候触发
componentUpdated(el, { value}) {el.$value = value;},
// 指令与元素解绑的时候,移除事件绑定
unbind(el) {el.removeEventListener('click', el.handler);
},
};
export default vCopy;
- 拖拽
<div ref="a" id="bg" v-drag></div>
directives: {
drag: {bind() {},
inserted(el) {el.onmousedown = (e) => {
let x = e.clientX - el.offsetLeft;
let y = e.clientY - el.offsetTop;
document.onmousemove = (e) => {
let xx = e.clientX - x + "px";
let yy = e.clientY - y + "px";
el.style.left = xx;
el.style.top = yy;
};
el.onmouseup = (e) => {document.onmousemove = null;};
};
},
},
}
原理
- 指令实质上是装璜器,是
vue
对HTML
元素的扩大,给HTML
元素减少自定义性能。vue
编译DOM
时,会找到指令对象,执行指令的相干办法。 - 自定义指令有五个生命周期(也叫钩子函数),别离是
bind
、inserted
、update
、componentUpdated
、unbind
原理
- 在生成
ast
语法树时,遇到指令会给以后元素增加directives
属性 - 通过
genDirectives
生成指令代码 - 在
patch
前将指令的钩子提取到cbs
中, 在patch
过程中调用对应的钩子 - 当执行指令对应钩子函数时,调用对应指令定义的办法
Vue-router 除了 router-link 怎么实现跳转
申明式导航
<router-link to="/about">Go to About</router-link>
编程式导航
// literal string path
router.push('/users/1')
// object with path
router.push({path: '/users/1'})
// named route with params to let the router build the url
router.push({name: 'user', params: { username: 'test'} })
答复范例
vue-router
导航有两种形式:申明式导航和编程形式导航- 申明式导航形式应用
router-link
组件,增加to
属性导航;编程形式导航更加灵便,可传递调用router.push()
,并传递path
字符串或者RouteLocationRaw
对象,指定path
、name
、params
等信息 - 如果页面中简略示意跳转链接,应用
router-link
最快捷,会渲染一个 a 标签;如果页面是个简单的内容,比方商品信息,能够增加点击事件,应用编程式导航 - 实际上外部两者调用的导航函数是一样的
请阐明 Vue 中 key 的作用和原理,谈谈你对它的了解
key
是为Vue
中的VNode
标记的惟一id
,在patch
过程中通过key
能够判断两个虚构节点是否是雷同节点,通过这个key
,咱们的diff
操作能够更精确、更疾速diff
算法的过程中, 先会进行新旧节点的首尾穿插比照, 当无奈匹配的时候会用新节点的key
与旧节点进行比对, 而后检出差别- 尽量不要采纳索引作为
key
- 如果不加
key
, 那么vue
会抉择复用节点(Vue 的就地更新策略), 导致之前节点的状态被保留下来, 会产生一系列的bug
- 更精确:因为带
key
就不是就地复用了,在sameNode
函数a.key === b.key
比照中能够防止就地复用的状况。所以会更加精确。 - 更疾速 :
key
的唯一性能够被Map
数据结构充分利用,相比于遍历查找的工夫复杂度O(n)
,Map
的工夫复杂度仅仅为O(1)
,比遍历形式更快。
源码如下:
function createKeyToOldIdx (children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}
答复范例
剖析
这是一道特地常见的问题,次要考查大家对虚构 DOM
和patch
细节的把握水平,可能反映面试者了解档次
思路剖析:
- 给出论断,
key
的作用是用于优化patch
性能 key
的必要性- 理论应用形式
- 总结:可从源码层面形容一下
vue
如何判断两个节点是否雷同
答复范例:
key
的作用次要是为了更高效的更新虚构DOM
vue
在patch
过程中 判断两个节点是否是雷同节点是key
是一个必要条件 ,渲染一组列表时,key
往往是惟一标识,所以如果不定义key
的话,vue
只能认为比拟的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个patch
过程比拟低效,影响性能- 理论应用中在渲染一组列表时
key
必须设置,而且必须是惟一标识,应该防止应用数组索引作为key
,这可能导致一些荫蔽的bug
;vue
中在应用雷同标签元素过渡切换时,也会应用key
属性,其目标也是为了让vue
能够辨别它们,否则vue
只会替换其外部属性而不会触发过渡成果 - 从源码中能够晓得,
vue
判断两个节点是否雷同时次要判断两者的key
和标签类型 (如 div)
等,因而如果不设置key
,它的值就是undefined
,则可能永远认为这是两个雷同节点,只能去做更新操作,这造成了大量的dom
更新操作,显著是不可取的
如果不应用
key
,Vue
会应用一种最大限度缩小动静元素并且尽可能的尝试就地批改 / 复用雷同类型元素的算法。key
是为Vue
中vnode
的惟一标记,通过这个key
,咱们的diff
操作能够更精确、更疾速
diff 程能够概括为:
oldCh
和newCh
各有两个头尾的变量 StartIdx
和EndIdx
,它们的2
个变量互相比拟,一共有4
种比拟形式。如果4
种比拟都没匹配,如果设置了key
,就会用key
进行比拟,在比拟的过程中,变量会往两头靠,一旦StartIdx>EndIdx
表明oldCh
和newCh
至多有一个曾经遍历完了,就会完结比拟, 这四种比拟形式就是首
、尾
、旧尾新头
、旧头新尾
相干代码如下
// 判断两个 vnode 的标签和 key 是否雷同 如果雷同 就能够认为是同一节点就地复用
function isSameVnode(oldVnode, newVnode) {return oldVnode.tag === newVnode.tag && oldVnode.key === newVnode.key;}
// 依据 key 来创立老的儿子的 index 映射表 相似 {'a':0,'b':1} 代表 key 为 'a' 的节点在第一个地位 key 为 'b' 的节点在第二个地位
function makeIndexByKey(children) {let map = {};
children.forEach((item, index) => {map[item.key] = index;
});
return map;
}
// 生成的映射表
let map = makeIndexByKey(oldCh);
Vue 组件间通信有哪几种形式?
Vue 组件间通信是面试常考的知识点之一,这题有点相似于凋谢题,你答复出越多办法当然越加分,表明你对 Vue 把握的越纯熟。Vue 组件间通信只有指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信,上面咱们别离介绍每种通信形式且会阐明此种办法可实用于哪类组件间通信。
(1)props / $emit
实用 父子组件通信
这种办法是 Vue 组件的根底,置信大部分同学耳闻能详,所以此处就不举例开展介绍。
(2)ref
与 $parent / $children
实用 父子组件通信
ref
:如果在一般的 DOM 元素上应用,援用指向的就是 DOM 元素;如果用在子组件上,援用就指向组件实例$parent
/$children
:拜访父 / 子实例
(3)EventBus($emit / $on)
实用于 父子、隔代、兄弟组件通信
这种办法通过一个空的 Vue 实例作为地方事件总线(事件核心),用它来触发事件和监听事件,从而实现任何组件间的通信,包含父子、隔代、兄弟组件。
(4)$attrs
/$listeners
实用于 隔代组件通信
$attrs
:蕴含了父作用域中不被 prop 所辨认 (且获取) 的个性绑定 (class 和 style 除外)。当一个组件没有申明任何 prop 时,这里会蕴含所有父作用域的绑定 (class 和 style 除外),并且能够通过v-bind="$attrs"
传入外部组件。通常配合 inheritAttrs 选项一起应用。$listeners
:蕴含了父作用域中的 (不含 .native 润饰器的) v-on 事件监听器。它能够通过v-on="$listeners"
传入外部组件
(5)provide / inject
实用于 隔代组件通信
先人组件中通过 provider 来提供变量,而后在子孙组件中通过 inject 来注入变量。provide / inject API 次要解决了跨级组件间的通信问题,不过它的应用场景,次要是子组件获取下级组件的状态,跨级组件间建设了一种被动提供与依赖注入的关系。
(6)Vuex 实用于 父子、隔代、兄弟组件通信
Vuex 是一个专为 Vue.js 利用程序开发的状态管理模式。每一个 Vuex 利用的外围就是 store(仓库)。“store”基本上就是一个容器,它蕴含着你的利用中大部分的状态 (state)。
- Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地失去高效更新。
- 扭转 store 中的状态的惟一路径就是显式地提交 (commit) mutation。这样使得咱们能够不便地跟踪每一个状态的变动。
Vue 中如何进行依赖收集?
- 每个属性都有本人的
dep
属性,寄存他所依赖的watcher
,当属性变动之后会告诉本人对应的watcher
去更新 - 默认会在初始化时调用
render
函数,此时会触发属性依赖收集dep.depend
- 当属性产生批改时会触发
watcher
更新dep.notify()
依赖收集简版
let obj = {name: 'poetry', age: 20};
class Dep {constructor() {this.subs = [] // subs [watcher]
}
depend() {this.subs.push(Dep.target)
}
notify() {this.subs.forEach(watcher => watcher.update())
}
}
Dep.target = null;
observer(obj); // 响应式属性劫持
// 依赖收集 所有属性都会减少一个 dep 属性,// 当渲染的时候取值了,这个 dep 属性 就会将渲染的 watcher 收集起来
// 数据更新 会让 watcher 从新执行
// 观察者模式
// 渲染组件时 会创立 watcher
class Watcher {constructor(render) {this.get();
}
get() {
Dep.target = this;
render(); // 执行 render
Dep.target = null;
}
update() {this.get();
}
}
const render = () => {console.log(obj.name); // obj.name => get 办法
}
// 组件是 watcher、计算属性是 watcher
new Watcher(render);
function observer(value) { // proxy reflect
if (typeof value === 'object' && typeof value !== null)
for (let key in value) {defineReactive(value, key, value[key]);
}
}
function defineReactive(obj, key, value) {
// 创立一个 dep
let dep = new Dep();
// 递归察看子属性
observer(value);
Object.defineProperty(obj, key, {get() { // 收集对应的 key 在哪个办法(组件)中被应用
if (Dep.target) { // watcher
dep.depend(); // 这里会建设 dep 和 watcher 的关系}
return value;
},
set(newValue) {if (newValue !== value) {observer(newValue);
value = newValue; // 让 key 对应的办法(组件从新渲染)从新执行
dep.notify()}
}
})
}
// 模仿数据获取,触发 getter
obj.name = 'poetries'
// 一个属性一个 dep,一个属性能够对应多个 watcher(一个属性能够在任何组件中应用、在多个组件中应用)// 一个 dep 对应多个 watcher
// 一个 watcher 对应多个 dep(一个视图对应多个属性)// dep 和 watcher 是多对多的关系
如果让你从零开始写一个 vue 路由,说说你的思路
思路剖析:
首先思考 vue
路由要解决的问题:用户点击跳转链接内容切换,页面不刷新。
- 借助
hash
或者 history api
实现url
跳转页面不刷新 - 同时监听
hashchange
事件或者popstate
事件处理跳转 - 依据
hash
值或者state
值从routes
表中匹配对应component
并渲染
答复范例:
一个 SPA
利用的路由须要解决的问题是 页面跳转内容扭转同时不刷新,同时路由还须要以插件模式存在,所以:
- 首先我会定义一个
createRouter
函数,返回路由器实例,实例外部做几件事 - 保留用户传入的配置项
- 监听
hash
或者popstate
事件 - 回调里依据
path
匹配对应路由 - 将
router
定义成一个Vue
插件,即实现install
办法,外部做两件事 - 实现两个全局组件:
router-link
和router-view
,别离实现页面跳转和内容显示 - 定义两个全局变量:
$route
和$router
,组件内能够拜访以后路由和路由器实例
Vue 中修饰符.sync 与 v -model 的区别
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
能够有多个
- 格局不同:
vue 初始化页面闪动问题
应用 vue 开发时,在 vue 初始化之前,因为 div 是不归 vue 管的,所以咱们写的代码在还没有解析的状况下会容易呈现花屏景象,看到相似于 {{message}} 的字样,尽管个别状况下这个工夫很短暂,然而还是有必要让解决这个问题的。
首先:在 css 里加上以下代码:
[v-cloak] {display: none;}
如果没有彻底解决问题,则在根元素加上style="display: none;" :style="{display:'block'}"
vue 如何监听对象或者数组某个属性的变动
当在我的项目中间接设置数组的某一项的值,或者间接设置对象的某个属性值,这个时候,你会发现页面并没有更新。这是因为 Object.defineProperty()限度,监听不到变动。
解决形式:
- this.$set(你要扭转的数组 / 对象,你要扭转的地位 /key,你要改成什么 value)
this.$set(this.arr, 0, "OBKoro1"); // 扭转数组 this.$set(this.obj, "c", "OBKoro1"); // 扭转对象
- 调用以下几个数组的办法
splice()、push()、pop()、shift()、unshift()、sort()、reverse()
vue 源码里缓存了 array 的原型链,而后重写了这几个办法,触发这几个办法的时候会 observer 数据,意思是应用这些办法不必再进行额定的操作,视图主动进行更新。举荐应用 splice 办法会比拟好自定义, 因为 splice 能够在数组的任何地位进行删除 / 增加操作
vm.$set
的实现原理是:
- 如果指标是数组,间接应用数组的 splice 办法触发相应式;
- 如果指标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式解决,则是通过调用 defineReactive 办法进行响应式解决(defineReactive 办法就是 Vue 在初始化对象时,给对象属性采纳 Object.defineProperty 动静增加 getter 和 setter 的性能所调用的办法)
Vuex 为什么要分模块并且加命名空间
- 模块 : 因为应用繁多状态树,利用的所有状态会集中到一个比拟大的对象。当利用变得非常复杂时,
store
对象就有可能变得相当臃肿。为了解决以上问题,Vuex
容许咱们将store
宰割成模块(module
)。每个模块领有本人的state
、mutation
、action
、getter
、甚至是嵌套子模块 - 命名空间:默认状况下,模块外部的
action
、mutation
和getter
是注册在全局命名空间的——这样使得多个模块可能对同一mutation
或action
作出响应。如果心愿你的模块具备更高的封装度和复用性,你能够通过增加namespaced: true
的形式使其成为带命名空间的模块。当模块被注册后,它的所有getter
、action
及mutation
都会主动依据模块注册的门路调整命名