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图:
根据观察者模式,分别找出响应式系统中的目标对象(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 getterthis.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两个类,并解决了状态通知问题。还剩下两个问题:
- 如何监听property变化?
- 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 subscribestatic 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 getterthis.getter = parsePath(exp);// read the property to subscribe the SubjectpushTarget(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) returnobj = 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
参考资料
-
深入响应式原理
-
《深入浅出Vue.js》
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
