【Vue深入】之响应式原理

582 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第6天,点击查看活动详情

vue深入系列包括以下内容,有兴趣的读者可以选择阅读:

【Vue深入】之虚拟DOM - 掘金 (juejin.cn)

【Vue深入】之DIFF算法 - 掘金 (juejin.cn)

【Vue深入】之生命周期 - 掘金 (juejin.cn)

【Vue深入】之路由router - 掘金 (juejin.cn)

【Vue深入】之Vuex状态管理 - 掘金 (juejin.cn)

引言

现如今Vue已成为当下主流框架,其中一些核心概念更是面试须知,本文主要进行对Vue响应式进行介绍,希望能够对大家有所帮助。

原理图

看过vue官方文档的读者,对这张图应该已然相当熟悉了,下面便就下图进行分析

Object.defineProperty

Vue的响应式原理是通过Object.defineProperty实现的。被绑定过的对象,会变成「响应式」化。也就是改变这个对象的时候会触发get和set方法。进而触发一些视图上的更新。

如下所示:

function defineReactive (obj, key, val) {
    Object.defineProperty(obj, key, {
        enumerable: true,                       //表示对象的属性是否可以枚举
        configurable: true,                     //表示对象的属性是否可以被删除
        get: () => {
            console.log('数据被读取了');
            return val;
        },
        set: newVal => {
            val = newVal;
            console.log("数据被改变了");
        }
    })
}


let data = {
    message: 'hello world',
};

defineReactive(data, 'message', data.message);  // 对data上的message属性进行绑定

console.log(data.text);                         //  数据被读取了
data.text = 'hello Vue';                        // 数据被改变了

主要通过defineReactive函数嵌套Object.defineProperty形成闭包环境,之后通过访问属性的get或set方法达到响应式的目的

但是object.defineproperty也存在一些缺点:

1、对于复杂的对象需要深度监听,递归到底,一次性计算量

2、无法通过修改新增属性(Vue.set)或删除属性(Vue.delete)进行响应式修改。(通过执行数组七大原生方法进行修改)

3、无法通过直接修改数组下标的方式进行响应式修改。(通过执行this.$emit()方法进行解决)

这也就是vue3改进的一方面,有兴趣的读者可以查看笔者的另一篇文章【面试宝典】之vue3.0对比vue2.x优势 - 掘金 (juejin.cn)

Observer

Vue中使用Observer类来管理响应式化Object.defineProperty的过程。

如下所示:

class Observer {
    constructor() {
    	observe(this.data);                              // 通过方法响应式绑定数据
    }
}

export function observe (data) {
    const keys = Object.keys(data);
    for (let i = 0; i < keys.length; i++) {
       defineReactive(obj, keys[i]);                     // 将data中定义的每个属性进行响应式绑定
    }
}

依赖收集

什么是依赖收集?

我们通过defineReactive方法将data中的数据进行响应式之后,虽然可以监听到数据的变化了,但是我们应该如何通知视图进行更新呢?这个时候便需要依赖收集进行处理。

完成流程图如下:

订阅者 Dep

为什么需要 Dep

收集依赖需要为依赖找一个存储依赖的地方,为此我们创建了Dep,它用来收集依赖、删除依赖和向依赖发送消息等。

Dep的简单实现

如下所示:

class Dep {
    constructor () {  
        this.subs = [];                          // 用来存放Watcher对象的数组 
    }
    addSub (sub) {                               // 在subs中添加一个Watcher对象 
        this.subs.push(sub);
    }
    notify () {                                  // 通知所有Watcher对象更新视图 
        this.subs.forEach((sub) => {
            sub.update();
        })
    }
}

Dep主要做两件事情:

  • addSub 方法可以在目前的 Dep 对象中增加一个 Watcher 的订阅操作;
  • notify 方法通知目前 Dep 对象的 subs 中的所有 Watcher 对象触发更新操作。

观察者 Watcher

为什么需要Watcher

当属性发生变化后,我们要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户写的一个watch,这时需要抽象出一个能集中处理这些情况的类。然后,我们在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个,再由它负责通知其他地方。

Watcher的简单实现

如下所示:

class Watcher {
  constructor(obj, key, cb) {
    Dep.target = this                        // 将 Dep.target 指向自己
    this.cb = cb
    this.obj = obj
    this.key = key
    this.value = obj[key]                    // 触发属性的 getter 添加监听
    Dep.target = null                        // 将 Dep.target 置空
  }
  update() {  
    this.value = this.obj[this.key]          // 获得新值
    this.cb(this.value)                      // 我们使用cb函数来模拟视图更新,调用它即代表更新视图
  }
}

在执行构造函数的时候将 Dep.target 指向自身,从而使得收集到了对应的 Watcher,在派发更新的时候取出对应的 Watcher ,然后执行 update 函数。

如何进行收集依赖

在getter中收集依赖,在setter中触发依赖。先收集依赖,即把用到该数据的地方收集起来,然后等属性发生变化时,把之前收集好的依赖循环触发一遍就行了。

最后我们对 defineReactive 函数进行改造,在自定义函数中添加依赖收集和派发更新相关的代码,实现一个简易的数据响应式。

如下所示:

function observe (obj) {
  if (!obj || typeof obj !== 'object') {      // 判断类型
    return
  }
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
  function defineReactive (obj, key, value) {
    observe(value)                            // 递归子属性
    let dp = new Dep()                        //新增
    Object.defineProperty(obj, key, {
      enumerable: true,                       //表示对象的属性是否可以枚举
      configurable: true,                     //表示对象的属性是否可以被删除
      get: () => {
        console.log('数据被读取了');
                                              // 将 Watcher 添加到订阅
       if (Dep.target) {
         dp.addSub(Dep.target)                // 新增
       }
        return value
      },
      set: function reactiveSetter (newVal) {
        observe(newVal)                       //如果赋值是一个对象,也要递归子属性
        if (newVal !== value) {
          console.log('数据被修改了') 
          render()
          value = newVal
                                              // 执行 watcher 的 update 方法
          dp.notify()                         //新增
        }
      }
    })
  }
}

class Vue {
    constructor(options) {
        this._data = options.data;
        observer(this._data);
        /* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象 */
        new Watcher();
        console.log('模拟视图渲染');
    }
}

总结

通过遍历所有data中的属性,使用Object.defineProperty为其添加getter和setter,在getter中每个属性会new Dep来被记录为一个依赖,每一个依赖都有若干个wachter进行监视,当data中数据改变时,通知给相应的dep,dep再通知给wachter,wachter就从对应的data中拿到值后渲染到页面。

结语

本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力。