Vue 响应式系统(二)- observe 工厂函数
接上篇文章回到 initData 函数的最后一句代码:
// observe data observe(data, true /* asRootData */)
调用了 observe 函数观测数据, observe 源码如下:
function observe(value, asRootData) { if (!isObject(value) || value instanceof VNode) { return } var ob; if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__; } else if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value); } if (asRootData && ob) { ob.vmCount++; } return ob }
observe 函数接收两个参数,第一个参数是要观测的数据,第二个参数是一个布尔值,代表将要被观测的数据是否是根级数据。
observe函数开始就是一个if 判断。
if (!isObject(value) || value instanceof VNode) { return }
如果要观测的数据不是一个对象或者是 VNode 实例,则直接 return 。
关于VNode实例可以去了解之前写的编译器相关的文章。
接下来又是一个if else 语句。
var ob; if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__; } else if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value); }
从字面意思不难理解,if 分支判断value 是否有"ob" 属性, 如果有是否为 Observer 实例,两者满足把 value.ob 值赋值给ob。 但为什么要这么做 ob 又是什么呢?
原因是当一个数据对象被观测之后将会在该对象上定义 ob 属性,换句话说不管哪个数据对象被观测了此对象上会扩展一个__ob__ 属性。在此 if 分支的作用是用来避免重复观测一个数据对象。
再来看看 else…if 分支,如果数据对象上没有定义 ob 属性,那么说明该对象没有被观测过,进而会判断 else…if 分支,那么会执行 ob = new Observer(value) 对数据对象进行观测。 前提是数据对象满足所有 else…if 分支的条件才会被观测,我们看看需要满足什么条件:
- shouldObserve 为 true ( 默认值 true )
- !isServerRendering() 函数的返回值是一个布尔值,用来判断是否是服务端渲染。Vue SSR 数据预取和状态
- Array.isArray(value) || isPlainObject(value) 被观测的数据对象必须是数组或者纯对象。
- value._isVue Vue实例才拥有_isVue 属性,在此是避免观测Vue实例对象。
- Object.isExtensible(value) 观测的数据对象必须是可扩展的。(对象默认可扩展)
阻止对象扩展方法有:Object.preventExtensions() 、Object.freeze() 、Object.seal()
当一个对象满足了以上五个条件时,就会执行 else…if 语句块的代码,创建一个Observer实例:
ob = new Observer(value);
真正将数据对象转换成响应式数据对象的是 Observer 函数 。
Observer 构造函数源码:
var Observer = function Observer(value) { this.value = value; this.dep = new Dep(); this.vmCount = 0; def(value, '__ob__', this); if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods); } else { copyAugment(value, arrayMethods, arrayKeys); } this.observeArray(value); } else { this.walk(value); } }; /** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value type is Object. */ Observer.prototype.walk = function walk(obj) { var keys = Object.keys(obj); for (var i = 0; i < keys.length; i++) { defineReactive$$1(obj, keys[i]); } }; /** * Observe a list of Array items. */ Observer.prototype.observeArray = function observeArray(items) { for (var i = 0, l = items.length; i < l; i++) { observe(items[i]); } };
Observer构造函数的实例对象将拥有三个实例属性,分别是 value、dep 和 vmCount 以及两个实例方法 walk 和 observeArray。来看下实例化 Observer 构造函数对象都做了什么。
实例对象的 value 属性引用了数据对象:
this.value = value;
实例对象的 dep 属性获取Dep构造函数实例的引用,用于依赖收集。
this.dep =new Dep();
实例对象的 vmCount 属性初始设置为0:
this.vmCount = 0;
接下来:
def(value, '__ob__', this);
使用 def 函数,为数据对象定义了一个 ob 属性,这个属性的值就是当前 Observer 实例对象。
def 源码:
function def(obj, key, val, enumerable) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }); }
有意思的是这里监听你对 obj.key 属性访问, 值为val。 但是把obj.key 设置为不可枚举的属性,之所以这么做的原因是后面遍历数据对象的时候防止遍历到 ob 属性。
假设我们的数据对象如下:
var data = {count: 1};
那么经过 def 函数处理之后,data 对象应该变成如下这个样子:
var data = { count: 1, // __ob__ 为不可枚举的属性 __ob__: { value: data, dep: new Dep(), vmCount: 0 } }
在看接下来的代码:
if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods); } else { copyAugment(value, arrayMethods, arrayKeys); } this.observeArray(value); } else { this.walk(value); }
该判断用来区分数据对象到底是数组还是一个纯对象,因为对于数组和纯对象的处理方式是不同的,先来看下如果数据对象是数组对象的处理方式。
通过以上代码了解到在数据对象是数组时,还会在做一个if…else 的判断,根据变量hasProto决定是去调用protoAugment函数 还是 copyAugment 函数,hasProto 是一个布尔值,它用来检测当前环境是否可以使用__proto__属性,再此我们只考虑正常情况不去扩展边界默认都为真。所以接下来进入到 protoAugment 函数中去。
protoAugment函数源码
function protoAugment(target, src) { /* eslint-disable no-proto */ target.__proto__ = src; /* eslint-enable no-proto */ }
这里做了什么事情?
它把我们即将要观测的数组原型链指向了src, src的值是调用protoAugment函数传过来的arrayMethods对象。
var arrayProto = Array.prototype; var arrayMethods = Object.create(arrayProto);
arrayMethods 对象的原型链又指向了Array.prototype,画个图来理解下。
如图所示,在这使用了一种叫"代理原型"的方式,在被观测的数组对象与Array.prototype之间插入一个纯对象。此时value. __ proto . proto __ === Array.prototype。
为什么要这么做?
因为数组是一个特殊的数据结构,它有很多实例方法,并且有些方法会改变数组自身的值,我们称其为变异方法,这些方法有:push、pop、shift、unshift、splice、sort以及reverse等。 这个时候我们就要考虑一件事,即当用户调用这些变异方法改变数组时需要触发依赖。并且需要知道何时调用了这些变异方法,这样才能在这些方法被调用时做对应的处理。而这步的操作就是通过代理原型的方式实现的。
var methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]; /** * Intercept mutating methods and emit events */ methodsToPatch.forEach(function(method) { // cache original method var original = arrayProto[method]; def(arrayMethods, method, function mutator() { var args = [], len = arguments.length; while (len--) args[len] = arguments[len]; var result = original.apply(this, args); var ob = this.__ob__; var inserted; switch (method) { case 'push': case 'unshift': inserted = args; break case 'splice': inserted = args.slice(2); break } if (inserted) { ob.observeArray(inserted); } // notify change ob.dep.notify(); return result }); });
推荐:
- 020 持续更新,精品小圈子每日都有新内容,干货浓度极高。
- 结实人脉、讨论技术 你想要的这里都有!
- 抢先入群,跑赢同龄人!(入群无需任何费用)
- 点击此处,与前端开发大牛一起交流学习
申请即送:
-
BAT大厂面试题、独家面试工具包,
-
资料免费领取,包括 各类面试题以及答案整理,各大厂面试真题分享!
- vue响应式系统之observe、watcher、dep的源码解析
- vue学习总结:响应式系统&vue实例
- 一张图理清 Vue 3.0 的响应式系统
- 一张图理清Vue 3.0的响应式系统,实现精简版的Vue 3.0响应式系统
- Vue如何实现响应式系统
- 一张图理清 Vue 3.0 的响应式系统
- vue原理探索--响应式系统
- Vue 路由系统和钩子函数
- 一张图理清 Vue 3.0 的响应式系统
- vue数据的响应式原理
- Windows系统调用架构分析—也谈KiFastCallEntry函数地址的获取
- PluginRepository负责加载nutch系统下的插件,可以通过installExtensionPoints()函数查看要加载的插件和对应路径
- --oracle 复习体系二:系统简单函数
- vue_cli下开发一个简单的模块权限系统之实现登录
- 收集一些系统 api 函数
- 信号与系统课程中关于各种编码MATLAB仿真的绘图函数
- Linux系统fork()函数简析
- vue的生命周期钩子函数
- 信号与系统(Python) 学习笔记 (7) 电路与系统函数
- SQL SERVER 系统函数