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

307 阅读2分钟

这是我参与更文挑战的第7天,活动详情查看: 更文挑战

一,前言

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

  • data暴露在vm._data实例属性上
  • 利用Object.definePropertyvm.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) // 操作数组

准备工作完成,进入断点调试:

image.png

2,Vue 的初始化入口

Vue 构造函数接收外部传入的options选项,调用原型方法_init,开始执行 Vue 初始化流程:

image.png

3,initMixin 方法

在 Vue 原型上挂载的_init方法,接收 Vue 初始化时传入的options选项作为入参:

image.png

initMixin方法中,会做以下两件事:

  • 数据的初始化(多种数据:datapropswatchcomputed...)
  • 数据初始化完成后,将数据渲染到页面

关于vm.$options的说明:

  • 将 Vue 初始化时传入的 options 选项,通过vm.$options对外进行暴露;
  • 使 Vue 中的其他方法能够通过实例的$options变量获取到options选项;
  • options放到$options变量上,options中属性不会污染vm实例;

4,initState 方法

initState方法:进行状态初始化操作(状态存在多种来源:datapropswatchcomputed...,目前仅对 data 数据进行处理)

如果options.data存在,执行initData进行 data 数据的初始化:

image.png

5,initData 方法

initData方法:进行 data 初始化操作;

image.png

通过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是一个对象(即引用类型),datavm._data会共享同一个引用;

6,observe 方法(观测入口)

observe方法:数据观测的入口

observe方法执行完成后,就实现了对 data 数据的观测,此时 data 已经成为响应式数据;

image.png

数据的观测会进行深层递归,observe 就是最开始进行数据观测的入口;

所以,这里是第一次调用 observe 方法,value 为整个 data 根对象;

  • 如果 value 不是对象,当前处理结束(在后续递归中,表示本层处理结束,返回到上一层);
  • 如果 value 是对象,将数据创建为 Observer 实例;(首次调用 observe,会将 data 根对象,创建为Observer 实例)

image.png

7,Observer 类

image.png

Observer类的构造方法中,对 value 为数组和对象的两种情况分别进行处理:

数据劫持-对象类型:

  • 第一次进入Observervalue为根数据(必须对象类型),调用walk方法;
  • walk方法:遍历对象属性,对data中每个属性调用defineReactive方法;
  • defineReactive方法:通过Object.defineProperty为属性添加 get、set 方法,实现数据劫持;

数据劫持-数组类型:

  • 对能够改变数组原数据的 7 个原型方法进行重写(如:splice、push、unshift)

image.png

8,walk 方法

walk方法:遍历对象的全部(可枚举)属性,并依次执行defineReactive方法进行数据劫持操作;

image.png

在本例中,遍历 3 个属性(message、obj、arr)并依次调用defineReactive进行数据劫持:

message:

image.png

obj:

image.png

arr:

image.png

9,defineReactive 方法

defineReactive方法:

  • 在外层的walk方法进行对象属性遍历时,调用defineReactive方法处理所有属性;
  • defineReactive方法内部,通过Object.defineProperty重写对象属性实现数据劫持;(这是一个深度优先的处理)

深层观测 data.message

image.png

第一层,第一次进入 defineReactive 方法:

状态:objdata根对象,key属性名为"messge",value属性值为字符串 "Hello Vue";

1,observe方法:

对当前属性进行深层递归处理,如果属性值内部仍存在属性值为对象的情况,就继续处理递归处理;

image.png

此时,在observe方法中,当前属性message的属性值为'Hello Vue'字符串并不是对象;

image.png

所以,observe方法执行结束(递归观测的终止条件),从observe方法 return 出来,继续处理上层的 message 属性:

2,defineReactive方法

通过Object.defineProperty重写 message 属性,添加 get 和 set 方法,实现数据观测:

image.png

这样,message 的处理就完成了

深层观测 data.obj

image.png

第一层,第二次进入 defineReactive 方法:

当前状态:objdata根对象,key属性名为"obj",value属性值为对象{ key : "val" }

1,observe方法:

对当前属性进行深层递归处理,如果属性值内部仍存在属性值为对象的情况,就继续处理递归处理;

image.png

此时,在observe方法中,当前属性obj的属性值为{ key : "val" }对象,所以,继续处理下一层,将{ key : "val" }创建成为一个 Observer 实例:

image.png

2,在Observer类的构造函数内,通过walk方法遍历对象中的所有属性(当前对象为{ key : "val" })

image.png

对每个属性依次调用defineReactive方法,继续对下一层进行观测(深层递归观测):

image.png

进入第二层,调用 defineReactive 方法:

当前状态:obj{ key : "val" }对象,key属性名为"key",value属性值为对象val

1,observe方法:

对当前属性进行深层递归处理,如果属性值内部仍存在属性值为对象的情况,就继续处理递归处理;

image.png

此时,在observe方法中,当前属性key的属性值为val字符串并不是对象

image.png

所以,observe方法执行结束(递归观测的终止条件),从observe方法 return 出来,继续处理上层{ key : "val" }对象中的key属性:

2,defineReactive方法

通过Object.defineProperty重写key属性,添加 get 和 set 方法,实现数据观测:

image.png

{ key : "val" }对象中的key属性处理结束后,对象内部这一层也就处理完成了,之后将回到上一层继续处理obj属性;

回到第一层,继续 defineReactive 方法:

observe方法执行完成后,回到上层defineReactive方法继续处理obj属性;

当前状态:objdata根对象,key属性名为"obj",value属性值为对象{ key : "val" }

通过Object.defineProperty重写 obj 属性,添加 get 和 set 方法,实现数据观测:

image.png

这样,obj 就处理就完成了

深层观测 data.arr

image.png

第一层,第三次进入 defineReactive 方法:

当前状态:objdata根对象,key属性名为"arr",value属性值为数组[1,2,3]

1,observe方法:

对当前属性进行深层递归处理,如果属性值内部仍存在属性值为对象的情况,就继续处理递归处理;

image.png

此时,在observe方法中,当前属性arr的属性值为[1,2,3]数组,所以,继续处理下一层,将[1,2,3]创建成为一个 Observer 实例:

image.png

2,在Observer类的构造函数内,修改数组原型链:

image.png

重写数组的 7 个原型方法,实现对数组的更新拦截处理:

image.png

Observe类实例化时,完成数组原型方法的重写;

3,defineReactive方法

通过Object.defineProperty重写arr属性,添加 get 和 set 方法,实现数据观测:

这样,arr 的处理就完成了,即整个 data 的处理就完成了,完成了对数据的观测

data 对象观测完成

观测后的 data 结果:

第一层 image.png

第二层

  • 数组类型:并没有对数组的每一项进行观测,只重写了数组的 7 个原型方法;
  • 对象类型:对对象中的每一个属性都进行了深层观测;

image.png


三,数据代理

1,实现数据代理

为了实现在实例上直接操作数据,将对象的所有属性都进行了一次代理

vm实例的取值代理:将所有vm.xxx取值操作,代理到vm._data.xxx上:

image.png

对 data 中 3 个属性分别做代理:

1)代理 message:

image.png

2)代理 obj:

image.png

3)代理 arr

image.png

实现原理:

利用Object.defineProperty为属性添加 get、set,相当于为属性的取值、更新添加了一层代理,实际操作还是在vm._data上进行的;

image.png

2,实例取值 vm.message

image.png

proxy方法中,通过Object.definePropertyvm.xxx添加了 get、set 方法;

当通过vm.xxx取值操作时,就会进入get方法,从而被代理到vm._data.xxx上:

image.png

vm._data.xxx的取值操作,又将触发vm._data.xxxget方法:

image.png

原理:

  • vm.message被代理到vm[_data][message]上取值;
  • 此时_data中数据已被Object.defineProperty劫持;
  • 所以,通过这样一层代理,就取到了原有message属性;

3,数组操作 vm.arr.push

image.png

执行vm.arr.push时,会先执行vm.arr取值操作,和上边相似:

proxy方法中,通过Object.definePropertyvm.arr添加了 get、set 方法;

当通过vm.arr取值操作时,就会进入get方法,从而被代理到vm._data.xxx上(vm.['_data'].['arr']

image.png

vm._data.arr的取值操作,又将触发vm._data.arrget方法:

image.png

取到arr数组后,再调用数组的push方法操作数组:

image.png

这时,就会进入数组重写的 push 方法:

image.png


四,当前版本问题分析

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:重新梳理并重构“数据代理”部分;