我们的项目通过 Webpack 进行编译,相关依赖如下:

    1. {
    2. "scripts": {
    3. "dev": "webpack-dev-server",
    4. "build:": "webpack"
    5. },
    6. "devDependencies": {
    7. "html-webpack-plugin": "^4.5.2",
    8. "webpack": "^4.46.0",
    9. "webpack-cli": "^3.3.12",
    10. "webpack-dev-server": "^3.11.3"
    11. }
    12. }

    配置文件如下:

    1. const path = require("path");
    2. const HtmlWebpackPlugin = require("html-webpack-plugin");
    3. module.exports = {
    4. entry: "./src/index.js",
    5. output: {
    6. filename: "bundle.js",
    7. path: path.resolve(__dirname, "dist")
    8. },
    9. devtool: "source-map",
    10. resolve: {
    11. // 表示解析模块引入的时候先从当前文件夹寻找模块,再去 node_modules 找模块
    12. modules: [
    13. path.resolve(__dirname, ""),
    14. path.resolve(__dirname, "node_modules")
    15. ]
    16. },
    17. plugins: [
    18. new HtmlWebpackPlugin({
    19. template: path.resolve(__dirname, "public/index.html")
    20. })
    21. ]
    22. };

    public/index.html 文件内容如下:

    1. <!DOCTYPE html>
    2. <html lang="en">
    3. <head>
    4. <meta charset="UTF-8" />
    5. <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    6. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    7. <title></title>
    8. </head>
    9. <body>
    10. <div id="app"></div>
    11. </body>
    12. </html>

    全部文件目录结构如图:
    image.png

    好了,接下来我们就开始发车!
    首先,我们需要编写我们的入口文件 index.js,该文件很普通主要就是实例一个模拟的 Vue 应用:

    1. // 我们在 webpack.config.js 中进行了配置,所以这里优先在当前目录下寻找 vue 文件,也就是我们的 vue/index.js 文件
    2. import Vue from "vue";
    3. let vm = new Vue({
    4. el: "#app",
    5. data() {
    6. return {
    7. title: "学生列表",
    8. classNum: 1,
    9. teacher: ["张三", "李四"],
    10. info: {
    11. a: {
    12. b: 1
    13. }
    14. },
    15. students: [
    16. {
    17. id: 1,
    18. name: "小红"
    19. },
    20. {
    21. id: 2,
    22. name: "小明"
    23. }
    24. ]
    25. };
    26. }
    27. });
    28. console.log(vm);

    vue/index.js 文件主要是负责初始化内容:

    1. import { initState } from "./init";
    2. function Vue(options) {
    3. this._init(options);
    4. }
    5. Vue.prototype._init = function (options) {
    6. // this 指向当前实例对象
    7. var vm = this;
    8. // 我们把 new Vue() 时候传递的数据统称为 options
    9. // 并且挂载到 Vue 的实例对象上
    10. vm.$options = options;
    11. // 调用 initState 初始化 data 数据
    12. initState(vm);
    13. };
    14. export default Vue;

    vue/init.js 文件暴露出一个initState方法,该方法主要是处理初始化的数据:

    1. import proxyData from "./proxy";
    2. import observer from "./observe"
    3. function initState(vm) {
    4. var options = vm.$options;
    5. // 如果 options 中存在 data 属性,我们才会继续处理
    6. if (options.data) {
    7. initData(vm);
    8. }
    9. }
    10. function initData(vm) {
    11. var data = vm.$options.data;
    12. // 把 data 数据单独保存到 Vue 的实例化对象上,方便我们获取
    13. // 如果 data 是一个函数,我们需要执行返回得到返回的对象
    14. data = vm._data = typeof data === "function" ? data.call(vm) : data || {};
    15. // 遍历 data 对象,通过 proxyData 对数据进行拦截
    16. for (const key in data) {
    17. // 传入的参数分别是:当前实例、key值(也就是 vm._data)、data 中的 key 值(例如 vm._data.title)
    18. proxyData(vm, "_data", key);
    19. }
    20. // 调用观察者模式
    21. observer(vm._data)
    22. }
    23. export {
    24. initState
    25. };

    以上代码,我们通过proxyDatadata中的数据进行拦截,详情如下:

    1. function proxyData(vm, target, key) {
    2. // 当访问 vm.title 的时候转换为 vm._data.title
    3. //(请记住这句话!!!)
    4. Object.defineProperty(vm, key, {
    5. get: function () {
    6. return vm[target][key];
    7. },
    8. set: function (newVal) {
    9. vm[target][key] = newVal;
    10. }
    11. });
    12. }
    13. export default proxyData;

    我们还调用了observer方法进行事件订阅,详细如下:

    1. import Observer from "./observer"
    2. function observe(data) {
    3. // 判断只处理对象,如果不是对象直接返回
    4. if (typeof data !== "object" || data === null) {
    5. return false;
    6. }
    7. // 观察数据
    8. return new Observer(data)
    9. }
    10. export default observe;

    接下来就是我们的核心文件vue/observer.js,该文件主要负责对数据类型进行判断,如果是数组就需要单独处理数组,这个我们后面再说:

    1. import defineReactiveData from "./reactive";
    2. import { arrMethods } from "./array";
    3. import observeArr from "./observeArr";
    4. // 这个方法会在多个地方调用,请记住这个方法以它的作用
    5. function Observer(data) {
    6. // 如果 data 是一个数组,那面需要单独处理
    7. if (Array.isArray(data)) {
    8. // 给数组新增一层原型
    9. data._proto__ = arrMethods;
    10. // 循环数组的每一项,然后让每一项都调用 Observer 方法进行订阅
    11. observeArr(data)
    12. } else {
    13. // 处理对象
    14. this.walk(data);
    15. }
    16. }
    17. Observer.prototype.walk = function (data) {
    18. // 获取到 data 全部的 key
    19. // 也就是我们定义的 ['title', 'classNum', 'teacher', 'info', 'students']
    20. let keys = Object.keys(data);
    21. for (var i = 0; i < keys.length; i++) {
    22. let key = keys[i];
    23. let value = data[key];
    24. // 拦截 data 数据
    25. // 分别传入参数为:vm._data、data 中的 key、data 中 key 对应的 value
    26. defineReactiveData(data, key, value);
    27. }
    28. };
    29. export default Observer;

    以上代码,我们分别对数组和对象执行不同的操作,我们先来看对象的操作:
    Observer构造函数中我们新增了一个walk方法,该方法获取到了所有的key值,然后调用了defineReactiveData进行处理。

    1. import observe from "./observe";
    2. function defineReactiveData(data, key, value) {
    3. // 例如 info.a 还是个对象,那么就递归观察
    4. observe(value);
    5. // 这里的 data 是 vm._data,所以这里拦截的也是 vm._data
    6. Object.defineProperty(data, key, {
    7. get() {
    8. console.log(`⤴️ 响应式获取:data.${key},`, value);
    9. return value;
    10. },
    11. set(newVal) {
    12. console.log(`🔁 响应式设置:data.${key},`, newVal);
    13. if (newVal === value) {
    14. return false;
    15. }
    16. // 如果新值还是对象,那么接着进行观察
    17. observe(newVal);
    18. value = newVal;
    19. }
    20. });
    21. }
    22. export default defineReactiveData;

    以上代码,我们是对**vm._data**进行拦截的,这是因为我们前面说的**proxyData**拦截的是**vm**对象,当访问**vm.title**的时候,**proxyData**的拦截就会生效,而**proxyData**内部是通过**vm._data**来获取的,这样又会触发**defineReactiveData**的拦截!

    回到vue/observer.js文件,我们还需要对数组进行处理:

    1. import defineReactiveData from "./reactive";
    2. import { arrMethods } from "./array";
    3. import observeArr from "./observeArr";
    4. // 这个方法会在多个地方调用,请记住这个方法以它的作用
    5. function Observer(data) {
    6. // 如果 data 是一个数组,那面需要单独处理
    7. if (Array.isArray(data)) {
    8. // 为数组更改原型
    9. data._proto__ = arrMethods;
    10. // 循环数组的每一项,然后让每一项都调用 Observer 方法进行订阅
    11. observeArr(data)
    12. } else {
    13. // ...
    14. }
    15. }
    16. Observer.prototype.walk = function (data) {
    17. // ...
    18. };
    19. export default Observer;

    以上代码我们对数组更改一个原型arrMethods,那看看它到底做了什么事情:

    1. // ARR_METHODS 是一些可以更改数组本身的方法,里面包括以下内容,我们就不展开看了
    2. // ["push", "pop", "shift", "unshift", "splic", "sort", "reverse"]
    3. import { ARR_METHODS } from "./config";
    4. import observeArr from "./observeArr";
    5. // 把数组本身的元素进行拷贝
    6. var originArrayMethods = Array.prototype;
    7. // 创建一个空对象,该空对象的原型就是数组的原型
    8. var arrMethods = Object.create(originArrayMethods);
    9. // 遍历这些数组的方法名称
    10. ARR_METHODS.forEach(function (m) {
    11. // 在新对象上重写数组的方法
    12. arrMethods[m] = function () {
    13. // 把数组接到的参数转换为一个数组
    14. var args = Array.prototype.slice.call(arguments);
    15. // 执行数组原本的方法
    16. var rt = originArrayMethods[m].apply(this, args);
    17. var newArr;
    18. switch (m) {
    19. case "push":
    20. case "unshift":
    21. // 例如 arr.push({a: 1})
    22. // args 就是 [{a: 1}]
    23. newArr = args;
    24. break;
    25. case "splice":
    26. // 例如 arr.splice(1, 0, {a: 1}, {b: 2})
    27. // args 就是 [{a: 1}, {b: 2}]
    28. newArr = args.slice(2);
    29. break;
    30. default:
    31. break;
    32. }
    33. // 如果有值那面就调用 observeArr 方法
    34. // observeArr 方法就是循环数组的每一项,然后让每一项都调用 Observer 方法进行订阅
    35. newArr && observeArr(newArr);
    36. return rt;
    37. };
    38. });
    39. export { arrMethods };

    以上代码我们重写了数组相关的方法,这是因为这些方法被并不能被Object.defineProperty拦截到。详细请看:
    11、v-for 列表循环
    所以我们通过重写方法的方式,让数组可以正常的执行方法,同时也能被我们的observeArr方法拦截到,所以数组现在就是这样多了一层我们写的原型,但最终它还是继承于Array构造函数的:
    image.png

    而我们的observeArr只是遍历了数组的每一项,让每一项都进行了拦截:

    1. import observe from "./observe";
    2. function observeArr(arr) {
    3. for (let i = 0; i < arr.length; i++) {
    4. // 又回到了起点,进行更新订阅
    5. observe(arr[i]);
    6. }
    7. }
    8. export default observeArr;

    然后我们去index.js文件获取属性,看看结果:

    1. import Vue from "vue";
    2. let vm = new Vue({
    3. el: "#app",
    4. data() {
    5. return {
    6. title: "学生列表",
    7. classNum: 1,
    8. teacher: ["张三", "李四"],
    9. info: {
    10. a: {
    11. b: 1
    12. }
    13. },
    14. students: [
    15. {
    16. id: 1,
    17. name: "小红"
    18. },
    19. {
    20. id: 2,
    21. name: "小明"
    22. }
    23. ]
    24. };
    25. }
    26. });
    27. console.log(vm);
    28. console.log(vm.title);
    29. console.log(vm.teacher);
    30. console.log(vm.info.a);

    image.png

    最后,源码地址献上:
    https://github.com/xiechen1201/JSPlusPlus/blob/main/%E8%85%BE%E8%AE%AF%E8%AF%BE%E5%A0%82/Vue%E6%9C%AC%E5%B0%8A04/06/src/index.js