首先请求服务器,获取当前用户的权限数据,比如请求 this.$http.get(“rights/list”);
获取到权限数据之后,在列表中使用v-if v-if-else的组合来展示不同的内容
首页
权限管理
权限列表
一级
二级
三级
属于发布订阅模式,在vue中使用observer和definereactive两个方法的结合对数据进行递归劫持,然后通过watch这个类来对属性进行订阅,Dep类用于解耦合,当数据变更的时候先触发数据的set方法,然后调用Dep.notiify通知视图更新
vue性能瓶颈的几种情况
一次渲染大量的数据的时候,存在大量数据并且都是复杂类型的时候,会导致vue对数据的劫持时间和渲染时间变长, js 连续执行时间过长,会导致页面长时间无法交互,而且渲染时间太慢,用户一次交互反馈的时间过长。
优化方案:可以使用
requestAnimation
这个方法,将数据进行分割,分批次渲染,减少了 js 的连续运行时间,并且加快了渲染时间,利用加长总运行时间换取了渲染时间,用户既能快速得到反馈,而且不会因为过长时间的 js 运行而无法与页面交互。
当页面中存在大量数据,只是修改了一小部分导致页面也会导致页面卡顿,因为vue的更新以组件为粒度进行更新的,只要修改了当前组件中所使用的数据,组件就会整个去进行更新,造成大量的时间浪费
优化方案:将不同的模块划分成不同的组件,这样有效降低虚拟dom的diff运算时间过长的问题,比如将大量数据的模块单独放一个组件,其它放一个组件,由于vue是以组件为粒度更新,修改其它组件的情况下不会导致table的重新diff,提升页面响应速度高达几百倍
动态插槽作用域或者静态插槽的更新
使用插槽作用域来替换这两种操作方式,一样能提升性能,因为使用
插槽作用域
之后,插槽内容会被封装到一个函数中,被子组件渲染,而不是在父组件
如何获取dom?在Vue中提供了一种特别的方式来获取dom,即给dom加上个ref属性,那么就可以通过this.$refs.名字来获取到该dom元素。
如何操作dom、更新dom?通过refs.名字就可以拿到对应的真实dom,然后就可以用原生JS进行操作和更新。当然vue框架本身就是不需要dom操作的,通过修改相应的数据并再配合指令、模板语法就可以轻松的操作和更新dom。
在Vue2.x中,双向数据绑定是通过 数据劫持 结合 发布订阅模式的方式来实现的,也就是说数据和视图同步,数据发生变化,视图跟着变化,视图变化,数据也随之发生改变。核心:关于VUE双向数据绑定,其核心是 Object.defineProperty()方法。
Vue3.x则是用ES6的语法Proxy对象来实现的。
Object.defineProperty()的缺点:
Proxy的优点:
let arr = [];
let proxy = new Proxy(arr, {
get: function(obj, prop){
return obj[prop];
},
set: function(obj, prop, value){
obj[prop] = value; //可以被监听到变化
return true;
}
});
setTimeout(()=>{
proxy.push(1);
}, 2000)
MVVM是Model-View-ViewModel的简写。它本质上就是MVC(Model-View-Controller)的改进版。在开发过程中,由于需求的变更或添加,项目的复杂度越来越高,代码量越来越大,此时我们会发现MVC维护起来有些吃力,尤其Controller控制层非常的厚重,非常的庞大,难以维护。
所以有人想到把Controller的数据和逻辑处理部分从中抽离出来,用一个专门的对象去管理,这个对象就是ViewModel。ViewModel 是由前端开发人员组织生成和维护的视图数据层。在这一层,前端开发者对从后端获取的 Model 数据进行转换处理,做二次封装,以生成符合 View 层使用预期的视图数据模型。
由于实现了双向绑定,ViewModel 的内容会实时展现在 View 层,这是激动人心的,因为前端开发者再也不必低效又麻烦地通过操纵 DOM 去更新视图,MVVM 框架已经把最脏最累的一块做好了,我们开发者只需要处理和维护 ViewModel,更新数据视图就会自动得到相应更新,真正实现数据驱动开发。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wUo4GvnJ-1655384400029)(./pic/MVVM.png)]
在前后端完全分离的情况下,Vue项目中实现token验证大致思路如下:
1、第一次登录的时候,前端调后端的登陆接口,发送用户名和密码
2、后端收到请求,验证用户名和密码,验证成功,就给前端返回一个token
3、前端拿到token,将token存储到localStorage和vuex中,并跳转路由页面
4、前端每次跳转路由,就判断 localStroage 中有无 token ,没有就跳转到登录页面,有则跳转到对应路由页面
5、每次调后端接口,都要在请求头中加token
6、后端判断请求头中有无token,有token,就拿到token并验证token,验证成功就返回数据,验证失败(例如:token过期)就返回401,请求头中没有token也返回401
7、如果前端拿到状态码为401,就清除token信息并跳转到登录页面
当你设置 vm.message = ‘new message’,该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。例如:
<div id="example">{{message}}div>
var vm = new Vue({
el: '#example',
data: {
message: 'old message'
}
})
vm.message = 'new message' // 更改数据
vm.$el.textContent // 'old message'
Vue.nextTick(function () {
vm.$el.textContent // 'new message'
})
首先Vue 在更新 DOM 时是异步执行的,也就是说数据变了,DOM不会立即改变,那么我们是如何知道DOM什么时候会改变呢?也就是说如何知道异步后的触发时机呢?
可以通过nextTick方法,这个方法在源码内,先监听是否具备Promise.then,利用promise来监听,如果当前环境不支持promise,那么就降级采用MutationObserver,如果MutationObserver不支持的话,那么就降级采用setImmediate,如果setImmediate不支持的话,那么就使用setTimeout(fn, 0)。
所以说nextTick和setTimeout区别总结就是:nextTick会先尝试使用promise、MutationObserver、setImmediate这些技术去监听,如果都不支持才会采用setTimeout
起初我们在使用JS/JQuery时,不可避免的会大量操作DOM,而DOM的变化又会引发回流或重绘,从而降低页面渲染性能。那么怎样来减少对DOM的操作呢?此时虚拟DOM应用而生,所以虚拟DOM出现的主要目的就是为了减少频繁操作DOM而引起回流重绘所引发的性能问题的!
虚拟DOM(Virtual Dom),起始本质上就是一个JS对象,当数据发生变化时,我们不直接操作真实DOM,因为很昂贵,我们去操作这个JS对象,就不会触发大量回流重绘操作,再加上diff算法,可以找到两次虚拟DOM之间改变的部分,从而最小量的去一次性更新真实DOM,而不是频繁操作DOM,性能得到了大大的提升。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ePCNHx2T-1655384400030)(./pic/虚拟DOM.png)]
虚拟DOM还有一个好处,可以渲染到 DOM 以外的平台,实现 SSR、同构渲染这些高级特性,Weex 等框架应用的就是这一特性。
父向子组件传值,可以利用prop方式。
子向父组件传值,可以利用自定义事件$emit方式。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z2ZAIwBs-1655384400032)(./pic/vue父子通信.png)]
多层级组件传值,可以使用provide/inject
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Szc8wWMR-1655384400033)(./pic/provide_inject.png)]
无关系的组件传值,利用vuex状态管理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u7BHKToo-1655384400034)(./pic/vuex.png)]
父 -> 子: 通过 Prop 向子组件传递数据,子组件通过props属性来接收。
<blog-post title="My journey with Vue">blog-post>
Vue.component('blog-post', {
props: ['title'],
template: '{{ title }}
' //获取父组件的值
})
子 -> 父: 父组件自定义事件,子组件利用$emit来完成。
<blog-post v-on:enlarge-text="postFontSize += $event">blog-post>
Vue.component('blog-post', {
props: ['title'],
template: '<h3 v-on:click="$emit('enlarge-text', 0.1)">{{ title }}h3>'
})
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wCkDuiyq-1655384400036)(./pic/vue父子通信.png)]
这类问题 首先分类 表明了解的比较多 具体就没说完 或者漏了 面试官也不会计较很多
组件通信的四大类 父与子 子与父 子与子 跨层级
在细说各种方式 加入自己的理解
1、props和$emit
父组件向子组件传递数据是通过prop传递的,子组件传递数据给父组件是通过$emit触发事件
2、$attrs和$listeners
3、中央事件总线 bus
上面两种方式处理的都是父子组件之间的数据传递,而如果两个组件不是父子关系呢?这种情况下可以使用中央事件总线的方式。新建一个Vue事件bus对象,然后通过bus.$emit触发事件,bus.$on监听触发的事件。
4、provide和inject
父组件中通过provider来提供变量,然后在子组件中通过inject来注入变量。不论子组件有多深,只要调用了inject那么就可以注入provider中的数据。而不是局限于只能从当前父组件的prop属性来获取数据,只要在父组件的生命周期内,子组件都可以调用。
5、v-model
父组件通过v-model传递值给子组件时,会自动传递一个value的prop属性,在子组件中通过this.$emit(‘input’,val)自动修改v-model绑定的值
6、$parent和$children
7、boradcast和dispatch
8、vuex处理组件之间的数据交互 如果业务逻辑复杂,很多组件之间需要同时处理一些公共的数据,这个时候才有上面这一些方法可能不利于项目的维护,vuex的做法就是将这一些公共的数据抽离出来,然后其他组件就可以对这个公共数据进行读写操作,这样达到了解耦的目的。
关于这个可以的key的作用 首先表明 key 不是一定要有的 不写可以代码也可以跑 但是建议加上
然后指出可以用的地方 key在v-for循环可以用用 在表单元素中也可以用key 减少缓存
一般说key 只要说配合v-for的使用
key是为Vue中的vnode标记的唯一id,通过这个key,我们的diff操作可以更准确、更快速
diff算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的key与旧节点进行比对,然后超出差异能讲清楚diff算法就继续讲
diff程可以概括为:oldCh和newCh各有两个头尾的变量StartIdx和EndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldCh和newCh至少有一个已经遍历完了,就会结束比较,这四种比较方式就是首、尾、旧尾新头、旧头新尾.
准确: 如果不加key,那么vue会选择复用节点(Vue的就地更新策略),导致之前节点的状态被保留下来,会产生一系列的bug. 快速: key的唯一性可以被Map数据结构充分利用,相比于遍历查找的时间复杂度O(n),Map的时间复杂度仅仅为O(1)
讲完以后 还要补充一点自己的看法
建议使用主键比如id
Virtual DOM 其实就是一棵以 JavaScript 对象( VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实的DOM上
下面就是一个真实DOM映射到虚拟DOM的例子:
- Item 1
- Item 2
- Item 3
var element = {
tagName: 'ul', // 节点标签名
props: { // DOM的属性,用一个对象存储键值对
id: 'list'
},
children: [ // 该节点的子节点
{tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
{tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
{tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},
]
}
在补充点虚拟DOM的好处
具备跨平台的优势
由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。
操作原生DOM慢,js运行效率高。我们可以将DOM对比操作放在JS层,提高效率。
因为DOM操作的执行速度远不如Javascript的运算速度快,因此,把大量的DOM操作搬运到Javascript中,运用patching算法来计算出真正需要更新的节点,最大限度地减少DOM操作,从而显著提高性能。
Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)
提升渲染性能
Virtual DOM的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新。
diff算法
vdom因为是纯粹的JS对象,所以操作它会很高效,但是vdom的变更最终会转换成DOM操作,为了实现高效的DOM操作,一套高效的虚拟DOM diff算法显得很有必要
diff算法包括一下几个步骤:
用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文
档当中
当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较(diff),记录两棵树差异
把2所记录的差异应用到步骤1所构建的真正的DOM树上(patch),视图就更新了
diff算法是通过同层的树节点进行比较而非对树进行逐层搜索遍历的方式,所以时间复杂度只有O(n),是一种相当高效的算法
实现虚拟DOM的过程
这种问题 是开放的 多说就是都是对的 可以讲差异 也可以讲新增 的知识点
比如说 常用的api特别好用
const a = ref(1) // {value:1}
const msg = toRef(obj, key)
// 把obj[key]变成响应式的const { msg } = toRefs(obj)
// 把整个obj都变成响应式的const x = unref(x)
// 如果x是ref变量则返回x.value,如果x不是ref变量则直接返回x。const bol = isRef(x)
function useUpdBoxStyle() {
const el = ref(null)
const updateStyle = color => {
el.value.style.color = color
}
return [el, updateStyle]
}
const obj = shallowRef({a:1,b:{c:{d:{e:2}}}})
triggerRef(obj)
// 当obj.value.b.c.d发生变化,triggerRef(obj) 强制更新视图。customRef((track, trigger) =>({get,set})
function useObj() {
const obj = { a: 1, b: { c: { d: 2 }}}
const obj1 = ref(obj)
const obj2 = shallowRef(obj)
// console.log('obj1', obj1)
// console.log('obj2', obj2)
const changeObj = (obj, newD) => {
// obj1.value.b.c.d = 100
obj.value.b.c.d = newD
triggerRef(obj)
}
return [[obj1, obj2], changeObj]
}
const arr = reactive([]) // {value: []}
const c = shallowReactive({a:{b:{c:100}}})
// 只对这个对象的第一层进行proxyconst user = readonly({name:1,age:2})
const bol = isReadonly(x)
const bol = isProxy(x)
const bol = isReactive(x)
function useUser() {
const user = readonly(reactive({name:'list',age:30}))
console.log('user', user)
// setTimeout(()=>user.age=40, 2000)
const x = 1
const y = readonly({a:1,b:{c:3}})
console.log('是否被proxy拦截过', isProxy(user), isProxy(x), isProxy(y.b))
return user
}
const a3 = toRow(reactive(a1))
// a1===a3是trueconst b2 = markRaw(b1)
// b2是无法被reactive的function useRaw() {
const a1 = { title: 100 }
const a2 = reactive(a1)
const a3 = toRaw(a2)
console.log('toRow(a2)===a1', a3===a1)
console.log('a2===a1', a2===a1)
return [a1,a2,a3]
}
const c = computed(()=>c1.value*c2.value)
// 只读const c = computed({get:()=>c1.value*c2.value,set:(newVal)=>c1.value=newVal})
// 可写可读const stop = watch(x, (new,old)=>{})
// 调用stop()可以停止监听const stop = watch([x,y], ([newX,newY],[oldX,oldY])=>{})
const stop = watchEffect(()=>ajax({cate,page,size}))
export default function useWatchComputed() {
const c1 = ref(10)
const c2 = ref(20)
const c3 = computed(()=>c1.value*c2.value) // 只读
// 可读也可写
const c4 = computed({
get: ()=>c1.value*c2.value,
set: (newVal)=>{
c1.value = parseInt(newVal) / c2.value
}
})
const stop1 = watch(c4, (newC4, oldC4)=>console.log('c4变了', newC4, oldC4))
const stop2 = watch([c1,c2], ([newC1,newC2],[oldC1,oldC2])=>{
console.log('[c1,c2] 新值:', [newC1, newC2])
console.log('[c1,c2] 旧值:', [oldC1, oldC2])
})
const stop3 = watchEffect(()=>{console.log('watch effect', c1.value, c2.value)})
const update = (c,v) => c.value = v
return [[c1,c2,c3,c4],[stop1,stop2,stop3,update]]
}
2:也可以说亮点
3.更可以说性能
1.diff算法更快
vue2.0是需要全局去比较每个节点的,若发现有节点发生变化后,就去更新该节点
vue3.0是在创建虚拟dom中,会根据DOM的的内容会不会发生内容变化,添加静态标记, 谁有flag!比较谁。
2、静态提升
vue2中无论元素是否参与更新,每次都会重新创建,然后再渲染 vue3中对于不参与更新的元素,会做静态提升,只被创建一次,在渲染时直接复用即可
3、事件侦听缓存
默认情况下,onclick为动态绑定,所以每次都会追踪它的变化,但是因为是同一函数,没有必要追踪变化,直接缓存复用即可
在之前会添加静态标记8 会把点击事件当做动态属性 会进行diff算法比较, 但是在事件监听缓存之后就没有静态标记了,就会进行缓存复用
这种原理性问题 不要直接说不清楚 不了解
先讲下使用
v-model本质上是一个语法糖,可以看成是value + input 方法的语法糖。可以通过model的prop属性和event事件来进行自定义。
2、v-model是vue的双向绑定的指令,能将页面上控件输入的值同步更新到相关绑定的data属性, 也会在更新data
绑定属性时候,更新页面上输入控件的值。
然后再来讲细节
vue的双向绑定是由数据劫持结合发布者-订阅者模式实现的,那么什么是数据劫持?vue是如何进行数据劫持的?说白了就是通过Object.defineProperty()来劫持对象属性的setter和getter操作,在数据变动时做你想要做的事情
我们已经知道实现数据的双向绑定,首先要对数据进行劫持监听,所以我们需要设置一个监听器Observer,用来监听所有属性。如果属性发生变化了,就需要告诉订阅者Watcher看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的。接着,我们还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令(如v-model,v-on)对应初始化成一个订阅者Watcher,并替换模板数据或者绑定相应的函数,此时当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。因此接下去我们执行以下3个步骤,实现数据的双向绑定:
1.实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
2.实现一个订阅者Watcher,每一个Watcher都绑定一个更新函数,watcher可以收到属性的变化通知并执行相应的函数,从而更新视图。
3.实现一个解析器Compile,可以扫描和解析每个节点的相关指令(v-model,v-on等指令),如果节点存在v-model,v-on等指令,则解析器Compile初始化这类节点的模板数据,使之可以显示在视图上,然后初始化相应的订阅者(Watcher)
3:最后补一下 vue2.0里面用Object.defineProperty 3.0里面用new Proxy 一个监听每个属性 一个监听整个对象
像这种问题其实问的不是特别详情 面试者可能不懂题目的意思 但是我们要学会揣摩 面试官的问题
如果不知道 你就直说vue的组件通信 在讲iframe的页面获取v
vue组件内嵌一个iframe,现在想要在iframe内获取父组件内信息,采用的是H5新特性PostMessage来解决跨域问题
采用postMessage内涵两个API:
onMessage:消息监听
postMessage:消息发送
代码和例子
clearMap(){
let map = document.getElementsByName("map")[0].contentWindow
map.postMessage("clearMap","*")
}
iframe内:
window.addEventListener('message', function (evt) {
if (evt.data == 'clearMap'){
clearMap()
}
//event.data获取传过来的数据
});
这种问题一样的 先回答经常用的一些指定 比如 v-for v-if v-model v-show等等之类的 指令分为全局和局部的
然后在回答自定义指令
通过directive来自定义指令,自定义指令分为全局指令和局部指令,自定义指令也有几个的钩子函数,常用的有bind和update,当 bind 和 update 时触发相同行为,而不关心其它的钩子时可以简写。一个表达式可以使用多个过滤器。过滤器之间需要用管道符“|”隔开。其执行顺序从左往右。
这个问题很有意思 因为平时我们一般问题异步和同步指的是 数据请求 同步和异步问题
这里加上了组件 还有修改data 这里给大家写个例子
{{num}}
以此可以说明
数据更新是同步的 但是视图更新是异步的
解决这个问题需要使用 $nextTick 解决视图异步更新的问题
首先看到 .sync 我们需要知道这是个修饰器 类似修饰器还有 .stop .prevent 之类
其实这个修饰符就是vue封装了 子组件要修改父组件传过来的动态值的语法糖,省去了父组件需要写的方法,但是子组件emit时要加上update
在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以修改父组件,且在父组件和子组件都没有明显的改动来源
代码解释
// 这里父组件,要给子组件传一个title的值
// 子组件
{{ title }}
这里关键就是emit里的参数要写成’update’+ ‘:’ +‘要修改的props’
以前是用的this.$emit(“自定义方法”)
这个问题其实也是属于组件通信 常见的组件通信有 父传子 子传父 子传子 以及跨层级
这个多组件嵌套通信其实就是跨层级的另一种问法
多组件通信
方法一:props 一层 一层的传递
方法二:依赖注入 provide 声明 inject接收
方法三:利用公共的bus = new Vue() bus. o n 声 明 b u s . on 声明 bus. on声明bus.emit() 调用
方法四:使用vuex 全局的状态管理
当前组件
// Test.vue
import Vue from 'vue'
import App from './App.vue'
//全局注册
import HelloWorld from './components/HelloWorld.vue'
Vue.component('hello-world',HelloWorld)
new Vue({
render: h => h(App),
}).$mount('#app')
// Test.vue
首先我们需要达成共识的是,目前浏览器,只能识别普通的html、css、javascript。
但是为了能够方便使用vue的组件化开发,需要我们将代码写在.vue单文件组件中。
.vue文件,以及其内部的template、style、script区域代码,不能直接交给浏览器去解析,因为它解析不了。
所以我们需要一个vue-loader进行.vue单文件组件代码的转换,也就是
.vue方便开发 ------> vue-laoder协助翻译 -----> 浏览器才能展示
vue-loader 的工作流程, 简单来说,分为以下几个步骤:
如果想要了解哪个生命周期中可以找到vue实例的data,那我们必须了解,vue实例初始化的基本流程。
从 new Vue(options)
开始作为入口,Vue
只是一个简单的构造函数,内部是这样的:
function Vue (options) {
this._init(options)
}
复制代码
进入了 _init
函数之后,先初始化了一些属性。
initLifecycle
:初始化一些属性如$parent
,$children
。根实例没有 $parent
,$children
开始是空数组,直到它的 子组件
实例进入到 initLifecycle
时,才会往父组件的 $children
里把自身放进去。所以 $children
里的一定是组件的实例。initEvents
:初始化事件相关的属性,如 _events
等。initRender
:初始化渲染相关如 $createElement
,并且定义了 $attrs
和 $listeners
为浅层
响应式属性。具体可以查看细节
章节。并且还定义了$slots
、$scopedSlots
,其中 $slots
是立刻赋值的,但是 $scopedSlots
初始化的时候是一个 emptyObject
,直到组件的 vm._render
过程中才会通过 normalizeScopedSlots
去把真正的 $scopedSlots
整合后挂到 vm
上。然后开始第一个生命周期:
callHook(vm, 'beforeCreate')
复制代码
beforeCreate
之后
初始化 inject
初始化
state
props
methods
data
computed
watch
初始化 provide
所以在 data
中可以使用 props
上的值,反过来则不行。
然后进入 created
阶段:
callHook(vm, 'created')
复制代码
调用 $mount
方法,开始挂载组件到 dom
上。
如果使用了 runtime-with-compile
版本,则会把你传入的 template
选项,或者 html
文本,通过一系列的编译生成 render
函数。
template
,生成 ast
抽象语法树。ast
,标记静态节点。(渲染过程中不会变的那些节点,优化性能)。ast
,生成 render
函数。对应具体的代码就是:
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options)
}
const code = generate(ast, options)
复制代码
如果是脚手架搭建的项目的话,这一步 vue-cli
已经帮你做好了,所以就直接进入 mountComponent
函数。
那么,确保有了 render
函数后,我们就可以往渲染
的步骤继续进行了
通过上面的vue实例化的前面几步我们可以知道,在created生命周期中,我们就可以对data做操作,因为此时根实例相关的属性都已准备完毕。那在created后面执行的生命周期,自然也都可以获取并操作data,所以能够操作data的常用生命周期就有如下几个:
顾名思义,是要定义在全局的,也就是我们 index.js
中的 router
对象。
全局前置守卫,在路由跳转前触发,它在 每次导航 时都会触发。
通过 router.beforeEach
注册一个全局前置守卫。
router.beforeEach((to, from, next) => {
console.log('??~ to:', to);
console.log('??~ from:', from);
next();
})
复制代码
参数
beforeEach
全局前置守卫接收三个参数
注意: next
参数可以不添加,但是一旦添加,则必须调用一次,否则路由跳转等会停止。
next()
方法的几种情况
from
路由对应的地址。router.push
中选项一致。router.onError()
注册过的回调。、全局解析守卫,在路由跳转前,所有 组件内守卫 和 异步路由组件 被解析之后触发,它同样在 每次导航 时都会触发。
通过 router.beforeResolve
注册一个全局解析守卫。
router.beforeResolve((to, from, next) => {
next();
})
复制代码
回调参数,返回值和 beforeEach
一样。也可以定义多个全局解析守卫。
全局后置钩子,它发生在路由跳转完成后,beforeEach
和 beforeResolve
之后,beforeRouteEnter
(组件内守卫)之前。它同样在 每次导航 时都会触发。
通过 router.afterEach
注册一个全局后置钩子。
router.afterEach((to, from) => {
console.log('??~ afterEach:');
})
复制代码
这个钩子的两个参数和 beforeEach
中的 to
和 from
一样。然而和其它全局钩子不同的是,这些钩子不会接受 next
函数,也不会改变导航本身。
顾名思义,就是跟路由相关的钩子,我们的路由守卫只有一个,就是 beforeEnter
。
需要在路由配置上定义 beforeEnter
守卫,此守卫只在进入路由时触发,在 beforeEach
之后紧随执行,不会在 params
、query
或 hash
改变时触发。
//index.js
{
path: '/a',
component: () => import('../components/A.vue'),
beforeEnter: (to, from) => {
console.log('??~ beforeEnter ');
},
},
复制代码
beforeEnter
路由守卫的参数是 to
、from
、next
,同 beforeEach
一样。
顾名思义,是定义在路由组件内部的守卫。
//A.vue
beforeRouteEnter(to, from,next) {
console.log('??~ beforeRouteEnter');
},
复制代码
路由进入组件之前调用,该钩子在全局守卫 beforeEach
和路由守卫 beforeEnter
之后,全局 beforeResolve
和全局 afterEach
之前调用。
参数包括 to
,from
,next
。
该守卫内访问不到组件的实例,也就是 this
为 undefined
,也就是他在 beforeCreate
生命周期前触发。
//A.vue
beforeRouteUpdate(to, from) {
console.log('??~ beforeRouteUpdate');
},
复制代码
对于 beforeRouteUpdate
来说,this
已经可用了,所以给 next
传递回调就没有必要了。
//A.vue
beforeRouteLeave(to, from) {
console.log('??~ beforeRouteLeave');
},
复制代码
对于 beforeRouteLeave
来说,this
已经可用了,所以给 next
传递回调就没有必要了。
完整的导航解析流程
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave
守卫。- 调用全局的
beforeEach
守卫。- 在重用的组件里调用
beforeRouteUpdate
守卫。- 在路由配置里调用
beforeEnter
。- 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter
。- 调用全局的
beforeResolve
守卫。- 导航被确认。
- 调用全局的
afterEach
钩子。- 触发
DOM
更新。- 调用
beforeRouteEnter
守卫中传给next
的回调函数,创建好的组件实例会作为回调函数的参数传入。
计算属性的英文是computed,其实很多时候,一些概念从英文对照为中文后,会导致其中一些含义发生错位与丢失,我们要想更好的理解computed计算属性,可以将这个概念分为两部分来看:
计算
这里的计算,是有种宏观的概念,指的是对于数据包的一种操作,比如:筛选、过滤、新增、删除等。
说明computed本身是具有处理数据的能力。
属性
属性的意思是,一种可以读取、渲染的数据,性质跟data的作用相似
说明computed最终会给出一个数据供页面渲染使用。
由此,我们可以得出一个结论:
computed计算属性,负责将一些数据在其内部按照指定逻辑处理后,最终给出处理后的结果数据,给到组件、页面进行渲染使用,可以让开发者更加便捷的处理一些动态变化的数据需求。
自动
识别到data的变化,从而做出新的操作,得出新的数据,从而自动更新视图。缓存
的能力,如果data不变,则页面中调用的都是computed第一次执行时的运算结果,提高渲染性能。当某个前端已经指定的data数据包,如果我们渲染前,对其有过滤、筛选等操作需求,就可以使用computed
new Vue({
el:'#app', //占地盘
data:{
'msg':'Hello Vue' //自定义数据包
},
computed:{
reMsg(){ //专门用于反转msg的计算属性
return this.msg.split('').reverse().join('')
}
}
})
全部学员
、及格学员
、不及格学员
,这种在本地进行筛选的需求可以快速通过computed实现,代码逻辑大致如下:new Vue({
el:'#app', //占地盘
data:{
stu:[
{name:'张三丰',score:100},
{name:'DDK',score:50},
{name:'张翠山',score:60},
{name:'张无忌',score:90},
{name:'PDD',score:45}
],
status:0 //0全部 1及格 2不及格
},
computed:{
filterStu(){
let {stu,status} = this
// let stu = this.stu
// let status = this.status
switch (status) {
case 1: //及格
return stu.filter(item=>{
return item.score>=60
})
case 2: //不及格
let arr = stu.filter(item=>{
return item.score<60
})
return arr
default: //全部
return stu
}
}
}
})
上一个问题我们已经详细探讨了computed的相关特征,在这里我们可以逐一对比一下:
新数据包
行为逻辑
状态管理就是,把组件之间需要共享的状态抽取出来,遵循特定的约定,统一来管理,让状态的变化可以预测。
组件之间通常会有一些共享的状态,在 Vue 或者 React 中我们一般会将这部分状态提升至公共父组件的 props
中,由父组件来统一管理共享的状态,状态的改变也是由父组件执行并向下传递。这样会导致两个问题:
在应用调试过程中,可能会有跟踪状态变化过程的需求,方便对某些应用场景的复现和回溯。这时候就需要统一对状态进行管理,并遵循特定的约定去变更状态,从而让状态的变化可预测。
因为在真实项目开发过程中,Store状态管理器中的数据会在很多组件中用到,如果不设定一个统一的规范去管理数据,最终将会导致数据混乱、使得项目变得难以维护。所以vuex状态管理器设计了如下几个核心api,与视图之间进行交互配合:
state
vuex提供的,用以集中存储共享的数据。
mutations
vuex提供的,专门用以触发state数据变化的方法集,并且要求mutations的方法执行结果必须时可预测的,在其内部不能出现异步请求等不可预测的逻辑。
actions
vuex提供的,专门用于让vuex进行异步请求处理的方法集,可选择使用。
view
视图层,整个项目组件的代称,我们在此处消费状态管理器提供的数据、方法。
数据走向必须遵循单向数据流的规范:
当我们初始化使用状态机数据时的流程是
store---->state----> view
当组件内部想要本地更新状态管理器的数据,其流程是
view触发---->mutations---->state---->store---->view更新
当组件内部想要在异步请求后,再更新本地状态管理器的数据,其流程是
view触发---->actions---->mutations---->state---->store---->view更新
vuex的 store 中的数据是保存在运行内存中的,当页面刷新时,页面会重新加载 vue 实例,vuex 里面的数据就会被重新赋值,这样就会出现页面刷新vuex中的数据丢失的问题。 如何解决浏览器刷新数据丢失问题呢?
全局监听,页面刷新的时候将 store 里 state 的值存到 sessionStorage 中,然后从sessionStorage 中获取,再赋值给 store ,并移除 sessionStorage 中的数据。在 app.vue 中添加以下代码:
created() {
window.addEventListener('beforeunload',()=>{
sessionStorage.setItem('list', JSON.stringify(this.$store.state))
})
try{
sessionStorage.getItem('list') && this.$store.replaceState(Object.assign({},this.$store.state,JSON.parse(sessionStorage.getItem('list'))))
}catch(err) {
console.log(err);
}
sessionStorage.removeItem("list");
}
1. npm install vuex-persistedstate -S //安装插件
2. 在 store/index.js 文件中添加以下代码:
import persistedState from 'vuex-persistedstate'
const store = new Vuex.Store({
state:{},
getters:{},
...
plugins: [persistedState()] //添加插件
})
这时候就需要使用 sessionStorage 进行存储,修改 plugins 中的代码
plugins: [
persistedState({ storage: window.sessionStorage })
]
key的值一般为string或则number的类型,它用于解决在进行虚拟dom更新时的更新对象快速定位,虚拟dom更新需要逐个对比虚拟dom对象绑定的数据,这个过程称为diff算法,所以key也是提升diff算法的一个标识符,因为数组可能会进行排序以及增删等动作,所以使用数组的下标来定义key是没有任何作用的
可以的,自定义一个指令,给节点绑定上添加input的功能,数据使用value绑定,在用户输入数据的时候,采用oninput事件来进行数据获取,并实时更新value数据,即可实现v-model的功能
在Vue2.x中采用ES5的对象属性定义方法(Object.defineProperty)来给每一个数据添加额外属性(getter和setter属性)进行数据拦截,在vue内部实现对拦截数据的setter方法数据更新消息发布机制来获取数据更新消息,对getter方法实现消息订阅机制来获取数据更新
多方面的原因引起的:
1、头像信息是否采用了应用缓存机制,如果有需要清除H5应用缓存
2、头像缓存在webStorage中,退出没有清除,直接清除
3、缓存在移动设备的数据,也需要清除
4、缓存在vuex或redux中的数据,直接清除即可
5、浏览器缓存,清除cookie或则强制清除浏览器缓存
1、获取传递进来的数据,然后对所有数据添加getter和setter方法,并在setter方法中添加消息订阅回调方法,在getter方法中实现数据更新发布回调方法
2、对作用域内的所有dom节点进行数据以来处理,根据不同指令来实现不同订阅或发布回调方法绑定:如v-model绑定的数据对象,在oninput事件中添加消息发布回调方法绑定,在v-text中添加value数据更新消息订阅回调方法绑定
当在输入框输入值的时候,出发oninput事件,出发消息发布回调方法更新数据
当数据发生变化,触发订阅方法,执行dom数据更新方法,把最新数据更新到视图上
它使用ES5的Object.defineProperty方法把所有的属性全部改为setter和getter属性,在每一个组件中都有一个watcher对象,当数据被赋值或变更的时候会通知页面的render方法对数据进行重新渲染,达到数据和视图的响应更新
因为js的固有特性,不能动态观察对象动态添加、删除属性和数组的长度添加,所以vue2.x不能够动态进行数据双向绑定,需要调用$set、$delete这些方法来实现动态添加双向绑定属性
这个是webpack中常常用于指定加载,对哪些文件进行加载的排除或包含的一个属性,include配置的文件路径中的所有文件都将采用配置的loader进行文件加载处理,exclude是配置的路径都不要进行这个加载器的处理
这个与vue没有太大关系,采用的是ES6的动态加载机制来实现页面的懒加载,主要使用的webpack语法库为:@babel/plugin-syntax-dynamic-import,在对页面引入的时候,需要把引入方式从:import MyComponent from 'path' 修改为:const MyComponent = () => import('path')
对组件不要做全局引入,可以采用动态引入和页面局部引入机制,减少文件首次加载的文件大小和文件进行打包时把所有依赖进行一次性打包造成的文件过大问题
可以采用:eventBus事件机制来进行数据传递;也可以采用逐层props和$emit事件传递来实现传值;vuex数据传递;使用v-model逐层数据传递等
在vue.config.js中使用devServer配置选项的proxy属性来进行多个代理配置
devServer: {
proxy: {
'/apis': {
target: 'http://www.baidu.com',
pathRewrite: {'/apis': ''}
},
'/info': {
target: 'http://www.sina.com',
pathRewrite: {'/info': ''}
}
}
}
因为Object.defineProperty方法的历史原因,如果要实现数据的劫持,需要遍历所有属性,如果是深层数据需要递归来进行属性绑定;而proxy是直接代理此数据对象,不需要遍历绑定属性
如果对象有数据属性添加或则属性更新,需要重新给定数据劫持方法绑定,而proxy直接代理对象,不需要对属性做另外的处理
因此可以在进行数据监听和劫持上节省很多数据遍历性能
回答:
**1、**封装组件会有哪些考虑,用户用你的组件要怎么用?
1)、封装组件和封装函数是同样的道理,需要考虑到组件的通用性。
2)、那么,组件的属性,组件事件,组件的内容都是需要考虑到的。
组件的属性:需要考虑到属性的类型限制,属性的默认值
组件的事件:组件事件对应的函数,需要考虑到函数的参数和返回值
组件的内容:要充分考虑到组件的通用性,灵活性。组件的内容给予使用者以更大的灵活空间。
**2、**如果用户在使用你组件的同时又想自己加一些按钮进去那么此时你的组件要怎么写?
1)、要么使用内容的方式(vue中是插槽)
2)、要么提供一个render函数,由用户提供需要渲染的内容(如:按钮)。同时,根据自定义组件的情况,可以考虑让用户把按钮渲染在指定的区域。即:render函数里有:渲染的区域和渲染的内容。
回答:
1、场景:
一个网页如果包含了很多的图片,那么,服务器压力就会很大。不仅影响渲染速度还会浪费带宽。
通俗的说:你不看的图片我先不加载,也许你不看呢(哈哈),我何苦要做无效的事情呢
你想看时,我再加载(哈哈)
2、原理:
1)、先将img标签的src链接设为同一张图片(默认图片:可以是loading),把图片的实际地址赋给一个自定义属性。这时候所有的图片只发送一次请求。
2)、然后,当js监听到某张图片进入可视窗口时(说明你想看了),再将实际地址赋给src属性。src属性的值发生变化时,浏览器才会发送请求加载当前图片。如果图片没有进入可视区域,就不会加载图片(说明你还没想看呢),这样就大大的提高了效率。
3、示例代码(兼容性在代码中有注释):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
*{
margin: 0;
padding: 0;
}
ul{
list-style: none;
}
li{
width: 100%;
height: 200px;
text-align: center;
line-height: 200px
}
img{
display: block;
height: 200px;
}
</style>
</head>
<body>
<div id="box">
</div>
</body>
</html>
<script>
let imgDoms = document.getElementsByTagName("img");
// 当前显示图片的最大下标:
let maxIndex = -1;
window.onload = function(){
// 1、从后端获取到所有的图片地址,先赋值给图片标签的自定义属性(data-src),给图片的src赋值为loading
getImgs();
// 2、加载可视区域的图片
loadingImg();
}
window.onscroll = function(){
loadingImg();
}
// 从后端获取到所有的图片地址,先赋值给图片标签的自定义属性(data-src),给图片的src赋值为loading
function getImgs(){
//这个数组中的图片,可以是从后端获取到的,也可以写死。
let imgs = ["img/1.jpg","img/2.jpg","img/3.jpg","img/4.jpg","img/5.jpg","img/6.jpg","img/7.jpg","img/8.jpg","img/9.jpg","img/10.jpg","img/11.jpg","img/12.jpg","img/13.jpg","img/14.jpg","img/15.jpg","img/16.jpg","img/17.jpg","img/18.jpg","img/19.jpg","img/20.jpg","img/21.jpg","img/22.jpg","img/23.jpg","img/24.jpg","img/25.jpg","img/26.jpg","img/27.jpg","img/28.jpg","img/29.jpg","img/30.jpg","img/31.jpg","img/32.jpg","img/33.jpg"];
let htmlStr = "";
for(let i=0;i<imgs.length;i++){
htmlStr+=`${imgs[i]}" />`;
}
document.getElementById("box").innerHTML = htmlStr;
}
function loadingImg(){
// 1、计算当前滚动的高度+可视区域的高度(此处考虑的兼容性)
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
let clientHeight = document.documentElement.clientHeight || document.body.clientHeight;
let height = scrollTop+clientHeight;
// 2、得到应该显示图片的序号(可视区域最下面的图片序号);
let index = Math.ceil(height/200)-1;
// 3、如果应该显示的图片的下标大于最大的下标,那就应该做图片的加载了。
if(index>maxIndex){
for(let i=maxIndex+1;i<=index;i++){
if(imgDoms[i].getAttribute("data-src")){
imgDoms[i].src = imgDoms[i].getAttribute("data-src");
imgDoms[i].removeAttribute("data-src");
}
}
}
maxIndex = index;
}
</script>
回答:
1、如果vue组件的data是一个对象,那么在复用vue组件时,由于对象是个引用类型。那么,每个组件的data会指向同一块内存空间,组件之间的data就会互相影响。所以,组件中的data不能是对象。
2、vue框架中把data定义成函数,函数里返回真正的数据(引用类型)。每次复用组件时,vue框架都会调用该函数。当调用该函数时,函数返回新的对象(申请新的空间)。那么不同的组件的内存空间就是独立的,不会互相影响。
回答:
1、vue的生命周期
1)、生命周期是什么?
Vue 实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模版、挂载 Dom -> 渲染、更新 -> 渲染、卸载等一系列过程,我们称这是 Vue 的生命周期。
2)、各个生命周期阶段及其钩子函数
vue的生命周期核心经历了四个阶段,在四个阶段的前后分别有两个钩子函数。
第一阶段:数据挂载阶段:把配置项data中的属性,赋给了vue对象本身,并做了数据劫持。
该阶段前后的两个钩子函数:beforeCreate和created
第二阶段:模板渲染阶段:把vue对象的数据渲染到模板上。
该阶段前后的两个钩子函数:beforeMount和mounted
第三阶段:组件更新阶段:当数据发送变化时,组件会进行重新渲染,所以,准确的说是,组件重新渲染阶段。
该阶段前后的两个钩子函数:beforeUpdate和updated
第四阶段:组件销毁阶段:组件销毁。
该阶段前后的两个钩子函数:beforeDestroy和destroyed
视情况可以补充:
当使用keep-alive包裹组件时,会有组件激活和停用之说,这两个钩子函数分别是:activited和deactivated
2、发送请求在生命周期的哪个阶段,为什么不可以是beforeMount,mounted中。
(如果组件的初始数据来自后端)发送请求建议在钩子函数created里,这个钩子函数里是最快,也能有助于一致性。
为什么?
1)、为什么不能放在beforeCreate里?
因为:
一般来说,数据从后端返回回来后,都会赋给vue中的属性(data挂载的),在beforeCreate钩子函数里,data的数据还没有挂载到vue对象本身上,所以,不能在这个钩子函数里用。而created钩子函数里,数据已经挂载到vue对象了。
2)、为什么不可以是beforeMount,mounted中
因为:
第一,在这两个钩子函数里,发送请求有点晚,会增加页面loading的时间;
第二,vue的SSR不支持beforeMount 、mounted 钩子函数,所以,放在 created 中有助于一致性
回答:
1、父子之间传值:
1)、父到子:props,子到父:$emit
2)、 r e f 、 ref、 ref、parent
3)、事件总线(event-bus)
4)、集中管理($root)
2、兄弟之间传值:
1)、事件总线(event-bus)
2)、集中管理($root)
3)、先 子到父(用事件),再 父到子(用props)
回答:
可以使用,但是,在循环时,通过v-if只能拿到少部分数据时,建议不要使用。
原因: v-for比v-if优先级高,如果遍历的数组元素个数比较多,但是满足v-if条件比较少的情况下。会浪费性能。而且,每次刷新页面时,都会执行这样性能不高的代码。
**解决:**可以在computed里循环数组,通过filter的方式,过滤出需要的数据。v-for直接循环计算属性的结果(不用v-if)。而computed是有缓存的,所以,在原始数据没有变化时,不会多次过滤数据,这样,就提高了效率。
回答:
使用反向代理,vue-cli3+的项目里,新建(编辑)vue.config.js文件,(增加)配置代码如下:
module.exports = {
devServer:{
//设置代理
proxy: { //代理是从指定的target后面开始匹配的,不是任意位置;配置pathRewrite可以做替换
'/api': { //这个是自定义的,当axios访问以/api开头的请求时,会代理到 target + /api
target: 'http://localhost:3001', //最终要访问的服务器地址
changeOrigin: true, //创建虚拟服务器
pathRewrite: {
'^/api': '' //重写接口,去掉/api, 在代理过程中是否替换掉/api/路径
}
}
}
}
}
回答:
v-bind指令是把标签的属性处理成动态的。分别可以把属性名和属性值处理成vue里的属性,常间的是属性值处理成动态的。
格式如下:
1、属性值动态绑定:v-bind:html属性="数据"
简写 :html属性=“数据”`
示例:
<img v-bind:src="imgstr"></div>
new Vue({
data:{
imgstr:'./imgs/1.jpg'
}
})
2、 属性名动态绑定: v-bind:[属性名]="数据"
此时,属性值也是动态的
示例:
<div v-bind:[attr]="idname" >我是div</div>
new Vue({
el: "#app",
data:{
attr:"class",
idname:"div01"
}
})
回答:
1、插槽的作用:
插槽是用来处理组件的内容的。插槽决定了组件的内容放在组件模板的何处。插槽使用的是vue官方提供的组件
来完成的。
2、vue中的插槽分为:
1)、单个插槽
在组件中只有一个插槽时,插槽不用起名字。默认的名字是:default
示例:
//1、定义组件
let book = {
template: `
我是上p
我是下p
`,
}
//2、父组件的模板
<div id="box">
<book>
<!--放在组件book标签的内容,最终会渲染在book标签的<slot>处。-->
<img src="imgs/2.jpg" />
</book>
</div>
2)、具名插槽
但组件中的插槽多于一个时,就需要给组件起名字,用名字来区分不同的插槽。用官方组件slot的name属性给插槽起名字
格式:
<slot name="插槽的名字"></slot>
示例:
//1、组件:
let book = {
template: `
我是上p
我是中p
我是下p
`,
}
//2、父组件模板里:
<div id="box">
<book>
<!--以下内容插在name为s1的插槽处-->
<template v-slot:s1>
<img src="imgs/2.jpg" />
</template>
<!--以下内容插在name为s2的插槽处-->
<template v-slot:s2>
<div>我是div</div>
</template>
</book>
</div>
回答:
1、$nextTick理解:
vue更新DOM时,使用的是异步更新队列。目的是提高性能,避免无效的重复的DOM更新。即:vue中更新数据后,并不会立即更新DOM,而是把数据引起的DOM更新放入到异步更新队列里。等待下次事件循环(tick),并在两个tick之间进行UI渲染。
按照这个思路,程序员就不能在更改数据后,立即获取更新后的DOM,也不知道什么时候DOM能够更新。基于此,vue提供了 n e x t T i c k 函 数 。 程 序 员 只 需 要 把 ∗ ∗ ” ∗ ∗ 操 作 更 新 后 D O M 的 代 码 ∗ ∗ “ ∗ ∗ 放 入 到 nextTick函数。程序员只需要把 **”**操作更新后DOM的代码**“** 放入到 nextTick函数。程序员只需要把∗∗”∗∗操作更新后DOM的代码∗∗“∗∗放入到nextTick的回调函数里。由$nextTick内部,在更新完DOM后,调用回调函数。
示例:
this.msg = "hello"
this.$nextTick(()=>{
操作“this.msg影响的DOM”的代码。
})
2、$nextTick理解和定时器有什么区别
**相同:**都是延迟加载,都使用事件队列
不同:
1)、定时器是下一个队列的队首。
2)、$nextTick()
是放在当前队列的最后一个。$nextTick()的回调函数执行要先于定时器。
event-bus是事件总线,是借助一个全局的vue对象,来完成事件的绑定和事件的触发。
**当:**我们需要把A组件的数据传给B组件时,在A、B两个组件里都引入全局的vue对象。然后,在B组件里绑定事件,在A组件里触发事件,就可以把A组件的数据传给B组件了。
示例:
//1、全局的vue对象: bus.js
export default new Vue();//vue 对象具有 $on 和 $emit 方法
//2、B组件的代码
import bus from "./bus.js"
export default {
………………
data(){
return {
bookmsg:""
}
},
created(){
// 绑定事件(用全局变量bus绑定一个事件)
bus.$on("eclick",str=>{
this.bookmsg = str;
})
}
}
//3、A组件的代码
import bus from "./bus.js"
export default {
………………
data(){
return {
msg:"要传给B组件的数据"
}
},
methods:{
chuan(){
// 触发事件(用全部变量bus触发事件)
bus.$emit("eclick",this.msg);
}
}
}
回答:
mounted和created都是vue对象生命周期的钩子函数,执行时机不同。
1、created 是在 data配置项的数据挂载到vue对象本身后,会调用的钩子函数。
此时,
1)、用 this. 的方式可以拿到data里的数据。
2)、但是数据还没有渲染到模板上。所以,访问dom时,内容还是原始的模板内容。
2、mounted是在组件的模板初次渲染完毕后会调用的钩子函数。
此时,
data里的数据已经渲染到模板上了。所以,访问dom时,已经和页面上看到的效果一样了。
回答:
1、v-model指令的作用
vue中的v-model指令是完成双向绑定的,用在表单元素上。双向绑定就是 M会影响V。V也会影响M。即:能将页面上输入的值同步更新到相关绑定的data属性,也会在更新data绑定属性时候,更新页面上输入控件的值。
2、v-model的原理
v-model指令是一个语法糖,是属性绑定和事件的语法糖。vue会根据不同的表单元素使用不同的属性和事件。
如下:
value
property 和 input
事件;checked
property 和 change
事件;value
作为 prop 并将 change
作为事件。以文本框为例剖析原理,以下是代码:
<!-- V -->
<div id="app">
<!--文本框使用value属性和input事件-->
<input type="text" v-bind:value="msg" @input="changeMsg" >
</div>
//M:
let vm = new Vue({
el: "#app",
data: {
msg:"hi"
},
methods: {
changeMsg(e){
this.msg = e.target.value;
}
}
})
而,使用v-model来完成以上功能的代码如下:
<!-- V-->
<div id="app">
<!-- 此处不需要使用value属性和input事件,直接使用v-mdoel指令即可 -->
<input type="text" v-model="msg" >
</div>
// M:model
let vm = new Vue({
el: "#app",
data: {
msg:"hi"
}
})
vue 为了保证每个实例上的 data 数据的独立性,规定了必须使用函数,而不是对象。 因为使用对象的话,每个实例(组件)上使用的 data 数据是相互影响的,这当然就不是我们想要的了。 对象是对于内存地址的引用,直接定义个对象的话组件之间都会使用这个对象,这样会造成组件之间数据相互影响。而函数具有内部作用域,可以解决这个问题。
在 vue 中我们可以使用 Vue.directive()方法注册全局指令。也可以只用 directives 选项注册局部指令。
const debounce = {
inserted: function (el, binding) {
let timer;
el.addEventListener("keyup", () => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
binding.value();
}, 1000);
});
},
};
export default debounce;
const copy = {
bind(el, { value }) {
el.$value = value;
el.handler = () => {
if (!el.$value) {
// 值为空的时候,给出提示。可根据项目UI仔细设计
console.log("无复制内容");
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();
const result = document.execCommand("Copy");
if (result) {
console.log("复制成功"); // 可根据项目UI仔细设计
}
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 copy;
const longpress = {
bind: function (el, binding, vNode) {
if (typeof binding.value !== "function") {
throw "callback must be a function";
}
// 定义变量
let pressTimer = null;
// 创建计时器( 2秒后执行函数 )
let start = (e) => {
if (e.type === "click" && e.button !== 0) {
return;
}
if (pressTimer === null) {
pressTimer = setTimeout(() => {
handler();
}, 2000);
}
};
// 取消计时器
let cancel = (e) => {
if (pressTimer !== null) {
clearTimeout(pressTimer);
pressTimer = null;
}
};
// 运行函数
const handler = (e) => {
binding.value(e);
};
// 添加事件监听器
el.addEventListener("mousedown", start);
el.addEventListener("touchstart", start);
// 取消计时器
el.addEventListener("click", cancel);
el.addEventListener("mouseout", cancel);
el.addEventListener("touchend", cancel);
el.addEventListener("touchcancel", cancel);
},
// 当传进来的值更新的时候触发
componentUpdated(el, { value }) {
el.$value = value;
},
// 指令与元素解绑的时候,移除事件绑定
unbind(el) {
el.removeEventListener("click", el.handler);
},
};
export default longpress;
let findEle = (parent, type) => {
return parent.tagName.toLowerCase() === type
? parent
: parent.querySelector(type);
};
const trigger = (el, type) => {
const e = document.createEvent("HTMLEvents");
e.initEvent(type, true, true);
el.dispatchEvent(e);
};
const emoji = {
bind: function (el, binding, vnode) {
// 正则规则可根据需求自定义
var regRule = /[^u4E00-u9FA5|d|a-zA-Z|rns,.?!,。?!…—&$=()-+/*{}[]]|s/g;
let $inp = findEle(el, "input");
el.$inp = $inp;
$inp.handle = function () {
let val = $inp.value;
$inp.value = val.replace(regRule, "");
trigger($inp, "input");
};
$inp.addEventListener("keyup", $inp.handle);
},
unbind: function (el) {
el.$inp.removeEventListener("keyup", el.$inp.handle);
},
};
export default emoji;
const LazyLoad = {
// install方法
install(Vue, options) {
const defaultSrc = options.default;
Vue.directive("lazy", {
bind(el, binding) {
LazyLoad.init(el, binding.value, defaultSrc);
},
inserted(el) {
if (IntersectionObserver) {
LazyLoad.observe(el);
} else {
LazyLoad.listenerScroll(el);
}
},
});
},
// 初始化
init(el, val, def) {
el.setAttribute("data-src", val);
el.setAttribute("src", def);
},
// 利用IntersectionObserver监听el
observe(el) {
var io = new IntersectionObserver((entries) => {
const realSrc = el.dataset.src;
if (entries[0].isIntersecting) {
if (realSrc) {
el.src = realSrc;
el.removeAttribute("data-src");
}
}
});
io.observe(el);
},
// 监听scroll事件
listenerScroll(el) {
const handler = LazyLoad.throttle(LazyLoad.load, 300);
LazyLoad.load(el);
window.addEventListener("scroll", () => {
handler(el);
});
},
// 加载真实图片
load(el) {
const windowHeight = document.documentElement.clientHeight;
const elTop = el.getBoundingClientRect().top;
const elBtm = el.getBoundingClientRect().bottom;
const realSrc = el.dataset.src;
if (elTop - windowHeight < 0 && elBtm > 0) {
if (realSrc) {
el.src = realSrc;
el.removeAttribute("data-src");
}
}
},
// 节流
throttle(fn, delay) {
let timer;
let prevTime;
return function (...args) {
const currTime = Date.now();
const 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;
function checkArray(key) {
let arr = ["1", "2", "3", "4"];
let index = arr.indexOf(key);
if (index > -1) {
return true; // 有权限
} else {
return false; // 无权限
}
}
const permission = {
inserted: function (el, binding) {
let permission = binding.value; // 获取到 v-permission的值
if (permission) {
let hasPermission = checkArray(permission);
if (!hasPermission) {
// 没有权限 移除Dom元素
el.parentNode && el.parentNode.removeChild(el);
}
}
},
};
export default permission;
function addWaterMarker(str, parentNode, font, textColor) {
// 水印文字,父元素,字体,文字颜色
var can = document.createElement("canvas");
parentNode.appendChild(can);
can.width = 200;
can.height = 150;
can.style.display = "none";
var cans = can.getContext("2d");
cans.rotate((-20 * Math.PI) / 180);
cans.font = font || "16px Microsoft JhengHei";
cans.fillStyle = textColor || "rgba(180, 180, 180, 0.3)";
cans.textAlign = "left";
cans.textBaseline = "Middle";
cans.fillText(str, can.width / 10, can.height / 2);
parentNode.style.backgroundImage = "url(" + can.toDataURL("image/png") + ")";
}
const waterMarker = {
bind: function (el, binding) {
addWaterMarker(
binding.value.text,
el,
binding.value.font,
binding.value.textColor
);
},
};
export default waterMarker;
const draggable = {
inserted: function (el) {
el.style.cursor = "move";
el.onmousedown = function (e) {
let disx = e.pageX - el.offsetLeft;
let disy = e.pageY - el.offsetTop;
document.onmousemove = function (e) {
let x = e.pageX - disx;
let y = e.pageY - disy;
let maxX =
document.body.clientWidth -
parseInt(window.getComputedStyle(el).width);
let maxY =
document.body.clientHeight -
parseInt(window.getComputedStyle(el).height);
if (x < 0) {
x = 0;
} else if (x > maxX) {
x = maxX;
}
if (y < 0) {
y = 0;
} else if (y > maxY) {
y = maxY;
}
el.style.left = x + "px";
el.style.top = y + "px";
};
document.onmouseup = function () {
document.onmousemove = document.onmouseup = null;
};
};
},
};
export default draggable;
vue 的移动端适配我们可以参考 vant-ui 组件库给我们提供的方案。
使用 amfe-flexible(用于自动定义跟字体大小)插件和 postcss-pxtorem(用于将 px 自动转成 rem)插件
在 main.ts 里面 import “amfe-flexible”
在根目录新建 .postcssrc.js 文件
module.exports = {
plugins: {
"postcss-pxtorem": {
rootValue: 37.5,
propList: ["*"],
},
},
};
rem 是相对于跟字体的倍数,如果我们整个项目都是用 rem 作为单位,那么当我们做移动端的响应式的时候只需要去改变跟字体的大小就能做到适配。
因为 store 里的数据是保存在运行内存中的,当页面刷新时,页面会重新加载 vue 实例,store 里面的数据就会被重新赋值初始化。
所以我们可以在修改 store 的数据同时将数据再存一份到本地存储(localStorage 或者 sessionStorage),本地存储的内容是存在浏览器里面的,不会因为刷新而丢失。
我们也可以用过比如 vuex-persistedstate 这样的第三方包来帮助我们做 vuex 的持久化数据。
在创建组件的时候我们不必在写唯一的根元素。移除了 vue2 中的 filters。
新增了 renderTracked 和 renderTriggered 两个生命周期。
我们可以使用 setup 函数来使用类似 react 的自定义 hooks 的功能,主要解决逻辑关注点分离的问题。
我们有必要先了解下模板转换成视图的过程整个过程:
简单点讲,在 Vue 的底层实现上,Vue 将模板编译成虚拟 DOM 渲染函数。结合 Vue 自带的响应系统,在状态改变时,Vue 能够智能地计算出重新渲染组件的最小代价并应到 DOM 操作上。
那么 vue 操作虚拟 DOM 有什么优异的地方呢?
那么为什么比原生操作 DOM 还快呢?
首先我们每次操作 dom 的时候,都会去执行浏览器的那 5 个步骤,尤其是当大量循环的时候,每次循环完都不知道后面还要不要修改,所以每次都要去重复这个过程,引发不必要的渲染。
但是在实际开发过程中,我们会发现虚拟 dom 并没有比真实 dom 更快。这个问题尤雨溪在知乎上面有过回答:
这是一个性能 vs. 可维护性的取舍。框架的意义在于为你掩盖底层的 DOM 操作,让你用更声明式的方式来描述你的目的,从而让你的代码更容易维护。没有任何框架可以比纯手动的优化 DOM 操作更快,因为框架的 DOM 操作层需要应对任何上层 API 可能产生的操作,它的实现必须是普适的。针对任何一个 benchmark,我都可以写出比任何框架更快的手动优化,但是那有什么意义呢?在构建一个实际应用的时候,你难道为每一个地方都去做手动优化吗?出于可维护性的考虑,这显然不可能。框架给你的保证是,你在不需要手动优化的情况下,我依然可以给你提供过得去的性能。
在开发中,我们经常会遇到使用 token,token 的作用是要验证用户是否处于登录状态,所以要请求一些只有登录状态才能查看的资源的时候,我们需要携带 token。
一般的后端接口设置的 token 是有时效的,超时后就会失效,失效之后的处理策略一般会做两种处理:
1、created
表示组件实例已经完全创建,data数据已经被 Object.defineProperty 劫持完成,属性也绑定成功,但真实dom还没有生成,$el还不可用。
2、mounted
el选项所对应的视图节点已经被新创建的 vm.$el 替换,并挂载到实例上去了。此时响应式数据都已经完成了渲染。
使用 对列表页面进行包裹,被包裹的列表页面就有了activated、deactivated这两个生命周期。
在离开列表页面时,在deactivated中记录页面的滚动条位置。
再次进入列表页面时,在activated中使用 this.$el.scrollTop 把页面滚动到离开时所记录的位置。
表单设置触发事件为blur,但是ctrl+A全选以后再删除时又触发了change事件,并提示一个原始报错
trigger: ['blur', 'change']
使用el-dialog 遮罩层把显示内容遮住了
v-bind:modal-append-to-body="false"
使用el-select 不能继承父元素的宽度
<el-select style="width:100%"></el-select>
要修改elementUI组件的样式,可以采用以下两种方式
全局样式
通过选择权重覆盖elementUI组件的样式,如修改复选框为圆角:
<style>
.edit-item .el-checkbox__inner {
border-radius: 50%;
}
style>
但这种方式为全局样式,会影响页面中所有复选框,如果不希望影响其它页面的样式,可以采用第二中方式
局部样式
<style scoped>
.edit-item .el-checkbox__inner {
border-radius: 50%;
}
style>
但如果仅仅是设置了scoped属性,样式无法生效,原因是以上样式会被编译成属性选择器,而elementUI组件内部的结构却无法添加该html属性,以上样式被编译成如下代码:
.edit-item[data-v-6558bc58] .el-checkbox__inner[data-v-6558bc58] {
border-radius: 50%;
}
解决方案也很简单,只需在选择器中要添加>>>
即可
<style scoped>
.edit-item >>> .el-checkbox__inner {
border-radius: 50%;
}
style>
如果是sass或less编写的样式,还可以使用/deep/
<style scoped lang="scss">
.edit-item /deep/ .el-checkbox__inner {
border-radius: 50%;
}
style>
以上写法样式都会编译成以下样式:
.edit-item[data-v-6558bc58] .el-checkbox__inner{}
所以elementUI中的样式就能成功覆盖
key是虚拟DOM对象的标识,在更新显示时key起着极其重要的作用,vue和react都是采用diff算法来对比新旧虚拟节点,而key的作用是为了在执行 diff算法 的时候,更快更准确地找到对应的虚拟节点,从而提高diff速度。
route
和 router
是vue-router中经常会操作的两个对象, route
表示当前的路由信息对象,包含了当前 URL 解析得到的信息,包含当前的路径、参数、query对象等,一般用于获取跳转时传入的参数。 router
对象是全局路由的实例,是router构造方法的实例,一般用户路由跳转,如router.push()
、router.replace()
等方法
React与Vue都鼓励使用组件化开发。这本质上是建议你将你的应用分拆成一个个功能明确的模块,每个模块之间可以通过特定的方式进行关联。这样可以更好的管理功能模块与复用,在团队项目中也能更好的分工协作
对真实DOM的操作很慢,所以Vue和React都实现了虚拟DOM,用户对数据的修改首先反映到虚拟DOM上,而不是直接操作真实DOM,然后在虚拟DOM环节进行优化,比如只修改差异项,对虚拟 DOM 进行频繁修改时进行合并,然后一次性修改真实 DOM 中需要改的部分,最后在真实 DOM 中进行排版与重绘,减少过多DOM节点排版与重绘损耗等
Vue可以通过v-model指令实现,react可通过单向绑定+事件来实现
Vue和React都有着强大的生态,比如路由、状态管理、脚手架等,用户可以根据需求自行选择,不需要重复造轮子
我们先来看看浏览器渲染一个页面的过和,大概需要以下5个步骤
众所周知,一个页面在浏览器中最大的开销就是DOM节点操作,页面的性能问题大多数是DOM操作引起的,当我们用原生js 或jquery这样的库去操作DOM时,浏览器会从构建DOM树开始执行完以上整个流程,所以频繁操作DOM会引起不需要的计算、重排与重绘,从而导致页面卡顿,影响用户体验
所以减少DOM的操作能达到性能优化的目的,事实上,虚拟DOM就是这么做的,虚拟DOM(VirtualDOM) 的实现原理主要包括以下 3 部分:
Virtual DOM 本质上就是一个javascript对象,数据的修改会生成一个新的虚拟DOM(一个新的javascript对象),然后与旧的虚拟DOM进行对比,得到两个对象的差异项,最后只更新差异对象中的内容到真实DOM,这样能做到最少限度地修改真实DOM,从而实现性能优化
在开发中后台应用过程中或多或少都会涉及到一个问题:权限,简单地说就是让不同的用户在系统中拥有不同的操作能力。
但在实际应用中我们一般不直接将权限赋予在用户身上,因为这样操作对有大量用户的系统来说过于繁琐,所以我们一般基于RBAC(Role-Based Access Control)权限模型,引入角色的概念,通过角色的媒介过渡,先将权限赋予在角色上,再关联相应的用户,对应的用户就继承了角色的权限
用户与角色,角色与权限都是多对多的关系
引入角色媒介的优点:
所谓数据绑定,就是指View
层和Model
层之间的映射关系。
单向数据绑定:Model
的更新会触发View
的更新,而View
的更新不会触发Model
的更新,它们的作用是单向的。
优点:所有状态变化都可以被记录、跟踪,状态变化通过手动调用触发,源头易追溯。
缺点:会有很多类似的样板代码,代码量会相应的上升。
双向数据绑定:Model
的更新会触发View
的更新,View
的更新也会触发Model
的更新,它们的作用是相互的。
优点:在操作表单时使用
v-model
方便简单,可以省略繁琐或重复的onChange
事件去处理每个表单数据的变化(减少代码量)。缺点:属于暗箱操作,无法很好的追踪双向绑定的数据的变化。
Vue的双向数据绑定是通过数据劫持结合发布者订阅者模式来实现的 要实现这种双向数据绑定,必要的条件有:
补充回答
在new Vue的时候,在 Observer 中通过 Object.defineProperty()
达到数据劫持,代理所有数据的 getter 和 setter 属性,在每次触发 setter 的时候,都会通过 Dep 来通知 Watcher,Watcher 作为Observer
数据监听器与Compile
模板解析器之间的桥梁,当 Observer 监听到数据发生改变的时候,通过 Updater 来通知 Compile 更新视图,而 Compile 通过 Watcher 订阅对应数据,绑定更新函数,通过 Dep 来添加订阅者,达到双向绑定。
Proxy的优势如下
ownKeys、deleteProperty、has
等是 Object.defineProperty
不具备的。Object.defineProperty
只能遍历对象属性直接修改;Object.defineProperty 的优势如下
Object.defineProperty 不足在于:
Object.defineProperty
只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Object.defineProperty
不能监听数组。是通过重写数据的那7个可以改变数据的方法来对数组进行监听的。Object.defineProperty
也不能对 es6
新产生的 Map
,Set
这些数据结构做出监听。Object.defineProperty
也不能监听新增和删除操作,通过 Vue.set()
和 Vue.delete
来实现响应式的。就是能够自动追踪数据的变化,而不必手动触发视图更新。Vue2.X通过Object.defineProperty()做数据劫持,而Vue3通过Proxy做数据代理,从而捕捉到对数据的get和set。
简单说就是用户更改数据(Data)时,视图可以自动刷新,页面UI能够响应数据变化。
因为这是Vue的立身之本呀,连尤雨溪都在给放文档上这样说,“Vue 最独特的特性之一,是其非侵入性的响应式系统。”
Vue就是通过getter 和setter来对数据进行操作,通过get()和set()函数可以生成一个虚拟属性,来直接调用函数操作数据。这一步是被封装起来的,我们是看不到的。
此外,还需要一个重要的媒介API,那就是Object.defineProperty,因为对象被定义完成后,想要更改或添加属性(像是get和set这样的属性),只能通过这个Object.defineProperty来添加。接着需要实现对数据属性的读写进行监控。能够监控就意味着能让vm(一般生成的实例)能够知道数据有变化,从而触发render(data)函数,页面UI就会做出响应。
1.减少对真实DOM的操作
Diff 算法探讨的就是虚拟 DOM 树发生变化后,生成 DOM 树更新补丁的方式。对比新旧两株虚拟 DOM 树的变更差异,将更新补丁作用于真实 DOM,以最小成本完成视图更新。
具体流程:
这样一个生成补丁、更新差异的过程统称为 diff 算法。
这里涉及3个要点:
通过diff算法能准确的获取到具体的节点 进行针对操作!!!
因为 Vue 的响应式系统已经在初次渲染时收集了渲染依赖的数据项,通过自动的方式就能够得到相当不错的性能。不过,在一些场景下,手动控制刷新检查还是能够进一步提升渲染性能的
vue 的机制,其实就是个依赖关系管理机制。不管是计算属性,watcher,以及 renderer,站在外部看,模型都是一样的,即初始化的时候通过一些方法分析出来这个模块依赖谁,等被依赖的数据更新了,这些模块就重新运算一遍产出物(实际上不尽然,比如计算属性有惰性机制)。
具体到 renderer,哪些数据的变更会引起 dom 变更,在模板编译的时候已经确定了,并且写死在生成的代码里了。
而 react 是没有这种自动机制的,它去执行 render 唯一的原因就是你主动让他 render。那你什么时候让它 render 呢?工程上一般是使用一个数据流工具,数据有变化的时候发出一个事件,一股脑把数据推过来,不区分哪个字段有变更(你区分了也没用,renderer 根本不认)。如果这个数据流模型是多个组件共用的,那必然是在 render 之前有个 hook 给我们个机会告诉组件“这次没你的事儿”,有利于性能优化。
那么,我们有没有可能不增加代码静态分析环节,搞清楚 react renderer 到底依赖哪些数据,继而把这个判断自动做掉呢?依我看不太可能,因为我们不能保证运行期跑一遍 render,它就会一次性访问它所有可能访问的数据。
1、key的作用主要是为了搞笑的更新虚拟dom,其原理是vue在patch过程中通过key可以精准判断两个节点是否是同一个,从而避免频繁更新不同元素,使得整个patch过程更加高效,减少dom操作量,提高性能。
2、另外,若不设置key还可能在列表更新时候引发一些隐藏的bug。
3、vue中在使用相同标签名元素的过渡切换时,也会使用到key属性,其目的也是为了让vue可以区分它们,否则vue只会替换其内部属性而不会触发过渡效果。
看了上面的介绍其实也很容易明白为什么不能用index作为key。
1、影响性能:当用index作为key的时候,删除节点后面的所有节点都会导致重新渲染,因为index变化了,可以也就变化了
有人说,当你的列表数据没有变化的时候可以用index作为key。也就是说列表不会触发更新元素,只有静态展示。这种说法你怎么看呢?
之所以说到这个问题,是在vue官方群里面一群人应为这个问题讨论半天。我弱弱回复一句,任何情况下都不要用index作为key。结果遭到炮轰,哎!(除非前端写死的list,且无操作不会引起key变化,只要是后端数据,前端怎么能保证数据不变呢)。
关于这个问题,我有这样三点想法:
1、代码的规范性
2、类比typescript,为什么变量要加类型,除了规范,也方便定位错误
3、列表的顺序稳定其实是难以保证的
JSX就是Javascript和XML结合的一种格式。React发明了JSX,利用HTML语法来创建虚拟DOM。当遇到<,JSX就当HTML解析,遇到{就当JavaScript解析.
是使用的的模板语法,Vue的模板实际上就是编译成了 render 函数,同样支持 JSX 语法。在 Vue 官网中,提供 createElement 函数中使用模板中的功能。
但是极少数的 VUE项目用JSX语法 JSX语法 根植在react上面 在vue上还是 template舒服!
我们使用vue进行开发的过程中,可能会遇到一种情况:当生成vue实例后,当再次给数据赋值时,有时候并不会自动更新到视图上去;
当我们去看vue文档的时候,会发现有这么一句话:如果在实例创建之后添加新的属性到实例上,它不会触发视图更新。例如这个代码:
姓名:{{ name }}
年龄:{{age}}
性别:{{sex}}
说明:{{info.content}}
运行结果:
姓名:张三
年龄:3
性别:
说明:my name is test
为什么会这样呢?当去查对应文档时,你会发现响应系统 ,把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项,Vue 将遍历它的属性,用 Object.defineProperty 将它们转为 getter/setter,如上示例,将data在控制台打印出来会发现:
在age及name都有get和set方法,但是在sex里面并没有这两个方法,因此,设置了sex值后vue并不会自动更新视图;
解决方法:
new Vue({
el:'#app',
data: data,
created: function(){
Vue.set(data,'sex', '男'); // 等同于this.$set(data,'sex','男')
}
});
总结:
由于 Vue 会在初始化实例时进行双向数据绑定,使用Object.defineProperty()对属性遍历添加 getter/setter 方法,所以属性必须在 data 对象上存在时才能进行上述过程 ,这样才能让它是响应的。如果要给对象添加新的属性,此时新属性没有进行过上述过程,不是响应式的,所以会出现数据变化,页面不变的情况。此时需要用到$set。
语法: this.$set(Object, key, value)
需要在created里面才可以,如下代码:
{{user}}
data () {
return {
user: { },
}
},
created () {
this.user.a = 1 // 这样界面就会显示 {a:1}
},
vue
中8种常规的通信方案
props
属性,定义接收父组件传递过来的参数Children.vue
props:{
// 字符串形式
name:String // 接收的类型参数
// 对象形式
age:{
type:Number, // 接收的类型为数值
defaule:18, // 默认值为18
require:true // age属性必须传递
}
}
Father.vue
$emit触发
自定义事件,$emit
第二个参数为传递的数值Children.vue
this.$emit('add', good)
Father.vue
ref
ref
来获取数据父组件
this.$refs.foo // 获取子组件实例,通过子组件实例我们就能拿到对应的数据
EventBus
$emit
触发自定义事件,$emit
第二个参数为传递的数值$on
监听自定义事件Bus.js
// 创建一个中央时间总线类
class Bus {
constructor() {
this.callbacks = {}; // 存放事件的名字
}
$on(name, fn) {
this.callbacks[name] = this.callbacks[name] || [];
this.callbacks[name].push(fn);
}
$emit(name, args) {
if (this.callbacks[name]) {
this.callbacks[name].forEach((cb) => cb(args));
}
}
}
// main.js
Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上
// 另一种方式
Vue.prototype.$bus = new Vue() // Vue已经实现了Bus的功能
Children1.vue
this.$bus.$emit('foo')
Children2.vue
this.$bus.$on('foo', this.handle)
$parent
或者$root
搭建通信侨联兄弟组件
this.$parent.on('add',this.add)
另一个兄弟组件
this.$parent.emit('add')
$attrs
和 $listeners
prop
被识别 (且获取) 的特性绑定 ( class 和 style 除外)。v-bind="$attrs"
传⼊内部组件// child:并未在props中声明foo
{{$attrs.foo}}
// parent
// 给Grandson隔代传值,communication/index.vue
// Child2做展开
// Grandson使⽤
{{msg}}
provide
属性,返回传递的值inject
接收组件传递过来的值祖先组件
provide(){
return {
foo:'foo'
}
}
后代组件
inject:['foo'] // 获取到祖先组件传递过来的值
Vuex
作用相当于一个用来存储共享变量的容器state
用来存放共享变量的地方getter
,可以增加一个getter
派生状态,(相当于store
中的计算属性),用来获得共享变量的值mutations
用来存放修改state
的方法。actions
也是用来存放修改state的方法,不过action
是在mutations
的基础上进行。常用来做一些异步操作props
与 $emit
进行传递,也可选择ref
$bus
,其次可以选择$parent
进行传递attrs
与listeners
或者 Provide
与 Inject
vuex
存放共享的变量Vue实例从创建到销毁的过程就是Vue的生命周期。
也就是说:开始创建->初始化实例->编译模板->挂载dom->数据更新和重新渲染虚拟dom->最后销毁。这一系列的过程就是Vue的生命周期。
beforeCreate:vue实例的挂载元素el和数据对象data还没有初始化,还是一个undefined的一个状态。
created:这个时候Vue实例的数据对象data已经有了,可以访问里面的数据和方法,el还没有,也没有挂载dom
beforeMount:在这里Vue实例的元素el和数据对象都有了,只不过在挂载之前还是虚拟的dom节点。
mounted:Vue实例已经挂载在真实的dom节点上了,可以对dom进行操作来获取dom节点。
beforeUpdate:响应式数据更新时调用,发生在虚拟dom打补丁之前,适合在更新之前访问现有的dom。
updated:虚拟dom重新渲染和打补丁之后调用,组成新的dom已经更新,避免在这个钩子函数种操作数据,防止死循环。
beforeDestory:Vue实例在销毁之前调用,在这里可以使用,通过this也能访问到实例,可以在这里对一些不用的定时器进行清除,解绑事件。
destoryed:vue销毁之后调用,调用之后所有的事件监听都会被移除,所有的实例都会被销毁。
beforeMount:在这里Vue实例的元素el和数据对象都有了,只不过在挂载之前还是虚拟的dom节点。
挂载前 :完成模板编译,虚拟Dom已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发updated。数据还没有更新到页面上去。当编译完成之后,只是在内存中已经有了编译好的页面,但并未渲染。
mounted:Vue实例已经挂载在真实的dom节点上了,可以对dom进行操作来获取dom节点。
挂载完成 : 将编译好的模板挂载到页面 (虚拟DOM挂载) ,可以在这进行异步请求以及DOM节点的访问,在vue用$ref操作
在vue项目中,难免会有列表页面或者搜索结果列表页面,点击某个结果之后,返回回来时,如果不对结果页面进行缓存,那么返回列表页面的时候会回到初始状态,但是我们想要的结果是返回时这个页面还是之前搜索的结果列表,这时候就需要用到vue的keep-alive技术了.
keep-alive 简介
keep-alive
是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染。
实际项目中,需要配合vue-router共同使用
router-view
也是一个组件,如果直接被包在 keep-alive
里面,所有路径匹配到的视图组件都会被缓存:
如果只想 router-view
里面某个组件被缓存,怎么办?
增加 router.meta 属性
// routes 配置
export default [
{
path: '/',
name: 'home',
component: Home,
meta: {
keepAlive: true // 需要被缓存
}
}, {
path: '/:id',
name: 'edit',
component: Edit,
meta: {
keepAlive: false // 不需要被缓存
}
}
]
(1)如果列表是纯粹的数据展示,不会有任何改变,就不需要做响应话, 使用object.freeze(data)
(2)如果是大数据长列表,可采用虚拟滚动,只渲染少部分区域的内容,参考vue-virtual-scroller、vue-virtual-scroll-list
vue组件销毁时,会自动解绑它的全部指令及事件监听,但是仅限于组件本身的限制
像element-ui这样的第三方组件库可以按需引入避免体积太大。
Vue的单页面模式进行开发的项目,网站信息搜索引擎无法做索引,导入收录只有是首页!
搜索引擎无法进行索引的核心原因就是,其在爬取网站数据的时候,是不会执行其中包含的JS过程的;
而采用Vue的方式开发的应用,其数据都是来源于axios或者其它的ajax方法获取的数据!也就是说,想要友好的支持搜索引擎,就必须采用服务器端渲染的相关技术,比如JSP,就是一个典型的服务器端渲染技术,用户请求一个地址然后展示到浏览器中的数据都是服务器端处理好的,浏览器只管展示;又比如静态页面,所有页面都是预先编写或生成好的,浏览器将请求拿到的数据直接展现即可。
所以为了实现seo的优化,我们可以采用比较成熟的服务端SSR的框架Nuxt实现。
当我们在vue中引入第三方组件库的时候,vue组件中样式的scoped就会成为我们修改样式的阻碍,有以下三种方法修改样式,并且不影响全局样式.
在样式外新增一个样式标签不添加scoped
使用deep样式穿透
使用>>>穿透
? 首先vue和原生js都可以用来开发web项目,但越来越多的项目在立项及技术选型时选择使用vue,主要是有以下优势.
控件跟数据自动绑定,可以直接使用data里面的数据值来提交表单,而不需要再使用$(“#myid”).val()那一套方法来获取控件的值,对控件赋值也方便很多,只需要改变data的值,控件就会自动改变值。将复杂的界面操作,转化为对数据进行操作.
页面参数传递和页面状态管理.
页面传值对于vue来说,可供选择的方法非常多。比如使用子组件实现,通过对props属性传值;也可以使用页面url参数的方法传值;或使用vuex全局状态管理的方法页面传值等等。而原生开发的时候,在页面有多个参数的时候,页面传值和初始化,要复杂很多。而vue直接将参数保存在对象里面,直接给子组件的属性或vuex存储一个对象就行了,比如 , 这样就可以将userinfo传到自定义组件。
模块化开发、无刷新保留场景参数更新
比如一个列表页面里面有添加功能,有修改功能,这时候我们可以通过引用子组件的形式,当子组件内容更新的时候,修改主组件的数据,比如修改了一条数据后,我们需要列表页同时刷新,但我们不希望改变原来列表页的页码和搜索条件。假如你用原生开发来实现这个,需要写很多业务逻辑保存上一个页面的搜索条件和页码这些参数,但假如你用vue开发,将变得非常简单.
代码的可阅读性
vue天生具有组件化开发的能力,因此不同的功能基本都是写在不同的模块里面,因此代码的可阅读性非常高。当一个新手接手一个旧项目的时候,基本上可以做到一天就能定位到要修改的代码,进行修改,快速接手项目.
丰富的生态圈
基于强大的nodejs,添加新的组件库,基本一句npm命令就能安装,比如当我需要使用axios组件的时候,直接npm install axios安装一下,就可以使用axios这个组件。
开发单页面应用非常友好
主路由、子路由、主页面、子组件的方式,可以让我们彻底抛弃iframe。写过前端页面的都知道,因为iframe的滚动条、和子页面跟其他页面的交互性这些原因、用户体验还是远远没有单页面架构友好。而且使用vue非常简单方便的实现系统菜单、导航等固定布局.
各子组件样式不冲突
各个组件之间,可以使用相同的样式名,但有不同的样式属性。比如组件A和组件B的button都绑定了class=“btn”, 但在两个组件里,我们可以实现两个不同的btn样式属性,互不影响
当然,vue也有不足,不足的地方如下:
vue是单页面页面,对于搜索引擎不友好,影响seo.因此不适合做公司官网。比如两个vue路由(页面),它的路径是这样的:index.html#aaa 和 index.html#bbb,但对于搜索引擎来说,都是同一个页面,就是index.html。这样搜索引擎就无法收录你的页面。
vue门槛较高,使用vue,需要先学习和摸索vue大概3天左右的时候,建议使用vue的时候,不需要看node.js自动帮你生成的js文件。你只需要编写你自己页面的代码就行了。
table表格组件的属性有:
为什么table组件要用key?
row-key属性用来优化 Table 的渲染, 如果不添加row-key,当重新请求渲染表格数据,会默认触发@current-change等等方法,因为重新渲染列表,current-change发生了改变,原本选择的row,变成不再选择任何一行,导致问题很难排查.
参考官网: table组件