# Vue系列之4-原理
# 前言
- 前端三大框架对比
框架 | 发布年份 | 出自 | star数 | 特性 |
---|---|---|---|---|
Angular | 2010年 | 73.7k | 双向数据绑定 | |
React | 2013年 | 169k | VirtualDOM、Redux | |
Vue | 2014年 | 尤雨溪EvanYou | 184k | 更轻量、易于上手、中文文档友好 |
框架趋同/互相借鉴
- Vue借鉴knockout模板引擎、借鉴Angular双向数据绑定、借鉴React虚拟dom/redux/JSX
- 跨端开发(Ionic/ReactNative/Weex)
- 桌面开发(electron支持vue/react)
- 总之,你有我有全都有,大家好才是真的好
类vue的开发模式(新赛道):小程序、uniapp
阅读本文后你将了解到:
- Vue是什么?解决了什么问题?
- MVVM架构
- 三要素(响应式、模板编译、vdom/diff算法)
- 组件渲染/更新过程
PS:本文内容主要针对Vue2,涉及少量vue3.0内容
# 一、Vue是什么?
vue是什么?
Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。
- 轻量级渐进式框架(便于与第三方库或既有项目整合)
- 生态丰富(vue-cli、vue-router、vuex、社区UI组件库...)
- 易于上手(入门简单、中文文档友好)
vue解决了什么问题?
- HTML:从0到1
- CSS:提供装饰
- JavaScript(DOM):支持页面动态化(例如倒计时)
- jQuery:解决浏览器兼容问题、优雅API(解放前端,造轮子)
- Vue:数据驱动视图,让开发者从DOM操作中解放(如倒计时,自加器)
疑问:数据驱动视图是怎么做到的?(数据变化 -> 视图更新)
# 1、如何理解MVVM
MVVM是一种软件架构模式,MVVM是MVP的变体,MVP模式和MVVM模式都是MVC模式的变体。MVC,MVP 和 MVVM 的图示 (opens new window)
软件构架对代码进行解耦分层,各层互不影响,有效降低了开发复杂度。我们可以通过调整MVC三者之前的通信模式,来达到一定的架构目的。
MVVM对视图更新模式的影响
- 静态渲染:更新需要进行DOM操作,如ASP/JSP/PHP,适合业务简单的场景
- 数据驱动视图:通过数据即可更新视图,如Vue/React/Angular,适合业务复杂的场景
MVVM包含3部分:
- View:用户看到屏幕的结构、布局和外观,也称UI
- ViewModel:是一个绑定器,能和
View
和Model
层进行通信 - Model:是数据和逻辑
# 2、MVVM在Vue中的体现
示例:
<template>
<div id="app">
<h1>{{ message }}</h1>
<button @click="reverse">reverse</button>
</div>
</template>
2
3
4
5
6
var app = new Vue({
el: "#app",
data: {
message: "Hello Vue123!!"
},
methods: {
reverse() {
this.message = this.message.split("").reverse().join("");
}
}
});
2
3
4
5
6
7
8
9
10
11
- View: template模板
- Model: data数据
- ViewModel: Vue实例
PS:Vue没有严格遵循 MVVM 模式:严格的MVVM要求View不能和Model直接通信,而Vue在组件中提供了$refs这个属性,让Model可以直接操作View,违反了这一规定。
疑问:vue是如何实现MVVM的?
# 3、Vue三要素
- 响应式:vue如何监听到 data 的每个属性变化?
- 模板引擎:vue的模板如何被解析?
- 渲染:vue如何进行高效渲染?
# 二、Vue三要素-响应式
响应式:组件 data 的数据一旦变化,立刻触发视图的更新。 响应式如何实现?
# Object.defineProperty(IE9+)
let obj = {};
let value = null;
Object.defineProperty(obj, 'a', {
get: () => {
console.log('trigger get');
// 收集依赖 todo...
return value;
},
set: (val) => {
console.log('trigger set');
if (val !== value) {
value = val;
// 数据变更,需要重新渲染 todo...
}
}
})
console.log(obj.a);
obj.a = 1;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
缺点
- 深度监听需要递归到底,一次性计算量大
- 无法监听新增属性/删除属性(Vue.$set)
- 无法原生监听数组,需要特殊处理
# proxy(Vue3.0,IE11+)
let obj = {}
let reactiveObj = new Proxy(obj, {
get: function(obj, prop) {
console.log('trigger get');
// 收集依赖 todo...
return obj[prop];
},
set: function(obj, prop, value) {
console.log('trigger set');
obj[prop] = value;
if (obj[prop] !== value) {
// 数据变更,需要重新渲染 todo...
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
缺点:
- 有兼容性的问题,caniuse 95%
- 它会修改JavaScript的一些底层代码的执行方式,所以它是无法被完全polyfill的
响应式带来的问题:
- jQuery可以自行控制DOM操作的时机,手动调整,而响应式的DOM操作则在内部进行;
- DOM 操作非常耗费性能
疑问:如何有效控制DOM操作?
# 三、Vue三要素-渲染:虚拟DOM(Virtual DOM)
Vue是数据驱动视图,如何有效控制DOM操作?
- 解决方案:
vdom
- JS执行速度快
- 用JS模拟DOM结构,计算出最小的变更,操作DOM
vdom
是优化方案,不是响应式必须vdom
是实现vue
和react
的重要基石diff算法
是vdom
中最核心、最关键的部分
- 用JS模拟DOM结构:
vnode
<div id="div1" class="container">
<p>vdom</p>
<ul style="font-size: 20px;">
<li>a</li>
</ul>
</div>
2
3
4
5
6
{
tag: 'div',
props: {
classname: 'container',
id: 'div1'
},
children: [
{
tag: 'p',
children: 'vdom'
},
{
tag: 'ul',
props: { style: 'font-size: 20px;' },
children: [
{
tag: 'li',
children: 'a'
}
]
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
vdom
小结:数据驱动视图的模式下,有效控制DOM操作vnode
:用JS模拟DOM结构diff算法
:新旧vnode对比,得出最小更新范围,最后更新DOM
# 四、diff算法-vdom核心部分
diff 算法是一种通过同层的树节点进行比较的高效算法,避免了对树进行逐层搜索遍历,所以时间复杂度只有 O(n)
# 概述:
- diff即对比,是一个广泛的概念(非独创),如linux diff命令,git diff等
- 两个js对象也可以做diff
- 两棵树做diff,如这里的vdom diff
# 树diff的时间复杂度O(n^3)
- 1、遍历tree1
- 2、遍历tree2
- 3、排序
- 1000个节点,要计算1亿次,算法不可用
# 优化时间复杂度到O(n)
- 只比较同一层级,不跨级比较
- tag不相同,则直接删掉重建,不再深度比较
- tag和key,两者都相同,则认为是相同节点,更新dom,并继续比较节点的子元素
# diff算法流程
1、首次渲染,取vnode进行渲染即可
2、数据更新后,将vnode和oldVnode进行对比
- 从根节点开始遍历,判断当前的旧节点和新节点是否同一节点(sel和key相同)
- 若不是同一节点,则删掉重建;
- 若是同一节点,则更新当前节点dom,继续处理子元素children
3、子元素children的对比算法(尽可能多地复用真实DOM,尽可能少的添加删除真实DOM) 思路:添加4个指针,分别指向新旧children的开始和结束比较的过程中,循环从两边向中间收拢;
- 第一步:分别进行开始开始、结束结束、开始结束、结束开始对比,若匹配成功,则指针向中间收拢;
- 第二步:若第一步未匹配,则在旧children中遍历查找是否匹配新children的开始节点,若匹配成功,则移动旧节点到对应位置;
- 第三步:循环结束后,根据新老节点的数目不同做相应的添加或者删除节点操作
各场景图示(这里看动画 👉 diff算法图解动画 (opens new window))
- k -> key
- a/b/c/d -> 元素内容
- 所有元素的tag都相同,可以理解均为
<li>
删除节点
新增节点
未设置key
设置key
需要说明的是,节点的更新有三种开销:
- 第一种是不能复用节点时,重建节点,更新dom
- 第二种是能复用节点时,直接更新dom
- 第三种是能复用节点,但需移动dom,然后更新
数组加key即符合第三种情况。
# 五、Vue三要素-模板编译
概览
- 前置知识:JS的with语法
- 改变{}内自由变量的查找规则,当做obj属性来查找
- 如果找不到匹配的obj属性,就会报错
- with要慎用,它打破了作用域规则,易读性变差
- vue模板(不是html,有指令、插值、JS表达式)到底是什么?
- vue如何处理模板?组件渲染和更新过程?
- 前置知识:JS的with语法
步骤:
- vue-template-complier将模板
<template>
编译为 render 函数 - 执行 render 函数生成 vnode
- 基于 vnode 再执行 patch 和 diff,进行渲染和更新
- 修改数据后,会生成新的vnode
- vue-template-complier将模板
vue模板被编译成什么(
vue-template-compiler
)
<p></p>
with(this){return _c('p', [_v(_s(message))])}
类似于:
render: function (createElement) {
return createElement(
'p', // 标签名称
this.message.toString() // 子节点数组
)
},
2
3
4
5
6
- 其他注意点:
- 使用webpack vue-loader,会在开发环境编译模板
- vue组件可以用render代替template,react默认使用render写法
# 六、Vue的组件渲染/更新过程
- 初次渲染过程
- 解析模板为render函数(或在开发环境已完成,vue-loader)
- (页面渲染时)执行render函数,生成vnode,patch(elem,vnode)
- 触发响应式,监听data属性getter setter
- 更新过程
- 修改data,触发setter(此前在getter中已被监听)
- 重新执行render函数,生成newVnode
- 对比新旧vnode,patch(vnode,newVnode),更新到dom上
# 七、Vue3.0的性能优化
性能比vue2.x快1.2~2倍
静态标记:diff方法优化vue2.x中的虚拟dom是进行全量的对比。而vue3.0新增了静态标记。在与上次虚拟节点进行对比的时候,只对比带有patch flag的节点,并且可以通过flag的信息得知当前节点要对比的具体内容。
静态提升:vue2.x中无论元素是否参与更新,每次都会重新创建,然后再渲染。vue3.0中对于不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用即可。
事件侦听器缓存:默认情况下,如onClick事件会被视为动态绑定,所以每次都会追踪它的变化,但是因为是同一个函数,所以不用追踪变化,直接缓存起来复用即可。
# 回顾
- Vue是什么?解决了什么问题?
- MVVM架构
- 三要素:响应式、模板编译、vdom/diff算法(重要)
- 组件渲染/更新过程(重要)
- 异步渲染
- $nextTick
- 汇总data的修改,一次性更新视图
- 减少DOM操作次数,提高性能