Vue解决了什么问题
- vue用虚拟DOM解决了JS直接操作DOM的弊端。
- vue很好的处理了业务分层。
框架对比
比较 | 内容 | 备注 |
---|---|---|
jQuery库 2006年诞生 | 解决了浏览器兼容问题 本质是封装了DOM操作,通过JS直接操作DOM | 没有解决业务层混乱的问题,而且大片操作DOM时,浏览器会出现白屏和闪烁的情况,需长时等待才渲染完成 |
Vue.js | 虚拟DOM Vue只操作数据,不直接操作DOM 且数据和视图是分开的 很好的处理了业务分层 | 核心是数据驱动视图(响应式原理)+组件化 优点是:渐进式,组件化,轻量级,虚拟dom,响应式,单页面路由,数据与视图分开; 缺点是单页面不利于seo,首屏加载时间长; |
React.js | 数据变化需要手动调用 apisetState 方法封装了视图的更新 | 小程序数据变化需调用 apisetData 方法封装了视图的更新 |
数据-视图变化模式
模式 | 内容 | 备注 |
---|---|---|
命令式 | 改变变量值、innerHTML | JS原生 |
MVVM模式 | 数据驱动视图 | 数据变化,视图会自动变化 |
vue 分层式架构
标题 | 内容 | 备注 |
---|---|---|
最底层 | ES5构造函数Vue | - |
原型上定义的方法 | _init、$watch、_render | - |
全局API | set、nextTick、use | 在构造函数自身定义的 |
跨平台和服务端渲染及编译器 | - | - |
vue 源码结构及功能
标题 | 功能 | 备注 |
---|---|---|
flow | 定义和检测类型 | vue3替换为ts |
src/compiler | 将template 模板编译为render 函数 | 用vue-loader 编译(完整版自带) |
src/observe | vue 检测数据数据变化改变视图 | |
src/vdom | 将render 函数转为vnode 从而 patch 为真实dom 以及diff 算法实现 |
Vue 执行流程
上图为vue在初始渲染过程中的主干流程:先对选项对象初始化,通过Object.defineProperty
建立一套响应式系统,然后将模板解析成render
函数,再使用render
函数生成虚拟节点vnode
,在渲染前,对vnode
进行diff操作,最后进行必要的渲染。具体每一步都做了什么:
初始化
// Vue构造函数
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
// 执行初始化逻辑
this._init(options)
}
从Vue构造函数中得知,当执行new Vue()
时,只执行了一个_init
方法。_init
会根据传入的选项对vue进行初始化。 props、data、生命周期,事件机制的初始化都是在此过程中完成的。以data的初始化为例,vue会通过 Object.defineProperty
的方式将data的属性定义到vue实例上。这也就解释了为什么我们可以在vue中通过对 this.msg
进行赋值,可以修改data中属性的值了。
响应式处理
为什么数据驱动视图,即数据变化,视图也随之变化呢?
如何对数据进行响应式观测,核心就是通过Object.defineProperty
对数据进行劫持,在getter
中收集依赖,setter
中派发依赖,完整的响应式原理,如修改数据后视图是如何更新视图的还需要结合Dep和Watcher来看。
响应式原理用到的函数、方法
主题 | 内容 | 作用 | 备注 |
---|---|---|---|
Object.defineProperty() 方法 | JS引擎的功能。在对象上定义新属性/隐藏属性 | 检测对象属性变化 实现对象的响应式 | 数据响应式原理的核心 |
defineReactive 函数 | 封装Object.defineProperty() 方法利用闭包特性来实现数据劫持 | 实现对象的响应式 | 适用于简单结构的对象 |
Observer 类 | 递归侦测对象全部属性 | 检测对象属性变化 逐层遍历属性,逐层 defineReactive 并Observe() | 适用于复杂嵌套结构的对象: 把嵌套结构对象转换为每个层级的属性都是响应式的 |
Object.setPrototypeOf 方法 | 新数组的原型指向数组备份arrayMethods | 实现数组的响应式 | 触发新的数组方法被调用 |
Dep 类 | 封装了依赖收集的代码 | 管理依赖 | 每个Observer 实例的成员中都有一个Dep 的实例 |
Watcher 类 | 数据发生变化时通过Watcher 中转 | 中转并通知组件 |
Object.defineProperty()
Object.defineProperty
是JS引擎的功能,用于检测对象属性变化,以完成数据劫持和数据代理。该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
var obj = {};
// defineProperty 定义 obj 对象的某个属性的值,主要是定义隐藏属性
// defineProperty方法定义属性 与 obj.a = 3 的区别在于
Object.defineProperty(obj, 'a', {
value: 3,
writable:false // 属性是否可写,false时不可改变
// getter
get: function(){ // ES6 可省略写为 get(){}
console.log('访问obj的a属性')
},
// setter
set(){
console.log('改变obj的a属性')
}
});
Object.defineProperty(obj, 'b', {
value: 5,
enumerable:false // 属性是否被枚举,false时不可遍历
});
console.log(obj); // {a:3, b:5}
console.log(obj.a); // 访问obj的a属性,也称为数据劫持
obj.a ++; // 改变obj的a属性
defineReactive
函数
基于Object.defineProperty()
并不好用,其getter
和setter
函数需要一个变量周转,才能正常工作,所以用defineReactive
函数封装Object.defineProperty()
,利用闭包特性来实现数据劫持:
// defineReactive.js
var obj = {};
function defineReactive(data, key, val){
Object.defineProperty(data, key, {
writable:true, // 可读写
enumerable:true, // 可枚举
get(){
console.log('访问 obj 的' + key + '属性');
return val;
},
set(newValue){
console.log('改变 obj 的' + key + '属性', newValue)
if(val === newValue){return;}
val = newValue;
}
});
}
defineReactive(obj, 'a', 10); // 访问 obj 的 a 属性
obj.b = 66;
console.log(obj.b); // 改变 obj 的 b 属性 66
Observer
类
对于对象内的嵌套结构,需要用循环递归的方法侦测。具体是通过创建observer
类,实现对任何嵌套结构的对象obj2
,转换为每个层级的属性都是响应式的,可以被侦测的obj
。
具体的实现思路是:
observe(obj2)
- 看
obj2
身上有没有__ob__
属性 - 若没有2中属性,则
new Observer()
,将产生的实例添加__ob__
上 - 遍历下一层属性,逐个
defineReactive
- 当设置某个属性值的时候,会触发set,里面有newValue,也被
observe()
一下
// utils.js 遍历工具函数
export const def = function(obj, key, value, enumerable){
Object.defineProperty(obj, key, {
value,
enumerable,
writable:true,
configurable:true
});
}
// Observer.js
// 创建类以后,就考虑如何实例化
import {def} from './ utils.js';
import defineReactive from 'defineReactive.js';
export default class Observer {
constructor(value){
// 给实例添加了 __ob__ 属性,值是这次new的实例
def(value, '__ob__', this, false);
console.log('我是Observer构造器', value);
this.walk(value);
}
// 遍历
walk(value){
for(let k in value){
defineReactive(value, k);
}
}
};
// defineReactive.js
var obj = {
a: {
m: {
n: 5
}
},
b: 4
};
function defineReactive(data, key, val){
if(arguments.length == 2){
val = obj[key];
}
Object.defineProperty(data, key, {
writable:true, // 可读写
enumerable:true, // 可枚举
get(){
console.log('访问 obj 的' + key + '属性');
return val;
},
set(newValue){
console.log('改变 obj 的' + key + '属性', newValue)
if(val === newValue){return;}
val = newValue;
}
});
}
// index.js
import defineReactive from './defineReactive.js';
import Observer from './Observer.js';
var obj = {
a: {
m: {
n: 5
}
},
b: 4
};
// 创建 observe 函数,只为对象服务
function observe(value){
if(typeof value != 'object') return;
var ob;
if(typeof value.__ob__ !== 'undefined'){
ob = value.__ob__;
} else {
ob = new Observer(value);
}
return ob;
}
observe(obj);
Object.setPrototypeOf
方法
用于实现数组的响应式:
- vue 改写了数组的7个方法:
push
/pop
/shift
/unshift
/splice
/sort
/reserve
所有数组的方法都在数组的原型Array.prototype
对象上
vue 中数组的响应式实现方式:以Array.prototype
为原型,创建了一个arrayMethods
为对象的备份,再用ES6语法中的Object.setPrototypeOf
方法强制的将新数组的原型指向了arrayMethods
,从而触发新的数组方法被调用。
Dep
类
需要用到数据的地方,称为依赖。在getter
中收集依赖,在setter
中触发依赖。对于 Vue2 为中等粒度依赖,用到数据的组件是依赖。把依赖收集的代码封装成一个Dep类,专门用来管理依赖,每个Observer
实例成员中都有一个Dep的实例。Watcher
是一个中介,数据发生变化时,通过Watcher
中转,通知组件。
// Dep.js
export default class Dep{
constructor(){
console.log('我是Dep类的构造器')
}
}
Watcher
类
// Watcher.js
export default class Watcher{
constructor(){
console.log('我是Watcher类的构造器')
}
}
import {def} from './ utils.js';
import defineReactive from 'defineReactive.js';
imoport Dep from './Dep.js';
export default class Observer {
constructor(value){
// 每一个Observer的实例,都有一个dep
this.dep = new Dep();
// 给实例添加了 __ob__ 属性,值是这次new的实例
def(value, '__ob__', this, false);
console.log('我是Observer构造器', value);
this.walk(value);
}
// 遍历
walk(value){
for(let k in value){
defineReactive(value, k);
}
}
};
模板解析
步骤 | 作用 | 备注 |
---|---|---|
1. Parse | 接收 template 原始模板 按照模板的节点 和数据 生成对应的 ast | ast 就是以数据的形式描述一个东西的所有特征 |
2. Optimize | 遍历递归每一个ast节点 标记静态的节点 | 排除掉静态节点,优化性能 |
3. Generate | 把前两步生成完善的 ast 组装成 render 字符串 | render 字符串形态后面会转变成函数 |
4. new Fun(render) | 转为render函数后保存在实例上 | vm.$options.render |
当数据发生变化时,会触页面的重新渲染。vue是如何进行渲染的?
首先,vue会把将我们编写的HTML模板解析成一个AST描述对象,该对象是通过children
和parent
链接而成的树形结构,完整地描述了HTML标签的所有信息。
<div id="app">
<p>{{msg}}</p>
</div>
最终会解析成如下形式的AST对象:
{
attrs: [{name: "id", value: ""app"", dynamic: undefined, start: 5, end: 13}],
attrsList: [{name: "id", value: "app", start: 5, end: 13}],
attrsMap: {id: "app"},
children: [{
attrsList: [],
attrsMap: {},
children: [],
end: 33,
parent: {type: 1, tag: "div", ...},
plain: true,
pre: undefined,
rawAttrsMap:{},
start: 19
tag: "p",
type: 1
}],
end: 263,
parent: undefined,
plain: false,
rawAttrsMap:{id: {name: "id", value: "app", start: 5, end: 13}},
start: 0
tag: "div",
type: 1
}
然后 vue
根据AST对象生成 render
函数,该函数的函数体大致如下:
with(this){
return _c('div', {attrs:{"id":"app"}}, [_c('p', [_v(_s(msg))])])
}
也就是说,我们的模板最终在vue内部都是会以一个render
函数的形式存在。
vue官网上对此也有提及,一般推荐大家使用template,el等方式来指定模板,此外还可以通过使用render来自定义个性化的编译函数,不过vue内部最终都会解析成render函数。
先虚后实
我们得到render
函数之后,vue并未直接渲染成DOM树,而是先通过render
函数得到一个vnode
。而render的作用,也是为了生成跟模板节点一一对应的vnode
。
实际上这一步是非常有必要的,我们都知道频繁大量地操作DOM节点是极耗性能的。vue在渲染之前通过对vnode
的比较,可以大大规避非必要的DOM操作。下面是一个vnode
大致结构:
{
tag: "div",
children: [{tag: "p", ...}],
data: {attrs: {id: "app"}}
elm: DOM节点(div#app),
parent: undefined,
context: Vue实例,
...
}
最后,vue根据diff之后的结果,执行真正的dom节点的插入更新删除等操作,同时触发vue实例的生命周期钩子函数。之后,vue要做的就是观察数据的变化,进而决定是否重新渲染页面了。
Diff 作用
Diff 是精细化比对最小量更新。Diff 的出现,就会为了减少更新量,找到最小差异部分DOM,只更新差异部分DOM就好了,这样消耗就会小一些,数据变化一下,没必要把其他没有涉及的没有变化的DOM 也替换了。
Diff 做法
同层级比较新旧Vnode
节点,而不是比较DOM
,并不需要递归。Vue 只会对新旧节点中 父节点是相同节点 的 那一层子节点 进行比较。只有两个新旧节点是相同节点的时候,才会去比较他们各自的子节点,
最后,vue根据diff之后的结果,执行真正的dom节点的插入更新删除等操作,同时触发vue实例的生命周期钩子函数。之后,vue要做的就是观察数据的变化,进而决定是否重新渲染页面了。
Diff 比较逻辑
Diff 比较的内核是 节点复用,所以 Diff 比较就是为了在 新旧节点中 找到 相同的节点。这个的比较逻辑是建立在上一步说过的同层比较基础之上的。所以说,节点复用,找到相同节点并不是无限制递归查找
最后,vue根据diff之后的结果,执行真正的dom节点的插入更新删除等操作,同时触发vue实例的生命周期钩子函数。之后,vue要做的就是观察数据的变化,进而决定是否重新渲染页面了。
博文参考:孟思行《图解 Vue 响应式原理》、 Sunshine_Lin《Vue 源码学习》
感谢尚硅谷的vue源码解析课程~
响应式参考资料:
数据响应原理
通过Object.defineProperty
建立一套响应式系统。只要在 Vue 实例中声明过的数据,那么这个数据就是响应式的。
Object.defineProperty
方法 - Vue响应式系统的核心
该方法是JS引擎的功能,用于检测对象属性变化。具体讲:使用该方法可以为对象中的每一个属性,设置get和set方法。当属性被访问时,会触发 getter 函数;当属性被赋值时,会触发 setter 函数:
var obj={
name:"梅老板"
}
Object.defineProperty(obj,"name",{
get(){
console.log("get 被触发")
},
set(val){
console.log("set 被触发")
}
})
// 当访问 obj.name 时,会打印 ' get 被触发 '
// 当为 obj.name 赋值时,obj.name = 5,会打印 ' set 被触发 '
数据驱动具体是怎么实现的呢?数据改变驱动视图自动更新的大致过程为:
当执行new Vue()
时,只执行了一个_init
方法。_init
会根据传入的选项对vue进行初始化。 props、data、生命周期,事件机制的初始化都是在此过程中完成的。
以data的初始化为例,vue会通过 Object.defineProperty
的方式将data的属性定义到vue实例上。创建一个observer
对象,该对象与data绑定,通过 Object.defineProperty
将data中的所有的属性转换成getter/setter
。当data中的属性在vue实例中被访问(会触发getter),observer
对象就会把该属性收集为watcher
实例的依赖,之后当data中的属性在vue实例中被改变(会触发setter), observer
会通知依赖该属性的 watcher
实例重新渲染页面。
依赖收集
data 中的声明的每个属性,都拥有一个数组,保存着 谁依赖(使用)了 它
new Vue({
data(){
return {
name:"神仙朱"
}
}
})
另一个页面A引用了name
<div>{{name}}</div>
此时,name 使用 Dep
把页面A 的Watcher
(每个Vue实例都会拥有一个专属的watcher,用于实例更新)存储在他的小本本(每个声明的属性都会有一个专属的依赖收集器 subs
)里,并标记这个页面A 依赖我。有了这个记录,它就可以在发生改变的时候,通知依赖他的页面A ,从而让其完成更新。
此外,依赖 name 的地方,不只是页面,还会有 computed
,watch
...等。
依赖更新
通知所有的依赖进行更新。
数据代理
代理 proxy 是一种设计模式,vm 就 myData的代理,对myData对象的属性读写,全权由另一个对象vm负责
vm = new Vue({data: myData})
- 会让 vm 成为 myData 的代理 proxy
- 会让 myData 的所有属性进行监控
- 为什么要监控,为了防止 myData 的属性变了,vm不知道;
- vm 知道属性变了就可以调用 render(data),UI=render(data)