實現一個簡單版本的Vue及原始碼解析(一)

San_Alex發表於2018-09-14

專案搭建

Vue概述

Vue.js是一套構建使用者介面的漸進式框架。與其他重量級框架不同的是,Vue 採用自底向上增量開發的設計。Vue 的核心庫只關注檢視層,它不僅易於上手,還便於與第三方庫或既有專案整合。另一方面,當與單檔案元件和 Vue 生態系統支援的庫結合使用時,Vue 也完全能夠為複雜的單頁應用程式提供驅動。 由於上述優點,Vue的使用在業界越來越廣泛,截至本文寫作時Vue在github上的star已經高達113322。

Vue的中介軟體

生命週期

一個Vue例項具有許多階段,例如observing data、initializing events、compiling template 及render,在這些階段我們可以註冊相應的鉤子函式。

  /*初始化生命週期*/
    initLifecycle(vm)
    /*初始化事件*/
    initEvents(vm)
    /*初始化render*/
    initRender(vm)
    /*呼叫beforeCreate鉤子函式並且觸發beforeCreate鉤子事件*/
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    /*初始化props、methods、data、computed與watch*/
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    /*呼叫created鉤子函式並且觸發created鉤子事件*/
    callHook(vm, 'created')
複製程式碼

響應式

之所以稱為響應式,是因為Vue.js實現了資料和檢視的雙向繫結,即資料發生變化後檢視也隨之變化。 其中Vue實現響應式的核心函式是Object.defineProperty,而且因為Object.defineProperty最低支援IE8,所以導致Vue只相容到IE8。Vue使用Object.defineProperty來實現響應式的具體原理會在接下來的內容中進行詳細講解。

虛擬DOM

虛擬DOM是真實DOM的抽象,Vue的主要操作先是在虛擬DOM上進行的,最後再根據不同的平臺來對映到真實DOM上,其中的核心是diff演算法.

部署開發環境

本專案使用npm作為包管理器,因此構建myVue專案的第一件事,便是在空資料夾下使用npm init命令初始化專案。 專案初始化之後,在正式編寫專案前,我們需還要下載和配置第三方工具,其中包括模組構建工具和測試工具。

設定Rollup

在該專案中我們使用Rollup作為打包工具。Rollup作為一款javascript打包工具,其支援ES2015語法,同時它也是Vue的打包工具。

使用Rollup前,首先是使用npm install rollup rollup-watch --save-dev命令安裝Rollup相關包,安裝完成後在根目錄下建立rollup.config.js檔案並進行一些基礎配置。關於Rollup的進一步使用,請自行前往官網。

export default {
  input: 'src/instance/index.js',
  output: {
    name: 'Vue',
    file: 'dist/vue.js',
    format: 'iife'
  },
};
複製程式碼

配置測試工具Karma和jasmine

本專案採用karma作為自動化測試工具,jasmine作為單元測試工具,同樣要想使用這兩個工具,使用前需下載相關包並進行配置。

npm install karma jasmine karma-jasmine karma-chrome-launcher buble --save-dev
npm install karma-rollup-plugin karma-rollup-preprocessor rollup-plugin-buble --save-dev
複製程式碼

下載相關包後在根目錄下建立karma.conf.js並配置。

module.exports = function(config) {
  config.set({
    files: [{ pattern: 'test/**/*.spec.js', watched: false }],
    frameworks: ['jasmine'],
    browsers: ['Chrome'],
    preprocessors: {
      './test/**/*.js': ['rollup']
    },
    rollupPreprocessor: {
      plugins: [
        require('rollup-plugin-buble')(),
      ],
      output: {
        format: 'iife',
        name: 'Vue',
        sourcemap: 'inline'
      }
    }
  })
}
複製程式碼

專案結構

專案搭建好的結構如下。

- package.json
- rollup.config.js
- node_modules
- dist
- test
- src
	- observer
	- instance
	- util
	- vdom
複製程式碼

配置啟動命令

專案搭建好後,要想啟動專案,我們可以在npm的配置檔案中進行配置。 package.json

"scripts": {
   "build": "rollup -c",
   "watch": "rollup -c -w",
   "test": "karma start"
}
複製程式碼

起步

要想編寫出高質量的程式碼,正式程式設計前我們最好養成編寫程式碼前先編寫測試用例進行單元測試的好習慣,下面我們就開始編寫測試用例吧。

test/options/options.spec.js

import Vue from "../src/instance/index";

describe('Proxy test', function() {
  it('should proxy vm._data.a = vm.a', function() {
  	var vm = new Vue({
  		data:{
  			a:2
  		}
  	})
    expect(vm.a).toEqual(2);
  });
});
複製程式碼

編寫測試用例後,我們的準備工作就全部完成了,我們就可以開始編寫真正的程式碼了,此時是不是感到很激動。(畢竟特煩配置什麼的了,特別是javaweb專案,想到當年被javaweb三大框架配置支配的恐懼就不禁內牛滿面啊...)

要想實現Vue,自然是先編寫Vue的建構函式,然後再一步一步的實現其他功能。

src/instance/index.js

import { initMixin } from './init'

function Vue (options) {
  this._init(options)
}

initMixin(Vue)

export default Vue
複製程式碼

上面只是Vue的構造器呼叫了this._init方法來初始化一個Vue的例項,而initMixin 的作用便是給Vue定義原型方法,具體程式碼如下。

src/instance/init.js

import { initState } from './state'

export function initMixin (Vue) {
  Vue.prototype._init = function (options) {
  	var vm = this
  	vm.$options = options
  	initState(vm)
  }
}
複製程式碼

initMixin方法只是在Vue的原型上定義了_init方法,而_init方法則是呼叫了initState方法來初始化data。

src/instance/state.js

export function initState(vm) {
  initData(vm)
}

function initData(vm) {
  var data = vm.$options.data
  vm._data = data
  // proxy data on instance
  var keys = Object.keys(data)

  var i = keys.length
  while (i--) {
    proxy(vm, keys[i])
  }
}

function proxy(vm, key) {
    Object.defineProperty(vm, key, {
      configurable: true,
      enumerable: true,
      get: function proxyGetter() {
        return vm._data[key]
      },
      set: function proxySetter(val) {
        vm._data[key] = val
      }
    })
}
複製程式碼

如上initState方法只是呼叫了initData方法,而initData方法則是初始化了data並使用proxy方法將vm._data.a代理成vm.a

proxyvm上使用同樣的key定義了一個屬性,並通過 get/set 從vm._data上存取data。

最後使用npm run test命令啟動專案。

相關文章