手写Vue源码之数据劫持示例详解


Posted in Vue.js onJanuary 04, 2021

源代码: 传送门

Vue会对我们在data中传入的数据进行拦截:

  • 对象:递归的为对象的每个属性都设置get/set方法
  • 数组:修改数组的原型方法,对于会修改原数组的方法进行了重写

在用户为data中的对象设置值、修改值以及调用修改原数组的方法时,都可以添加一些逻辑来进行处理,实现数据更新页面也同时更新。

Vue中的响应式(reactive): 对对象属性或数组方法进行了拦截,在属性或数组更新时可以同时自动地更新视图。在代码中被观测过的数据具有响应性

创建Vue实例

我们先让代码实现下面的功能:

<body>
<script>
 const vm = new Vue({
 el: '#app',
 data () {
 return {
 age: 18
 };
 }
 });
 // 会触发age属性对应的set方法
 vm.age = 20;
 // 会触发age属性对应的get方法
 console.log(vm.age);
</script>
</body>

在src/index.js中,定义Vue的构造函数。用户用到的Vue就是在这里导出的Vue:

import initMixin from './init';

function Vue (options) {
 this._init(options);
}

// 进行原型方法扩展
initMixin(Vue);
export default Vue;

在init中,会定义原型上的_init方法,并进行状态的初始化:

import initState from './state';

function initMixin (Vue) {
 Vue.prototype._init = function (options) {
 const vm = this;
 // 将用户传入的选项放到vm.$options上,之后可以很方便的通过实例vm来访问所有实例化时传入的选项
 vm.$options = options;
 initState(vm);
 };
}

export default initMixin;

在_init方法中,所有的options被放到了vm.$options中,这不仅让之后代码中可以更方便的来获取用户传入的配置项,也可以让用户通过这个api来获取实例化时传入的一些自定义选选项。比如在Vuex 和Vue-Router中,实例化时传入的router和store属性便可以通过$options获取到。

除了设置vm.$options,_init中还执行了initState方法。该方法中会判断选项中传入的属性,来分别进行props、methods、data、watch、computed 等配置项的初始化操作,这里我们主要处理data选项:

import { observe } from './observer';
import { proxy } from './shared/utils';

function initState (vm) {
 const options = vm.$options;
 if (options.props) {
 initProps(vm);
 }
 if (options.methods) {
 initMethods(vm);
 }
 if (options.data) {
 initData(vm);
 }
 if (options.computed) {
 initComputed(vm)
 }
 if (options.watch) {
 initWatch(vm)
 }
}

function initData (vm) {
 let data = vm.$options.data;
 vm._data = data = typeof data === 'function' ? data.call(vm) : data;
 // 对data中的数据进行拦截
 observe(data);
 // 将data中的属性代理到vm上
 for (const key in data) {
 if (data.hasOwnProperty(key)) {
 // 为vm代理所有data中的属性,可以直接通过vm.xxx来进行获取
 proxy(vm, key, data);
 }
 }
}

export default initState;

在initData中进行了如下操作:

  1. data可能是对象或函数,这里将data统一处理为对象
  2. 观测data中的数据,为所有对象属性添加set/get方法,重写数组的原型链方法
  3. 将data中的属性代理到vm上,方便用户直接通过实例vm来访问对应的值,而不是通过vm._data来访问

新建src/observer/index.js,在这里书写observe函数的逻辑:

function observe (data) {
 // 如果是对象,会遍历对象中的每一个元素
 if (typeof data === 'object' && data !== null) {
 // 已经观测过的值不再处理
 if (data.__ob__) {
 return;
 }
 new Observer(data);
 }
}

export { observe };

observe函数中会过滤data中的数据,只对对象和数组进行处理,真正的处理逻辑在Observer中:

/**
 * 为data中的所有对象设置`set/get`方法
 */
class Observer {
 constructor (value) {
 this.value = value;
 // 为data中的每一个对象和数组都添加__ob__属性,方便直接可以通过data中的属性来直接调用Observer实例上的属性和方法
 defineProperty(this.value, '__ob__', this);
 // 这里会对数组和对象进行单独处理,因为为数组中的每一个索引都设置get/set方法性能消耗比较大
 if (Array.isArray(value)) {
 Object.setPrototypeOf(value, arrayProtoCopy);
 this.observeArray(value);
 } else {
 this.walk();
 }
 }

 walk () {
 for (const key in this.value) {
 if (this.value.hasOwnProperty(key)) {
 defineReactive(this.value, key);
 }
 }
 }

 observeArray (value) {
 for (let i = 0; i < value.length; i++) {
 observe(value[i]);
 }
 }
}

需要注意的是,__ob__属性要设置为不可枚举,否则之后在对象遍历时可能会引发死循环

Observer类中会为对象和数组都添加__ob__属性,之后便可以直接通过data中的对象和数组vm.value.__ob__来获取到Observer实例。

当传入的value为数组时,由于观测数组的每一个索引会耗费比较大的性能,并且在实际使用中,我们可能只会操作数组的第一项和最后一项,即arr[0],arr[arr.length-1],很少会写出arr[23] = xxx的代码。

所以我们选择对数组的方法进行重写,将数组的原型指向继承Array.prototype新创建的对象arrayProtoCopy,对数组中的每一项继续进行观测。

创建data中数组原型的逻辑在src/observer/array.js中:

// if (Array.isArray(value)) {
// Object.setPrototypeOf(value, arrayProtoCopy);
// this.observeArray();
// }
const arrayProto = Array.prototype;
export const arrayProtoCopy = Object.create(arrayProto);

const methods = ['push', 'pop', 'unshift', 'shift', 'splice', 'reverse', 'sort'];

methods.forEach(method => {
 arrayProtoCopy[method] = function (...args) {
 const result = arrayProto[method].apply(this, args);
 console.log('change array value');
 // data中的数组会调用这里定义的方法,this指向该数组
 const ob = this.__ob__;
 let inserted;
 switch (method) {
 case 'push':
 case 'unshift':
 inserted = args;
 break;
 case 'splice': // splice(index,deleteCount,item1,item2)
 inserted = args.slice(2);
 break;
 }
 if (inserted) {ob.observeArray(inserted);}
 return result;
 };
});

通过Object.create方法,可以创建一个原型为Array.prototype的新对象arrayProtoCopy。修改原数组的7个方法会设置为新对象的私有属性,并且在执行时会调用arrayProto 上对应的方法。

在这样处理之后,便可以在arrayProto中的方法执行前后添加自己的逻辑,而除了这7个方法外的其它方法,会根据原型链,使用arrayProto上的对应方法,并不会有任何额外的处理。

在修改原数组的方法中,添加了如下的额外逻辑:

const ob = this.__ob__;
let inserted;
switch (method) {
 case 'push':
 case 'unshift':
 inserted = args;
 break;
 case 'splice': // splice(index,deleteCount,item1,item2)
 inserted = args.slice(2);
 break;
}
if (inserted) {ob.observeArray(inserted);}

push、unshift、splice会为数组新增元素,对于新增的元素,也要对其进行观测。这里利用到了Observer中为数组添加的__ob__属性,来直接调用ob.observeArray ,对数组中新增的元素继续进行观测。

对于对象,要遍历对象的每一个属性,来为其添加set/get方法。如果对象的属性依旧是对象,会对其进行递归处理

function defineReactive (target, key) {
 let value = target[key];
 // 继续对value进行监听,如果value还是对象的话,会继续new Observer,执行defineProperty来为其设置get/set方法
 // 否则会在observe方法中什么都不做
 observe(value);
 Object.defineProperty(target, key, {
 get () {
 console.log('get value');
 return value;
 },
 set (newValue) {
 if (newValue !== value) {
 // 新加的元素也可能是对象,继续为新加对象的属性设置get/set方法
 observe(newValue);
 // 这样写会新将value指向一个新的值,而不会影响target[key]
 console.log('set value');
 value = newValue;
 }
 }
 });
}

class Observer {
 constructor (value) {
 // some code ...
 if (Array.isArray(value)) {
 // some code ...
 } else {
 this.walk();
 }
 }

 walk () {
 for (const key in this.value) {
 if (this.value.hasOwnProperty(key)) {
 defineReactive(this.value, key);
 }
 }
 }

 // some code ... 
}

数据观测存在的问题

检测变化的注意事项

我们先创建一个简单的例子:

const mv = new Vue({
 data () {
 return {
 arr: [1, 2, 3],
 person: {
 name: 'zs',
 age: 20
 }
 }
 }
})

对于对象,我们只是拦截了它的取值和赋值操作,添加值和删除值并不会进行拦截:

vm.person.school = '北大'
delete vm.person.age

而对于数组,用索引修改值以及修改数组长度不会被观测到:

vm.arr[0] = 0
vm.arr.length--

为了能处理上述的情况,Vue为用户提供了$set和$delete方法:

  • $set: 为响应式对象添加一个属性,确保新属性也是响应式的,因此会触发视图更新
  • $delete: 删除对象上的一个属性。如果对象是响应式的,确保删除触发视图更新。

结语

通过实现Vue的数据劫持,将会对Vue的数据初始化和响应式有更深的认识。

在工作中,我们可能总是会疑惑,为什么我更新了值,但是页面没有发生变化?现在我们可以从源码的角度进行理解,从而更清楚的知道代码中存在的问题以及如何解决和避免这些问题。

代码的目录结构是参考了源码的,所以看完文章的小伙伴,也可以从源码中找出对应的代码进行阅读,相信你会有不一样的理解!

到此这篇关于手写Vue源码之数据劫持的文章就介绍到这了,更多相关Vue源码之数据劫持内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Vue.js 相关文章推荐
Vue使用Element实现增删改查+打包的步骤
Nov 25 Vue.js
vue $router和$route的区别详解
Dec 02 Vue.js
vue添加自定义右键菜单的完整实例
Dec 08 Vue.js
基于vue+echarts数据可视化大屏展示的实现
Dec 25 Vue.js
jenkins自动构建发布vue项目的方法步骤
Jan 04 Vue.js
聊聊vue 中的v-on参数问题
Jan 29 Vue.js
vue3.0封装轮播图组件的步骤
Mar 04 Vue.js
vue-cli4.5.x快速搭建项目
May 30 Vue.js
vite+vue3.0+ts+element-plus快速搭建项目的实现
Jun 24 Vue.js
Vue的过滤器你真了解吗
Feb 24 Vue.js
Vue组件更新数据v-model不生效的解决
Apr 02 Vue.js
vue项目如何打包之项目打包优化(让打包的js文件变小)
Apr 30 Vue.js
vue+vant 上传图片需要注意的地方
Jan 03 #Vue.js
vue调用微信JSDK 扫一扫,相册等需要注意的事项
Jan 03 #Vue.js
vue中使用echarts的示例
Jan 03 #Vue.js
vue 动态生成拓扑图的示例
Jan 03 #Vue.js
Vue中强制组件重新渲染的正确方法
Jan 03 #Vue.js
vue中activated的用法
Jan 03 #Vue.js
vue实现登录功能
Dec 31 #Vue.js
You might like
很实用的一个完整email发送程序
2006/10/09 PHP
PHP获取数组中单列值的方法
2017/06/10 PHP
ThinkPHP5.1+Ajax实现的无刷新分页功能示例
2020/02/10 PHP
innerHTML,outerHTML,innerTEXT三者之间的区别
2007/01/28 Javascript
jQuery 1.3 和 Validation 验证插件1.5.1
2009/07/09 Javascript
javascript 手动给表增加数据的小例子
2013/07/10 Javascript
基于javascript实现checkbox复选框实例代码
2016/01/28 Javascript
AngularJS 过滤与排序详解及实例代码
2016/09/14 Javascript
JS 实现Base64编码与解码实例详解
2016/11/07 Javascript
JS实现自动阅读单词(有道单词本添加功能)
2016/11/14 Javascript
自动适应iframe右边的高度
2016/12/22 Javascript
js判断用户是输入的地址请求的路径(实例讲解)
2017/07/18 Javascript
vue使用Font Awesome的方法步骤
2019/02/26 Javascript
Javascript删除数组里的某个元素
2019/02/28 Javascript
layui实现二维码弹窗、并下载到本地的方法
2019/09/25 Javascript
JavaScript进阶(二)词法作用域与作用域链实例分析
2020/05/09 Javascript
基于Python的接口测试框架实例
2016/11/04 Python
老生常谈python的私有公有属性(必看篇)
2017/06/09 Python
python 随机数使用方法,推导以及字符串,双色球小程序实例
2017/09/12 Python
Python数据分析库pandas基本操作方法
2018/04/08 Python
spark: RDD与DataFrame之间的相互转换方法
2018/06/07 Python
基于python代码实现简易滤除数字的方法
2018/07/17 Python
Python制作动态字符图的实例
2019/01/27 Python
python的turtle库使用详解
2019/05/10 Python
Pytorch的mean和std调查实例
2020/01/02 Python
PyCharm2020.1.1与Python3.7.7的安装教程图文详解
2020/08/07 Python
用python计算文件的MD5值
2020/12/23 Python
python 实现百度网盘非会员上传超过500个文件的方法
2021/01/07 Python
澳大利亚连衣裙和女装在线:Esther
2017/11/11 全球购物
留学自荐信
2013/10/10 职场文书
技术总监的工作职责
2013/11/13 职场文书
退学证明范本3篇
2014/10/29 职场文书
公司出差管理制度范本
2015/08/05 职场文书
SQL Server基本使用和简单的CRUD操作
2021/04/05 SQL Server
PHP连接MSSQL数据库案例,PHPWAMP多个PHP版本连接SQL Server数据库
2021/04/16 PHP
Java代码规范与质量检测插件SonarLint的使用
2022/08/05 Java/Android