大厂面试题分享 面试题库
前后端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库 web前端面试题库 VS java后端面试题库大全
VUE
Vue2和3对比
脚手架创建项目
之前有个国企,问到了怎么用脚手架创建vue项目。
2.x
npm install -g vue-cli (npm uninstall -g vue-cli 删)
vue init webpack "项目名称"
进入项目 npm run dev
3.x
npm install -g @vue/cli (npm uninstall -g @vue/cli 删)
或者npm install vue@next(最新最稳定版)
npm install -g @vue/cli-init
vue create 项目名称
进入项目 npm run serve
vue3新特性
- 支持碎片Fragments,模板可以有多个根元素
- 提供了composition api,更好的逻辑复用与代码组织
- 响应式数据声明方式改变
- 生命周期的改变
- 父子组件传参不同
- vue3 Teleport瞬移组件(类似于react的Portals传送门)
- vue3中v-for与v-if,只会把当前v-if当做v-for中的一个判断语句,不会相互冲突
- vue3中移除keyCode作为v-on的修饰符,当然也不支持config.keyCodes
- vue3中移除过滤器filter
- computed和watch变成组合式的
- 数据响应重新实现(ES6的proxy代替Es5的Object.defineProperty)
- 源码使用ts重写,更好的类型推导
- 虚拟DOM新算法(更快,更小)
一.响应原理对比
vue2使用Object.defineProperty方法实现响应式数据
缺点:
- 无法检测到对象属性的动态添加和删除
- 无法检测到数组的下标和length属性的变更
- 深度监听需要递归到底,性能层面考虑不太好
解决方案:
- vue2提供Vue.$set动态给对象添加属性
- Vue.$delete动态删除对象属性
- 也可以通过splice解决数组中的问题,object.assign解决批量添加对象属性的问题
Object.defineProperty还存在一个缺点:不能检测数组变化。2.x中是通过重写数组方法实现的对数组的监听。
vue3使用proxy实现响应式数据
优点:
- 可以检测到代理对象属性的动态新增和删除
- 可以检测数组的下标和length属性的变化
缺点:
- es6的proxy不支持低版本浏览器 IE11
- 会针对IE11出一个特殊版本进行支持
- 无法polyfill
二. Composition API
Vue2使用选项类型API(Options API):选项型API在代码里分割了不同的属性: data,computed属性,methods,等等。
Vue3使用合成型API(Composition API):新的合成型API能让我们用方法(function)来分割,相比于旧的API使用属性来分组,这样代码会更加简便和整洁。
// 2.0 export default { props: { title: String }, data() { return { username: '', password: '' } }, methods: { login() { // 登陆方法 } }, components: { "buttonComponent": btnComponent }, computed: { fullName() { return this.firstName + " " + this.lastName; } } } // 3.0 export default { props: { title: String }, setup() { const state = reactive({ //数据 username: '', password: '', lowerCaseUsername: computed(() => state.username.toLowerCase()) //计算属性 }) //方法 const login = () => { // 登陆方法 } return { login, state } } }
composition API 解决了什么问题
options API
- 条例清晰:相同的放在相同的地方,比如方法都放在methods中,状态都在data中
- 调用时使用this,逻辑过多时this指向不明确
- 代码分散:一个功能的代码往往散落在不同的options种,比如data methods等等,这也导致新添加功能的时候需要在各种options中反复横跳,这时候如果代码行数较多那是要命的
- 逻辑过于复杂的场景可以将某个功能代码抽象出mixin,但是这会导致数据来源不明确,在template中有个count变量,你会不知道他到底是来源于data还是mixin还是vue.prototype设置的全局变量。除此之外,如果存在多个mixin还可能存在同名变量被覆盖的问题。
composition API
- 将一个功能的代码整合到一起,方便开发的同时也便于代码复用。总之就是更好的代码组织方式和更好的代码复用。
- 没有对this的使用,避免了指向不明确的情况
- 全部都是函数,更加方便类型推断
三.建立数据 data
VUE2.0中将数据放入到data属性中,在VUE3.0中使用setup()方法,此方法在组件初始化构造的时候触发。
使用以下三步来建立响应式数据:
- 从vue引入reactive
- 使用reactive()方法来声名我们的数据为响应性数据
- 使用setup()方法来返回我们的响应性数据,从而template可以获取这些响应性数据
可以通过state.username和state.password获得数据的值。
<template> <div> <h2> {{ state.username }} </h2> </div> </template>
四.碎片
// 2.0 <template> <div class='form-element'> <h2> {{ title }} </h2> </div> </template> // 3.0 <template> <div class='form-element'> </div> <h2> {{ title }} </h2> </template>
五.生命周期钩子函数改变
Vue2--------------vue3 beforeCreate -> setup()开始创建组件之前,在beforeCreate和created之前执行。创建的是data和method created -> setup() beforeMount -> onBeforeMount 组件挂载到节点上之前执行的函数。 mounted -> onMounted 组件挂载完成后执行的函数。 beforeUpdate -> onBeforeUpdate 组件更新之前执行的函数。 updated -> onUpdated 组件更新完成之后执行的函数。 beforeDestroy -> onBeforeUnmount 组件卸载之前执行的函数。 destroyed -> onUnmounted 组件卸载完成后执行的函数 activated -> onActivated 被包含在中的组件,会多出两个生命周期钩子函 数。被激活时执行 。 deactivated -> onDeactivated 比如从 A组件,切换到 B 组件,A 组件消失 时执行。
六.父子传参
- setup 函数时,它将接受两个参数:(props、context(包含attrs、slots、emit))
- setup函数是处于生命周期函数 beforeCreate 和 Created 两个钩子函数之前的函数
- 执行 setup 时,组件实例尚未被创建(在 setup() 内部,this 不会是该活跃实例的引用,即不指向vue实例,Vue 为了避免我们错误的使用,直接将 setup函数中的this修改成了 undefined)
- 与模板一起使用:需要返回一个对象 (在setup函数中定义的变量和方法最后都是需要 return 出去的不然无法在模板中使用)
- 使用渲染函数:可以返回一个渲染函数,该函数可以直接使用在同一作用域中声明的响应式状态
- setup 函数中的 props 是响应式的,当传入新的 prop 时,它将被更新。但是,因为 props 是响应式的,你不能使用 ES6 解构,因为它会消除 prop 的响应性。如果需要解构 prop,可以通过使用 setup 函数中的toRefs 来完成此操作
父传子
所以当父组件向子组件中传递,和2.x版本中没有太多区别,但是如果需要从props中派生处数据时,需要从setup函数中接收。
// 父组件 <template> <Son :msg="state.msg"/> </template> <script> import Son from "@/components/transParams/Son"; import {reactive} from "vue"; export default { name: "Parent", components: {Son}, setup(){ const state =reactive({ msg:'父组件传递给子组件的参数' }); return { state } } } </script> <style scoped> </style> // 子组件 <template> <div>这里是子组件啦</div> <div>这里是父组件传递过来的值呀:{{msg}}</div> </template> <script> export default { name: "Son", props:{ msg:{ type:String, default:'' } }, setup(props){ console.log(props) //Proxy {msg: '父组件传递给子组件的参数'} } } </script> <style scoped> </style>
子传父(emit event)
子组件向父组件传递至差别比较大:
//父组件 <template> <div>这里是父组件啦</div> <div>这里是子组件传递过来的值呀:{{state.sonMsg}}</div> <hr/> <!-- 子传父:定义子组件emit时的函数sonSendMsg,并且绑定到父组件中注册的receiveMessageFromSon函数 --> <Son @sonSendMsg="receiveMessageFromSon" :msg="state.msg"/> </template> <script> import Son from "@/components/transParams/Son"; import {reactive} from "vue"; export default { name: "Parent", components: {Son}, setup(){ const state =reactive({ msg:'父组件传递给子组件的参数', sonMsg:'' }); //子传父:定义接收函数 const receiveMessageFromSon =(data)=>{ state.sonMsg=data.sonMsg } return { state, receiveMessageFromSon } } } </script> <style scoped> </style> // 子组件 <template> <div>这里是子组件啦</div> <button @click="sendMsgToParent">向父组件传递数据</button> <div>这里是父组件传递过来的值呀:{{msg}}</div> </template> <script> export default { name: "Son", //差别一:子组件向父组件传值要注册emits emits:['sonSendMsg'], props:{ msg:{ type:String, default:'' } }, setup(props,{ attrs, slots, emit }){ const sendMsgToParent =()=>{ // 差别二:向父组件传递参数:不再通过this.$emit触发函数 emit('sonSendMsg', { sonMsg:'子组件传递过来的数据' }) }; return { sendMsgToParent } } } </script> <style scoped> </style>
setup函数只能是同步的不能是异步的
七.vue3 Teleport瞬移组件
类比react中的传送门将组件挂载到想挂载的DOM上。
比如创建一个modal组件:
<template> <teleport to="#modal"> <div id="center" v-if="isOpen"> <h2><slot>this is a modal</slot></h2> <button @click="buttonClick">Close</button> </div> </teleport> </template> <script> export default { name: "TeleportComponent", props: { isOpen: Boolean, closeModal:Function }, emits: { 'closeModal': null }, setup(props, context) { const buttonClick = () => { context.emit('closeModal') } return { buttonClick } } } </script> <style scoped> #center { width: 200px; height: 200px; border: 2px solid black; background: white; position: fixed; left: 50%; top: 50%; margin-left: -100px; margin-top: -100px; } </style>
使用方法如下:
<template> <div id="modal">这是modal即将挂载的元素</div> <button @click="showModal">点击我,打开Modal</button> <teleport-component :isOpen="isModalOpen" :closeModal="closeModal" /> </template> <script> import TeleportComponent from '@/components/TeleportComponent.vue'; import { ref } from 'vue' export default { name: "TeleportPage", components:{TeleportComponent}, setup(){ const isModalOpen = ref(false) const closeModal = function(){ isModalOpen.value=false }; const showModal = function(){ isModalOpen.value = true } return { closeModal,isModalOpen,showModal } } } </script> <style scoped> </style>
使用方式有两个,第一个是在app.vue中使用,但是这样存在的问题是:modal是在app的 DOM节点之下的,父节点的dom结构和css都会给modal产生影响。
为了避免这个问题,我们可以在public文件夹下的index.html中增加一个节点
,这样可以看到modal组件就是没有挂载在app下,不再受app组件的影响了
八.computed和watch
//2.0中 computed:{ //计算属性 _suming(){ return parseInt(this.one)+parseInt(this.two) }, dataTimeing(){ console.log("计算属性方法"); // return "计算属性方法"+new Date() return "普通方法"+this.time } }, watch: { userName: { handler(val,res){ console.log(val); console.log(res); }, immediate:true, deep:true }, } //3.0中 <template> <section>my firstName is : {{ nameObj.firstName }}</section> <section>my lastName is : {{ nameObj.lastName }}</section> <section>my fullName is : {{ fullName }}</section> <p>my desc : {{ nameObj.desc }}</p> <button @click="changeLastName">click me to add a '!' to lastName</button> <p>打开控制台可以查看watch的情况</p> <button @click="changeHobby">修改hobby,测试深度监听</button> </template> <script> import { computed, reactive, watch } from 'vue' export default { name: 'ComputedAndWatch', setup() { const nameObj = reactive({ firstName: 'Forever', lastName: 'Young', hobby: { ball: { isLike: false } } }); // 计算属性 const fullName = computed(() => { return `${nameObj.firstName}_${nameObj.lastName}` }); nameObj.desc = computed({ get() { return `my name is ${nameObj.firstName}_${nameObj.lastName}` }, // 此处的set好像没生效 set(value) { console.log('set', value) } }) const changeLastName = function () { nameObj.lastName = nameObj.lastName + '!' } // 监听属性 // 可以深度监听到isLike属性的变化 watch(nameObj, (newVal) => { console.info('watch nameObj 改变了', newVal) }) // 只有更改nameObj.lastName会触发这个watch watch(()=>nameObj.lastName, (newVal) => { console.info('watch nameObj.lastName', newVal) }) const changeHobby = function () { nameObj.hobby.ball.isLike = !nameObj.hobby.ball.isLike } return { nameObj, fullName, changeLastName, changeHobby } } } </script>
如果需要监听深度属性怎么办呢,我们都知道reactive是响应式数据属性,如果这个属性是对象,那么我们就可以开启深度监听
注意:我理解这不应该说成是深度监听,只能说是监听某个属性。由上述代码可以看出直接监听nameObj就可以监听到他内部isLike属性的变化,我觉得这已经足够深度监听了
//第一种 watch(()=> names.job.salary,(newValue,oldValue)=>{ console.log('names改变了',newValue,oldValue) }) //第二种 watch(()=> names.job,(newValue,oldValue)=>{ console.log('names改变了',newValue,oldValue) },{deep:true})
十.defineProperty实现响应式
// 触发更新视图 function updateView() { console.log('视图更新啦啦啦') } // -------------------------处理数组start------------------- // 重新定义数组原型 const oldArrayProperty = Array.prototype // 创建新的对象,原型指向oldArrayProperty,在扩展新的方法不会影响原型 // Array.prototype.push=function(){} 会污染全局 const arrProto = Object.create(oldArrayProperty); //重写数组方法,弥补不能监听数组变化的缺陷 ['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodsName => { arrProto[methodsName] = function () { updateView()//更新视图 oldArrayProperty[methodsName].call(this, ...arguments) } }) // -------------------------处理数组end------------------- // 重新定义属性,监听起来 function defineReactive(target, key, value) { // 深度监听-递归处理,在性能上有影响 observer(value) Object.defineProperty(target, key, { get() { return value }, set(newVal) { if (newVal !== value) { // 设置新值,深度监听 observer(newVal) value = newVal // 触发视图更新 updateView() } } }) } // 监听对象属性 function observer(target) { if (typeof target !== 'object' || target === null) { // 不是对象或者数组 return target } // -------------------------处理数组start------------------- if (Array.isArray(target)) { target.__proto__ = arrProto } // -------------------------处理数组end------------------- // 重新定义属性,for-in可以遍历数组 for (let key in target) { defineReactive(target, key, target[key]) } } // 准备数据 const data = { name: 'jerry', age: 18, info: { address: 'beijing'//深度监听 }, nums: [1, 2, 3, 4] } // 监听数据 observer(data) // 测试 // data.name = 'tom' // console.log('更新后名字',data.name) // data.age = 20 // // 深度监听 // data.info.address = '唐山' // // 设置新值深度监听 // data.age = { num: 21 } // data.age.num = 22 // delete data.name// 删除属性监听不到 // data.x = '新增属性'//新增属性监听不到 // 数组监听 data.nums.push(5)
十一.proxy实现响应式
function reactive(target = {}) { if (typeof target !== 'object' || target == null) { // 不是数组或者对象 return target } // 代理配置 const proxyConf = { get(target, key, receiver) { // 只处理非原型的属性 const ownKeys = Reflect.ownKeys(target) if (ownKeys.includes(key)) { console.log('get', key) } const result = Reflect.get(target, key, receiver) console.log('get', key) // return result //返回结果 //深度监听 return reactive(result) }, set(target, key, val, receiver) { // 不重复修改数据 const oldVal = target[key] if (oldVal === val) { return true } // 区别已有的key还是新增的key const ownKeys = Reflect.ownKeys(target) if (ownKeys.includes(key)) { console.log('已有的key', key) } else { console.log('新增的key', key) } const result = Reflect.set(target, key, val, receiver) console.log('set', key, val) console.log('set-result', result) return result //是否设置成功 }, deleteProperty(target, key) { const result = Reflect.deleteProperty(target, key) console.log('deleteProperty', key) console.log('delete result', result) return result //是否删除成功 } } // 生成代理对象 const observed = new Proxy(target, proxyConf) return observed } // 测试数据 const data = { name: 'jerry', age: 18, info: { address: 'beijing' } } // const data = [1, 2, 3] const proxyData = reactive(data)
vue3中ref与reactive
使用ref例子如下:
<template> <p>姓名:{{ name }}</p> <p>年龄:{{ age }}</p> <p>职业:{{ job.occupation }}</p> <p>薪资:{{ job.salary }}</p> <button @click="change">修改薪资和年龄</button> <button @click="deepChange">检测深度监听</button> </template> <script> import { is } from '@babel/types' import { ref,watch } from 'vue' export default { name: 'RefAndReactive', setup() { let name = ref('中介') let age = ref(18) let job = ref({ occupation: '程序员', salary: '10k', up:{ isUp:true } }) // 直接监听job监听不到isUp的变化 watch(()=>job.value.up.isUp,(newVal)=>{ console.log('watch',newVal) }) //方法 function change() { job.value.salary = '12k' age.value = 19 } function deepChange(){ job.value.up.isUp = !job.value.up.isUp } return { name, age, job, change, deepChange } } } </script>
ref与reactive区别
- ref定义的是基本数据类型
- ref通过Object.defineProperty()的get和set实现数据劫持(如果参数是对象类型时,其实底层的本质还是reactive,系统会自动根据我们给ref传入的值转换成reactive)
- ref操作数据.value,读取时不需要.value
- reactive定义对象或数组数据类型
- reactive通过Proxy实现数据劫持
- reactive操作和读取数据不需要.value
- 监听深度不同
$nextTick 实现原理
nextTick
存在的原因是Vue
在更新 DOM
时是异步执行的。只要侦听到数据变化,Vue
将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue
刷新队列并执行实际 (已去重的) 工作。Vue
在内部对异步队列尝试使用原生的 Promise.then
、MutationObserver
和 setImmediate
,如果执行环境不支持,则会采用 setTimeout(fn, 0)
代替。
nextTick
具体实现过程为:
- 将回调函数添加到callbacks中等待执行
- 将执行函数放入到宏任务(
setImediate或者setTimeout
)或者微任务(promise,mutationObserver
)中 - 事件循环到了微任务或者宏任务,执行函数依次执行callbacks中的回调
可以看出来nextTick
是对setTimeout
进行了多种兼容性的处理,宽泛的也可以理解为将回调函数放入setTimeout
中执行;不过nextTick
优先放入微任务执行,而setTimeout
是宏任务,因此nextTick
一般情况下总是先于setTimeout
执行
源码如下
let pending = false; let callbacks = []; //存放的是回调函数,存放的第一个回调函数是数据更新的回调函数 let timerFunc = null; //调用this.$nextTick时执行的函数 function nextTick(cb, ctx) { var _resolve; // 将回调函数已添加到callbacks callbacks.push(function () { if (cb) { try { cb.call(ctx); } catch (e) { handleError(e, ctx, 'nextTick'); } } else if (_resolve) { _resolve(ctx); } }); //在数据首次修改时,pending为false,修改后,pending变成true if (!pending) { pending = true; //在这里用到了事件循环 timerFunc(); } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(function (resolve) { _resolve = resolve; }) } } //定义eventLoop函数---借助promise,mutationObserver,setImediate或者setTimeout将函数添加到事件循环的宏任务或者微任务中 // 判断是否支持promise if (typeof Promise !== 'undefined' && isNative(Promise)) { var p = Promise.resolve(); timerFunc = function () { p.then(flushCallbacks); if (isIOS) { setTimeout(noop); } }; isUsingMicroTask = true; } else if (!isIE && typeof MutationObserver !== 'undefined' && ( // 判断是否支持MutationObserver isNative(MutationObserver) || // PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]' )) { var counter = 1; var observer = new MutationObserver(flushCallbacks); var textNode = document.createTextNode(String(counter)); observer.observe(textNode, { characterData: true }); timerFunc = function () { counter = (counter + 1) % 2; textNode.data = String(counter); }; isUsingMicroTask = true; } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { // 判断是否支持setImmediate timerFunc = function () { setImmediate(flushCallbacks); }; } else { // 前几项都不支持则使用setTimeout timerFunc = function () { setTimeout(flushCallbacks, 0); }; } //清空事件队列中的回调函数,第一个回调函数是flushSchedulerQueue () function flushCallbacks() { pending = false; var copies = callbacks.slice(0); callbacks.length = 0; for (var i = 0; i < copies.length; i++) { copies[i](); } } //flushSchedulerQueue的核心代码,执行数据更新操作 function flushSchedulerQueue() { currentFlushTimestamp = getNow(); flushing = true; var watcher, id; queue.sort(function (a, b) { return a.id - b.id; }); for (index = 0; index < queue.length; index++) { watcher = queue[index]; if (watcher.before) { //调用beforeUpdate()钩子函数 watcher.before(); } id = watcher.id; has[id] = null; //执行更新 watcher.run(); } }
v-for中没有Key和有key两种情况,diff算法怎么比较
diff
diff
算法主要进行vDOM
的对比,找出差异,减少不必要的dom
操作。
两个 js 对象或者两棵树都可以进行diff,但是两棵树的diff复杂度达到o(n^3),基本上不可用。
所以需要优化:
- 只对比同一层的节点,不跨级比较
- tag不同则删除重建,不进行深层
- tag和key都相同,认为是同一节点,不深度比较
用到的重要函数
- h函数,生成VNode
- patch函数,接受两个参数,第一个是
VNode
或者element
,第二个是VNode
sameVnode
函数,接受两个vnode
,判断两者的key和sel
(即tag)是否相同,返回boolean值
patch函数
- 第一个参数不是
vnode
, 创建一个空的vnode
并关联到这个element
- 两个参数都是
vnode
:
- 两个
VNode
相同(sameVnode
返回true):patchVnode
对比节点文本变化或子节点变化 - 两个
VNode
不同:创建新的VNode
,插入到VDom
pacthVnode
主要作用是对比两个VNode
。
old === new return
new.text === undefined
(这基本上意味着新节点的children不为空)
- 旧节点有children:
updateChildren
- 旧节点没有children:旧节点的text设置为空,然后
addNodes
添加children - 旧节点有text:清空text
new.text !== undefined
(新节点的children不存在)
old.text !== new.text
删除旧节点的children,设置新的节点的text
updateChildren
维持四个指针:
- 新children的开始
- 新children的结束
- 旧children的开始
- 旧children的结束
两两对比(sameVnode
):
- 新的开始和旧的开始
- 新的开始和旧的结束
- 新的结束和旧的结束
- 新的结束和旧的开始
- 以上四个判断命中的话:
pacthVnode
并且移动指针 - 以上四个均未命中:
- 对比当前新节点的key是否对应旧节点的某个key
- 没对应上:创建新的
vnode
并插入 - 对应上了:找到对应旧节点,看
sel
是否相同
- 不相同就创建新的
vnode
并且插入 - 相同:
patchVnode
在没有 Key 的情况下。sameVnode
下的 key 都是 undefined ,所以是相同的
答案
先说出diff本来已经存在以及遍历两棵树时,时间复杂度的问题以及优化。
vue维持四个指针相互进行比较,比较过程通过key和tag判断节点是否相同。
有key的情况下:key不同则认为两节点不同,没必要比较子节点直接销毁重建;
没有key时:明明两个节点不同但是由于他们的key都是undefined,此时如果tag相同,那么就会认为是相同节点,会去比较深层的节点
keep-alive实现原理
keep-alive组件的props维持includes,excludes和max分别表示要缓存的组件,不缓存的组件和缓存的最大值。
在created里边初始化cache和keys来存储已经保存的vnode和响应的keys。
在mounted里边监听includes和excludes 来实时更新cache和keys。
渲染的时候首先会找到keep-alive第一个子组件对象以及他的name,然后检查:是否满足不在include或者在exclude中
(也就是检查是否满足不需要缓存组件),如果满足的话直接返回vnode,不满足的话(需要缓存)根据tag和ID生成缓存key,检查是否已经被缓存过,如果已经缓存过那就取出缓存并且更新key在keys中的位置,以便达到max时做资源置换,如果没缓存过,那就缓存到cache中,然后检测已缓存的实例对象是否达到max,已经达到的话按照LRU置换策略舍弃掉最近最不常用的那个(index===0的那个),keep-alive设置为true。
vue-router和window.location跳转的区别(对浏览器来讲)
- vue-router使用pushState进行路由更新,静态跳转,页面不会重新加载;location.href会触发浏览器页面重新加载一次
- vue-router使用diff算法,实现按需加载,减少dom操作
- vue-router是路由跳转或同一个页面跳转;location.href是不同页面间跳转;
- vue-router是异步加载this.$nextTick(()=>{获取url});location.href是同步加载
Vue如何进行组件封装
组件封装基本上需要考虑三个方面:
- props:外部传递给组件的数据
- 事件:组件触发外部的方法
- slot:外部注入到组件的视图
具体使用:
- 定义组件,像普通页面那样;
- 局部使用:在使用的页面中import,之后在components中注册;
全局使用:在main.js
中引入,然后使用Vue.component(组件名,组件)
双向数据绑定的机制
Vuejs的数据驱动是通过MVVM这种框架来实现的。MVVM框架主要包含3个部分:model、view和 viewModel。
- Model:指的是数据部分,对应到前端就是javascript对象
- View:指的是视图部分,对应前端就是dom
- ViewModel:就是连接视图与数据的中间件
ViewModel是实现数据驱动视图的核心,当数据变化的时候,ViewModel能够监听到这种变化,并及时的通知view做出修改。同样的,当页面有事件触发时,ViewModel也能够监听到事件,并通知model进行响应。ViewModel就相当于一个观察者,监控着双方的动作,并及时通知对方进行相应的操作。
首先,vuejs在实例化的过程中,会对遍历传给实例化对象选项中的data 选项,遍历其所有属性并使用 Object.defineProperty 把这些属性全部转为 getter/setter。
同时每一个实例对象都有一个watcher实例对象,他会在模板编译的过程中,用getter去访问data的属性,watcher此时就会把用到的data属性记为依赖,这样就建立了视图与数据之间的联系。当之后我们渲染视图的数据依赖发生改变(即数据的setter被调用)的时候,watcher会对比前后两个的数值是否发生变化,然后确定是否通知视图进行重新渲染。这样就实现了所谓的数据对于视图的驱动。
一、vue-cli工程技术集合介绍
1. 构建的vue-cli工程都用到了哪些技术,它们的作用是什么?
vue.js:vue-cli工程的核心,主要特点是数据双向绑定和组件系统 vue-router:vue官方推荐使用的路由框架 vuex:专为Vue.js应用项目开发的状态管理器,主要用于维护vue组件间共用的一些变量和方 法。 asios:用于发起GET或POST等的http请求,基于Promise 设计 创建一个eenit.js文件,用于vue事件机制的管理 webpack:模块加载和vue-cli工程打包器
2. vue-cli工程中常用的npm命令有哪些?
下载node_modules资源包的命令:npm install
启动vue-cli开发环境的npm命令:npm run dev
vue-cli生成生产环境部署资源的npm命令:npm run build
用于查看vue-cli生产环境部署资源文件大小的npm命令:npm run build --report
会在浏览器上自动弹出一个展示vue-cli工程打包后app.js、manifest.js、vendor.js文件里面所包含代码的页面。可以具此优化vue-cli生产环境部署的静态资源,提升页面的加载速度。
二、vue-cli工程目录结构介绍
1.请说出vue-cli工程中每个文件夹和文件的用处
vue-cli目录解析:
- build 文件夹:用于存放 webpack 相关配置和脚本。开发中仅偶尔使用 到此文件夹下webpack.base.conf.js 用于配置 less、sass等css预编译库,或者配置一下 UI 库。
- config 文件夹:主要存放配置文件,用于区分开发环境、线上环境的不同。 常用到此文件夹下config.js 配置开发环境的端口号、是否开启热加载或者 设置生产环境的静态资源相对路径、是否开启gzip压缩、npm run build 命令打包生成静态资源的名称和路径等。
- dist 文件夹:默认 npm run build 命令打包生成的静态资源文件,用于生产部署。
- node_modules:存放npm命令下载的开发环境和生产环境的依赖包。
- src: 存放项目源码及需要引用的资源文件。
- src下assets:存放项目中需要用到的资源文件,css、js、images等。
- src下componets:存放vue开发中一些公共组件:header.vue、footer.vue等。
- src下emit:自己配置的vue集中式事件管理机制。
- src下router:vue-router vue路由的配置文件。
- src下service:自己配置的vue请求后台接口方法。
- src下page:存在vue页面组件的文件夹。
- src下util:存放vue开发过程中一些公共的.js方法。
- src下vuex:存放 vuex 为vue专门开发的状态管理器。
- src下app.vue:使用标签渲染整个工程的.vue组件。
- src下main.js:vue-cli工程的入口文件。
- index.html:设置项目的一些meta头信息和提供用于挂载 vue 节 点。
- package.json:用于 node_modules资源部 和 启动、打包项目的 npm 命令管理。
3. 请你详细介绍一些 package.json 里面的配置
- scripts:npm run xxx 命令调用node执行的 .js 文件
- dependencies:生产环境依赖包的名称和版本号,即这些依赖包都会打包进生产环境的JS文件里面
- devDependencies:开发环境依赖包的名称和版本号,即这些依赖包只用于代码开发的时候,不会打包进生产环境js文件里面。
4. public和assets文件夹的区别
相同点:两者中的资源都可以被html使用
不同点:
若把图片放在assets和public中,html页面都可以使用,但是在动态绑定中,assets路径的图片会加载失败(因为webpack使用的是commenJS规范,必须使用require才可以)。
public放不会变动的文件,public建议放一些外部第三方资源。
public目录下的文件并不会被Webpack处理:它们会直接被复制到最终的打包目录(默认是dist/static)下。必须使用绝对路径引用这些文件。
assets放可能会变动的文件, 自己的文件放在assets。
assets目录中的文件会被webpack处理解析为模块依赖,只支持相对路径形式。
通过 webpack 处理会有如下好处:
- 脚本和样式表会被压缩且打包在一起,从而避免额外的网络请求。
- 文件丢失会直接在编译时报错,而不是到了用户端才产生 404 错误。
- 最终生成的文件名包含了内容哈希,因此你不必担心浏览器会缓存它们的老版本。
三、Vue.js核心知识点高频试题一
1. Vue.js的两个核心是什么?
- 数据驱动,也叫双向数据绑定
Vue.js数据观测原理在技术实现上,利用的是 Object.defineProperty和存储器 getter和setter(所以只兼容IE9及以上版本),可称为基于依赖收集的观测机制。核心是VM,即ViewModel,保证数据和视图的一致性。 - 组件系统 vue组件的核心选项:
- 模板(template):模板声明了数据和最终展现给用户的DOM之间的映射关系
- 初始数据(data):一个组件的初始数据状态。对于可复用的组件来说,这通常是私有的状态。
- 接受的外部参数(props):组件之间通过参数来进行数据的传递和共享
- 方法(methods):对数据的改动操作一般都在组建的方法内进行
- 生命周期钩子函数(lifeCycle hooks):一个组件会出发多个生命周期钩子函数
- 私有资源(assets):Vue.js当中经用户自定义的指令、过滤器、组件等统称为资源。一个组件可以声明自己的爱有资源。私有资源只有该组件和它的子组件可以调用
2.对于 Vue 是一套构建用户界面的渐进式框架的理解
Vue的核心的功能,是一个视图模板引擎,但这不是说Vue就不能成为一个框架。
在声明式渲染(视图模板引擎)的基础上,我们可以通过添加组件系统、客户端 路由、大规模状态管理来构建一个完整的框架。更重要的是,这些功能相互独立,你可以在核心功能的基础上任意选用其他的部件,不一定要全部整合在一起。可以看到,所说的“渐进式”,其实就是Vue的使用方式,同时也体现了Vue的设计的理念
渐进式代表的含义是:没有多做职责之外的事。
vue.js只提供了 vue-cli 生态中最核心的组件系统和双向数据绑定。
像vuex、vue-router都属于围绕 vue.js开发的库。
比如说,你要使用Angular,必须接受以下东西:
- 必须使用它的模块机制
- 必须使用它的依赖注入-
- 必须使用它的特殊形式定义组件(这一点每个视图框架都有,难以避免)
所以Angular是带有比较强的排它性的,如果你的应用不是从头开始,而是要不断考虑是否跟其他东西集成,这些主张会带来一些困扰。
比如说,你要使用React,你必须理解:
- 函数式编程的理念,
- 需要知道什么是副作用,
- 什么是纯函数,
- 如何隔离副作用
它的侵入性看似没有Angular那么强,主要因为它是软性侵入。
Vue与React、Angular的不同是,但它是渐进的:
- 你可以在原有大系统的上面,把一两个组件改用它实现,当jQuery用;
- 也可以整个用它全家桶开发,当Angular用;
- 还可以用它的视图,搭配你自己设计的整个下层用。
- 你可以在底层数据逻辑的地方用OO和设计模式的那套理念,
- 也可以函数式,都可以,它只是个轻量视图而已,只做了最核心的东西。
3.请说出vue几种常用的指令
- v-if:根据表达式的值的真假条件渲染元素。在切换时元素及它的数据绑定 / 组件被销毁并重建。
- v-show:根据表达式之真假值,切换元素的 display CSS 属性。
- v-for:循环指令,基于一个数组或者对象渲染一个列表,vue 2.0以上必须需配合 key使用。
- v-bind:动态地绑定一个或多个特性,或一个组件 prop 到表达式。
- v-on:用于监听指定元素的DOM事件,比如点击事件。绑定事件监听器。
- v-model:实现表单输入和应用状态之间的双向绑定
- v-pre:在模板中跳过vue的编译,直接输出原始值。就是在标签中加入v-pre就不会输出vue中的data值了。跳过大量没有指令的节点会加快编译。比如
这时并不会输出我们的message值,而是直接在网页中显示{{message}}{{message}}
- v-once:只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。
4.请问 v-if 和 v-show 有什么区别
共同点:
v-if 和 v-show 都是动态显示DOM元素。
区别:
- 编译过程: v-if 是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。v-show 的元素始终会被渲染并保留在 DOM 中。v-show 只是简单地切换元素的 CSS 属性display。
- 编译条件: v-if 是惰性的:如果在初始渲染时条件为假,则什么也不做。直到条件第一次变为真时,才会开始渲染条件块。v-show不管初始条件是什么,元素总是会被渲染,并且只是简单地基于CSS 进行切换。
- 性能消耗: v-if有更高的切换消耗。v-show有更高的初始渲染消耗。
- 应用场景: v-if适合运行时条件很少改变时使用。v-show适合频繁切换。
5.vue常用的修饰符
- v-on 指令常用修饰符:
- .stop - 调用 event.stopPropagation(),禁止事件冒泡。
- .prevent - 调用 event.preventDefault(),阻止事件默认行为。
- .capture - 添加事件侦听器时使用 capture 模式。
- .self - 只当事件是从侦听器绑定的元素本身触发时才触发回调。
- .native - 监听组件根元素的原生事件。
- .once - 只触发一次回调。
- .left - (2.2.0) 只当点击鼠标左键时触发。
- .right - (2.2.0) 只当点击鼠标右键时触发。
<!-- 阻止单击事件继续传播 --> <a v-on:click.stop="doThis"></a>
1. <!-- 提交事件不再重载页面 --> 2. <form v-on:submit.prevent="onSubmit"></form>
<!-- 修饰符可以串联 --> <a v-on:click.stop.prevent="doThat"></a>
<!-- 只有修饰符 --> <form v-on:submit.prevent></form> <!-- 添加事件监听器时使用事件捕获模式 --> <!-- 即元素自身触发的事件先在此处理,然后才交由内部元素进行处理 --> <div v-on:click.capture="doThis">...</div> <!-- 只当在 event.target 是当前元素自身时触发处理函数 --> <!-- 即事件不是从内部元素触发的 --> <div v-on:click.self="doThat">...</div> <!-- 点击事件将只会触发一次 --> <a v-on:click.once="doThis"></a> <!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 --> <!-- 而不会等待 `onScroll` 完成 --> <!-- 这其中包含 `event.preventDefault()` 的情况 --> <div v-on:scroll.passive="onScroll">...</div> <!-- 只有在 `key` 是 `Enter` 时调用 `vm.submit()` --> <input v-on:keyup.enter="submit">
- 注意: 如果是在自己封装的组件或者是使用一些第三方的UI库时,会发现并不起效果,这时就需要用.native修饰符了,如:
//使用示例: <el-input v-model="inputName" placeholder="搜索你的文件" @keyup.enter.native="searchFile(params)" > </el-input>
- v-bind 指令常用修饰符:
- .sync (2.3.0+) 语法糖,会扩展成一个更新父组件绑定值的 v-on 侦听器。
- v-model 指令常用修饰符:
- .lazy - 取代 input 监听 change 事件
- .number - 输入字符串转为数字
- .trim - 输入首尾空格过滤
6.v-on可以监听多个方法吗?
v-on可以监听多个方法,例如:
<input type="text" :value="name" @input="onInput" @focus="onFocus" @blur="onBlur" />
但是同一种事件类型的方法,vue-cli工程会报错,例如:
<a href="javascript:;" @click="methodsOne" @click="methodsTwo"></a>
7.vue中 key 值的作用
key值:用于管理可复用的元素。因为vue会尽可能高效的渲染元素,通常会复用已有元素而不是从头开始渲染。这么做会使Vue变得非常快,但是这样也不总是符合实际需求。
2.2.0+ 的版本里,当在组件中使用 v-for 时,key 现在是必须的。
例如,如果你允许用户在不同的登录方式之间切换:
<template v-if="loginType === 'username'"> <label>Username</label> <input placeholder="Enter your username"> </template> <template v-else> <label>Email</label> <input placeholder="Enter your email address"> </template>
那么在上面的代码中切换loginType 将不会清除用户已经输入的内容。因为两个模板使用了相同的元素,不会被替换掉,仅仅是替换了它的placeholder。
这样也不总是符合实际需求,所以Vue为你提供了一种方式来表达这两个元素是完全独立的,不要复用它们。只需添加一个具有唯一值的 key 属性即可:
<template v-if="loginType === 'username'"> <label>Username</label> <input placeholder="Enter your username" key="username-input"> </template> <template v-else> <label>Email</label> <input placeholder="Enter your email address" key="email-input"> </template>
现在,每次切换时,输入框都将被重新渲染。
8.vue事件中如何使用event对象?
如果直接传递具体的值。可以像如下:
<button @click="event('123')">修饰符</button> event(message){ console.log(message); //123 }
如果需要访问原始的DOM事件,可以使用特殊变量 $event,使用方法如下:
<button @click="event($event)">修饰符</button> event(e){ console(e); //MouseEvent事件 }
<button @click="event($event)">修饰符</button> event(e){ console(e); //MouseEvent事件 }
ref的使用:
<div ref="name"> <button data-id='event' @click="event($event)">修饰符</button> </div> event(){ console.log(this.$refs.name) }
vue高频面试题(一)(下):https://developer.aliyun.com/article/1414996