作为计算机工程师,框架是提高开发效率的重要工具。理解框架的核心原理,有助于更好地使用它和定位问题。同时,一个优秀的框架,其设计方案和实现原理也是值得我们学习和借鉴的。本文将通过实现一个简单的响应式系统,来理解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)
响应式系统的核心是状态通知,可基于观察者模式进行设计。
观察者模式:
当一个对象的状态发生改变时,所有关联的对象会得到通知并自动更新
。解决的是一个对象状态改变给其他对象通知的问题
观察者模式中有两种角色:
下面是观察者模式的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 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两个类,并解决了状态通知问题。还剩下两个问题:
对于第一个问题,我们知道:当且仅当程序对一个变量进行“写”操作时,变量的值可能会改变。所以可通过拦截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:
/**
* 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
深入响应式原理
《深入浅出Vue.js》