# vue

  1. 对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操作代码,在提高开发效率、可读性同时还保持了优越的性能表现。
  1. 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处理。

# 转换规则

  1. 如果 URL 是一个绝对路径 (例如 /images/foo.png ),它将会被保留不变。
<Img alt="Vue logo" src="/assets/logo.png">
<Img alt="Vue logo" src="http://image.xx.com/logo.png">
  1. 如果 URL 以 . 开头会作为一个相对模块请求被解释并基于文件系统相对路径。
<Img alt="Vue logo" src="./assets/logo.png">
  1. 如果 URL 以 ~ 开头会作为一个模块请求被解析。这意味着你甚至可以引用 Node 模块中的资源
<Img src="~some-npm-package/foo.png">
  1. 如果 URL 以 @ 开头会作为一个模块请求被解析。Vue CLI 默认会设置一个指向 src 的别名 @ 。
import Hello from '@/components/Hello.vue'

# css Module

CSS Modules 是一个流行的,用于模块化和组合 CSS 的系统。 vue-loader 提供了与 CSS Modules 的一流集成,可以作为模拟 scoped CSS 的替代方案。

  1. 添加module
<style module lang="scss"> 
	.red { 
		color: #f00; 
	}
	.bold { 
		font-weight: bold; 
	}
</style>
  1. 模板中通过$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>
  1. JS中访问
<script> 
export default { 
	created () { 
		// -> "red_1VyoJ-uZ" 
		// 一个基于文件名和类名生成的标识符
		console.log(this.$style.red) 
	} 
}
</script>

# vue部分面试题

  1. 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的将为默认的渲染组件。
<template>
  <div id="app">
     <router-view></router-view>
     <router-view  name="Hello"></router-view> //将渲染Hello组件
     <router-view  name="text"></router-view>   //将渲染text组件
  </div>
</template>

vue面试题 (opens new window)

参考网站 (opens new window)

发布订阅 (opens new window)

发布订阅区别 (opens new window)

最后更新: 1/29/2023, 8:15:20 AM