这是我参与更文挑战的第7天,活动详情查看: 更文挑战
一,前言
上篇,介绍了 Vue 数据初始化流程中,Vue 实例上数据代理的实现,核心思路如下:
- 将
data
暴露在vm._data
实例属性上 - 利用
Object.defineProperty
将vm.xxx
操作代理到vm._data
上
本篇,对当前版本的数据劫持和实例取值代理进行断点调试与流程梳理;
二,数据劫持
1,调试 Demo
debugger;
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
、props
、watch
、computed
...,目前仅对 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 为数组和对象的两种情况分别进行处理:
数据劫持-对象类型:
- 第一次进入
Observer
时value
为根数据(必须对象类型),调用walk
方法; walk
方法:遍历对象属性,对data
中每个属性调用defineReactive
方法;defineReactive
方法:通过Object.defineProperty
为属性添加 get、set 方法,实现数据劫持;
数据劫持-数组类型:
- 对能够改变数组原数据的 7 个原型方法进行重写(如:splice、push、unshift)
8,walk 方法
walk
方法:遍历对象的全部(可枚举)属性,并依次执行defineReactive
方法进行数据劫持操作;
在本例中,遍历 3 个属性(message、obj、arr)并依次调用defineReactive
进行数据劫持:
message:
obj:
arr:
9,defineReactive 方法
defineReactive
方法:
- 在外层的
walk
方法进行对象属性遍历时,调用defineReactive
方法处理所有属性; - 在
defineReactive
方法内部,通过Object.defineProperty
重写对象属性实现数据劫持;(这是一个深度优先的处理)
深层观测 data.message
第一层,第一次进入 defineReactive 方法:
状态:obj
为data
根对象,key
属性名为"messge",value
属性值为字符串 "Hello Vue";
1,observe
方法:
对当前属性进行深层递归处理,如果属性值内部仍存在属性值为对象的情况,就继续处理递归处理;
此时,在observe
方法中,当前属性message
的属性值为'Hello Vue'
字符串并不是对象;
所以,observe
方法执行结束(递归观测的终止条件),从observe
方法 return 出来,继续处理上层的 message 属性:
2,defineReactive
方法
通过Object.defineProperty
重写 message 属性,添加 get 和 set 方法,实现数据观测:
这样,message 的处理就完成了
深层观测 data.obj
第一层,第二次进入 defineReactive 方法:
当前状态:obj
为data
根对象,key
属性名为"obj",value
属性值为对象{ key : "val" }
1,observe
方法:
对当前属性进行深层递归处理,如果属性值内部仍存在属性值为对象的情况,就继续处理递归处理;
此时,在observe
方法中,当前属性obj
的属性值为{ key : "val" }
对象,所以,继续处理下一层,将{ key : "val" }
创建成为一个 Observer 实例:
2,在Observer
类的构造函数内,通过walk
方法遍历对象中的所有属性(当前对象为{ key : "val" })
对每个属性依次调用defineReactive
方法,继续对下一层进行观测(深层递归观测):
进入第二层,调用 defineReactive 方法:
当前状态:obj
为{ key : "val" }
对象,key
属性名为"key",value
属性值为对象val
1,observe
方法:
对当前属性进行深层递归处理,如果属性值内部仍存在属性值为对象的情况,就继续处理递归处理;
此时,在observe
方法中,当前属性key
的属性值为val
字符串并不是对象
所以,observe
方法执行结束(递归观测的终止条件),从observe
方法 return 出来,继续处理上层{ key : "val" }
对象中的key
属性:
2,defineReactive
方法
通过Object.defineProperty
重写key
属性,添加 get 和 set 方法,实现数据观测:
{ key : "val" }
对象中的key
属性处理结束后,对象内部这一层也就处理完成了,之后将回到上一层继续处理obj
属性;
回到第一层,继续 defineReactive 方法:
observe
方法执行完成后,回到上层defineReactive
方法继续处理obj
属性;
当前状态:obj
为data
根对象,key
属性名为"obj",value
属性值为对象{ key : "val" }
;
通过Object.defineProperty
重写 obj 属性,添加 get 和 set 方法,实现数据观测:
这样,obj 就处理就完成了
深层观测 data.arr
第一层,第三次进入 defineReactive 方法:
当前状态:obj
为data
根对象,key
属性名为"arr",value
属性值为数组[1,2,3]
1,observe
方法:
对当前属性进行深层递归处理,如果属性值内部仍存在属性值为对象的情况,就继续处理递归处理;
此时,在observe
方法中,当前属性arr
的属性值为[1,2,3]
数组,所以,继续处理下一层,将[1,2,3]
创建成为一个 Observer 实例:
2,在Observer
类的构造函数内,修改数组原型链:
重写数组的 7 个原型方法,实现对数组的更新拦截处理:
当Observe
类实例化时,完成数组原型方法的重写;
3,defineReactive
方法
通过Object.defineProperty
重写arr
属性,添加 get 和 set 方法,实现数据观测:
这样,arr 的处理就完成了,即整个 data 的处理就完成了,完成了对数据的观测
data 对象观测完成
观测后的 data 结果:
第一层
第二层
- 数组类型:并没有对数组的每一项进行观测,只重写了数组的 7 个原型方法;
- 对象类型:对对象中的每一个属性都进行了深层观测;
三,数据代理
1,实现数据代理
为了实现在实例上直接操作数据,将对象的所有属性都进行了一次代理
vm
实例的取值代理:将所有vm.xxx
取值操作,代理到vm._data.xxx
上:
对 data 中 3 个属性分别做代理:
1)代理 message:
2)代理 obj:
3)代理 arr
实现原理:
利用Object.defineProperty
为属性添加 get、set,相当于为属性的取值、更新添加了一层代理,实际操作还是在vm._data
上进行的;
2,实例取值 vm.message
在proxy
方法中,通过Object.defineProperty
为vm.xxx
添加了 get、set 方法;
当通过vm.xxx
取值操作时,就会进入get
方法,从而被代理到vm._data.xxx
上:
而vm._data.xxx
的取值操作,又将触发vm._data.xxx
的get
方法:
原理:
vm.message
被代理到vm[_data][message]
上取值;- 此时
_data
中数据已被Object.defineProperty
劫持; - 所以,通过这样一层代理,就取到了原有
message
属性;
3,数组操作 vm.arr.push
执行vm.arr.push
时,会先执行vm.arr
取值操作,和上边相似:
在proxy
方法中,通过Object.defineProperty
为vm.arr
添加了 get、set 方法;
当通过vm.arr
取值操作时,就会进入get
方法,从而被代理到vm._data.xxx
上(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 提供的功能,分析了当前版本数据观测的问题和不足
下一篇,数组的深层观测
维护日志
- 20230106:修改目录层级,优化部分描述;
- 20230110:重新梳理并重构“数据劫持”部分;
- 20230111:重新梳理并重构“数据代理”部分;