一,前言
上篇,介绍了 Vue 数据初始化流程中,Vue 实例上数据代理的实现,核心思路如下:
- 将 data 暴露在 vm._data 实例属性上
- 利用 Object.defineProperty 将 vm.xxx 操作代理到 vm._data 上
本篇,对当前版本的数据劫持和实例取值代理进行断点调试与流程梳理
二,数据劫持
1,调试 Demo
let vm = new Vue({
el: '#app',
data() {
return {
message: 'Hello Vue', // 值
obj: { key: "val" }, // 嵌套对象
arr:[1,2,3]} // 数组
}
});
vm.message // 访问属性
vm.arr.push(4) // 操作数组
准备工作完成,进入断点调试:
2,Vue 的初始化入口
Vue 构造函数,传入外部 options 选项,调用原型方法 _init 开始 Vue 的初始化流程
3,initMixin 方法
在 Vue 原型上挂载 _init 方法,以 Vue 初始化时传入的 options 选项作为参数
initMixin 方法做了以下几件事:
- 数据的初始化(多种数据:data、props、watch、computed…)
- 数据初始化完成后,将数据渲染到页面
vm.$options 说明:
- 将 Vue 初始化时传入的 options 选项,通过 vm.$options 对外进行暴露,
- 使 Vue 中的方法能够通过实例的 $options 变量轻松获取到 options 选项
- 将 options 放到$options变量上,options 中属性不会污染 vm 实例
4,initState 方法
initState 方法:进行状态的初始化操作(这里的状态就是数据,数据有多种来源)
目前仅对 data 进行处理,如果options.data存在,进入 initData 进行 data 数据的初始化
5,initData 方法
initData 方法:进行 data 初始化操作
通过 vm.$options.data 可以直接获取到 Vue 初始化时传入的 data 属性;
这里的 data,可以是函数,也有可能是对象:
- data 是函数时:调用 data 拿到函数的返回值-对象,作为当前实例数据源 data
- data 不是函数时:data 一定是对象,直接作为实例数据源 data
通过这一步处理之后,data 被统一处理为对象类型,供后续流程使用
data = vm._data 的说明:
- 外部实例 vm,目前是无法直接访问到 initData 方法内部 data 属性的;
- 为了使外部实例 vm 能够直接访问到 data 属性,在 vm 实例上添加了 _data 实例属性,即 data = vm._data;
- data 是一个对象,即引用类型,所以 data 与 vm._data 共享引用
6,observe 方法(观测入口)
observe 方法:数据观测的入口
observe 被调用后,就实现了对 data 数据的观测,此时 data 即为响应式数据
数据的观测是会进行深层递归的,这里的 observe 就是最开始进行数据观测的入口;
所以,这里是第一次调用 observe 方法,value 为整个 data 根对象;
- 如果 value 不是对象,当前处理结束(在后续递归中,表示本层处理结束,返回到上一层);
- 如果 value 是对象,将数据创建为 Observer 实例;(首次调用 observe,会将 data 根对象,创建为Observer 实例)
7,Observer 类
在 Observer 类的构造方法中,对 value 为数组和对象的两种情况做分别处理
数据劫持-对象类型:
- walk 方法:遍历对象属性,为每个属性调用 defineReactive方法;
- defineReactive方法:通过 Object.defineProperty 为属性添加 get、set 方法,进行数据劫持
数据劫持-数组类型:
- 对能够改变数组原数据的 7 个原型方法进行重写(如:splice、push、unshift)
- 第一次进入 Observer 时:value 为根数据(必是对象类型),调用 walk 方法
8,walk 方法
walk 方法:遍历对象上的全部(可枚举)属性,依次执行 defineReactive 方法进行数据劫持操作;
在循环中,依次为 3 个属性调用 defineReactive 添加数据劫持
message:
obj:
arr:
9,defineReactive 方法
defineReactive 是在外部 walk 方法进行对象属性遍历时,为每个属性递归调用;
defineReactive 方法:为每个属性递归创建 Object.defineProperty,是一个深度优先的处理
深层观测 data.message
第一次进入 defineReactive 方法:
obj 为 data 根对象,key 为 “messge”,value 为字符串 “Hello Vue”
先走 observe 方法,做深层递归处理(深度优先)
observe 方法中,由于 message 值为字符串’Hello Vue’,并不是对象,
所以,无需向下递归出路,return 出来处理 data 中 message 属性
在 defineReactive 方法中:
通过 Object.defineProperty 在 obj 对象上(第一次进入,obj 为根对象 data)
重新定义 message 属性,值分别是 get 和 set
这样,message 的处理就完成了
深层观测 data.obj
第二次,进入 defineReactive 方法:
obj 为 data 根对象,key 为 “obj”,value 为对象 { key : “val” }
先走 observe 方法,做深层递归处理(深度优先)
由于 value 是对象类型 { key : “val” },需要递归处理下层(是对象,就递归)
所以,将 value 创建成为一个 Observer 实例
Observer 构造函数内,执行对象处理逻辑,通过 walk 方法遍历对象所有属性
继续对每一个属性调用 defineReactive 方法,继续做深层递归观测
walk 方法会遍历参数 data(值为对象 { key : “val” } )的所有属性,调用 defineReactive 处理下一层:
第一次调用defineReactive方法:
obj 为对象 { key : “val” }, key 为 “key” value 为"val"
observe 方法中,由于 key 值为字符串’val’,并不是对象,
所以,无需向下递归出路,return 出来处理 { key : “val” } 对象的"key"属性
为对象{ key : “val” } 中的"key"属性,通过Object.defineProperty,实现 key 属性的观测
{ key : “val” } 中的 "key"属性处理完成后,对象内部一层就除了完了,继续处理{ key : “val” } 对象观测
执行 observe 方法后,{ key : “val” } 对象内部属性的深层观测已完成
继续除了{ key : “val” } 对象的观测,此时 defineReactive 方法:
obj 为 data 根对象,key 为"obj",value为对象 { key : “val” }
这样,obj 的处理就完成了
深层观测 data.arr
第三次,进入 defineReactive 方法:
obj 为 data 根对象,key 为 " arr",value 为数组 [1,2,3]
先走 observe 方法,做深层递归处理(深度优先)
由于value 是数组 { key : “val” }(对象类型),需要递归处理下层,
所以,将 value 创建成为一个 Observer 实例
value 为数组类型,需重写部分数组原型上的方法:
重写以下 7 个方法,相当于拦截了数组的更新
observe 内部对数组进行数组原型方法的重写后
再对 data 的 arr 属性进行 Object.defineProperty 处理
这样,arr 的处理就完成了,即整个 data 的处理就完成了,完成了对数据的观测
完成 data 的观测
内层:
- 数组类型:没有监听数组的每一项,只重写了数组部分原型方法;
- 对象类型:对对象的属性进行了深层观测
备注:
由于在递归处理中,将会为对象进行深层处理,
所以,如果数组中有对象,也会实现深层观测;
三,数据代理
1,实现数据代理
为了实现在实例上直接操作数据,将对象的所有属性都进行了一次代理
vm实例的取值代理:将所有 vm.xxx 取值操作,代理到 vm._data.xxx 上
对 data 中 3 个属性分别做代理:
1)代理 message:
2)代理 obj:
3)代理 arr
实现原理:
使用 Object.defineProperty,为取值、更新添加一层代理,代理到 vm._data 上
2,实例取值 vm.message
由于 proxy 方法中,通过 Object.defineProperty 将 vm.xxx 代理到了 vm._data.xxx上
所以,当通过 vm.xxx 进行取值操作时,会进入 get 方法,将被代理到 vm._data.xxx 上
vm._data.xxx会,再触发vm._data 的 get 方法
原理:
vm.message 被代理到了 vm[_data][message] 上进行取值,
通过这样一层代理,就取到了原有 message 属性,
(因为,此时 _data 中数据已成功被defineProperty劫持)
3,数组操作 vm.arr.push
此操作会先执行 vm.arr 取值操作,和上边一样:
由于 proxy 方法中,通过 Object.defineProperty 将 vm.xxx 代理到了 vm._data.xxx上
所以,当通过 vm.xxx 进行取值操作时,会进入 get 方法,被将代理到 vm._data.xxx 上
即,将 vm.arr 的取值,代理到 vm.[‘_data’].[‘arr’] 上
vm.[‘_data’].[‘arr’] 会再触发vm._data.arr 的 get 方法
取到 arr 数组后,再调用 .push 方法操作数组:
这时,会进入数组重写的 push 方法:
四,当前版本问题分析
1,深层观测逻辑
当前版本的源码,实现深层观测的逻辑如下:
- 对 data 根对象进行深层观测
- data 内部的属性如果是对象,会进行递归观测
- data 内部的属性如果是数组,会重写数组的原型链上的方法(7 个)
2,已支持的数据观测
当前版本,已支持的数据观测有以下情况:
- data 根对象(对象及嵌套对象实现了深层观测)
- data 中的值(为对象中的属性添加 get、set 方法)
- data 中的数组(重写数组原型方法,目前没有做递归处理,仅实现了数组的单层劫持)
- data 中的对象(对象及嵌套对象实现了深层观测)
- data 中的对象中的对象…(对象及嵌套对象实现了深层观测)
- data 中的对象中的值(对象及嵌套对象深层观测,同时为对象中的属性添加 get、set 方法)
3,尚不支持的数据观测
当前版本,尚不支持的数据观测有以下情况:
- data 中的数组中的对象(重写数组原型方法,目前没有做递归处理,仅实现了数组的单层劫持)
- data 中的数组中的数组(重写数组原型方法,目前没有做递归处理,仅实现了数组的单层劫持)
- 新加入的对象不会被观测
- 新加入的数组不会被观测
4,Vue2.x 的机制
- 修改数组下标和长度不会触发更新(仅重写了数组部分原型方法);【可使用 vm.$set 实现】
- 对于新增属性无法,无法进行数据观测,不会触发更新;【可使用 vm.$set 实现】
四,结尾
本篇通过 Vue Demo 断点调试,对当前版本的数据劫持和数据代理进行流程梳理
同时,对照 Vue2.x 提供的功能,分析了当前版本数据观测的问题和不足
下一篇,数组的深层观测