我们都知道,只要在 Vue 实例中声明过的数据,那么这个数据就是响应式的。
什么是响应式,也即是说,数据发生改变的时候,视图会重新渲染,匹配更新为最新的值。
- 那么Vue如何监听数据变化?
- 数据变化后,如何通知视图更新?
- 数据变化后,视图怎么知道何时更新
思考以上3个问题
先介绍下Object.defineProperty(),因为这个方法是Vue实现响应式系统的重中之重
Object.defineProperty()
MDN上的定义:
Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
语法:
Object.defineProperty(obj, prop, desc)
使用:
const obj = {name: 'Bob'}
Object.defineProperty(obj, 'name', {
get(){
console.log('get 被触发')
},
set(val){
console.log('set 被触发')
}
})
obj.name //get 被触发
obj.name = 'Lily' //set 被触发
Object.defineProperty可以为对象属性设置get、set函数,
get:属性被访问时触发
set:属性被赋值时触发
其实Object.defineProperty()
可以为属性设置很多特性,例如configurable,enumerable
,但是现在不过多解释
到这里,就可以回答第一个问题:Vue如何监听数据变化?其实Vue是在属性的set方法中做了处理,这样,只要数据改变,触发set方法,就能知道数据变化了
一、如何进行依赖收集
data 中声明的每个属性,都有一个依赖收集器 subs,保存着依赖了它的页面(或者watch、computed)
new Vue({
data(){
return {
name:"111"
}
}
})
//页面 A 引用了name
<div>{{name}}</div>
此时name属性结构如下
{
Dep:{
subs: [watcher]
}
}
可以看到,name 属性,使用了 一个 dep 保存了 页面A 这个依赖,而保存的实际上是 页面A的 Watcher。
Tip
简单说一下,watcher 是什么,每个 Vue 实例都会拥有一个专属的 watcher,可用于实例更新
那数据是如何进行依赖收集的呢?
依赖收集又分为两个流程
1、数据初始化流程
首先,在实例初始化的时候,需要对数据进行响应式处理,也就是给每个属性都使用 Object.defineProperty 处理
先看下源码的初始化流程
1、实例初始化中,调用 initState 处理部分选项数据
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
Vue.prototype._init = function (options) {
const vm: Component = this
...
initState(vm)
...
}
function initState (vm) {
const 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() { ....依赖更新 }
});
}
2、依赖收集流程
- 一个模版渲染时,首先执行渲染函数
with(this){
return _c('div',{},[name])
}
- 渲染函数读取实例上的data
- 读取data触发Object.defineProperty.get方法,开始收集 watcher
function defineReactive(obj, key) {
var dep = new Dep();
var val = obj[key]
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
// 收集依赖
dep.addSub(Dep.target)
}
return val
}
});
}
可以看到上面代码中有Dep、Dep.target、dep.addSub,解释下
Dep是一个构造函数,用于创建实例,并带有很多方法
实例会包含一个属性 subs 数组,用于存储不同数据 【收集的依赖】
var Dep = function Dep () {
this.id = uid++;
this.subs = [];//保存watcher的数组
};
Dep.target指向当前watcher,watcher 在 依赖收集中只起到被收集的作用,具体怎么用于依赖更新下面再讲
dep.addSub原型上的方法,作用是往 dep.subs 存储器中 中直接添加 watcher
Dep.prototype.addSub = function(sub) {
this.subs.push(sub);
};
所以,上面有个例子打印出data中name属性的结构就是这么来的
{
Dep:{
subs: [watcher]
}
}
以上便可以回答了开篇的第二个问题 数据变化后,如何通知视图更新?
恩,通知那些存在 依赖收集器中的 watcher
PS
以上的源码解析的都是当data内属性是基本类型时的流程,是对象或者数组时,递归处理为每个子项都添加dep.subs
另外一个重要的不同点:数据是对象或者数组时,会另外在每级子项存储一个__ob__
{
__ob__:{
dep:{
subs:[wacher]
}
}
}
可以看到__ob__下面还是我们熟悉的存储依赖的结构,为什么这么做呢?
上面依赖收集是通过Object.defineProperty
的get、set
实现的,他的缺陷是对于本身不存在对象中的属性是监控不到的,新增或删除一个子项就需要另外处理了
vue内部为对象实现的$set、$del
读取__ob__内容,实现通知视图更新
vue内部重写数组的push、shift等也是读取__ob__内容,实现通知视图更新
二、 如何进行依赖更新
经过上面的讲解,我们都知道,每个属性都会保存有一个 依赖收集器 subs保存着各种watcher
数据变化时,触发Object.defineProperty - set
,遍历subs
,逐个通知watcher
,然后调用watcher.update
完成更新
function defineReactive(obj, key, val) {
var dep = new Dep();
var childOb = observe(val);
Object.defineProperty(obj, key, {
get(){
... 属性被读取,完成依赖收集
return val
},
set(newVal) {
// 值没有变化
if (newVal ===val) return
// 附新值
val = newVal;
// 触发更新
dep.notify();
}
});
}
1、dep.notify()
重点在dep.notify()
,会遍历subs
,为每一项都执行update
,由于subs里保存的是watcher
,所以实际上调用的是watcher.update
var Dep = function Dep() {
this.subs = []; // 依赖存储器
};
// 遍历 subs ,逐个通知依赖,就是逐个调用 watcher.update
Dep.prototype.notify = function() {
var subs = this.subs.slice();
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
2、watcher.update
那么watcher.update是如何做更新的呢
function Watcher(vm, expOrFn) {
this.vm = vm;
// 保存传入的更新函数
this.getter = expOrFn;
// 新建 watcher 的时候,立即执行更新函数
this.get();
};
Watcher.prototype.get = function() {
// 执行更新函数
this.getter.call(this.vm,this.vm);
};
Watcher.prototype.update = function() {
this.get()
}
看到上面的源码
- Watcher 新建实例的时候,会保存传入的函数(这个函数会作为更新用)
- watcher 实例有 update 方法,作用是执行上一步保存的更新函数
3、new Watcher
那么 watcher 是什么时候开始创建的呢?
function Vue(options) {
this._init(options);
}
Vue.prototype._init = function(options) {
// ...处理组件选项等
this.$mount()
}
Vue.prototype.$mount = function() {
// ...解析template成redner函数保存
/** 每个实例新建一个watcher,
并且利用watcher 保存更新函数 **/
new Watcher(this,
// 这个函数是更新函数,传入watcher保存下来,用于后面页面初始化或者页面更新
function() {
/** ...调用保存的渲染函数生成VNode,
并生成DOM插入页面中**/
}
);
};
这便可以回答了开篇的第三个问题 数据变化后,视图怎么知道何时更新
恩,在数据变化触发 set 函数时,通知视图,执行watcher.update,触发页面渲染函数生成VNode,插入页面,完成更新
来源:CSDN
作者:WDF.
链接:https://blog.csdn.net/weixin_44874595/article/details/104813515