Part.0 概念认知
文章伊始,先提几个与 Vue 相关的概念,譬如:
- Mvvm
- 双向绑定
- 数据驱动
诸如此类,均可以概况 Vue 的一些特征,粗略的概括一下这些词的含义,大概就是:使用Vue的时候,我们可以专注于视图,专注于数据,而不会因为数据和视图之间的复杂关系而牵扯精力,因为Vue会用更高效的方式帮我们完成这些中间操作,这些费时费力的中间操作,我就大胆归结为"Dom操作"
所以简而言之 ,
"Vue帮助我们避免了直接操作Dom"
Part.1 为什么要避免直接操作Dom?
所以为什么要避免操作Dom,我认为可以从两个层面来解释:
针对开发过程来讲:
直接操作Dom会牵扯我们大量的时间和精力,比如用过jQuery开发的人都知道,我们经常要借助jQuery的各种选择器,以及Dom方法,穿越层层嵌套的标签,来帮助我们获取数据,更新数据,更新视图
针对开发结果来讲:
页面中如果存在着复杂的Dom操作,就会引起大量的重绘重排,从而严重影响页面运行的性能
所以Vue在项目中起到的核心作用我认为可以归结为两点:
1.帮助我们更新视图,提升开发效率
2.帮助我们以更聪明的方式更新视图,提升页面性能
Part.2 Vue是怎样做到这些的?
至此,涉及Vue的内部实现原理,
我粗略浏览了网上一些资料,大致提炼出如下几个概念:
- 响应式系统
- 模板编译
- 虚拟dom
- diff算法
其中【响应式系统】更倾向于帮助我们更新视图,因为这些原理,Vue才会知道何时去更新视图
而【虚拟dom】,和【diff算法】这些是一种很先进的、更新视图的方式,从而保证了性能的最大化
本文仅仅着眼于【响应式系统】
Part.3 响应式系统的原理
究竟何谓"响应"?我理解的是:面对数据变化时Vue做出的响应
当数据发生变化时,我们需要解决两个问题:
- 我们要知道哪个值发生变化了?
- 当这个值发生变化时,有哪些依赖于这个数值的其他数值也需要随之变化?
在解决这两个问题之前,我们先认识一个方法: Object.defineProperty()
我们肯定都知道一个对象可以有很多属性,而且每个属性具有不同的特性,比如是否可枚举,是否可修改等等
Object.defineProperty() 这个方法就可以用来设置这些特性,其使用方法如下
Object.defineProperty(obj, prop, descriptor)
// 这个方法包含三个参数,分别为:
// obj: Object 目标对象
// prop: String 目标属性
// descriptor: Object 特性对象
// 其中 descriptor包含了如下等特性可供设置
// enumerable: Boolean 属性是否可枚举,默认 true
// configurable: Boolean 属性是否可以被修改或者删除,默认 true
// get: Function 获取属性时调用的方法
// set: Function 设置属性时调用方法
// ...
// 以下例子简单演示了这个方法的使用
// 声明一个对象
var a = {
name: 'miaogang'
}
// 使用propertyIsEnumerable()方法检测发现,新声明的属性默认是可枚举的
a.propertyIsEnumerable('name') // true
// 通过Object.defineProperty()方法将其设置为不可枚举的
Object.defineProperty(a, 'name', {
enumerable: false
})
// 结果说明,设置生效了
a.propertyIsEnumerable('name') // false
// 注:属性的enumerable这一特性会影响Object.keys()、for..in等语法的结果
明白了**Object.defineProperty()**如何使用之后,话题继续回到【响应式系统】
在【响应式系统】中,需要用到属性的两个特性:set方法/get方法
用来解决我们实现响应式系统需要解决的两个问题:
1.我们要知道哪个值发生变化了?
2.当这个值发生变化时,有哪些依赖于这个数值的其他数值也需要随之变化?
当某个属性的set方法被调用的时候,我们就知道该属性的值被更改了,此时Vue会去更新视图,从而完成了对这一更改的【响应】
当某个属性的get方法被调用时,就说明有其他的属性【依赖】于这一属性,那我们就将这个依赖关系记录下来,所以当这一属性发生更改时,所有【依赖】于这一属性的其他属性也要相应变化,这个将属性间的依赖关系记录下来的过程就像叫做【依赖收集】
Part.4 响应式系统的代码实现
为了使代码更容易理解,我们先不考虑【依赖收集】的部分
Vue内部实现了一个**defineReactive()**方法用于将数据【响应化】
**defineReactive()**方法实现如下:
function defineReactive (obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
return val;
},
set: function reactiveSetter (newVal) {
// 当对一个属性进行赋值操作的时候,就会调用该属性的get方法
// 但是给一个属性赋的新值与之前的值相等时,则不必更新视图
// 所以在此添加一层判断从而避免不必要的视图更新
if (newVal === val) return;
val = newVal;
cb(newVal); // 更新视图
}
});
}
**defineReactive()**方法用于将一个对象的一个属性【响应化】
所以Vue中又实现了一个**Observer()**方法用于将一个对象的所有可枚举属性,批量【响应化】
**Observer()**方法具体实现如下:
function observer (value) {
if (!value || (typeof value !== 'object')) {
return;
}
// Object.keys()会返回一个对象的所有可枚举属性的属性名所组成的数组
Object.keys(value).forEach((key) => {
// 使用defineReactive()使遍历到的每个属性【响应化】
defineReactive(value, key, value[key]);
});
}
// 当我们new一个Vue实例的时候,Vue的构造函数会进行以下处理:
let o = new Vue({
data: {
test: "I am test."
}
})
// 我们编写的options传入到Vue的构造方法中
class Vue {
constructor(options) {
this._data = options.data;
// 然后Vue会使用oberver()函数,来将data中的数据【响应化】
observer(this._data);
}
}
// 整个过程简单演示了Vue进行初始化时是如何构建响应式系统的
// 但是这只是一个方便理解的直观例子,并没有覆盖多数真实场景
// 比如: defineReactive()方法应该是递归调用的,用于让data中声明的属性以及其所有后代属性【响应化】
// 再比如: 目前的defineReactive()方法并不适用于处理数组,面对数组时还需要额外的处理
Part.5 响应式系统中的依赖收集
依赖收集是基于 观察者/订阅者 的设计模式来实现
我们【响应化】的每个属性都是一个订阅者,而依赖于这个属性的每个其他属性都是这个属性的观察者
当订阅者发生变化时,会通知其所有观察者发生变化
// 实现一个订阅者Dep
class Dep {
constructor () {
// 每个订阅者会有一个数组,用于存放他的观察者们
this.subs = [];
}
// 用于添加观察者的方法
addSub (sub) {
this.subs.push(sub);
}
// 通知所有观察者进行视图更新
notify () {
this.subs.forEach((sub) => {
sub.update();
})
}
}
// 实现一个观察者Watcher
class Watcher {
constructor () {
// 每次新增一个Watcher实例的时候,会把当前的实例本身暴露出去
Dep.target = this;
}
// 用于更新视图
update () {
console.log("视图更新啦~");
}
}
实现了观察者以及订阅者之后,我们调整一下之前的代码
function defineReactive (obj, key, val) {
// 处理每个对象的时候,首先实例化一个订阅者
const dep = new Dep();
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;
dep.notify(); // 当该属性发生变化时
cb(newVal); // 更新视图
}
});
}
class Vue {
constructor(options) {
this._data = options.data;
observer(this._data);
// 实例化一个Watcher对象
new Watcher();
// 模拟视图最初的渲染
console.log('render~', this._data.test);
}
}
Part.6 响应式系统的实际应用
Vue 官网文档有这样一句话:
由于 Vue 不允许动态添加根级响应式属性,所以你必须在初始化实例前声明根级响应式属性,哪怕只是一个空值
例子:JSFIDDLE
参考资料:剖析 Vue.js 内部运行机制
菜鸟前端,尚不成熟,有偏差还望指正 以上
Mragon