我们的项目通过 Webpack 进行编译,相关依赖如下:
{"scripts": {"dev": "webpack-dev-server","build:": "webpack"},"devDependencies": {"html-webpack-plugin": "^4.5.2","webpack": "^4.46.0","webpack-cli": "^3.3.12","webpack-dev-server": "^3.11.3"}}
配置文件如下:
const path = require("path");const HtmlWebpackPlugin = require("html-webpack-plugin");module.exports = {entry: "./src/index.js",output: {filename: "bundle.js",path: path.resolve(__dirname, "dist")},devtool: "source-map",resolve: {// 表示解析模块引入的时候先从当前文件夹寻找模块,再去 node_modules 找模块modules: [path.resolve(__dirname, ""),path.resolve(__dirname, "node_modules")]},plugins: [new HtmlWebpackPlugin({template: path.resolve(__dirname, "public/index.html")})]};
public/index.html 文件内容如下:
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title></title></head><body><div id="app"></div></body></html>
全部文件目录结构如图:
好了,接下来我们就开始发车!
首先,我们需要编写我们的入口文件 index.js,该文件很普通主要就是实例一个模拟的 Vue 应用:
// 我们在 webpack.config.js 中进行了配置,所以这里优先在当前目录下寻找 vue 文件,也就是我们的 vue/index.js 文件import Vue from "vue";let vm = new Vue({el: "#app",data() {return {title: "学生列表",classNum: 1,teacher: ["张三", "李四"],info: {a: {b: 1}},students: [{id: 1,name: "小红"},{id: 2,name: "小明"}]};}});console.log(vm);
vue/index.js 文件主要是负责初始化内容:
import { initState } from "./init";function Vue(options) {this._init(options);}Vue.prototype._init = function (options) {// this 指向当前实例对象var vm = this;// 我们把 new Vue() 时候传递的数据统称为 options// 并且挂载到 Vue 的实例对象上vm.$options = options;// 调用 initState 初始化 data 数据initState(vm);};export default Vue;
vue/init.js 文件暴露出一个initState方法,该方法主要是处理初始化的数据:
import proxyData from "./proxy";import observer from "./observe"function initState(vm) {var options = vm.$options;// 如果 options 中存在 data 属性,我们才会继续处理if (options.data) {initData(vm);}}function initData(vm) {var data = vm.$options.data;// 把 data 数据单独保存到 Vue 的实例化对象上,方便我们获取// 如果 data 是一个函数,我们需要执行返回得到返回的对象data = vm._data = typeof data === "function" ? data.call(vm) : data || {};// 遍历 data 对象,通过 proxyData 对数据进行拦截for (const key in data) {// 传入的参数分别是:当前实例、key值(也就是 vm._data)、data 中的 key 值(例如 vm._data.title)proxyData(vm, "_data", key);}// 调用观察者模式observer(vm._data)}export {initState};
以上代码,我们通过proxyData对data中的数据进行拦截,详情如下:
function proxyData(vm, target, key) {// 当访问 vm.title 的时候转换为 vm._data.title//(请记住这句话!!!)Object.defineProperty(vm, key, {get: function () {return vm[target][key];},set: function (newVal) {vm[target][key] = newVal;}});}export default proxyData;
我们还调用了observer方法进行事件订阅,详细如下:
import Observer from "./observer"function observe(data) {// 判断只处理对象,如果不是对象直接返回if (typeof data !== "object" || data === null) {return false;}// 观察数据return new Observer(data)}export default observe;
接下来就是我们的核心文件vue/observer.js,该文件主要负责对数据类型进行判断,如果是数组就需要单独处理数组,这个我们后面再说:
import defineReactiveData from "./reactive";import { arrMethods } from "./array";import observeArr from "./observeArr";// 这个方法会在多个地方调用,请记住这个方法以它的作用function Observer(data) {// 如果 data 是一个数组,那面需要单独处理if (Array.isArray(data)) {// 给数组新增一层原型data._proto__ = arrMethods;// 循环数组的每一项,然后让每一项都调用 Observer 方法进行订阅observeArr(data)} else {// 处理对象this.walk(data);}}Observer.prototype.walk = function (data) {// 获取到 data 全部的 key// 也就是我们定义的 ['title', 'classNum', 'teacher', 'info', 'students']let keys = Object.keys(data);for (var i = 0; i < keys.length; i++) {let key = keys[i];let value = data[key];// 拦截 data 数据// 分别传入参数为:vm._data、data 中的 key、data 中 key 对应的 valuedefineReactiveData(data, key, value);}};export default Observer;
以上代码,我们分别对数组和对象执行不同的操作,我们先来看对象的操作:
在Observer构造函数中我们新增了一个walk方法,该方法获取到了所有的key值,然后调用了defineReactiveData进行处理。
import observe from "./observe";function defineReactiveData(data, key, value) {// 例如 info.a 还是个对象,那么就递归观察observe(value);// 这里的 data 是 vm._data,所以这里拦截的也是 vm._dataObject.defineProperty(data, key, {get() {console.log(`⤴️ 响应式获取:data.${key},`, value);return value;},set(newVal) {console.log(`🔁 响应式设置:data.${key},`, newVal);if (newVal === value) {return false;}// 如果新值还是对象,那么接着进行观察observe(newVal);value = newVal;}});}export default defineReactiveData;
以上代码,我们是对**vm._data**进行拦截的,这是因为我们前面说的**proxyData**拦截的是**vm**对象,当访问**vm.title**的时候,**proxyData**的拦截就会生效,而**proxyData**内部是通过**vm._data**来获取的,这样又会触发**defineReactiveData**的拦截!
回到vue/observer.js文件,我们还需要对数组进行处理:
import defineReactiveData from "./reactive";import { arrMethods } from "./array";import observeArr from "./observeArr";// 这个方法会在多个地方调用,请记住这个方法以它的作用function Observer(data) {// 如果 data 是一个数组,那面需要单独处理if (Array.isArray(data)) {// 为数组更改原型data._proto__ = arrMethods;// 循环数组的每一项,然后让每一项都调用 Observer 方法进行订阅observeArr(data)} else {// ...}}Observer.prototype.walk = function (data) {// ...};export default Observer;
以上代码我们对数组更改一个原型arrMethods,那看看它到底做了什么事情:
// ARR_METHODS 是一些可以更改数组本身的方法,里面包括以下内容,我们就不展开看了// ["push", "pop", "shift", "unshift", "splic", "sort", "reverse"]import { ARR_METHODS } from "./config";import observeArr from "./observeArr";// 把数组本身的元素进行拷贝var originArrayMethods = Array.prototype;// 创建一个空对象,该空对象的原型就是数组的原型var arrMethods = Object.create(originArrayMethods);// 遍历这些数组的方法名称ARR_METHODS.forEach(function (m) {// 在新对象上重写数组的方法arrMethods[m] = function () {// 把数组接到的参数转换为一个数组var args = Array.prototype.slice.call(arguments);// 执行数组原本的方法var rt = originArrayMethods[m].apply(this, args);var newArr;switch (m) {case "push":case "unshift":// 例如 arr.push({a: 1})// args 就是 [{a: 1}]newArr = args;break;case "splice":// 例如 arr.splice(1, 0, {a: 1}, {b: 2})// args 就是 [{a: 1}, {b: 2}]newArr = args.slice(2);break;default:break;}// 如果有值那面就调用 observeArr 方法// observeArr 方法就是循环数组的每一项,然后让每一项都调用 Observer 方法进行订阅newArr && observeArr(newArr);return rt;};});export { arrMethods };
以上代码我们重写了数组相关的方法,这是因为这些方法被并不能被Object.defineProperty拦截到。详细请看:
11、v-for 列表循环
所以我们通过重写方法的方式,让数组可以正常的执行方法,同时也能被我们的observeArr方法拦截到,所以数组现在就是这样多了一层我们写的原型,但最终它还是继承于Array构造函数的:
而我们的observeArr只是遍历了数组的每一项,让每一项都进行了拦截:
import observe from "./observe";function observeArr(arr) {for (let i = 0; i < arr.length; i++) {// 又回到了起点,进行更新订阅observe(arr[i]);}}export default observeArr;
然后我们去index.js文件获取属性,看看结果:
import Vue from "vue";let vm = new Vue({el: "#app",data() {return {title: "学生列表",classNum: 1,teacher: ["张三", "李四"],info: {a: {b: 1}},students: [{id: 1,name: "小红"},{id: 2,name: "小明"}]};}});console.log(vm);console.log(vm.title);console.log(vm.teacher);console.log(vm.info.a);

