一、vue2.0部分
1、v-show和v-if的区别
- v-show 通过 css display 控制显示和隐藏的
- v-if 是组件真正的渲染和销毁,不是显示和隐藏
- 频换切换的时候使用 v-show , 否则使用 v-if
2、v-for中key的作用
diff 算法中通过 tag 和 key 来判断是否是 sameNode ,减少渲染次数,提升渲染性能。key 必须使用的 不能是 index 和 random 。
3、v-for 和 v-if不能同时使用
v-for 比v-if计算优先级高 而且如果每一次都需要遍历整个数组,将会影响速度, 解决方案 使用计算属性 computed 或者在父级标签判断
4、vue的生命周期(有父子组件的情况)
主要有三个阶段:
- 创建阶段(注册实例与挂载): beforeCreate、created、beforeMount、mounted
- 更新阶段:beforeUpdate、updated
- 注销阶段:beforeDestroy、destroyed
单个组件的生命按正常的顺序执行。这里主要说一下父子组件的情况
- 创建阶段: 父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted
- 子组件更新过程: 父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
- 父组件更新过程:父 beforeUpdate -> 父 updated
- 销毁过程: 父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
5、vue组件如何通讯
- 父子组件:父子沟通用的是props,子父沟通用的是 this.$emit
- 自定义事件: event.
off event.$emit
- vuex
6、对 mvvm 的理解
view -> viewModel -> model view <- viewModel <- model
view: DOM model: javascript Objects viewModel: vue (DOM listeners 和 directives )
7、双向数据绑定v-model 的实现原理
v-model 是一个语法糖真正实现是靠 v-bind绑定响应式数据和触发oninput 事件并传递数据实现,
- input 元素的 value = this.name
- 绑定 input 事件 this.name = $event.target.value
- data 更新出发 re-render
自定义 v-model
- 定一个 props ,在 model 中 关联 prop:"自己定义的属性默认为 value ", enent:"事件名称默认为 input "
8、$nextTick
异步渲染,将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。
9、slot
1)、什么是插槽?
- 插槽(Slot)是Vue提出来的一个概念,正如名字一样,插槽用于决定将所携带的内容,插入到指定的某个位置,从而使模板分块,具有模块化的特质和更大的重用性。
- 插槽显不显示、怎样显示是由父组件来控制的,而插槽在哪里显示就由子组件来进行控制
2)、默认插槽
父组件
<template>
<div>
我是父组件
<slotOne1>
<p style="color:red">我是父组件插槽内容</p>
</slotOne1>
</div>
</template>
在父组件引用的子组件中写入想要显示的内容(可以使用标签,也可以不用)
子组件(slotOne1)
<template>
<div class="slotOne1">
<div>我是slotOne1组件</div>
<slot></slot>
</div>
</template>
在子组件中写入slot,slot所在的位置就是父组件要显示的内容
3)、具名插槽
子组件
<template>
<div class="slottwo">
<div>slottwo</div>
<slot name="header"></slot>
<slot></slot>
<slot name="footer"></slot>
</div>
</template>
在子组件中定义了三个slot标签,其中有两个分别添加了name属性header和footer
父组件
<template>
<div>
我是父组件
<slot-two>
<p>啦啦啦,啦啦啦,我是卖报的小行家</p>
<template slot="header">
<p>我是name为header的slot</p>
</template>
<p slot="footer">我是name为footer的slot</p>
</slot-two>
</div>
</template>
在父组件中使用template并写入对应的slot值来指定该内容在子组件中现实的位置(当然也不用必须写到template),没有对应值的其他内容会被放到子组件中没有添加name属性的slot中
4)、作用域插槽
子组件
<template>
<div>
我是作用域插槽的子组件
<slot :data="user"></slot>
</div>
</template>
<script>
export default {
name: 'slotthree',
data () {
return {
user: [
{name: 'Jack', sex: 'boy'},
{name: 'Jone', sex: 'girl'},
{name: 'Tom', sex: 'boy'}
]
}
}
}
</script>
在子组件的slot标签上绑定需要的值
父组件
<template>
<div>
我是作用域插槽
<slot-three>
<template slot-scope="user">
<div v-for="(item, index) in user.data" :key="index">
{{item}}
</div>
</template>
</slot-three>
</div>
</template>
在父组件上使用slot-scope属性,user.data就是子组件传过来的值
10、keep-alive
- 缓存组件
- 频繁切换,不需要重新渲染
- Vue的性能优化
Props:
- include - 字符串或正则表达式。只有名称匹配的组件会被缓存。
- exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
- max - 数字。最多可以缓存多少组件实例。
使用:
<!-- 基本 -->
<keep-alive>
<component :is="view"></component>
</keep-alive>
<!-- 多个条件判断的子组件 -->
<keep-alive>
<comp-a v-if="a > 1"></comp-a>
<comp-b v-else></comp-b>
</keep-alive>
<!-- 和 `<transition>` 一起使用 -->
<transition>
<keep-alive>
<component :is="view"></component>
</keep-alive>
</transition>
include 和 exclude prop 允许组件有条件地缓存。二者都可以用逗号分隔字符串、正则表达式或一个数组来表示:
<!-- 逗号分隔字符串 -->
<keep-alive include="a,b">
<component :is="view"></component>
</keep-alive>
<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
<component :is="view"></component>
</keep-alive>
<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
<component :is="view"></component>
</keep-alive>
max:最多可以缓存多少组件实例。一旦这个数字达到了,在新实例被创建之前,已缓存组件中最久没有被访问的实例会被销毁掉。
<keep-alive :max="10">
<component :is="view"></component>
</keep-alive>
11、vuex
- state: 数据仓库,唯一数据源
- getters: store 的计算属性。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
- action: Action 提交的是 mutation,而不是直接变更状态。Action 可以包含任意异步操作。
- mutation: 更改 Vuex 的 store 中的状态的唯一方法
用于组件中的
- dispatch:
- commit:
- mapState:
- mapGetters:
- mapActions:
- mapMutations:
12、vue 响应式原理
核心API: Object.defineProperty
Object.defineProperty 的简单使用:
Object.defineProperty(obj, prop, desc)
-
obj 需要定义属性的当前对象
-
prop 当前需要定义的属性名
-
desc 属性描述符()
let Person = {} let temp = null Object.defineProperty(Person, 'name', { get: function () { console.log("get"); return temp }, set: function (val) { console.log("set",val); temp = val } }) Person.name = "风信子" console.log(Person.name)
Object.defineProperty如何监听数组和对象
// 触发更新视图
function updateView() {
console.log('视图更新')
}
// 重新定义数组原型
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)
// Array.prototype.push.call(this, ...arguments)
}
})
// 重新定义属性,监听起来
function defineReactive(target, key, value) {
// 深度监听
observer(value)
// 核心 API
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue !== value) {
// 深度监听 (设置新值的时候)
observer(newValue)
// 设置新值
// 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
value = newValue
// 触发更新视图
updateView()
}
}
})
}
// 监听对象属性
function observer(target) {
if (typeof target !== 'object' || target === null) {
// 不是对象或数组
return target
}
// 污染全局的 Array 原型
// Array.prototype.push = function () {
// updateView()
// ...
// }
if (Array.isArray(target)) {
target.__proto__ = arrProto
}
// 重新定义各个属性(for in 也可以遍历数组)
for (let key in target) {
defineReactive(target, key, target[key])
}
}
// 准备数据
const data = {
name: 'zhangsan',
age: 20,
info: {
address: '北京' // 需要深度监听
},
nums: [10, 20, 30]
}
// 监听数据
observer(data)
// 测试
// data.name = 'lisi'
// data.age = 21
// // console.log('age', data.age)
// data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
// delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
// data.info.address = '上海' // 深度监听
data.nums.push(4) // 监听数组
Object.defineProperty 的缺点(Vue3.0启用Proxy)
- 深度监听,需要递归到底,一次性计算量大
- 无法监听新增属性/删除属性 (没有在data中初始化的属性,需要vue.set(),vue.delete())
- 无法原生监听数组,需要特殊处理
Proxy 有兼容性问题 无法兼容polyfill
13、虚拟DOM (virtual DOM)
DOM 操作非常耗费性能,之前需手动操作DOM。Vue和React是数据驱动视图,如何操作DOM.
解决方案-VDOM: 项目有了一定复杂度,想减少计算次数比较难。能不能把计算,更多的转移为js计算,因为js执行速度相对很快的。
vdom: 用js模拟DOM结构,计算出最小的变更,操作DOM
1)用js模拟DOM结构原理
// 原html结构
<div class="content">
<ul style="font-size:20px;" >
<li>a</li>
</ul>
</div>
//js模拟DOM结构
{
tag:"div",
props:{
className:"content",
id:"div1"
},
childrent:{
tag:"ul",
props:{
style:"font-size:20px;",
},
childrent:{
tag:"li",
childrent:"a"
}
}
}
2)通过 snabbdom 学习 vdom
(1) diff算法概述
- diff即对比,是一个广泛的概念,如linux diff命令、git diff等
- 两个js 也可以 diff 对比。如:github.com/cujojs/jiff
- 两棵树做 diff , 如 vdom diff。
diff算法时间复杂度O(n) o(n): 1千个节点 时间复杂度就是1千。
时间复杂度如何优化到O(n)
- 只比较同级,不跨级比较,减少算法复杂度。
- tag 不相同,则直接删除重建,不在深度比较。
- tag 和 key ,两个相同,则认为是相同节点,不在深度比较。
(2) diff算法总结
钩子方法 :
// 钩子
export interface Hooks {
// 在 `patch` 开始执行的时候调用
pre?: PreHook;
// 在 `createElm`,进入的时候调用init
// vnode转换为真实DOM节点时触发
init?: InitHook;
// 创建真实DOM的时候,调用 create
create?: CreateHook;
// 在`patch`方法接近完成的时候,才收集所有的插入节点,遍历调用响应的钩子
// 可以认为插入到DOM树时触发
insert?: InsertHook;
// 在两个节点开始对比前调用
prepatch?: PrePatchHook;
// 更新过程中,调用update
update?: UpdateHook;
// 两个节点对比完成时候调用
postpatch?: PostPatchHook;
// 删除节点的时候调用,包括子节点的destroy也会被触发
destroy?: DestroyHook;
// 删除当前节点的时候调用。元素从父节点删除时触发,和destory略有不同,remove只影响到被移除节点中最顶层的节点
remove?: RemoveHook;
// 在`patch`方法的最后调用,也就是patch完成后触发
post?: PostHook;
}
每个 modules 下的 hook 方法提取出来存到 cbs 里面
sameVnode:判断是否是相同的虚拟节点
patch:init 方法最后返回一个 patch 方法 。
patch 方法主要的逻辑如下 :
- 触发 pre 钩子
- 如果老节点非 vnode, 则新创建空的 vnode
- 新旧节点为 sameVnode 的话,则调用 patchVnode 更新 vnode , 否则创建新节点
- 触发收集到的新元素 insert 钩子
- 触发 post 钩子
patchVnode 函数:比较两个vnode节点是否相似 相似patch 不同直接进行移除和添加
patchVnode 方法主要的逻辑如下 :
- 触发 prepatch 钩子
- 触发 update 钩子, 这里主要为了更新对应的 module 内容
- 非文本节点的情况 , 调用 updateChildren 更新所有子节点
- 文本节点的情况 , 直接 api.setTextContent(elm, vnode.text as string);
updateChildren 方法:patchVnode 里面最重要的方法,也是整个 diff 里面的最核心方法
updateChildren 主要的逻辑如下:
-
优先处理特殊场景,先对比两端。也就是
- 旧 vnode 头 vs 新 vnode 头
- 旧 vnode 尾 vs 新 vnode 尾
- 旧 vnode 头 vs 新 vnode 尾
- 旧 vnode 尾 vs 新 vnode 头
-
拿新节点的key,能否对应上 oldCh 中的某个节点的key。 找不到则新建元素
-
如果找到 key,但是,元素选择器变化了,也新建元素
-
如果找到 key,并且元素选择没变, 则移动元素
-
两个列表对比完之后,清理多余的元素,新增添加的元素
addVnodes 方法:
addVnodes 就比较简单了,主要功能就是添加 Vnodes 到 真实 DOM 中
removeVnodes 方法:
删除 VNodes 的主要逻辑如下:
- 循环触发 destroy 钩子,递归触发子节点的钩子
- 触发 remove 钩子,利用 createRmCb , 在所有监听器执行后,才调用 api.removeChild,删除真正的 DOM 节点
createElm 方法:
主要逻辑如下:
- 触发 init 钩子
- 处理注释节点
- 创建元素并设置 id , class
- 触发模块 create 钩子 。
- 处理子节点
- 处理文本节点
- 触发 vnodeData 的 create 钩子
详细请看系列文章结束:segmentfault.com/a/119000001…
14、模板编译
- 模板不是html,有指令、插值、js表达式、能实现判断、循环
- HTML是标签,只有js才能实现判断、循环(图灵完备语言)
- 模板一定转换为某种js代码,即编译模板
1)、 js 的 with 语法
// 使用 with 能够改变 {} 内自由变量的查找方式
// 将 {} 内自由变量当做 obj 的属性来查找,如果找不到会报错,
// 不建议使用,打破了作用域规则,
const obj = {
a: 1000,
b: 1000
}
with(obj){
console.log(a);
console.log(b);
console.log(c); // 报错 c is not defined
}
2)、vue template complier 将模板编译成 render 函数
const compiler = require('vue-template-compiler')
// 插值
// const template = `<p>{{message}}</p>`
// with(this){return createElement('p',[createTextVNode(toString(message))])}
// h -> vnode
// createElement -> vnode
// // 表达式
// 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")]):_c('p',[_v("B")])])}
// 循环
// 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">`
// 主要看 input 事件
// 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
// patch
// 编译
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._t = renderSlot;
// 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;
// }
3)、执行 render 函数生成 vnode
VNode表示Virtual DOM,用JavaScript对象来描述真实的DOM把DOM标签,属性,内容都变成对象的属性。
详见上边 js 模拟 Dom 元素
15、渲染和更新过程
回顾知识:vue的三大核心
- 响应式:监听 data 属性 getter setter (包括数组)。Object.defineProperty
- 模板编译:模板到 render 函数 再到 vnode
- vdom: patch(elem,vnode)vnode渲染的新的elem 和 patch(vnode,newVnode) 新的vode更新到旧的vnode。
1)、初次渲染过程
- 解析模板为 render 函数 (或者在开发环境已完成,vue-loader)
- 触发响应式,监听 data 属性 getter setter (会触发和模板(视图)有关的变量)
- 执行 render 函数,生成 vnode, patch(elem,vnode)
2)、更新过程
- 修改data,触发 setter (此前在 getter 中已被监听)
- 重新执行 render 函数,生成 newVnode
- patch(vnode,newVnode)
3)、异步渲染
- $nextTick 函数
- 汇总 data 的修改,一次性更新视图
- 减少 DOM 操作次数,提高性能
16、前端路由原理
1)、hash模式
- hash 变化会触发网页跳转,即浏览器的前进、后退
- hash 变化不会刷新页面,spa必须的特点
- hsah 永远不会提交到 server 端
- window.onhashchange 监听
2)、 H5 history模式
- 用url规范的路由,但跳转时不刷新页面(只是刷新刚进来的页面)
- history.pushState (此方式,跳转浏览器不会刷新页面)
- window.onpopstate (监听浏览器前进和后退,由history.pushState()或者history.replaceState()形成的历史节点中前进后退)
to B 的系统推荐使用 hash,简单易用,对 url 规范不敏感。
to C 的系统,可以选着 H5 history,对于 seo 优化的使用 H5 history
17、computed 的特点
- 缓存,data 不变不会重新计算。
- 提高性能
18、为何组件 data 必须是个函数
data 必须声明为返回一个初始数据对象的函数,因为组件可能被用来创建多个实例。如果 data 仍然是一个纯粹的对象,则所有的实例将共享引用同一个数据对象,通过提供 data 函数,每次创建一个新实例后,我们能够调用 data 函数,从而返回初始数据的一个全新副本数据对象。
19、数据请求应该刚在那个生命周期
- mounted
- js 是单线程的,数据请求异步获取数据
- 放在 mounted 之前是没有用的,只会让逻辑更加混乱,在 js 没有渲染完 异步数据也是在查询中不会提前加载。
20、如何将组建所有 props 传递给子组件
props")
21、多个组件有相同的逻辑,如何抽离
使用 mixin
多个组件可以共享数据和方法,在使用mixin的组件中引入后,mixin中的方法和属性也就并入到该组件中,可以直接使用。数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。钩子函数会两个都被调用,mixin中的钩子首先执行。
存在问题:
变量来源不明确,多 mixin 可能造成命名冲突。 mixin 和组件可能存在多对多的关系,业务逻辑复杂。
22、何时使用异步组件
- 加载大组件
- 路由异步加载
23、何时使用 keep-alive
- 缓存组件,不需要重复渲染
- 多个静态的 tab 页
24、何时使用 beforeDestory
- 解除自定义事件 event.$off
- 清除定时器
- 解除自定义 DOM 事件,如 window scroll 等。
25、请用 vnode 描述一个 DOM 结构
详见 用js模拟DOM结构原理
26、监听data 变化的核心 API 是什么
详见 vue 响应式原理 Object.defineProperty
27、vue 常见性能优化
- 合理使用 v-show 和 v-if
- 合理使用computed
- v-for 时加 key , 避免和 v-if 同时使用
- 自定义事件个dom 事件及时销毁
- 合理使用异步组件
- 合理使用 keep-alive
- data 层级不要太深(响应式绑定递归遍历比较深)
- webpack 层面的优化
- 前端通用性能优化 (图片懒加载等)
- 使用SSR
二、vue3.0 部分
1)、vue3 升级内容
- 全部使用 ts 重写 (响应式,vdom,模板编译)
- 性能提升,打包后代码减少
- 调整部分API
2)、proxy 重写响应式
(1)、proxy 的基本使用
const data = {
name: 'zhangsan',
age: 20,
}
const proxyData = new Proxy(data, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
console.log('get',key)
return result // 返回结果
},
set(target, key, val, receiver) {
const result = Reflect.set(target, key, val, receiver)
console.log('set', key, val)
return result // 是否设置成功
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
console.log('delete property', key)
return result // 是否删除成功
}
})
Reflect 和 Proxy 是搭配使用的。
Reflect 的作用
- Reflect 和 Proxy 的能力一一对应。
- 规范化、标准化、函数式。(如查找一个对象中的 key 一般是 "a" in obj, Reflects是 Reflect.has(obj,"a"), 删除的话 delete obj.a , ReflecteleteProperty(obj,"a"))
- 代替 Object 上的工具函数
(1)、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)
// 深度监听
// 性能如何提升的?
return reactive(result)
},
set(target, key, val, receiver) {
// 重复的数据,不处理
if (val === target[key]) {
return true
}
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('result', result) // true
return result // 是否设置成功
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
console.log('delete property', key)
// console.log('result', result) // true
return result // 是否删除成功
}
}
// 生成代理对象
const observed = new Proxy(target, proxyConf)
return observed
}
// 测试数据
const data = {
name: 'zhangsan',
age: 20,
info: {
city: 'beijing',
a: {
b: {
c: {
d: {
e: 100
}
}
}
}
}
}
const proxyData = reactive(data)
- 深度监听,不是全部一次性监听,性能更好
- 可监听 新增/删除 属性
- 可监听数组变化