前言
我们已经大致了解了初始化工作中 vue 到底做了哪些事情,这篇开始,进入第二大块内容 —— 数据响应,逐步探究 vue 是如何实现数据的动态响应?
再次回顾状态初始化 initState 方法:
1 | function initState(vm: Component) { |
这里先忽略 props 、methods 、computed 、watcher 的“处理工作”,先选 options.data 属性作为起点,因为 data 使我们开发中最常用的属性,借他了解整个 动态响应的全貌 :
1 | function initData(vm: Component) { |
经过 initData 的初始化处理,发现最终调用了观察方法 observe(data) ,这是数据动态响应的第一部分。
observe 观察数据
代码主体
先看下方法代码定义:
1 | function observe(value: any, asRootData: ?boolean): Observer | void { |
条件判断
我们知道目前是如何调用此 observe 方法的:
1 | observe(data, true /* asRootData */); |
所以目前参数列表中 value 就是 options.data , asRootData 值为 true:
首先,回去判断某些不做观察的条件:
1 | if (!isObject(value) || value instanceof VNode) { |
不做观察的条件 : value 非对象形式,并且不是 VNode 虚拟节点对象。
再是,判断 value 是否已经被观察过了:
1 | hasOwn(value, "__ob__") && value.__ob__ instanceof Observer; |
如果 value 对象上含有 __ob__ 属性,并且该属性是 Observer 对象,则直接返回 __ob__ 属性值:
1 | if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) { |
接下来是一段很长的判断:
1 | shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue; |
shouldObserve 默认为 true ,通过调用 toggleObserving 方法来切换其值:
1 | function toggleObserving(value: boolean) { |
shouldObserve 像是一个开关来控制是否来执行 observe 方法,你们在 vue 中找到几处 toggleObserving 开关的使用。
比如,在对 inject 处理 provide 的传值时的数据封装:
1 | function initInjections(vm: Component) { |
最终 inject 上的第一级属性被定义了响应化,但子级属性因为 toggleObserving(false) 而直接 return ,这就是为何:你更新 inject 相关值时,页面没有更新的原因(有些扯远了)。
isServerRendering 判断是否是 SSR 服务端渲染,本文的运行环境是浏览器中,所以这里判断为 true 。
后续只要 value 符合是 引用类型对象(对象字面量、数组),并且是属性可扩展,和非 vue 框架对象 (_isVue 为 false ),则会创建 Observer 观察者对象:
1 | ob = new Observer(value); |
Observer 观察者对象
代码主体
这是 Observer 类的代码主体:
1 | class Observer { |
首先我们先看下 Observer 的基本属性:
1 | this.value = value; |
每次新建的 Observer 对象将初始化一个 Dep 对象(这块下篇再看)定义为 this.dep ,并且还会初始化 this.value 待观察对象,this.vmCount 。
对象属性 __ob__
在 value 对象上新增一个观察属性 __ob__:
1 | def(value, "__ob__", this); |
def 方法内部就是通过定义 数据类型属性 :
1 | Object.defineProperty(obj, key, { |
注意这里的 enumerable 为 false ,会在遍历 value 是屏蔽掉当前属性 __ob__ 。这个属性 __ob__ 以后有什么用,我们在 Dep 对象中再看。
先跳过 Array.isArray(value) 的判断,直接调用 this.walk 方法:
1 | this.walk(value); |
1 | walk (obj: Object) { |
walk 会遍历 value 上的所有属性(除了 __ob__),并通过 defineReactive 定义 响应式数据 。
数组拦截
再回到数据类型的判断:
1 | if (Array.isArray(value)) { |
首先我们知道 __proto__ 是个有争议的属性,因为他不属于 Web 规范,只是被浏览器厂商实现着,用来访问对象 [[Prototype]] 属性。
所以 vue 会有如下工具函数 hasProto
1 | "__proto__" in {}; |
判断当前环境是否支持 hasProto 分别调用 protoAugment 、 copyAugment 来对 value 数组上的每个元素进行针对数组类型的数据做响应。
根据 Array.prototype 创建数组(方法)对象 arrayMethods ,通过 def 设置 mutator 方法,如果数组数据涉及变化( push、unshift、splice )则再次调用 notify 触发数据响应:
1 | methodsToPatch.forEach(function(method) { |
上面就是 vue 怎么对原生数组方法进行拦截的代码,有兴趣可以细看。
解析完数组队列后,最后执行 this.observeArray :
1 | observeArray (items: Array<any>) { |
内部还是执行 observe 方法。
假设你在 data 上定义了一个数组数据,最后的结果就会长成这样:
defineReactive 响应式数据的定义
已经知道,无论是否是 Array 类型的数据, value 最终都会调用 walk 方法,执行 defineReactive
1 | walk (obj: Object) { |
代码主体
现在来看下 defineReactive 的代码:
1 | function defineReactive(obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean) { |
创建 Dep 对象
已进入该方法,就看到了新建了 Dep 对象,并定义为 dep ,这个是依赖对象,我们目前只需知道每个对象属性都会调用 defineReactive 方法,并且都会创建 Dep 实例。具体下篇再细谈。
1 | const dep = new Dep(); |
合法验证
对象属性是否可配置
根据对象属性描述特征 configurable 来判断是否要进行数据动态响应定义:
1 | const property = Object.getOwnPropertyDescriptor(obj, key); |
如果特性 configurable 为 false ,就没有之后的意义了(将对象属性定义为 访问器属性 )。
预取值
我们通常在 data 选项属性上会定义 {name:’foo’} ,是有值的,但没有定义 getter 函数。所以需要通过某种方式来事先计算一次:
1 | const getter = property && property.get; |
那如果 getter 存在呢?那就会在底下的 getter/setter 中执行 getter.call() 进行执行赋值操作。
1 | const value = getter ? getter.call(obj) : val; |
判断子元素是否需要观察
1 | let childOb = !shallow && observe(val); |
如果我们的数据是这样(对象嵌套):
1 | data:{ |
当第一次遍历 user 时,当前的 val 就是 child 对象,需要继续通过 observe 观察。
得到 childOb ,供后续逻辑使用。
对象属性定义访问器属性
1 | Object.defineProperty(obj, key, { |
getter
首先通过 getter ,来预执行:
1 | const value = getter ? getter.call(obj) : val; |
然后就是判断 Dep.target :
1 | if (Dep.target) { |
当然目前没不知道这个 Dep.target 是什么,不过从代码上看,当它存在时,就会调用 depend 来执行依赖相关操作。
setter
第一步还是根据判断 getter 进行预执行操作,获得 value 。
之后判断 newVal 和 value 是否有变化?
1 | if (newVal === value || (newVal !== newVal && value !== value)) { |
值得一提的是,newVal !== newVal 这样的矛盾判断有什么用?你可以运行下如下代码:
1 | Number("foo") == Number("foo"); //false |
然后通过 customSetter 在开发模式打印些 warn 信息:
1 | if (process.env.NODE_ENV !== "production" && customSetter) { |
如果 getter && !setter 就没必要继续之后操作了,直接 return 。
如果定义了 setter 函数,则会执行下:
1 | if (setter) { |
最后将这个新 newVal 进行观察,同时出发 notify 进行依赖更新:
1 | childOb = !shallow && observe(newVal); |
总结
本文篇幅较长,尽可能把 vue 数据动态响应的第一部分原理说明白了。
通过 observe 方法判断对象上,那些属性需要观察,来新建对应的 Observer 观察者对象。Observer 内部循环遍历属性,调用 definedReactive 来定义动态响应方式。
这个动态响应的基本原理还是基于对象的访问属性 getter/setter 。
其内部真正实现数据的响应机制,还是要看之后的 Dep 和 Watcher 对象。