手写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 + el-form 实现的多层循环表单验证
Nov 25 Vue.js
vue中如何自定义右键菜单详解
Dec 08 Vue.js
vue实现图片裁剪后上传
Dec 16 Vue.js
Vue实现指令式动态追加小球动画组件的步骤
Dec 18 Vue.js
vue编写简单的购物车功能
Jan 08 Vue.js
Vue中的nextTick作用和几个简单的使用场景
Jan 25 Vue.js
如何在 Vue 中使用 JSX
Feb 14 Vue.js
vue backtop组件的实现完整代码
Apr 07 Vue.js
如何理解Vue前后端数据交互与显示
May 10 Vue.js
Vue Element UI自定义描述列表组件
May 18 Vue.js
Vue全局事件总线你了解吗
Feb 24 Vue.js
vue 自定义的组件绑定点击事件
Apr 21 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
实现了一个PHP5的getter/setter基类的代码
2007/02/25 PHP
php报表之jpgraph柱状图实例代码
2011/08/22 PHP
利用 fsockopen() 函数开放端口扫描器的实例
2017/08/19 PHP
用javascript来实现动画导航效果的代码
2007/12/16 Javascript
js获取页面description的方法
2015/05/21 Javascript
jQuery 获取屏幕高度、宽度的简单实现案例
2016/05/17 Javascript
JavaScript实现使用Canvas绘制图形的基本教程
2016/10/27 Javascript
vue init失败简单解决方法(终极版)
2017/12/22 Javascript
React 组件间的通信示例
2018/06/14 Javascript
脚手架vue-cli工程webpack的基本用法详解
2018/09/29 Javascript
layui2.0使用table+laypage实现真分页
2019/07/27 Javascript
js回溯法计算最佳旅行线路代码实例
2019/09/11 Javascript
[02:51]DOTA2英雄基础教程 艾欧
2014/01/13 DOTA
phpsir 开发 一个检测百度关键字网站排名的python 程序
2009/09/17 Python
python中定义结构体的方法
2013/03/04 Python
django实现前后台交互实例
2017/08/07 Python
python print 按逗号或空格分隔的方法
2018/05/02 Python
Python中垃圾回收和del语句详解
2018/11/15 Python
python读取几个G的csv文件方法
2019/01/07 Python
使用pyqt5 tablewidget 单元格设置正则表达式
2019/12/13 Python
Python使用正则实现计算字符串算式
2019/12/29 Python
python对一个数向上取整的实例方法
2020/06/18 Python
Python3爬虫关于识别检验滑动验证码的实例
2020/07/30 Python
如何在 Matplotlib 中更改绘图背景的实现
2020/11/26 Python
python 将html转换为pdf的几种方法
2020/12/29 Python
HTML5去掉输入框type为number时的上下箭头的实现方法
2020/01/03 HTML / CSS
viagogo英国票务平台:演唱会、体育比赛、戏剧门票
2017/03/24 全球购物
东方通信股份有限公司VC面试题
2014/08/27 面试题
音乐专业自荐信
2014/02/07 职场文书
小学数学课题方案
2014/06/15 职场文书
初三毕业感言
2015/07/31 职场文书
2016入党培训心得体会范文
2016/01/08 职场文书
Python预测分词的实现
2021/06/18 Python
python四种出行路线规划的实现
2021/06/23 Python
javascript数组includes、reduce的基本使用
2021/07/02 Javascript
Java基于Dijkstra算法实现校园导游程序
2022/03/17 Java/Android