浅谈vue响应式原理

以下只是个人在学习过程中的总结,原理大概了解了一番,把自己的理解写在了概述里。

但是看源码的时候就一脸懵逼,后续有更深入的理解再回来完善(捂脸),如有疏漏,欢迎指出。

预备知识

首先需要明白三个概念:

  • Vue响应式
  • Object.defineProperty(obj , prop , descriptor)
  • 观察者模式(又名,发布者订阅者模式)

Vue响应式

响应式就是当数据发生变化后会重新渲染页面。

要完成这个过程,需要:

  • 监听数据的变化
  • 收集视图依赖了哪些数据
  • 数据变化时,自动通知需要更新的视图部分进行更新

换成专业术语来讲,即需要:

  • 数据劫持/数据代理
  • 依赖收集
  • 观察者模式

Object.defineProperty

该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

三个参数分别表示:要定义属性的对象、要定义或修改的属性的名称或 Symbol、要定义或修改的属性描述符。

该方法中有两个可选字段:set 和 get,分别代表这个属性的 getter函数 和 setter函数 。

当要取该属性的值时,会调用set函数,并返回一个值。

当修改该属性的值时,会调用get函数,将传入的参数赋给属性。

// 在对象中添加一个属性与存取描述符的示例
var bValue;
var o = {};

Object.defineProperty(o, "b", {
  enumerable : true,
  configurable : true,
  get : function(){
    console.log('监听正在获取b')
    return bValue;
  },
  set : function(newValue){
    console.log('监听正在设置b')
    bValue = newValue;
  },
});

o.b = 38;//监听正在设置b
console.log(o.b)//监听正在获取b
                //38

观察者模式

观察者模式分为 注册环节 和 发布环节 

比如我们订阅一本杂志的时候,杂志方会把我们的信息记录下来,这就是观察者模式中的注册环节

当新一期杂志发行后,杂志方就会一次性通知所有记录了的客户,这就是观察者模式中的发布环节

  function Dependency() {
    this.customers = []

    register(people) {
      this.customers.push(people)
    }

    notify() {
      this.customers.forEach(people => {
        console.log('give ' + people + ' a book');
      })
    }
  }

  const bookStore = new Dependency();
  // 记录每一个需要订阅的用户
  bookStore.register('sandy')
  bookStore.register('lily')
  bookStore.register('andy')

  // 最后杂志发行之后,通知所有的客户
  bookStore.notify()

概述

  1. 当创建一个Vue实例对象时,底层代码的Observer会对Vue.data进行reactive化,即对Vue.data中的属性进行遍历,用Object.defineProperty给属性加上set和get,以达到监听对象属性修改的目的。另外,会给每个属性分别创建一个对应的Dep对象,其中有一个subs数组可用来记录模板中使用该属性的组件的Watcher对象(下文将提到)等。
  2. 而后底层代码的Compile会解析Vue实例中的el模板,每当发现有组件(比如,{{name}})使用了data中的属性,就会触发属性的get方法,然后给组件创建一个Watcher对象,并把这个Watcher对象存入该属性对应Dep对象的subs数组中,该过程称为依赖收集;同时,get方法返回一个值给这个组件,组件把得到的数据渲染到页面View上,页面初次渲染完成。
  3. 当data中某一个属性发生了改变时会触发属性的set方法。在修改值的同时,set还会调用相应的Dep的notify()方法,即Dep.notify(),来遍历Dep中的subs数组,让每个Watcher执行自己的update()方法更新视图。

响应式原理三个阶段

摘自知乎某答案,不完全。

看起来是ts写的伪代码,我看得也不是很懂,不过可以对照着上述概述加深理解。

init阶段

 Vue的data的属性都会被reactive化,即加上 set / get函数。

function defineReactive(obj: Object, key: string, ...) {
    const dep = new Dep()

    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        ....
        dep.depend()
        return value
        ....
      },
      set: function reactiveSetter (newVal) {
        ...
        val = newVal
        dep.notify()
        ...
      }
    })
  }
  
  class Dep {
      static target: ?Watcher;
      subs: Array<Watcher>;

      depend () {
        if (Dep.target) {
          Dep.target.addDep(this)
        }
      }

      notify () {
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i < l; i++) {
          subs[i].update()
        }
      }

mount 阶段

mount 阶段的时候,会创建Watcher类的对象。

这个Watcher实际上是连接Vue组件与Dep的桥梁,每一个Watcher对应一个vue component。

mountComponent(vm: Component, el: ?Element, ...) {
    vm.$el = el

    ...

    updateComponent = () => {
      vm._update(vm._render(), ...)
    }

    new Watcher(vm, updateComponent, ...)
    ...
}

class Watcher {
  getter: Function;

  // 代码经过简化
  constructor(vm: Component, expOrFn: string | Function, ...) {
    ...
    this.getter = expOrFn
    Dep.target = this                      // 注意这里将当前的Watcher赋值给了Dep.target
    this.value = this.getter.call(vm, vm)  // 调用组件的更新函数
    ...
  }
}

更新阶段

当data属性发生改变的时候,就去调用Dep的notify函数,然后通知所有的Watcher调用update函数更新。

notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
}

参考

猜你喜欢

转载自blog.csdn.net/weixin_42207975/article/details/107338724