响应式数据
响应式系统
什么是响应式,也即是说,数据发生改变的时候,视图会重新渲染,匹配更新为最新的值。
使用 Object.defineProperty 可以为对象中的每一个属性,设置 get 和 set 方法。
- Vue 是怎么知道数据改变?
- Vue 在 属性的 set 方法中做了手脚,因而当数据改变时,触发 属性的 set 方法,Vue 就能知道数据有改变
- Vue 在数据改变时,怎么知道通知哪些视图更新?
- 通过Object.defineProperty-get,通知那些存在依赖收集器中的视图
- Vue 在数据改变时,视图怎么知道什么时候更新?
- 触发Object.defineProperty-set,在数据变化触发 set 函数时,通知视图,视图开始更新
依赖收集
- data 中的声明的每个属性,都拥有一个数组(依赖收集器subs),保存着 谁依赖(使用)了 它。
- 当页面使用到 某个属性时,页面的 watcher 就会被 放到 依赖收集器 subs 中

name 属性,使用了 一个 dep 保存了 页面A 这个依赖,而保存的实际上是 页面A的 Watcher。
数据初始化流程
1、实例初始化中,调用 initState 处理部分选项数据,initData 用于处理选项 data
Vue.prototype._init=function(){
...
initState(this)
...
}
function initState(vm) {
var opts = vm.$options;
... props,computed,watch 等选项处理
if (opts.data) {
initData(vm);
}
};
2、initData 遍历 data,definedReactive 处理每个属性
function initData(vm) {
var data = vm.$options.data;
data = typeof data === 'function' ?
data.call(vm, vm) : data || {};
// ... 遍历 data 数据对象的key,重名检测,合规检测等代码
new Observer(data);
}
function Observer(value) {
var keys = Object.keys(value);
// ...被省略的代码
for (var i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
};
3、definedReactive 给对象的属性 通过 Object.defineProperty 设置响应式
function defineReactive(obj, key) {
// dep 用于中收集所有 依赖我的 东西
var dep = new Dep();
var val = obj[key]
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() { ...依赖收集},
set() { ....依赖更新}
});
}
依赖收集-基本数据类型
1、页面的渲染函数执行, name 被读取
2、触发 name的 Object.defineProperty.get 方法
3、页面的 watcher 就会被收集到 name 专属的闭包dep 的 subs 中。dep.addSub(Dep.target)
依赖收集-引用数据类型
引用数据类型,使用 【闭包dep】 和 【 __ ob __.dep】 两种来存储依赖
1、引用类型会多添加一个 __ ob __属性,其中包含 dep,用于存储 收集到的依赖
2、对象使用 __ ob __.dep,作用在 Vue 自定义的方法 set 和 del 中
3、数组使用 __ ob __.dep,作用在 Vue 重写的数组方法 push 等中 。用到Vue封装方法 set 和 del,set 和 del 会通知依赖更新,所以子项对象也要保存。
依赖更新
-
当 name 改变的时候,name 会遍历自己的 依赖收集器 subs,逐个通知 watcher,让 watcher 完成更新
-
这里 name 会通知页面A,页面A 重新读取新的 name ,然后完成渲染
通知更新做了这些工作
1、直接调用 watcher.update,也就是重新调用给 watcher 保存的更新函数
2、更新更新函数就是执行渲染函数,然后读取实例最新的值(已被修改过的值),最后重新生成DOM 节点
3、DOM 节点 插入或替换页面,完成更新
代理data
初始化数据
function initData(vm) {
var data = vm.$options.data;
var keys = Object.keys(data);
var i = keys.length;
data = vm._data =
( typeof data === 'function' ?
data.call(vm) : data ) || {};
while (i--) {
var key = keys[i];
if (只要不是_和$开头的属性) {
proxy(vm, "_data", key);
}
}
}
- 拿到data数据,如果data是函数,执行就拿到返回值,否则就拿设置的对象data
- 初始化数据,是为了拿到数据,然后放到存到实例上,作为代理总部
return data.call(vm)
-
data 函数执行的时候 用 call 方法,让 vm 继承了 data 的属性和方法,将data的属性和方法指向vm。 所以我们可以使用 this.xxx
-
call 知识点:call 是 Function 对象自带的一个方法,可以改变函数体内部的 this 指向,第一个参数就是 this要指向的对象,也就是想指定的上下文,后面的参数它会按顺序传递进去。它的函数会被立即调用。
-
官网解释:调用一个对象的一个方法,以另一个对象替换当前对象。也就是继承模式:挟持另一个对象的方法,继承另外一个对象的属性
data 为什么是个函数而不是个对象?
如果 data 是个对象,那么整个vue实例将共享一份数据,也就是各个组件实例间可以随意修改其他组件的任意值,但是 data 定义成一个函数,将会 return 出一个唯一的对象,不会和其他组件共享一个对象。
代理开花
proxy(vm,"_data",key)
// 定义了一个构造函数
function Vue(options) {
this.$data = options.data || {};
this.initState(options);
}
Vue.prototype.initState = function (opts) {
if(opts.data)
this.initData(opts.data);
if(opts.methods)
this.initMethods(opts.methods);
if(opts.created)
opts.created.call(this);
}
Vue.prototype.initData = function (data) {
var keys = Object.keys(data);
var i = keys.length;
while (i--){
const key = keys[i];
this.proxy("$data",key);
}
}
Vue.prototype.initMethods = function (methods) {
for (var key in methods) {
this[key] = methods[key];
}
}
Vue.prototype.proxy =function(sourceKey, key) {
Object.defineProperty(this, key, {
get() {
return this[sourceKey][key]
},
set(val){
this[sourceKey][key] = val;
}
});
};
在Vue.prototype.proxy中的Object.defineProperty中,绑定了this和key的值,使得this.key 可以访问data中的值