VueJS原始碼學習——專案結構&目錄

VueJS原始碼學習——專案結構&目錄
1 Star2 Stars3 Stars4 Stars5 Stars 給文章打分!
Loading...

本專案的原始碼學習筆記是基於 Vue 1.0.9 版本的也就是最早的 tag 版本,之所以選擇這個版本,是因為這個是最原始沒有太多功能拓展的版本,有利於更好的看到 Vue 最開始的骨架和脈絡以及作者的最初思路。而且能和後續的 1.x.x 版本做對比,發現了作者為了修復 bug 而做出的很多有趣的改進甚至回退,如 vue nextTick 的版本迭代經歷了更新、回退和再次更新

原文地址
專案地址

囧克斯的一篇原始碼解析文章

Vue.js 是一個典型的 MVVM 框架,整個程式從最上層分為

1 全域性設計:包括全域性介面、預設選項

2 vm 例項設計: 包括介面設計(vm 原型)、例項初始化過程設計(vm建構函式)

建構函式核心的工作內容:

整個例項初始化過程,關鍵在於將 資料(Model) 和 檢視(view)建立起關聯關係:

通過 observer 對 data 進行了監聽,並且提供訂閱某個資料項的變化的能力

把 template 解析成一段 document fragment,然後解析其中的 directive,得到每一個 directive 所依賴的資料項及其更新方法。比如 v-text=”message” 被解析之後 (這裡僅作示意,實際程式邏輯會更嚴謹而複雜):

所依賴的資料項 this.$data.message,以及

相應的檢視更新方法 node.textContent = this.$data.message

通過 watcher 把上述兩部分結合起來,即把 directive 中的資料依賴訂閱在對應資料的 observer 上,這樣當資料變化的時候,就會觸發 observer,進而觸發相關依賴對應的檢視更新方法,最後達到模板原本的關聯效果。

所以整個 vm 的核心,就是如何實現 observer, directive (parser), watcher 這三樣東西

以上摘自囧克斯部落格的一篇文章

從 v1.0.9 版本開始

這個時候的專案結構如下:

原始碼在 src 裡面,build 為打包編譯的程式碼,dist 為打包後程式碼放置的位置, test 為測試程式碼目錄。

從 package.json 裡可以瞭解到專案用到的依賴包以及專案的開發和執行方式,其中編譯程式碼是:

    "build": "node build/build.js",

於是我們到對應的這個檔案裡:

var fs = require('fs')
var zlib = require('zlib')
var rollup = require('rollup')
var uglify = require('uglify-js')
var babel = require('rollup-plugin-babel')
var replace = require('rollup-plugin-replace')
var version = process.env.VERSION || require('../package.json').version
var banner =
'/*!\n'  
' * Vue.js v'   version   '\n'  
' * (c) '   new Date().getFullYear()   ' Evan You\n'  
' * Released under the MIT License.\n'  
' */'
// CommonJS build.
// this is used as the "main" field in package.json
// and used by bundlers like Webpack and Browserify.
rollup.rollup({
entry: 'src/index.js',
plugins: [
babel({
loose: 'all'
})
]
})
...

可以知道這個時候用的是 rollup 來進行打包編譯的, 入口檔案是 __src/index.js__,index.js 的程式碼很簡潔:

import Vue from './instance/vue'
import directives from './directives/public/index'
import elementDirectives from './directives/element/index'
import filters from './filters/index'
import { inBrowser } from './util/index'
Vue.version = '1.0.8'
/**
* Vue and every constructor that extends Vue has an
* associated options object, which can be accessed during
* compilation steps as `this.constructor.options`.
*
* These can be seen as the default options of every
* Vue instance.
*/
Vue.options = {
directives,
elementDirectives,
filters,
transitions: {},
components: {},
partials: {},
replace: true
}
export default Vue
// devtools global hook
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production') {
if (inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__) {
window.__VUE_DEVTOOLS_GLOBAL_HOOK__.emit('init', Vue)
}
}

從這裡可以知道例項 vue 的實現在 src/instance/vue 中, 還涉及了 directives 應該是用於指令解析的方法和 filter 過濾器,這個在 2.0 已經不存在但在 1.0 使用比較頻繁的功能, 同時 inBrowser 應該是用來判斷是否是瀏覽器環境,說明 src/util 是一個工具類的目錄,這裡一個個驗證

工具類方法 inBrowser

首先看 inBrowser__, 發現 __util/index.js 也只是一個工具函式入口檔案:

export * from './lang'
export * from './env'
export * from './dom'
export * from './options'
export * from './component'
export * from './debug'
export { defineReactive } from '../observer/index'

從字面可以知道涉及到的工具類有 語言、環境?、dom操作、options?、元件化、開發類、實時定義? 這些型別的工具, 而 inBrowser 應該屬於 env 或者 dom,在 util/env 中找到了其實現:

...
// Browser environment sniffing
export const inBrowser =
typeof window !== 'undefined' &&
Object.prototype.toString.call(window) !== '[object Object]'
...

這裡利用瀏覽器的全域性物件 window 做區分,因為在 nodejs 環境下是沒有 window 這個全域性物件的,所以判斷 typeof window 是否不為 ‘undefined’ 且不是由使用者自己建立的一個普通物件,如果是的話, Object.prototype.toString.call(window) // === [object Object]

Vue 例項建構函式實現

再來看 __src/instance/vue__, 應該是實現了vue的例項初始化函式,從程式碼可以知道是一個例項的建構函式,也是頂層實現,底層程式碼位於子目錄的 api 和 internal,分別實現了公用的方法和私有的方法變數等

import initMixin from './internal/init'
import stateMixin from './internal/state'
import eventsMixin from './internal/events'
import lifecycleMixin from './internal/lifecycle'
import miscMixin from './internal/misc'
import globalAPI from './api/global'
import dataAPI from './api/data'
import domAPI from './api/dom'
import eventsAPI from './api/events'
import lifecycleAPI from './api/lifecycle'
/**
* The exposed Vue constructor.
*
* API conventions:
* - public API methods/properties are prefixed with `$`
* - internal methods/properties are prefixed with `_`
* - non-prefixed properties are assumed to be proxied user
*   data.
*
* @constructor
* @param {Object} [options]
* @public
*/
function Vue (options) {
this._init(options)
}
// install internals
initMixin(Vue)
...
// install APIs
globalAPI(Vue)
...
export default Vue

目錄如下:

從註釋可以知道,尤大用字首 $ 標記公用方法和變數,用 _標記私有的方法和變數,沒有字首的變數可能用來代理使用者資料

從引入的檔案可以知道私有方法和變數分別有 lifecycleMixin 生命週期、eventsMixin 事件機制、stateMixin 狀態、miscMixin 過濾器, 以及例項的共有方法API: 全域性 globalAPI 、資料繫結 dataAPI、DOM操作 domAPI、事件操作 eventsAPI、生命週期 lifecycleAPI

通過 initMixin(Vue)

具體如何實現都在 apiinternal 這兩個資料夾裡面,所以 src/instance 是 vue 例項建構函式的實現

directives、 filter 和 elementDirectives

// src/index.js
import Vue from './instance/vue'
import directives from './directives/public/index'
import elementDirectives from './directives/element/index'
import filters from './filters/index'
import { inBrowser } from './util/index'
Vue.options = {
directives,
elementDirectives,
filters,
transitions: {},
components: {},
partials: {},
replace: true
}
export default Vue
// devtools global hook
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production') {
if (inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__) {
window.__VUE_DEVTOOLS_GLOBAL_HOOK__.emit('init', Vue)
}
}

index.js 裡剩下這三個都是作為 Vue.options 裡的變數存在的,前面知道了 Vue 的建構函式實現,知道了利用 工具類 inBrowser 來判斷是否處於瀏覽器,在判斷window是否存在 __VUE_DEVTOOLS_GLOBAL_HOOK__ 這個變數, 如果存在,那麼代表瀏覽器安裝了 vue 的除錯外掛,那麼還會呼叫這個變數的方法 init 告訴外掛已經初始化好了 vue 物件。

1.0 官網文件 custom-directive 中可以知道 directive 是讓開發者開發自己的指令,具體例子如下

element-directive 和 directive 類似,只是形式上是作為一個元素存在,無法傳輸給元素資料,但是可以操作元素的屬性

這是一個強大的功能,讓開發者決定資料改變時以怎樣的形式渲染到檢視裡,強大的功能程式碼量也不少,光 directives

可以看到 directive 包含了 文字操作、邏輯操作(迴圈、條件)、雙向繫結(這個是比較有趣且重要的額部分)、事件繫結、資料繫結、dom繫結還有一個cloak用於未渲染完成的樣式情況

two-way binding 即 vue 中的 v-model 屬性,是對錶單輸入型別的元素如 textarea、select 以及不同 type 的 input 元素做雙向繫結,其餘型別的元素則不支援這種繫結

// src/directives/public/index.js
import { warn, resolveAsset } from '../../../util/index'
import text from './text'
import radio from './radio'
import select from './select'
import checkbox from './checkbox'
const handlers = {
text,
radio,
select,
checkbox
}
export default {
priority: 800,
twoWay: true,
handlers: handlers,
params: ['lazy', 'number', 'debounce'],
/**
* Possible elements:
*   <select>
*   <textarea>
*   <input type="*">
*     - text
*     - checkbox
*     - radio
*     - number
*/
bind () {
// friendly warning...
this.checkFilters()
if (this.hasRead && !this.hasWrite) {
process.env.NODE_ENV !== 'production' && warn(
'It seems you are using a read-only filter with '  
'v-model. You might want to use a two-way filter '  
'to ensure correct behavior.'
)
}
var el = this.el
var tag = el.tagName
var handler
if (tag === 'INPUT') {
handler = handlers[el.type] || handlers.text
} else if (tag === 'SELECT') {
handler = handlers.select
} else if (tag === 'TEXTAREA') {
handler = handlers.text
} else {
process.env.NODE_ENV !== 'production' && warn(
'v-model does not support element type: '   tag
)
return
}
el.__v_model = this
handler.bind.call(this)
this.update = handler.update
this._unbind = handler.unbind
},
/**
* Check read/write filter stats.
*/
checkFilters () {
var filters = this.filters
if (!filters) return
var i = filters.length
while (i--) {
var filter = resolveAsset(this.vm.$options, 'filters', filters[i].name)
if (typeof filter === 'function' || filter.read) {
this.hasRead = true
}
if (filter.write) {
this.hasWrite = true
}
}
},
unbind () {
this.el.__v_model = null
this._unbind && this._unbind()
}
}

通過判斷元素的元素名稱來確定採用哪一種繫結和更新,對於 textarea 處理方法和 type 為 text 的 input 一樣,而 type 為 number 也和 type 為 test的一樣
這裡的 priority 還不確定是幹什麼的

再挑其中比較常見的 text handle:

import { _toString } from '../../util/index'
export default {
bind () {
this.attr = this.el.nodeType === 3
? 'data'
: 'textContent'
},
update (value) {
this.el[this.attr] = _toString(value)
}
}

它利用節點型別 nodeType 來判斷是文字還是元素, nodeType 為 3 的時候為文字節點,繫結取值方法為 this.attr[‘data’], 如果為元素節點,則為 this.attr[‘textContent’] 取得元素內的所有文字,如果對整個 html 取 textcontent,那麼就會取到所有的文字內容。

節點型別:

(摘自http://www.w3school.com.cn/js…

資源

另一位作者關於 vue 2.1.7 原始碼解析

相關文章

前端開發 最新文章