把【Vue响应式原理】掰开了,揉碎了讲给你听

417 阅读7分钟

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做出的响应

当数据发生变化时,我们需要解决两个问题:

  1. 我们要知道哪个值发生变化了?
  2. 当这个值发生变化时,有哪些依赖于这个数值的其他数值也需要随之变化?

在解决这两个问题之前,我们先认识一个方法: 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