# 面试题
# Vue父子组件通讯
props $emit
# 自定义事件
// 绑定自定义事件
mouunted() {
event.$on('onAddTitle',this.handleEvent)
}
// 解绑事件
beforeDestroy() {
// 及时销毁,否则会造成内存泄漏
event.$off('onAddTitle',this.handleEvent)
}
// 调用自定义事件
event.$emit('onAddTitle',val)
# 生命周期(父子组件)
- 加载渲染过程
父 beforeCreate => 父 created => 父 beforeMount => 子 beforeCreate => 子 created => 子 beforeMount => 子 mounted => 父 mounted
- 子组件更新过程
父 beforeUpdate => 子 beforeUpdate => 子 updated => 父 updated
- 父组件更新过程
父 beforeUpdate => 父 updated
- 销毁过程
父 beforeDestroy => 子 beforeDestroy => 子 destroyed => 父 destroyed
# 自定义v-model
<template>
<div>
<!-- 自定义model -->
<p>{{name}}</p>
<CustomVModel v-model="name"></CustomVModel>
</div>
</template>
<script>
import CustomVModel from './CustomVModel'
export default {
components: {
CustomVModel,
},
data() {
return {
name: "hanzo"
}
}
}
</script>
<template>
<div>
<input type="text"
:value="textV"
@input="$emit('changeV',$event.target.value)"
/>
<!--
1. 上面的 input 使用了 :value 而不是 v-model
2. 上面的 changeV 和 model.event 要对应起来
3. textV 属性对应起来
-->
</div>
</template>
<script>
export default {
model: {
prop: "textV", // 对应 props text
event: 'changeV'
},
props: {
textV: {
type: String,
default: ""
}
}
};
</script>
# $nextTick
- Vue是异步渲染
- data改变后,DOM不会立刻渲染
- $nextTick会在DOM渲染之后被触发,以获取最新的DOM节点
<template>
<div>
<ul ref="ul">
<li v-for="(val,index) in list" :key="index">
{{val}}
</li>
</ul>
<button @click="addVal">添加一个</button>
</div>
</template>
<script>
export default {
data() {
return {
list: ['a','b','c']
}
},
methods: {
addVal() {
this.list.push(`${Date.now()}`)
this.list.push(`${Date.now()}`)
this.list.push(`${Date.now()}`)
// 获取 DOM 元素
// const ulElem = this.$refs.ul
// console.log(ulElem.children.length)
// console.log(ulElem.childNodes.length)
// 1. 异步渲染,$nextTick 待 DOM 渲染完了 再回调
// 2. 页面渲染时会将 data 的修改做整合, 多次 data 修改只会渲染一次
this.$nextTick(() => {
const ulElem = this.$refs.ul
console.log(ulElem.children.length)
console.log(ulElem.childNodes.length)
})
}
}
}
</script>
# slot
- 基本使用
// 父组件
<template>
<div id="app">
<SlotDemo url="www.baidu.com">
<span>百度</span>
</SlotDemo>
</div>
</template>
<script>
import SlotDemo from './components/Slot'
export default {
name: 'app',
components: {
SlotDemo
},
}
</script>
// 子组件
<template>
<div>
<a :href="url">
<slot>
默认没有内容就展示
</slot>
</a>
</div>
</template>
<script>
export default {
props: {
url: {
type: String,
default: ''
}
}
}
</script>
作用域插槽
<template> <div id="app"> <SlotDemo :url="website.url"> <template v-slot="slotProps"> {{slotProps.slotData.title}} </template> </SlotDemo> </div> </template> <script> import SlotDemo from './components/Slot' export default { name: 'app', components: { SlotDemo, }, data() { return { website: { url: "http://www.baidu.com", title: "hanzo", subTitle:"花村老大" }, } }, } </script>
<template> <div> <a :href="url"> <slot :slotData="website">{{website.subTitle}}</slot> </a> </div> </template> <script> export default { props: { url: { type: String, default: "" } }, data() { return { website: { url: "http://www.baidu.com", title: "genji", subTitle: "花村弟弟" } }; } }; </script>
> 总结: > > 1、在子组件中定义 :slotData将子组件data数据传向组组件 > > 2、在父组件使用 <template v-slot="slotProps"></template> 接手 > > 3、slotProps 就是 子组件 向 父组件传递过来的值 {{slotProps.slotData.title}} 就能拿到子组件的title
具名插槽
<template>
<div class="nav-bar" :class="{'is_fixed':isFixed}">
<div class="container">
<slot name="buy"></slot>
</div>
</div>
</div>
</template>
<template>
<div class="product">
<product-param :name="product.name">
<template v-slot:buy>
<button class="btn" @click="buy">立即购买</button>
</template>
</product-param>
</div>
</template
# Vue动态组件
- :is = “component-name” 用法
- 需要根据数据,动态渲染的场景。即组件类型不确定
# 简单使用
<template>
<div id="app">
<!-- 动态组件 -->
<component :is="NextTickName"></component>
</div>
</template>
<script>
import NextTick from './components/NextTick'
export default {
name: 'app',
components: {
NextTick,
},
data() {
return {
NextTickName: "NextTick",
}
},
}
</script>
# 场景使用
<template>
<div id="app">
<div v-for="(val,index) in newsData" :key="index">
<component :is="val.type" />
</div>
</div>
</template>
<script>
import NextTick from './components/NextTick'
export default {
name: 'app',
components: {
NextTick,
},
data() {
return {
newsData: [
{id: 1,type: 'text'},
{id: 2,type: 'text'},
{id: 3,type: 'image'},
]
}
},
}
</script>
# Vue异步加载组件
- import() 函数
- 按需加载,异步加载大组件
<template>
<div id="app">
<!-- 异步组件 -->
<Model v-if="show" />
<button @click="show = true">click</button>
</div>
</template>
<script>
export default {
name: 'app',
components: {
Model: () => import('./components/Model')
},
data() {
return {
show: false,
}
},
}
</script>
# Keep-alive
- 缓存组件
- 频繁切换,不需要重复渲染
- Vue常见性能优化
# mixin
- 多个组件有相同的逻辑,抽离出来
- mixin 并不是完美的解决方案,会有一些问题
- Vue 3 提出的Composition API 旨在解决这些问题
# 使用
- MixinDemo.vue
<template>
<div>
<p>{{name}} {{major}} {{city}}</p>
<button @click="showName">显示姓名</button>
</div>
</template>
<script>
import myMixin from './../mixin'
export default {
name: 'mixin',
mixins:[myMixin], // 可以添加多个,会自动合并起来
data() {
return {
name: "hanzo",
major: "前端"
}
},
mounted() {
console.log('component mounted',this.name)
}
}
</script>
- mixin.js
export default {
data() {
return {
city: "成都"
}
},
methods: {
showName() {
console.log(this.name)
}
},
mounted() {
console.log('mixin mounted',this.name)
},
}
# mixin的问题
- 变量来源不明确,不利于阅读
- 多 mixin 可能会造成命名冲突
- mixin 和 组件可能会出现多对多的关系,复杂度较高
# Vue-Router
# 路由模式
- hash模式,例如 http://www.baidu.com/#/user/19
- H5 history模式,例如 http://www.baidu.com/user/19
- 后者是需要server端支持的,因此无特殊需求可以选择前者
# 动态路由
const User = {
template: `<div>{{$router.params.id}}</div>`
}
const router = new VueRouter({
routes: [
// 动态路径参数 以冒号开头 能命中 `/user/10` `/user/20` 等格式的路由
{path: '/user/:id', component: User}
]
})
# 懒加载
const router = new VueRouter({
routes: [
// 动态路径参数 以冒号开头 能命中 `/user/10` `/user/20` 等格式的路由
{
path: '/user/:id',
component: () => import('./components/User.vue')
}
]
})
# 如何理解MVVM
# 组件化基础
- 数据驱动视图(MVVM,setState)
# Vue的响应式
- 组件data的数据一旦变化,立刻触发视图的更新
- 核心 API - Object.defineProperty
- Vue3.0 使用的是 Proxy
- Proxy兼容性不好,且无法polyfill
# Object.defineProperty实现响应式
- 监听对象,监听数组
- 复杂对象,深度监听
- 几个缺点
# 代码演示
// 触发更新视图
function updateView() {
console.log("视图更新")
}
// 重新定义属性,监听起来
function defineReactive(target, key, value) {
// 核心API
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue !== value) {
// 设置新值
// 注意,value一直在闭包中,此处设置完之后,再get时也是
value = newValue
// 触发视图
updateView()
}
}
})
}
// 监听对象
function observer(target) {
if (typeof target !== 'object' || target === null) {
// 不是对象数组
return target
}
// 重新定义各个属性 (for in 也可以遍历数组)
for (let key in target) {
defineReactive(target, key, target[key])
}
}
// 准备数据
const data = {
name: "hanzo",
age: 20,
// info: {
// address: "成都"
// },
// nums: [1,2,3]
}
// 监听数据
observer(data)
// 测试
data.name = "genji"
data.age = 18
# Object.defineProperty缺点
- 深度监听,需要递归到底,一次性计算量大
- 无法监听新增属性/删除属性 (Vue.set / Vue.delete)
- 无法原生监听数组,需要特殊处理
// 触发更新视图
function updateView() {
console.log("视图更新")
}
// 3.
// 重新定义数组原型
const oldArrayProperty = Array.prototype;
// 创建新对象,原型指向 oldArrayProperty,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
arrProto[methodName] = function () {
updateView() // 触发视图更新
oldArrayProperty[methodName].call(this, ...arguments)
}
})
// 重新定义属性,监听起来
function defineReactive(target, key, value) {
// 1. 深度监听
observer(value)
// 核心API
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue !== value) {
// 1. 深度监听
observer(newValue)
// 设置新值
// 注意,value一直在闭包中,此处设置完之后,再get时也是能获取最新值
value = newValue
// 触发视图
updateView()
}
}
})
}
// 监听对象
function observer(target) {
if (typeof target !== 'object' || target === null) {
// 不是对象数组
return target
}
// 3
if (Array.isArray(target)) {
target.__proto__ = arrProto
}
// 重新定义各个属性 (for in 也可以遍历数组)
for (let key in target) {
defineReactive(target, key, target[key])
}
}
// 准备数据
const data = {
name: "hanzo",
age: 20,
info: {
address: "成都" // 1. 需要深度监听
},
nums: [1, 2, 3]
}
// 监听数据
observer(data)
// 测试
data.name = "genji"
// data.age = 18
data.age = { num: 21 }
data.age.num = 22
data.info.address = "达州"
data.x = "100" // 新增属性,监听不到 --- 所有有了 Vue.set
delete data.name // 删除属性,监听不到 --- 所有有了 Vue.delete
data.nums.push(4)
# 虚拟DOM和diff
- DOM操作非常消耗性能
- Vue 和 React 是数据驱动视图
# 解决方案
- Vdom - 用 JS 模拟 DOM 结构,计算出最小的变更,操作DOM1
示例:
<div id="div1" class="coontainer">
<p>vdom</p>
<ul style="font-size:20px">
<li>a</li>
</ul>
</div>
{
tag: 'div',
props: {
className: "container",
id: "div1"
},
children: [
{
tag: 'p',
children: 'vdon'
},
{
tag: 'ul',
props: {
style: "font-size: 20px"
},
children: [
{
tag: "li",
children: 'a'
}
]
}
]
}
# diff算法
- diff算法是vdom中最核心、最关键的部分
# 模板编译
- 模板是 vue 开发中最常用的部分,即与使用相关联的原理
- 它不是html,有指令、插值、JS表达式
- JS 的 with 语法
- vue template complier 将模板编译为render函数
- 执行render函数生产vnode
# with语法
const obj = { a: 100, b: 200 }
console.log(obj.a) // 100
console.log(obj.b) // 200
console.log(obj.c) // undefined
// --------------------------------
// 使用with,能改变 {} 内自由变量的查找方式
// 将 {} 内自由变量,当做 obj 的属性来查找
with(obj) {
console.log(a) // 100
console.log(b) // 200
console.log(c) // c is not defined
}
- 改变 {} 内自由变量的查找方式,当做 obj 属性来查找
- 如果找不到匹配的 obj 属性,就会报错
- with 要慎用,它打破了作用域规则,易读性变差
const compiler = require('vue-template-compiler')
// 插值
// const template = `<p>{{message}}</p>`
// with(this){return _c('p',[_v(_s(message))])}
// with(this){return crateElement('p',[createTextVNode(toString(message))])}
// 表达式
// const template = `<p>{{flag ? message : 'no message found'}}</p>`
// with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}
// 属性和动态属性
// const template = `
// <div id="div1" class="container">
// <img :src="imgUrl" />
// </div>
// `
// with(this){return _c('div',{staticClass:"container",attrs:{"id":"div1"}},[_c('img',{attrs:{"src":imgUrl}})])}
// 条件
// const template = `
// <div>
// <p v-if="flag === 'a'">A</p>
// <p v-else">B</p>
// </div>
// `
// with(this){return _c('div',[(flag === 'a')?_c('p',[_v("A")]):_e(),_v(" "),_v("\">B"),_c('p')])}
// 循环
// const template = `
// <ul>
// <li v-for="item in list" :key="item.id">{{item.title}}</li>
// </ul>
// `
// with(this){return _c('ul',_l((list),function(item){return _c('li',{key:item.id},[_v(_s(item.title))])}),0)}
// 事件
// const template = `
// <button @click="clickHandler">submit</button>
// `
// with(this){return _c('button',{on:{"click":clickHandler}},[_v("submit")])}
// v-model
const template = `<input type="text" v-model="name" />`
// with(this){return _c('input',{directives:[{name:"model",rawName:"v-model",value:(name),expression:"name"}],attrs:{"type":"text"},domProps:{"value":(name)},on:{"input":function($event){if($event.target.composing)return;name=$event.target.value}}})}
// render 函数
// 返回 vnode
// 编译
const res = compiler.compile(template)
console.log(res.render)
// 从vue源码中找到缩写函数的含义
// function installRenderHelpers(target) {
// target._o = markOnce
// target._n = toNumber
// target._s = toString
// target._l = renderList
// target._q = looseEqual
// target._i = looseIndexOf
// target._m = renderStatic
// target._f = resolveFilter
// target._k = checkKeyCodes
// target._b = bindObjectProps
// target._v = createTextVNode
// target._e = createEmptyVNode
// target._u = resolveScopedSlots
// target._g = bindObjectListeners
// target._d = bindDynamicKeys
// target._p = prependModifier
// }
# 编译模板
- 模板编译为 render 函数,执行 render 函数返回 vnode
- 基于 vnode 再执行 patch 和 diff
- 使用 webpack vue-loader,会在开发环境下编译模板
# vue组件中使用render代替template
Vue.component('demo',{
// template: 'xxx'
render: function(createElement) {
return createElement(
'h' + this.level,
[
createElement('a',{
attrs: {
name: "headerId",
href: '#' + 'headerId'
}
},'this is a tag')
]
)
}
})
# 组件 渲染/更新 过程
- 初次渲染过程
- 更新过程
- 异步渲染
# 初次渲染过程
- 解析模板为 render 函数(或在开发环境已完成,vue-loader)
- 触发响应式,监听data属性 getter setter
- 执行 render 函数,生出 vnode ,patch(elem,vnode)
# 更新过程
- 修改 data,触发 setter (此前在 getter 中已被监听)
- 重新执行 render 函数,生出 newVnode
- patch(vnode,newVnode)
# 异步渲染
- $nextTick
- 汇总 data 的修改,一次性更新视图
- 减少 DOM 操作次数,提高性能
# 前端路由原理
# hash
- hash 变化会触发网页跳转,即浏览器的前进,后退
- hash 变化不会刷新页面,SPA 必需的特点
- hash 永远不会提交到server端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- <script src="./index.js"></script> -->
<p>hash test</p>
<button id="btn1">修改 hash</button>
<script>
// hash 变化, 包括:
// a. js 修改 url
// b. 手动修改 url 的 hash
// c. 浏览器前进、后退
window.onhashchange = (event) => {
console.log('old url', event.oldURL)
console.log('new url', event.newURL)
console.log('hash', location.hash)
}
// 页面初次加载,获取 hash
document.addEventListener('DOMContentLoaded',() => {
console.log('hash:', location.hash)
})
document.getElementById("btn1").addEventListener('click', () => {
location.hash = '#/user'
})
</script>
</body>
</html>
# H5 history
- 用url规范的路由,但跳转时不刷新页面
- history.pushState
- window.onpopstate
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p>histroy API test</p>
<button id="btn1">修改 url</button>
<script>
// 初次加载页面,获取 hash
document.addEventListener('DOMContentLoaded', () => {
console.log('load', location.pathname)
})
// 打开一个新的路由
// 使用 pushState 方式,浏览器不会刷新页面
document.getElementById("btn1").addEventListener("click", () => {
const state = { name: 'page1' }
console.log('切换路由到:', 'page1')
history.pushState(state, '', 'page1')
})
// 监听浏览器前进、后退
window.onpopstate = function (event) {
console.log("onpopstate", event.state, location.pathname)
}
</script>
</body>
</html>
# 总结
- hash -- window.onhashchange
- H5 history - history.pushState 和 window.onpopstate
# 两者选择
to B 的系统推荐用 hash,简单易用,对 url 规范不敏感
to C 的系统可以使用 H5 history,但需要服务端支持
# v-for为何要使用key
- 必须用key,且不能是index和random
- diff算法中通过tag和key来判断,是否是sameNode(是否是相同的节点)
- 减少渲染次数,提升渲染性能
# 为何组件中data必须是个函数
# ajax请求应该放在哪个声明周期
- mounted
- JS 是单线程的,ajax 异步获取数据
- 放在 mounted 之前没有用,只会让逻辑更加混乱
# 如何将组件中所有的props传递给子组件
- $props
<User v-bind="$props" />
# 何时需要使用beforeDestory
- 解除自定义事件 event.$off
- 清除定时器
- 解绑自定义的DOM事件,例如:window scroll
# Vuex中 action 和 mutation 有何区别
- action可以处理异步,mutation不能
- action可以整合多个mutation
# Vue常见性能优化
- 合理使用 v-show 和 v-if
- 合理使用computed
- v-for 时要加key,避免和 v-if同时使用
- 自定义事件,DOM事件及时销毁
- 合理使用异步组件
- 合理使用keep-alive
- data层级不要太深