Skip to content

前言 Vue是参考mvvm模式设计的一套用于构建用户界面的渐进式框架,可以自底向上逐层应用,采用非侵入性的响应式系统。在修改数据时,视图也会跟着更新,开发过程中只需关注数据。

开发中的响应式:

<template>
  <div id="app">
    <p>数量:{{ num }}</p>
    <p>价格:{{ num * price }}</p>
    <p>总计:{{ total }}</p>
    <button @click="btn">点击按钮</button>
  </div>
</template>
<template>
  <div id="app">
    <p>数量:{{ num }}</p>
    <p>价格:{{ num * price }}</p>
    <p>总计:{{ total }}</p>
    <button @click="btn">点击按钮</button>
  </div>
</template>
<script>
expprt default {
  data() {
    return {
      num: 1,
      price: 2
    }
  },
  computed: {
    total: function() {
      return this.num * this.price;
    }
  },
  methods: {
    btn: function() {
      this.num = 10;
    }
  }
}
</script>
<script>
expprt default {
  data() {
    return {
      num: 1,
      price: 2
    }
  },
  computed: {
    total: function() {
      return this.num * this.price;
    }
  },
  methods: {
    btn: function() {
      this.num = 10;
    }
  }
}
</script>

页面属性num发生变化时,经历了

  • 获取属性num
  • 更新属性num值
  • 计算total值,更新页面 数据发生变化后,页面会重新更新数据。

想要完成整个过程,需要:

  • 侦测数据变化 (简称:数据劫持)
  • 收集视图依赖数据 (简称:依赖收集)
  • 数据变化,通知视图需要更新的部分 (简称:发布订阅模式)

数据劫持

Vue2.x版本中使用Object.defineProperty进行数据劫持。

Vue通过对象属性getter/setter监听数据变化,通过getter进行依赖收集,每个setter方法就是一个观察者,数据发生变化时通知订阅者更新视图。

var data = {
  title: 'Vue',
  obj: {
    age: 6
  }  
}
function observer(data) {
  if(data && typeof data == 'object') {
    Object.keys(data).forEach(function(key) {
      defineReactive(data, key, data[key]);
    })
  }
}

function defineReactive(data, key, value) {
  observer(value); // 劫持每一层数据
  Object.defineProperty(data, key, {
    enumberable: true, // 可枚举
    configurable: true, // 可配置
    get: function() {
      console.log('get', value)
      return value;
    },
    set: function(newVal) {
      console.log('set', newVal);
      observer(newVal) // 劫持新值
      if(newVal !== value) { 
          value = newVal;      
       }
    }
  })
}
observer(data);
data.title // get vue
data.obj = {
  age: 60
} // set { age: 60 }
var data = {
  title: 'Vue',
  obj: {
    age: 6
  }  
}
function observer(data) {
  if(data && typeof data == 'object') {
    Object.keys(data).forEach(function(key) {
      defineReactive(data, key, data[key]);
    })
  }
}

function defineReactive(data, key, value) {
  observer(value); // 劫持每一层数据
  Object.defineProperty(data, key, {
    enumberable: true, // 可枚举
    configurable: true, // 可配置
    get: function() {
      console.log('get', value)
      return value;
    },
    set: function(newVal) {
      console.log('set', newVal);
      observer(newVal) // 劫持新值
      if(newVal !== value) { 
          value = newVal;      
       }
    }
  })
}
observer(data);
data.title // get vue
data.obj = {
  age: 60
} // set { age: 60 }

函数observe传入一个需要被追踪变化的对象data,遍历对象每个属性都使用defineReactive处理,实现侦测对象变化。

侦测Vue中的data数据。

function Vue(options) {
  this.$el = options.el;
  this.$data = options.data;
  this.$options = options;
  if(this.$el) {
    observer(this.$data);
  }
}
function Vue(options) {
  this.$el = options.el;
  this.$data = options.data;
  this.$options = options;
  if(this.$el) {
    observer(this.$data);
  }
}

只需要new Vue一个对象,就会将data中数据进行追踪变化。

需要注意的是Object.defineProperty有以下缺点:

  • 无法检测对象属性的添加和删除

因为Vue通过Object.defineProperty将对象的key转化成getter/setter依赖追踪变化,而getter/setter只能追踪数据是否被修改,却无法追踪新增属性和删除属性。

对于新增属性,使用Vue.set()方法,可以将新增属性添加到Vue响应式系统中;如:在data对象下新增一个size属性,使用Vue.set(data, 'size', '10KB'),参数依次是:目标对象,目标对象新增属性,目标对象新增属性值。

也可以给这个对象重新赋值,如:Vue.set(data, 'title', 'MVue') 。

对于删除属性,使用Vue.delete(目标对象, 删除目标对象属性);如:Vue.delete(data, 'obj')。

  • 不能监听数组变化,可以对数组方法进行重写(参考深入浅出Vue.js)
var arr = ['小社区', '社区', '大社区'];
// 定义数组方法
var arrMethods = ['push', 'shift', 'pop', 'unshift'];
// 获取数组原型
var arr_proto = Array.prototype;
// 创建新原型对象
var _proto = Object.create(arr_proto); 
arrMethods.forEach(function(method) {
  _proto[method] = function() {
      var res = arr_proto[method].call(this, ...arguments);     
      return res;
    }
})
function observer(data) {
  if(Array.isArray(data)) {
      data.__proto__ = _proto;
      return;
  }
  if(data && typeof data == 'object') {
    Object.keys(data).forEach(function(key) {
      defineReactive(data, key, data[key]);
    })
  }
} 
function defineReactive(data, key, value) {
  observer(value);
  Object.defineProperty(data, key, {
    enumberable: true,
    configurable: true,
    get: function() {
      console.log('get', value);
      return value;
    },
    set: function(newVal) {
      console.log('set', newVal);
      observer(newVal);
      if(newVal !== value) {
        value = newVal;
      }
    }
  })
}
//添加属性重新赋值给新的对象
function $set(data, key, value) {
  defineProperty(data, key, value);
}
observer(arr);
arr.shift(); // 使用定义数组中方法
console.log(obj); // ["社区", "大社区"]
var arr = ['小社区', '社区', '大社区'];
// 定义数组方法
var arrMethods = ['push', 'shift', 'pop', 'unshift'];
// 获取数组原型
var arr_proto = Array.prototype;
// 创建新原型对象
var _proto = Object.create(arr_proto); 
arrMethods.forEach(function(method) {
  _proto[method] = function() {
      var res = arr_proto[method].call(this, ...arguments);     
      return res;
    }
})
function observer(data) {
  if(Array.isArray(data)) {
      data.__proto__ = _proto;
      return;
  }
  if(data && typeof data == 'object') {
    Object.keys(data).forEach(function(key) {
      defineReactive(data, key, data[key]);
    })
  }
} 
function defineReactive(data, key, value) {
  observer(value);
  Object.defineProperty(data, key, {
    enumberable: true,
    configurable: true,
    get: function() {
      console.log('get', value);
      return value;
    },
    set: function(newVal) {
      console.log('set', newVal);
      observer(newVal);
      if(newVal !== value) {
        value = newVal;
      }
    }
  })
}
//添加属性重新赋值给新的对象
function $set(data, key, value) {
  defineProperty(data, key, value);
}
observer(arr);
arr.shift(); // 使用定义数组中方法
console.log(obj); // ["社区", "大社区"]

上述把数组原自带的方法进行重写,覆盖掉原数组方法;重写后的数组方法需要被拦截,但是Vue对这些重写的方法是拦截不到的,也就不能响应。

比如:修改上述数组某一项值,无法侦测数组变化。

arr[1] = '物业';
arr[1] = '物业';

Vue3.x使用proxy作为实现代理,proxy具有代理、拦截与劫持的特征。

proxy实现特征:

let arr = ['小社区', '社区', '大社区'];
let handler = {
  get(data, key) {
    if(typeof data[key] == 'object' && 
    data[key] !== null) {
      return new Proxy(data[key], handler);
    }
    return Reflect.get(data, key);
  },
  set(data, key, value) {
    if(key == 'length') return true;
    return Reflect.set(data, key, value);
  }
}
let proxy = new Proxy(arr, handler);
proxy[0] = '物业';
console.log(proxy); // Proxy {0: "物业", 1: "社区", 2: "大社区"}
let arr = ['小社区', '社区', '大社区'];
let handler = {
  get(data, key) {
    if(typeof data[key] == 'object' && 
    data[key] !== null) {
      return new Proxy(data[key], handler);
    }
    return Reflect.get(data, key);
  },
  set(data, key, value) {
    if(key == 'length') return true;
    return Reflect.set(data, key, value);
  }
}
let proxy = new Proxy(arr, handler);
proxy[0] = '物业';
console.log(proxy); // Proxy {0: "物业", 1: "社区", 2: "大社区"}

对比Object.defineProperty与proxy:

Object.defineProperty必须遍历对象每个属性;无法检测对象属性的新增属性与删除属性;无法监听重写数组方法的变化。

proxy只需做一层代理就能监听同级结构下所有属性,支持代理数组变化。(深层次的数据结构,还是需要递归)

收集依赖

观察数据目的是当数据属性发生变化时,可以通知那些使用了该数据的地方。

比如:开篇用到的数据num,当数据num发生变化时,会通知所有用到数据num的地方。

如果是多个Vue实例共用一个变量,比如:

var str = 'Vue';
var vm1 = new Vue({
  data: str,
  template: '<div> {{ str }} </div>'
})
var vm2 = new Vue({
  data: str,
  template: '<div> {{ str }} </div>'
})
var str = 'Vue';
var vm1 = new Vue({
  data: str,
  template: '<div> {{ str }} </div>'
})
var vm2 = new Vue({
  data: str,
  template: '<div> {{ str }} </div>'
})

此时更改str属性值,这两个实例视图会更新。那么只有通过收集依赖才能知道哪些地方依赖了数据str,以及数据str派发更新数据。

收集依赖核心思想是事件发布订阅模式,这里有两个角色:订阅者Dep和观察者Watcher。

收集依赖是为依赖寻找一个存储依赖的地方,因此创建了Dep。使用订阅者Dep用来收集依赖,删除依赖、向依赖发送消息。

简单实现订阅者Dep:

function Dep() {
  this.subs = []; // 存储Watcher
}
Dep.prototype.addSub = function(sub) {
  this.subs.push(sub); // 添加Watcher
}
Dep.prototype.notify = function() {
  this.subs.forEach(function(sub) {
    sub.update(); // 通知Watcher更新视图
  })
}
function Dep() {
  this.subs = []; // 存储Watcher
}
Dep.prototype.addSub = function(sub) {
  this.subs.push(sub); // 添加Watcher
}
Dep.prototype.notify = function() {
  this.subs.forEach(function(sub) {
    sub.update(); // 通知Watcher更新视图
  })
}

从上面代码订阅者Dep的作用是存储观察者Watcher,可以把观察者Watcher理解成一个中转站,当数据发生变化时通知观察者Watcher,再有观察者Watcher通知其他地方。

当需要依赖收集时调用函数addSub,当需要派发更新时调用函数notify。

var dep = new Dep();
dep.addSub(function() {
  console.log('add')
})
dep.notify();
var dep = new Dep();
dep.addSub(function() {
  console.log('add')
})
dep.notify();

如何收集依赖:在getter中收集依赖,在setter中触发依赖;就是把用到该数据的地方收集起来,等到属性发生变化时,把之前收集好的依赖循环触发一边。

具体是当外界通过观察者Watcher读取数据时就会触发getter,将观察者Watcher添加到依赖中。哪个观察者Watcher触发getter就把哪个观察者Watcher收集到Dep中;当数据发生变化时,会循环依赖列表,把所有的观察者Watcher都通知一遍。

观察者Watcher

Vue官方定义一个Watcher类用来表示观察订阅依赖。其中《深入浅出Vue.js》给出这样的解释:为什么要引入观察者Watcher。

在属性发生变化后,需要通知用到该数据的地方。而该数据可能被很多地方用到,并且类型还不一样,可能是模版,可能是开发者编写的watch。这时候需要抽象出一个能集中处理这些情况的类,然后在依赖收集阶段只收集这个封装好的类的实例,通知也只通知这个封装好的类的实例,再由这个封装好的类的实例通知到其他。

依赖收集的目的是将观察者Watcher存放到当前订阅者Dep的subs中。

简单实现观察者Watcher:

function Watcher(data, key, cb) { // cb ->callback缩写
  Dep.target = this;
  // Dep.target 指向自己,在触发getter时添加监听
  this.data = data;
  this.key = key;
  this.cb = cb;
  this.value = data[key];
  Dep.target = null; 
  // 如果不为null,每次都追加一个Dep
  // 比如:第一次是['Dep'], 第二次是['Dep', 'Dep'], ...
}
Watcher.prototype.update = function() {
  this.value = this.data[this.key];
  this.cb(this.value);
}
function Watcher(data, key, cb) { // cb ->callback缩写
  Dep.target = this;
  // Dep.target 指向自己,在触发getter时添加监听
  this.data = data;
  this.key = key;
  this.cb = cb;
  this.value = data[key];
  Dep.target = null; 
  // 如果不为null,每次都追加一个Dep
  // 比如:第一次是['Dep'], 第二次是['Dep', 'Dep'], ...
}
Watcher.prototype.update = function() {
  this.value = this.data[this.key];
  this.cb(this.value);
}

执行构造函数把Dep.target指向自身,收集到对应的Watcher,在派发更新时取出对应的观察者Watcher,执行函数update。

总结

结合以上内容实现一个简单响应式

var data = {
  title: 'vue',
  obj: {
    age: 6
  }
}
function Dep() {
  this.subs = [];
}
Dep.prototype.addSub = function(sub) {
  this.subs.push(sub);
}
Dep.prototype.notify = function() {
  console.log('Dep notify');
  // this.subs.forEach(function(sub) {
  // sub.update()
  // })
}
function render() {
  console.log('模版render...');
}
function observer(data) {
  if(data && typeof data == 'object') {
    Object.keys(data).forEach(function(key) {
      defineReactive(data, key, data[key]);
    })
  }
}
function defineReactive(data, key, value) {
  observer(value);
  var dep = new Dep();
  Object.defineProperty(data, key, {
    enumberable: true,
    configurable: true,
    get: function() {
      console.log('get:' value);
      Dep.target && dep.addSub(Dep.target);
      return value;
    },
    set: function (newVal) {
      console.log('set:', newVal);
      if(newVal !== value) {
        value = newVal;
        dep.notify();
        render();
      }
    }
  })
}
function MVue(options) {
  this.$options = options;
  this.$el = options.el;
  this.$data = options.data;
  if(this.$el) observer(this.$data);
}
new MVue({
  el: '#app',
  data: data
})
data.title; // 首次访问数据
data.title = 'MVue'; // 修改数据
data.title; // 访问修改后的数据
var data = {
  title: 'vue',
  obj: {
    age: 6
  }
}
function Dep() {
  this.subs = [];
}
Dep.prototype.addSub = function(sub) {
  this.subs.push(sub);
}
Dep.prototype.notify = function() {
  console.log('Dep notify');
  // this.subs.forEach(function(sub) {
  // sub.update()
  // })
}
function render() {
  console.log('模版render...');
}
function observer(data) {
  if(data && typeof data == 'object') {
    Object.keys(data).forEach(function(key) {
      defineReactive(data, key, data[key]);
    })
  }
}
function defineReactive(data, key, value) {
  observer(value);
  var dep = new Dep();
  Object.defineProperty(data, key, {
    enumberable: true,
    configurable: true,
    get: function() {
      console.log('get:' value);
      Dep.target && dep.addSub(Dep.target);
      return value;
    },
    set: function (newVal) {
      console.log('set:', newVal);
      if(newVal !== value) {
        value = newVal;
        dep.notify();
        render();
      }
    }
  })
}
function MVue(options) {
  this.$options = options;
  this.$el = options.el;
  this.$data = options.data;
  if(this.$el) observer(this.$data);
}
new MVue({
  el: '#app',
  data: data
})
data.title; // 首次访问数据
data.title = 'MVue'; // 修改数据
data.title; // 访问修改后的数据

函数render被渲染时,读取所需对象的值,会触发getter方法把当前观察者Watcher收集到函数Dep中;如果需要修改对象的值,会触发setter方法,通知函数Dep中的notify方法,触发所有观察者Watcher对象中的update方法更新对应视图。

总结Vue响应式原理

通过数据劫持结合订阅与发布者模式的方式,通过Object.defineProperty劫持各个属性的getter/setter,在数据发生变化时发布消息给订阅者,触发相应的回调函数。

执行new Vue整个过程发生了:

new Vue后,Vue会调用函数_init进行初始化。在这个过程data通过函数observer转化成getter/setter追踪数据变化;当被设置的对象被读取时会执行getter方法,当对象被重新赋值时会执行setter方法。

函数render执行时,会读取所需对象的值,会触发getter方法把观察者Watcher添加到依赖中进行依赖收集。

修改对象的值时,会触发相应的setter方法;setter方法通知之前依赖收集得到的Dep中每一个观察者Watcher,再有观察者Watcher通知其他,自己的值被更改了需要重新渲染视图;这时观察者Watcher就会调用update方法更新视图。