手摸手從0實現簡版Vue --- (對象劫持)

1. 工欲善其事,必先利其器,首先搭建我們的開發環境

首先使用npm init -y 創建初始化的配置文件,然后下載一下我們后面需要的開發依賴:

npm i webpack webpack-cli webpack-dev-server html-webpack-plugin --save-dev

新建 webpack.config.js 用來編寫webpack配置, 新建src/index.js做為項目入口文件,然后新建public/index.html做為模板文件。webpack 基本配置內容如下:

const path = require('path');
const htmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
  entry: './src/index.js', // 以我們src 的index.js為入口進行打包
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  devtool: 'source-map',
  resolve: {
    // 默認讓import去我們的source文件夾去查找模塊,其次去node_modules查找
    modules: [path.resolve(__dirname, 'source'), path.resolve('node_modules')]
  },
  plugins: [
    new htmlWebpackPlugin({
      template: path.resolve(__dirname, 'public/index.html')
    })
  ]
}

使用npm scripts啟動我們的項目, 將 package.json里面的scripts修改為:

"scripts": {
  "start": "webpack-dev-server",
  "build": "webpack"
}

然后執行npm start可啟動我們的項目了。

2. 創建我們的基本框架

上面修改了我們的默認引入文件路徑,此時我們在source 文件夾下新建vue/index.js,然后在我們的src/index.js中引入

import Vue from 'vue'; // 會默認查找 source 目錄下的 vue 文件夾

此時在source/vue/index.jsjs就可以正常執行了。

下面繼續編寫我們的src/index.js,前面已經引入了我們自己編寫的Vue,后面我們模擬Vue的語法去編寫:

import Vue from 'vue'; // 會默認查找 source 目錄下的 vue 文件夾

let vm = new Vue({
  el: '#app', // 表示要渲染的元素是#app
  data() {
    return {
      msg: 'hello',
      school: {
        name: 'black',
        age: 18
      },
      arr: [1, 2, 3],
    }
  },
  computed: {
  },
  watch: {
  }
});

接著我們就要去編寫實現我們 Vue 代碼了, vue/index.js

import {initState} from './observe';

function Vue(options) { // Vue 中原始用戶傳入的數據
  this._init(options); // 初始化 Vue, 并且將用戶選項傳入
}

Vue.prototype._init = function(options) {
  // vue 中的初始化 this.$options 表示 Vue 中的參數
  let vm = this;
  vm.$options = options;

  // MVVM 原理, 需要數據重新初始化
  initState(vm);
}

export default Vue; // 首先默認導出一個Vue

首先我們需要去初始化Vue,將用戶傳入的數據進行初始化處理,新建observe/index.js去實現用戶傳入數據的初始化:

export function initState(vm) {
  let opts = vm.$options;
  if (opts.data) {
    initData(vm); // 初始化數據
  }
  if (opts.computed) {
    initComputed(); // 初始化計算屬性
  }
  if (opts.watch) {
    initWatch(); // 初始化 watch
  }
}

/**
 * 初始化數據
 * 將用戶傳入的數據 通過Object.defineProperty重新定義
 */
function initData(vm) {
}

/**
 * 初始化計算屬性
 */
function initComputed() {
}

/**
 * 初始化watch
 */
function initWatch() {
} 

3. 數據的初始化

首先我們去實現用戶傳入的data的初始化,也就是經常提到的使用Object.defineProperty進行數據劫持,重寫getter

setter,下面去實現 initData的具體內容:

/**
 * 將對vm上的取值、賦值操作代理到 vm._data 屬性上
 * 代理數據 實現 例如:vm.msg = vm._data.msg
 */
function proxy(vm, source, key) {
  Object.defineProperty(vm, key, {
    get() {
      return vm[source][key];
    },
    set(newValue) {
      vm[source][key] = newValue;
    }
  })
}

/**
 * 初始化數據
 * 將用戶傳入的數據 通過Object.defineProperty重新定義
 */
function initData(vm) {
  let data = vm.$options.data; // 用戶傳入的data
  data = vm._data = typeof data === 'function' ? data.call(vm) : data || {};

  for(let key in data) {
    proxy(vm, '_data', key); // 將對vm上的取值、賦值操作代理到 vm._data 屬性上,便于我們直接使用vm取值
  }

  observe(vm._data); // 觀察數據
}

export function observe(data) {
  if(typeof data !== 'object' || data == null) {
    return; // 不是對象或為null 不執行后續邏輯
  }
  return new Observer(data);
}

4. 數據劫持核心方法實現

我們去該目錄下新建observer.js去實現我們的Observer

import {observe} from './index';

/**
 * 定義響應式的數據變化
 * @param {Object} data  用戶傳入的data
 * @param {string} key data的key
 * @param {*} value data對應key的value
 */
export function defineReactive(data, key, value) {
  observe(value);   // 如果value依舊是一個對象,需要深度劫持
  Object.defineProperty(data, key, {
    get() {
      console.log('獲取數據');
      return value;
    },
    set(newValue) {
      console.log('設置數據');
      if (newValue === value) return;
      value = newValue;
    }
  });
}

class Observer {
  constructor(data) { // data === vm._data
    // 將用戶的數據使用 Object.defineProperty重新定義
    this.walk(data);
  }

  /**
   * 循環數據遍歷
   */
  walk(data) {
    let keys = Object.keys(data);
    for(let i = 0; i < keys.length; i++) {
      let key = keys[i];
      let value = data[key];
      defineReactive(data, key, value);
    }
  }
}

export default Observer; 

5. 結果

到這我們就初步實現了對用戶傳入data的數據劫持,看一下效果。

此時如果我們在src/index.js去取和修改data中的值:

console.log(vm.msg);
console.log(vm.msg = 'world');
image-20200307144823192

到現在為止我們就實現了第一部分,對用戶數據傳入的data進行了數據劫持,但是如果我們使用vm.arr.push(123),會發現只有獲取數據而沒有設置數據,這也就是Object.defineProperty的弊端,沒有實現對數組的劫持,下一部分去實現一下數組的數據劫持。

代碼部分可看本次提交commit

希望各位大佬點個star,小弟跪謝~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容