手写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实现动态加载表单
Dec 13 Vue.js
Vue SPA 首屏优化方案
Feb 26 Vue.js
vue实现拖拽进度条
Mar 01 Vue.js
Vue.js 带下拉选项的输入框(Textbox with Dropdown)组件
Apr 17 Vue.js
Vue如何实现组件间通信
May 15 Vue.js
vue基于Teleport实现Modal组件
May 31 Vue.js
Vue Element-ui表单校验规则实现
Jul 09 Vue.js
Vue3.0 手写放大镜效果
Jul 25 Vue.js
一篇文章告诉你如何实现Vue前端分页和后端分页
Feb 18 Vue.js
vue实现可以快进后退的跑马灯组件
Apr 08 Vue.js
使用vuex-persistedstate本地存储vuex
Apr 29 Vue.js
vue el-table实现递归嵌套的示例代码
Aug 14 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
2021年最新CPU天梯图
2021/03/04 数码科技
PHP+Ajax异步通讯实现用户名邮箱验证是否已注册( 2种方法实现)
2011/12/28 PHP
thinkPHP多表查询及分页功能实现方法示例
2017/07/03 PHP
XRegExp 0.2: Now With Named Capture
2007/11/30 Javascript
jquery下动态显示jqGrid以及jqGrid的属性设置容易出现问题的解决方法
2010/10/22 Javascript
jquery打开直接跳到网页最下面、最低端实现代码
2013/04/22 Javascript
深入理解Javascript动态方法调用与参数修改的问题
2013/12/10 Javascript
动态载入js提高网页打开速度的方法
2014/07/04 Javascript
jQuery动画出现连续触发、滞后反复执行的解决方法
2015/01/28 Javascript
JS中script标签defer和async属性的区别详解
2016/08/12 Javascript
js初始化验证实例详解
2016/11/26 Javascript
使用Javascript判断浏览器终端设备(PC、IOS(iphone)、Android)
2017/01/04 Javascript
详解webpack和webpack-simple中如何引入css文件
2017/06/28 Javascript
Angular实现表单验证功能
2017/11/13 Javascript
微信小程序wx.getImageInfo()如何获取图片信息
2018/01/26 Javascript
vue通过指令(directives)实现点击空白处收起下拉框
2018/12/06 Javascript
jQuery zTree插件使用简单教程
2019/08/16 jQuery
vue 使用element-ui中的Notification自定义按钮并实现关闭功能及如何处理多个通知
2019/08/17 Javascript
微信小程序 checkbox使用实例解析
2019/09/09 Javascript
解决VUE项目使用Element-ui 下拉组件的验证失效问题
2020/11/07 Javascript
jquery实现拖拽添加元素功能
2020/12/01 jQuery
打开电脑上的QQ的python代码
2013/02/10 Python
Python中如何获取类属性的列表
2016/12/26 Python
TensorFlow实现AutoEncoder自编码器
2018/03/09 Python
matlab中实现矩阵删除一行或一列的方法
2018/04/04 Python
Python爬虫之正则表达式基本用法实例分析
2018/08/08 Python
opencv3/python 鼠标响应操作详解
2019/12/11 Python
如何使用python的ctypes调用医保中心的dll动态库下载医保中心的账单
2020/05/24 Python
Python实现验证码识别
2020/06/15 Python
使用Keras构造简单的CNN网络实例
2020/06/29 Python
纽约服装和生活方式品牌:Saturdays NYC
2017/08/13 全球购物
世界经理人咨询有限公司面试
2014/09/23 面试题
制药工程专业职业生涯规划范文
2014/03/10 职场文书
大学生村官考核材料
2014/05/23 职场文书
2014年教师节活动总结
2014/08/29 职场文书
《水浒传》读后感3篇(范文)
2019/09/19 职场文书