手写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中通过render函数给子组件设置ref操作
Nov 17 Vue.js
vue3.0中setup使用(两种用法)
Dec 02 Vue.js
对vue生命周期的深入理解
Dec 03 Vue.js
vue添加自定义右键菜单的完整实例
Dec 08 Vue.js
vue的hash值原理也是table切换实例代码
Dec 14 Vue.js
基于VUE实现简单的学生信息管理系统
Jan 13 Vue.js
Vue 3自定义指令开发的相关总结
Jan 29 Vue.js
聊聊vue 中的v-on参数问题
Jan 29 Vue.js
如何使用vue3打造一个物料库
May 08 Vue.js
vue使用节流函数的踩坑实例指南
May 20 Vue.js
Vue监视数据的原理详解
Feb 24 Vue.js
vue自定义右键菜单之全局实现
Apr 09 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
php数组相加 array(“a”)+array(“b”)结果还是array(“a”)
2012/09/19 PHP
使用php+apc实现上传进度条且在IE7下不显示的问题解决方法
2013/04/25 PHP
PHP中imagick函数的中文解释
2015/01/21 PHP
ZendFramework2连接数据库操作实例
2017/04/18 PHP
php实现微信支付之现金红包
2018/05/30 PHP
浅谈php的TS和NTS的区别
2019/03/13 PHP
JavaScript 数组的 uniq 方法
2008/01/23 Javascript
设置下载不需要倒计时cookie(倒计时代码)
2008/11/19 Javascript
Javascript 兼容firefox的一些问题
2009/05/21 Javascript
在IE和VB中支持png图片透明效果的实现方法(vb源码打包)
2011/04/01 Javascript
利用CSS、JavaScript及Ajax实现高效的图片预加载
2013/10/16 Javascript
15款jQuery分布引导插件分享
2015/02/04 Javascript
使用iojs的jsdom库实现同步系统时间
2015/04/20 Javascript
基于jquery实现轮播焦点图插件
2016/03/31 Javascript
require.js配合插件text.js实现最简单的单页应用程序
2016/07/12 Javascript
Angular.js中ng-if、ng-show和ng-hide的区别介绍
2017/01/20 Javascript
jQuery插件HighCharts绘制2D带Label的折线图效果示例【附demo源码下载】
2017/03/08 Javascript
从零开始学习Node.js系列教程六:EventEmitter发送和接收事件的方法示例
2017/04/13 Javascript
nodejs中安装ghost出错的原因及解决方法
2017/10/23 NodeJs
vue cli安装使用less的教程详解
2019/07/12 Javascript
vue路由分文件拆分管理详解
2020/08/13 Javascript
tensorflow入门:tfrecord 和tf.data.TFRecordDataset的使用
2020/01/20 Python
Python闭包与装饰器原理及实例解析
2020/04/30 Python
html5 canvas实现圆形时钟代码分享
2013/12/25 HTML / CSS
奥地利婴儿用品和玩具购物网站:baby-markt.at
2020/01/26 全球购物
房地产活动策划方案
2014/05/14 职场文书
体育口号大全
2014/06/18 职场文书
领导干部民主生活会自我剖析材料范文
2014/09/20 职场文书
2014年惩防体系建设工作总结
2014/12/01 职场文书
2014普法依法治理工作总结
2014/12/18 职场文书
中学生思想品德评语
2014/12/31 职场文书
校车安全管理责任书
2015/05/11 职场文书
2016年八一建军节活动总结
2016/04/05 职场文书
导游词之茶卡盐湖
2019/11/26 职场文书
MYSQL数据库使用UTF-8中文编码乱码的解决办法
2021/05/26 MySQL
基于Android10渲染Surface的创建过程
2022/08/14 Java/Android