写点什么

【Vue2.x 源码学习】第七篇 - 阶段性梳理

用户头像
Brave
关注
发布于: 2021 年 06 月 07 日
【Vue2.x 源码学习】第七篇 - 阶段性梳理

一,前言


上篇,主要介绍了 Vue 数据初始化流程中,Vue 实例上数据代理的实现,核心思路如下:


  1. 将 data 暴露在 vm._data 实例属性上

  2. 利用 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 方法做了一下事情:

  1. 数据的初始化(多种数据:data、props、watch、computed...)

  2. 数据初始化完成后,将数据渲染到页面


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,可以是函数,也有可能是对象:

  1. data 是函数时:调用 data 拿到函数的返回值-对象,作为当前实例数据源 data

  2. 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,已支持的数据观测


当前版本,已支持的数据观测有以下情况:

  1. data 根对象(对象及嵌套对象实现了深层观测)

  2. data 中的值(为对象中的属性添加 get、set 方法)

  3. data 中的数组(重写数组原型方法,目前没有做递归处理,仅实现了数组的单层劫持)

  4. data 中的对象(对象及嵌套对象实现了深层观测)

  5. data 中的对象中的对象...(对象及嵌套对象实现了深层观测)

  6. data 中的对象中的值(对象及嵌套对象深层观测,同时为对象中的属性添加 get、set 方法)

3,尚不支持的数据观测

当前版本,尚不支持的数据观测有以下情况:

  1. data 中的数组中的对象(重写数组原型方法,目前没有做递归处理,仅实现了数组的单层劫持)

  2. data 中的数组中的数组(重写数组原型方法,目前没有做递归处理,仅实现了数组的单层劫持)

  3. 新加入的对象不会被观测

  4. 新加入的数组不会被观测

4,Vue2.x 的机制


  1. 修改数组下标和长度不会触发更新(仅重写了数组部分原型方法);【可使用 vm.$set 实现】

  2. 对于新增属性无法,无法进行数据观测,不会触发更新;【可使用 vm.$set 实现】


四,结尾


又成功水了一篇,还剩 14 篇


本篇通过 Vue Demo 的断点调试,对当前版本数据劫持、数据代理进行了简单的流程梳理

同时,对照 Vue2.x 提供的功能,分析了当前版本数据观测的问题和不足


下一篇,数组的深层观测


用户头像

Brave

关注

还未添加个人签名 2018.12.13 加入

还未添加个人简介

评论

发布
暂无评论
【Vue2.x 源码学习】第七篇 - 阶段性梳理