vue响应式原理:观察者模式

 作为计算机工程师,框架是提高开发效率的重要工具。理解框架的核心原理,有助于更好地使用它和定位问题。同时,一个优秀的框架,其设计方案和实现原理也是值得我们学习和借鉴的。本文将通过实现一个简单的响应式系统,来理解vue.js的响应式原理。

关键词:响应式原理、观察者模式、defineProperty、proxy

 在vue.js中,允许用模板语法声明式地描述页面。例如下面代码:

<div id="app">
  {{ message }}
</div>
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

 不难看出,它描述了一个div元素,其文本内容关联了变量message。当message被修改时,视图会进行更新,这就是响应式。

 这里的message是Vue构造函数的data选项的property。Vue实例被创建时,会把这些property加入响应式系统,系统负责监听变化和订阅依赖,然后在property变化时通知组件更新。(注:这里的依赖是指 组件依赖于property)

 响应式系统的核心是状态通知,可基于观察者模式进行设计。

状态通知

观察者模式:当一个对象的状态发生改变时,所有关联的对象会得到通知并自动更新。解决的是一个对象状态改变给其他对象通知的问题

观察者模式中有两种角色:

  • 目标对象(Subject):拥有一个观察者列表,并提供注册、删除观察者的方法,以及在状态发生改变时,通知所有已注册的观察者对象。
  • 观察者(Object):提供一个更新自身状态的方法,以便给目标对象(Subject)状态发生改变时可以调用。

下面是观察者模式的UML图:

image-20220305164252491

 根据观察者模式,分别找出响应式系统中的目标对象(Subject)和观察者(Object):这里的Subject负责在property变化时通知Observer,而Observer在接收到变更通知时触发组件更新。代码如下:

Subject:

/**
 * A subject is an observable that can have multiple
 * observers subscribing to it.
 */
export default class Subject {
  constructor() {
    this.observers = [];
  }

  subscribe(observer) {
    this.observers.push(observer);
  }

  notify() {
    const observers = this.observers;
    observers.forEach(observer=>{
        observer.update();
    });
  }
}

Observer:

/**
 * A observer parses an expression
 * and fires callback when the expression value changes.
 * This is used for both the $watch() api and directives.
 */
export default class Observer {
    constructor(vm, exp, cb) {
        this.vm = vm;
        this.cb = cb;
        // parse expression for getter
        this.getter = parsePath(exp);
    }
    get() {
        let value;
        const vm = this.vm;
        value = this.getter.call(vm, vm);
        return value;
    }
    /**
     * Observer interface.
     * Will be called when a subject changes.
     */
    update() {
        const value = this.get();
        if (value !== this.value) {
            const oldValue = this.value;
            this.value = value;
            this.cb.call(this.vm, value, oldValue);
        }
    }
}

监听变化和订阅依赖

 基于观察者模式,我们抽象出Subject和Object两个类,并解决了状态通知问题。还剩下两个问题:

  1. 如何监听property变化?
  2. Observer对象如何订阅依赖的Subject对象?

 对于第一个问题,我们知道:当且仅当程序对一个变量进行“写”操作时,变量的值可能会改变。所以可通过拦截property的”写“操作或代理的方式来监听变化。

 第二个问题,只有当组件被渲染时才知道依赖了哪些property,此时对property进行”读“操作,并把Observer对象传给Subject对象,这样就可以通过拦截property的”读“操作或代理的方式来订阅Subject。

 实现方案有两种,Vue2用的是Object.defineProperty()来拦截读写操作,而Vue3是用ES6的Proxy代理方式。

 在创建Vue实例时,遍历构造函数的data选项的所有property,并用Object.defineProperty() 给 property设置set()和get(),这样property在被访问/修改时会触发get()和set(),即可以在get()中订阅Subject,在set()中通知变更。

function defineReactive(data,pro,val){
    Object.defineProperty(data,pro,{
        enumerable:true,
        configurable:true,
        set(data){
            // do someting when write(监听变化)
            val = data;
        },
        get(){
            // do someting when read(订阅Subject)
            return val;
        }
    });
}

总结

 本文实现了一个简单的响应式系统,来帮助大家理解响应式原理。主要包括监听变化、订阅依赖和状态通知三个部分:

  • 状态通知:我们基于观察者模式,抽象出Subject类和Observer类,解决了状态通知的问题。
  • 监听变化:在创建Vue实例时,遍历构造函数的data选项的所有property,并用Object.defineProperty() 设置 property的set()和get()。当property被修改时,会触发set()中的Subject对象通知组件更新。
  • 订阅依赖:每个组件实例都对应一个Observer对象,它会在组件渲染时收集依赖的property,并通过“读”操作触发get(),完成订阅property对应的Subject对象。当Observer对象接收到变更通知时,会对组件进行更新。

完整代码

Subject:

/**
 * A subject is an observable that can have multiple
 * observers subscribing to it.
 */
class Subject {
    // the target observer which want to subscribe
    static target;
    constructor() {
        this.observers = [];
    }
    subscribe() {
        let observer = Subject.target;
        if (!this.observers.includes(observer)) {
            this.observers.push(Subject.target);
        }
    }
    notify() {
        this.observers.forEach(observer=>{
            observer.update();
        });
    }
}

// The current target watcher being evaluated.
// This is globally unique because only one observer
// can be evaluated at a time.
Subject.target = null;
const targetStack = [];

function pushTarget(target) {
  targetStack.push(target);
  Subject.target = target;
}

function popTarget() {
  targetStack.pop();
  Subject.target = targetStack[targetStack.length - 1];
}

Observer:

/**
 * A observer parses an expression
 * and fires callback when the expression value changes.
 * This is used for both the $watch() api and directives.
 */
class Observer {
  constructor(vm, exp, cb) {
      this.vm = vm;
      this.cb = cb;
      // parse expression for getter
      this.getter = parsePath(exp);
      // read the property to subscribe the Subject
      pushTarget(this);
      this.value = this.get();
      popTarget();
  }
 /**
  * get the property value
  */
  get() {
      const vm = this.vm;
      return this.getter.call(vm, vm);
  }
  /**
   * Observer interface.
   * Will be called when a subject changes.
   */
  update() {
      const oldValue = this.value;
      this.value = this.get();
      this.cb.call(this.vm, this.value, oldValue);
  }
}

function parsePath (path) {
  const segments = path.split('.')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}

defineReactive

function defineReactive(data,pro,val){
    let subject = new Subject();
    Object.defineProperty(data,pro,{
        enumerable:true,
        configurable:true,
        set(data){
            // do someting when write(监听变化)
            if(data === val){
                return;
            }
            val = data;
            subject.notify();
        },
        get(){
            // do someting when read(订阅Subject)
            // 在创建Observer实例时,会对property执行一次读操作,并把Observer实例通过全局变量传参。
            subject.subscribe();
            return val;
        }
    });
}

测试代码:

let data = { pro1: "0" };
defineReactive(data, "pro1", "0");
let observer = new Observer(data, "pro1", (newVal, oldVal) => {
  console.log(`数据变化,刷新视图。newVal=${newVal},oldVal=${oldVal}`);
});

// > data.pro1='666';
// > 数据变化,刷新视图。newVal=666,oldVal=0

参考资料

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值