前言
Object.defineProperty() 方法,是理解 vue2.0 数据劫持的关键,若是还不了解此方法,可以先去学习,然后在阅读本文或源码。去学习。
本文所写的数据劫持 demo,主要参考 vue2.0 源码。为简便易懂,仅抽离了核心功能并做出了一点小小的改变。希望本文对你阅读 vue2.0 源码的数据劫持部分有所帮助。最后,奉上案例源码,以便大家学习理解。
环境搭建
注意:确保已安装好 node 环境
-
新建一个文件夹: xxx
-
在当前文件夹,打开 PowerShell 或 DOS 窗口
-
执行
npm init进行初始化(一直按回车) -
使用 npm 安装所需依赖包
npm webpack webpack-cli webpack-dev-server html-webpack-plugin -D
- 打开 package.json 文件,修改 scripts,添加两行命令。其它基本属性,自行修改。
"scripts": {
"dev": "webpack-dev-server", // 运行 npm run dev
"build": "webpack" // 打包 npm run build
},
最终结果(package.json):
{
"name": "vue-demo",
"version": "1.0.0",
"description": "vue-数据劫持",
"main": "index.js",
"scripts": {
"dev": "webpack-dev-server",
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"html-webpack-plugin": "^4.4.1",
"webpack": "^4.44.1",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0"
}
}
- 新建 webpack.config.js,进行基本配置。
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js', // 入口文件
// output 指示 webpack 如何去输出、以及在哪里输出你的「bundle、asset 和
// 其他你所打包或使用 webpack 载入的任何内容」。
output: {
filename: 'js/bundle.js',
path: path.resolve(__dirname, 'dist')
},
devtool: 'source-map', // 生成 source map
resolve: {
// 如下配置 modules 后,你可以在入口文件中使用 import Vue from 'vue';
// 这种引入方式,就像在 vue 项目的 main.js 中使用一样
modules: [path.resolve(__dirname, ''), path.resolve(__dirname, 'node_modules')]
},
devServer: {
host: '127.0.0.1', // 本地服务
port: 8088 // 默认端口
},
plugins: [
// 该插件将为你生成一个 HTML5 文件, 在 body 中使用 script 标签引入
// 你所有 webpack 生成的 bundle。
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'public/index.html')
})
]
}
项目结构预览
- src/index.js 项目入口 js 文件
- src/vue 下的文件,是实现数据劫持的核心
- public/index.html 页面入口文件
代码解析
src/index.js 创建实例
通过 new 关键字,创建 Vue 实例。
import Vue from 'vue';
const vm = new Vue({
el: '#app',
data () {
return {
studentNum: 1,
subject: ['历史', '文化'],
bookInfo: {
name: '三国演义',
author: {
name: '罗贯中',
age: 18
}
},
studentList: [{ id: 1, name: '小明' }]
}
}
});
console.log('vm实例', vm);
vm.studentList.push({id: 2, name: '葡萄'});
console.log('几个人', vm.studentList);
vue/index.js 入口
定义 Vue 函数并导出,以便通过 new 关键字进行实例化。同时,执行 initMixin(Vue) 函数,将 _init 初始化放法,挂载到 Vue.prototype 上。
import { initMixin } from './init';
function Vue (options) {
// 通过关键字 new 创建 Vue实例时,便会调用 Vue 原型方法 _init 初始化数据
this._init(options);
}
// 执行 initMixin,会在 Vue.prototype(Vue原型)上挂载 _init 方法
initMixin(Vue);
export default Vue;
vue/init.js 初始化
调用 initState(vm) 函数,初始化数据。
import { initState } from './state';
function initMixin (Vue) {
Vue.prototype._init = function (options) {
const vm = this; // 存储 this( Vue实例 )
vm.$options = options; // 将 options 挂载到 vm 上,以便后续使用
// Vue 实例中的 data、 props、methods、computed 和 watch,都会在 initState 函数中
// 进行初始化。由于我们主要解说:Vue 数据劫持,所以只对 data 进行处理。
initState(vm);
}
}
export {
initMixin
}
vue/state.js 初始化 data
如若 data 存在,则调用 initData(vm) 函数初始化 data。
import proxy from './proxy';
import observe from './observe/index';
function initState (vm) {
const options = vm.$options;
if (options.data) {
initData(vm); // 初始化 data
}
}
function initData (vm) {
let data = vm.$options.data;
// Vue 中的 data 可以是函数(Vue 中建议将 data 作为一个函数来使用),
// 也可以是 Object --> {}
data = vm.$data = typeof data === 'function' ? data.call(vm) : data || {};
for (var key in data) {
// proxy 实现数据代理,vm.name --> vm.$data.name
proxy(vm, '$data', key);
}
// observe,对数据进行观测,以便在其发生改变时,做出反应。
observe(vm.$data);
}
export {
initState
}
vue/proxy.js 数据代理
proxy() 函数实现数据代理的关键在于,使用了 Object.defineProperty() 方法。
function proxy (vm, target, key) {
// Object.defineProperty() 方法会直接在一个对象上定义一个新属性,
// 或者修改一个对象的现有属性,并返回此对象。
// 将属性都挂载到 vm(Vue实例)上,并设置属性的 getter/setter,
// 以实现数据代理:vm.name --> vm.$data.name
Object.defineProperty(vm, key, {
get () {
return vm[target][key]; // vm[target][key] --> vm.$data.name
},
set (newValue) {
vm[target][key] = newValue;
}
});
}
export default proxy;
vue/observe/index.js 观察数据
执行 observe(val) 函数时,会先判断参数 val 是否为对象(数组也是对象)。若不是,则阻止运行。若是,则调用 new Observer(val) ,它会根据 val 是数组( [object Array] )还是对象( [object Object] )来调用不同的方法观察数据对象。
import observeArray from './observeArray';
import { defineReactive } from './reactive';
import { arrayMethods } from './array';
import { isObject } from '../shared/util';
function observe (val) {
// 检查 val 是否为对象(注意:在 js 中,数组也是对象,isObject并不排除数组)。
if (!isObject(val)) return;
return new Observer(val);
}
function Observer (val) {
if (Array.isArray(val)) {
// arrayMethods 中存储的是:重写的数组方法,例如:push、unshift 等。
// 重写为了在更改数组中的数据时,做出更多操作。比如,通过 push 方法向
// 数组中新添数据时, 需要对新的数据进行劫持,设置 getter/setter,否则
// 它们将不能在后续的修改中做出反应。实际上,重写的数组方法,其内部依旧使用
// 数组的原生方法来实现数据的增、删。
val.__proto__ = arrayMethods; // 使用 __proto__ 拦截原型链来增加目标对象
observeArray(val); // 观察数组(Array)的每一项
} else {
this.walk(val); // 观察对象(Object --> {})
}
}
// 遍历所有属性并将它们转换为 getter/setter。仅当值类型为 Object 时才应调用此方法
Observer.prototype.walk = function (data) {
// Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组,
// 数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。
const keys = Object.keys(data);
for (let i = 0; i < keys.length; i ++) {
const key = keys[i]; // 属性
const value = data[key]; // 属性值
// 在对象上定义一个反应性属性
defineReactive(data, key, value);
}
}
export default observe;
vue/shared/util.js
// 对象检测
export function isObject (obj) {
return obj !== null && typeof obj === 'object'
}
vue/observe/array.js 数组方法重写
重写数组方法(例如:push、unshift 等),并不是要替换它们,而是为了在更改数组中的数据时,做出更多操作。比如,通过 push 方法向数组中新添数据时,要对这些新的数据进行劫持,否则它们在后续的更改中无法做出反应。
import observeArray from './observeArray';
// 存储数组方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
const slice = Array.prototype.slice;
const arrayProto = Array.prototype; // 存储数组原型
const arrayMethods = Object.create(arrayProto); // 创建一个新的数组原型对象
methodsToPatch.forEach(function (method) {
const original = arrayProto[method]; // 缓存数组的原方法
arrayMethods[method] = function () {
let inserted; // 存储数组中新增的值,默认undefined
let args = slice.call(arguments); // 将 arguments 转成一个新的数组并返回
// 这里可以不要返回值,直接写:original.apply(this, args)
const result = original.apply(this, args); // 调用数组原生方法,对数组进行增、删。
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
// splice() 方法用于添加或删除数组中的元素
// 删除:splice(0, 1) --> args 即 [0, 1]
// 增加:splice(1, 0, '新增') --> args 即 [1, 0, '新增']
// slice() 方法可从已有的数组中返回选定的元素
// args.slice(2),固定下标值为 2,是因为 splice 的使用方式:
// splice,若是删除,则 args.slice(2) 返回空数组
// splice,若是新增,则 args.slice(2) 返回一个新数组,里面是所有新增的数据
inserted = args.slice(2);
break;
default:
break;
}
// inserted 为真(空数组 --> [],也是真),则调用 observeArray() 方法对其进行观察
inserted && observeArray(inserted);
return result;
}
});
export {
arrayMethods
}
vue/observe/observeArray.js 递归观察数组的每一项
import observe from './index';
function observeArray (arr) {
// 遍历数组 arr, 递归观察它的每一项
for (let i = 0; i < arr.length; i ++) {
observe(arr[i]);
}
}
export default observeArray;
vue/observe/reactive.js 劫持数据
import observe from "./index";
function defineReactive (data, key, value) {
// 递归观察value,它可能是一个对象
observe(value);
// Object.defineProperty() 方法会直接在一个对象上定义一个新属性,
// 或者修改一个对象的现有属性,并返回此对象。它是实现数据劫持的关键所在。
Object.defineProperty(data, key, {
get: function reactiveGetter () {
return value;
},
set: function reactiveSetter (newValue) {
if (newValue === value) return; // 同名属性,不需要重新赋值或观察
observe(value); // 递归观察value,它可能是一个对象
value = newValue;
}
});
}
export {
defineReactive
;
结束语
只阅读而不敲代码,这是不行的。记得敲一遍,边敲边调试,唯有如此,方能深刻理解。