【Vue2.x 源码学习】第七篇 - 阶段性梳理
一,前言
上篇,主要介绍了 Vue 数据初始化流程中,Vue 实例上数据代理的实现,核心思路如下:
将 data 暴露在 vm._data 实例属性上
利用 Object.defineProperty 将 vm.xxx 操作代理到 vm._data 上
本篇,对当前版本的数据劫持和实例取值代理进行断点调试与流程梳理
二,数据劫持
1,调试 Demo
准备工作完成,进入断点调试:
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 根对象;
1)如果 value 不是对象,当前处理结束(在后续递归中,表示本层处理结束,返回到上一层);
2)如果 value 是对象,将数据创建为 Observer 实例;(首次调用 observe,会将 data 根对象,创建为 Observer 实例)
7,Observer 类
在 Observer 类的构造方法中,对 value 为数组和对象的两种情况做分别处理
1)数据劫持-对象类型:
walk 方法:遍历对象属性,为每个属性调用 defineReactive 方法;
defineReactive 方法:通过 Object.defineProperty 为属性添加 get、set 方法,进行数据劫持
2)数据劫持-数组类型:
对能够改变数组原数据的 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 实现】
四,结尾
又成功水了一篇,还剩 14 篇
本篇通过 Vue Demo 的断点调试,对当前版本数据劫持、数据代理进行了简单的流程梳理
同时,对照 Vue2.x 提供的功能,分析了当前版本数据观测的问题和不足
下一篇,数组的深层观测
评论