# vue
- 对MVVM开发模式的理解 MVVM分为Model、View、ViewModel三者。MVVM框架的三要素:响应式、模板引擎及其渲染
- Model 代表
数据模型,数据和业务逻辑都在Model层中定义; - View 代表UI视图,负责数据的展示;
- ViewModel 负责监听 Model 中数据的改变并且控制视图的更新,处理用户交互操作; Model 和 View 并无直接关联,而是通过 ViewModel 来进行联系的,Model 和 ViewModel 之间有着双向数据绑定的联系。因此当 Model 中的数据改变时会触发 View 层的刷新,View 中由于用户交互操作而改变的数据也会在 Model 中同步。
这种模式实现了 Model 和 View 的数据自动同步,开发者只需专注对数据的维护操作即可,不需要自己dom。
- MVC模式出现较早主要应用在后端,如Spring MVC等,在前端领域早期也有应用,如Backbone.js。它的优点是分层清晰,
缺点是数据流混乱,灵活性带来的维护性问题。 - MVP模式在是MVC的进化形式,Presenter作为中间层负责MV通信,解决了两者耦合问题,但P层 过于臃肿会导致维护问题。
- MVVM模式在前端领域有广泛应用,它不仅解决MV耦合问题,还同时解决了维护两者映射关系的 大量繁杂代码和DOM操作代码,在提高开发效率、可读性同时还保持了优越的性能表现。

- vue设计理念
- 可以作为库融入既有项目,也可作为框架
- 拆分明细,渐进式
# MVVM MVC
MVC模式中,M指模型(Model),是后端传递的数据 ;V指视图(View),是用户所看到的页面;C指控制器(Controller),是页面业务逻辑。
使用MVC模式的目的是将Model和View的代码分离,实现Web应用系统的职能分工。MVC模式是单向通信,也就是View和Model,需要通过Controller来承上启下。

MVVM借鉴了MVC思想,在前端页面中,把Model用纯JavaScript对象表示,View负责显示,两者做到了最大限度的分离。
把Model和View关联起来的就是ViewModel。ViewModel负责把Model的数据同步到View显示出来,还负责把View的修改同步回Model。
ViewModel如何编写?需要用JavaScript编写一个通用的ViewModel,这样,就可以复用整个MVVM模型了。
# vue基本设计思路
# 初始化及挂载
在 new Vue() 之后。 Vue 会调用 _init 函数进行初始化,也就是这里的 init 过程,它会初始化生命周期、事件、 props、 methods、 data、 computed 与 watch 等。其中最重要的是通过 Object.defineProperty设置 setter 与 getter 函数,用来实现「响应式」以及「依赖收集」。
初始化之后调用 $mount 会挂载组件,如果是运行时编译,即不存在 render function 但是存在 template 的情况,需要进行「编译」步骤。
# 编译
compile编译可以分成 parse、optimize 与 generate 三个阶段,最终需要得到 render function。
- parse
parse 会用正则等方式解析 template 模板中的指令、class等数据,形成AST。
- optimize
optimize 的主要作用是标记 static 静态节点,这是 Vue 在编译过程中的一处优化,后面当 update 更新界面时,会有一个 patch 的过程, diff 算法会直接跳过静态节点,从而减少了比较的过程,优化了 patch 的性能。
- generate
generate 是将 AST 转化成 render function 字符串的过程,得到结果是 render 的字符串以及 staticRenderFns 字符串。
在经历过 parse、optimize 与 generate 这三个阶段以后,组件中就会存在渲染 VNode 所需的 render function 了。
# 响应式
当 render function 被渲染的时候,因为会读取所需对象的值,所以会触发 getter 函数进行「依赖收集」,「依赖收集」的目的是将观察者 Watcher 对象存放到当前闭包中的订阅者 Dep 的 subs 中。
在修改对象的值的时候,会触发对应的 setter, setter 通知之前「依赖收集」得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher 就会开始调用 update 来更新视图
# Virtual DOM
render function 会被转化成 VNode 节点。Virtual DOM 其实就是一棵以 JavaScript 对象( VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。可通过操作使这棵树映射到真实环境上。Virtual DOM 以 JS 对象为基础而不依赖真实环境,使它有跨平台的能力,比如说Weex、Node 等。
{
tag: 'div', /*说明这是一个div标签*/
children: [ /*存放该标签的子节点*/
{
tag: 'a', /*说明这是一个a标签*/
text: 'click me' /*标签的内容*/
}
]
}
渲染后可以得到
<div>
<a>click me</a>
</div>
# 更新视图
在修改一个对象值的时候,会通过 setter -> Watcher -> update 的流程来修改对应的视图,那么最终是如何更新视图的呢?
当数据变化后,执行 render function 就可以得到一个新的 VNode 节点,如果想要得到新的视图,最简单粗暴的方法就是直接解析这个新的 VNode 节点,然后用 innerHTML 直接全部渲染到真实 DOM 中。
将新的 VNode 与旧的 VNode 一起传入 patch 进行比较,经过 diff 算法得出它们的「差异」。最后我们只需要将这些「差异」的对应 DOM 进行修改即可。
- 响应式系统的依赖收集追踪原理
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach((sub) => {
sub.update();
})
}
}
class Watcher {
constructor() {
Dep.target = this;
}
update() {
console.log("视图更新啦!!~");
}
}
function observer(value) {
if (!value || (typeof value !== 'object')) {
return;
}
Object.keys(value).forEach((key) => {
defineReactive(value, key, value[key]);
});
}
function defineReactive(obj, key, val) {
const dep = new Dep();
console.log(key)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
dep.addSub(Dep.target);
return val;
},
set: function reactiveSetter(newVal) {
if (newVal === val) return;
val = newVal;
dep.notify();
}
});
}
class Vue {
constructor(options) {
this._data = options.data;
observer(this._data);
new Watcher();
console.log('render~', this._data.time);
console.log("again",this._data.test)
}
}
let o = new Vue({
data: {
test: "I am test.",
time:"2019",
hello:"word"
}
});
o._data.test = "hello,test.";
o._data.time="2020"
o._data.time="2023"
Dep.target = null;
/*
test
time
hello
render~ 2019
again I am test.
视图更新啦!!~
视图更新啦!!~
视图更新啦!!~
*/
# 实现一个VNode
实现一个简单的 VNode 类,加入一些基本属性,为了便于理解,先不考虑复杂的情况。
class VNode {
constructor (tag, data, children, text, elm) {
/*当前节点的标签名*/
this.tag = tag;
/*当前节点的一些数据信息,比如props、attrs等数据*/
this.data = data;
/*当前节点的子节点,是一个数组*/
this.children = children;
/*当前节点的文本*/
this.text = text;
/*当前虚拟节点对应的真实dom节点*/
this.elm = elm;
}
}
比如我目前有这么一个 Vue 组件。
<template>
<span class="demo" v-show="isShow">
This is a span.
</span>
</template>
用 JavaScript 代码形式就是这样的。
function render () {
return new VNode(
'span',
{
/* 指令集合数组 */
directives: [
{
/* v-show指令 */
rawName: 'v-show',
expression: 'isShow',
name: 'show',
value: true
}
],
/* 静态class */
staticClass: 'demo'
},
[ new VNode(undefined, undefined, undefined, 'This is a span.') ]
);
}
看看转换成 VNode 以后的情况。
{
tag: 'span',
data: {
/* 指令集合数组 */
directives: [
{
/* v-show指令 */
rawName: 'v-show',
expression: 'isShow',
name: 'show',
value: true
}
],
/* 静态class */
staticClass: 'demo'
},
text: undefined,
children: [
/* 子节点是一个文本VNode节点 */
{
tag: undefined,
data: undefined,
text: 'This is a span.',
children: undefined
}
]
}
然后我们可以将 VNode 进一步封装一下,可以实现一些产生常用 VNode 的方法。
class VNode {
constructor (tag, data, children, text, elm) {
this.tag = tag;
this.data = data;
this.children = children;
this.text = text;
this.elm = elm;
}
}
//* 创建一个空节点
function createEmptyVNode () {
const node = new VNode();
node.text = '';
return node;
}
//* 创建一个文本节点
function createTextVNode (val) {
return new VNode(undefined, undefined, undefined, String(val));
}
//* 克隆一个 VNode 节点
function cloneVNode (node) {
const cloneVnode = new VNode(
node.tag,
node.data,
node.children,
node.text,
node.elm
);
return cloneVnode;
}
总的来说,VNode 就是一个 JavaScript 对象,用 JavaScript 对象的属性来描述当前节点的一些状态,用 VNode 节点的形式来模拟一棵 Virtual DOM 树。
# template模板编译
compile 编译可以分成 parse、optimize 与 generate 三个阶段,最终需要得到 render function。 插件vue-template-compiler
cnpm i vue-template-compiler --save-dev
const compiler = require(" vue-template-compiler ")
const template = `<p>{{message}}}</p>`
const res = compiler.compile(template)
console.log(res.render)
//with(this){return _c('p',[_v(_s(message)+"}")])}
<div :class="c" class="demo" v-if="isShow">
<span v-for="item in sz">{{item}}</span>
</div>
var html = '<div :class="c" class="demo" v-if="isShow"><span v-for="item in sz">{{item}}</span></div>';
parse 会用正则等方式将 template 模板中进行字符串解析,得到指令、class、style等数据,形成 AST
得到的 AST 的样子
{
/* 标签属性的map,记录了标签上属性 */
'attrsMap': {
':class': 'c',
'class': 'demo',
'v-if': 'isShow'
},
/* 解析得到的:class */
'classBinding': 'c',
/* 标签属性v-if */
'if': 'isShow',
/* v-if的条件 */
'ifConditions': [
{
'exp': 'isShow'
}
],
/* 标签属性class */
'staticClass': 'demo',
/* 标签的tag */
'tag': 'div',
/* 子标签数组 */
'children': [
{
'attrsMap': {
'v-for': "item in sz"
},
/* for循环的参数 */
'alias': "item",
/* for循环的对象 */
'for': 'sz',
/* for循环是否已经被处理的标记位 */
'forProcessed': true,
'tag': 'span',
'children': [
{
/* 表达式,_s是一个转字符串的函数 */
'expression': '_s(item)',
'text': '{{item}}'
}
]
}
]
}
模板到render函数,再到vnode,再到渲染和更细腻,而在react中,则可以直接使用render而不需要模板这个步骤
# vue自身的性能优化做法
- 路由组件懒加载,合理使用异步组件
- v-show合理使用,使用v-show复用DOM,v-if会移除再添加
- keep-alive
- computed 缓存
- beforeDestory销毁自定义事件定时器等避免内存泄漏(Vue 组件销毁时,会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。)
- v-for加key,v-for 遍历避免同时使用 v-if (vue3中v-if的优先级>v-for)
- 图片懒加载
<img v-lazy="/static/img/1.png">
- SSR
- 按需加载
- 变量本地化
<template>
<div :style="{ opacity: start / 300 }">
{{ result }}
</div>
</template>
<script>
import { heavy } from '@/utils'
export default {
props: ['start'],
computed: {
base () { return 42 },
result () {
const base = this.base // 不要频繁引用this.base
let result = this.start
for (let i = 0; i < 1000; i++) {
result += heavy(base)
}
return result
}
}
}
</script>
- 子组件分割,如果某块区域数据变动频繁,那么单独拆成组件
<template>
<div>
<ChildComp/>
</div>
</template>
<script>
export default {
components: {
ChildComp: {
methods: {
heavy () { /* 耗时任务 */ }
},
render (h) {
return h('div', this.heavy()) }
}
}
}
</script>
- 无状态的组件标记为函数式组件
<template functional>
<div class="cell">
<div v-if="props.value" class="on"></div>
<section v-else class="off"></section>
</div>
</template>
<script>
export default {
props: ['value']
}
</script>
- data层级不要太深
- 长列表性能优化
- 如果列表是纯粹的数据展示,不会有任何改变,就不需要做响应化
- 如果是大数据长列表,可采用 虚拟滚动 ,只渲染少部分区域的内容
export default {
data: () => ({
users: []
}),
async created() {
const users = await axios.get("/api/users");
this.users = Object.freeze(users);
}
};
<recycle-scroller
class="items"
:items="items"
:item-size="24"
>
<template v-slot="{ item }">
<FetchItemView
:item="item"
@vote="voteItem(item)"
/>
</template>
</recycle-scroller>
参考: vue-virtual-scroller、vue-virtual-scroll-list、vue-lazyload
# vue以插件方式安装element
vue add element
会在src目录下生成一个plugins文件夹,里面一个element.js
import Vue from 'vue'
import { Button,Checkbox } from 'element-ui'
Vue.use(Button)
.use(Checkbox)
mainjs引用了
import './plugins/element.js'
# vuecli与图片
处理资源路径:当在 JavaScript、CSS 或 *.vue 文件中使用相对路径 (必须以 . 开头) 引用一个静态资源时,该资源将被webpack处理。
# 转换规则
- 如果 URL 是一个绝对路径 (例如 /images/foo.png ),它将会被保留不变。
<Img alt="Vue logo" src="/assets/logo.png">
<Img alt="Vue logo" src="http://image.xx.com/logo.png">
- 如果 URL 以 . 开头会作为一个相对模块请求被解释并基于文件系统相对路径。
<Img alt="Vue logo" src="./assets/logo.png">
- 如果 URL 以 ~ 开头会作为一个模块请求被解析。这意味着你甚至可以引用 Node 模块中的资源
<Img src="~some-npm-package/foo.png">
- 如果 URL 以 @ 开头会作为一个模块请求被解析。Vue CLI 默认会设置一个指向 src 的别名 @ 。
import Hello from '@/components/Hello.vue'
# css Module
CSS Modules 是一个流行的,用于模块化和组合 CSS 的系统。 vue-loader 提供了与 CSS Modules 的一流集成,可以作为模拟 scoped CSS 的替代方案。
- 添加module
<style module lang="scss">
.red {
color: #f00;
}
.bold {
font-weight: bold;
}
</style>
- 模板中通过$style.xx访问
<a :class="$style.red">awesome-vue</a>
<a :class="{[$style.red]:isRed}">awesome-vue</a>
<a :class="[$style.red, $style.bold]">awesome-vue</a>
- JS中访问
<script>
export default {
created () {
// -> "red_1VyoJ-uZ"
// 一个基于文件名和类名生成的标识符
console.log(this.$style.red)
}
}
</script>
# vue部分面试题
- vue组件和路由中的 name 作用
- 组件中的name
- 当项目使用keep-alive时,可搭配组件name进行缓存过滤
- DOM做递归组件时,递归迭代时需要调用自身name
- 当使用vue-tools时:vue-devtools调试工具里显示的组见名称是由vue中组件name决定的
- 路由中的name
- 可以用name跳转路由
- 通过name属性,为一个页面中不同的router-view渲染不同的组件,如:将上面代码的Hello渲染在 name为Hello的router-view中,将text渲染在name为text的router-view中。不设置name的将为默认的渲染组件。
- 组件中的name
<template>
<div id="app">
<router-view></router-view>
<router-view name="Hello"></router-view> //将渲染Hello组件
<router-view name="text"></router-view> //将渲染text组件
</div>
</template>
vuex →