Appearance
前言 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方法更新视图。