Vue入門淺析

Wind發表於2023-05-15
title: vue入門淺析
author: Sun-Wind
date: May 14,2022

寫這篇博文的目的在於為初學vue的同學對vue有一些更進一步的瞭解
讀這篇博文前,您應該至少安裝了vue環境,能在本地執行一個簡單的demo
本文將淺析vue專案工程的結構,以及用npm執行專案的過程中發生的一些事件
註明:該文字應在2022.5.14發表,由於博主有其他安排耽擱後面忘了,現在補上。

專案的檔案結構

主檔案結構

一般的vue工程專案核心部分都在src裡
存放 vue 專案的原始碼。其資料夾下的各個檔案(資料夾)分別為:

  • assets​:資原始檔,比如存放 css,圖片等資源
  • component​:元件資料夾,用來存放 vue 的公共元件(註冊於全域性,在整個專案中透過關鍵詞便可直接輸出)。
  • router​:用來存放 ​index.js​,這個 js 用來配置路由
  • tool​:用來存放工具類 js,將 js 程式碼封裝好放入這個資料夾可以全域性呼叫(比如常見的​ api.js​,​http.js​ 是對 http 方法和 api 方法的封裝)
  • views​:用來放主體頁面,雖然和元件資料夾都是 vue 檔案,但 views 下的 vue 檔案是可以用來充當路由 view 的。
  • main.js​:是專案的入口檔案,作用是初始化 vue 例項,並引入所需要的外掛。
  • app.vue​:是專案的主元件,所有頁面都是在該元件下進行切換的.

其他檔案結構

  • public:用於存放靜態檔案
  • public/index.html:是一個模板檔案,作用是生成專案的入口檔案,webpack打包的js,css也會自動注入到該頁面中。我們瀏覽器訪問專案的時候就會預設開啟生成好的index.html
  • package.json: 模組基本資訊專案開發所需要模組,版本,專案名稱
  • vue.config.js:包含vue專案的其他配置,包括埠等資訊
  • node_modules:專案的依賴模組
  • dist:打包檔案

npm run serve/dev淺析

我們在本地執行vue專案,常見的指令就是npm run serve/dev;與其說是指令,不如說是指令碼
我們通常會在package.json中配置 script 欄位作為 NPM 的執行指令碼。
以個人開發專案為例,Vue.js 原始碼構建的指令碼如下:

  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "stylelint": "stylelint src/css/*.* --fix",
    "htmlhint": "htmlhint **.html",
    "eslint": "eslint src/**/*.js src/**/*.vue",
    "eslint-fix-js": "eslint src/**/*.js --fix",
    "eslint-fix-vue": "eslint src/**/*.vue --fix"
  },

所以當我們在終端執行npm run serve時,實際上執行的是vue-cli-service serve
透過這個指令碼去構建整個vue專案

構建的過程中發生了什麼

public/index.html

之前我們提到過,這個檔案作為專案的入口檔案,首先載入這個html檔案
下面這些程式碼是個例子

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <link rel="icon" href="./icon.png">
  <title></title>
</head>
  <div id="app"></div>
  <!-- built files will be auto injected -->
</body>
</html>

我們注意到一個特別的div塊,它的id為app

src/main.js

這裡的app其實與src/main.js檔案有關

import Vue from 'vue';
new Vue({
  el: '#app',
  render: h => h(app)
});

我們都知道,new 關鍵字在 Javascript 語言中代表例項化是一個物件,而 Vue 實際上是一個類,類在 Javascript 中是用 Function 來實現的,在vue.js原始碼中是這樣定義的

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

可以看到vue只能透過關鍵字初始化,this._init函式這裡就不再具體介紹
Vue 初始化主要就幹了幾件事情,合併配置,初始化生命週期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等。

在初始化的最後,檢測到如果有 el 屬性,則呼叫 vm.$mount 方法掛載 vm,掛載的目標就是把模板渲染成最終的DOM
在compiler版本的$mount實現中,它對 el 做了限制,Vue 不能掛載在 body、html 這樣的根節點上。
接下來的是很關鍵的邏輯 —— 如果沒有定義 render 方法,則會把 el 或者 template 字串轉換成 render 方法。

這裡我們要牢記,在 Vue 2.0 版本中,所有 Vue 的元件的渲染最終都需要 render 方法,無論我們是用單檔案 .vue 方式開發元件,還是寫了 el 或者 template 屬性,最終都會轉換成 render 方法,那麼這個過程是 Vue 的一個線上編譯的過程。

最後,呼叫原先原型上的 $mount 方法掛載。

結合之前public/index.html中的例子

<div id="app">
</div>

實際上是編寫了如下render函式

render: function (createElement) {
  return createElement('div', {
     attrs: {
        id: 'app'
      },
  })
}

vm._render 最終是透過執行 createElement 方法並返回的是 vnode,它是一個虛擬 Node

Virtual DOM介紹

瀏覽器真正的DOM通常是非常龐大的,因為瀏覽器產生DOM的標準本身就比較複雜,當我們頻繁地進行DOM更新,就會產生一系列的效能問題
而 Virtual DOM 就是用一個原生的 JS 物件去描述一個 DOM 節點,所以它比建立一個 DOM 的代價要小很多。在 Vue.js 中,Virtual DOM 是用 VNode 這麼一個 Class 去描述
在 Vue.js 中,VNode 的 create 是透過之前提到的 createElement 方法建立的。

生命週期

img

這也是一張比較經典的圖了
在開發過程中,我們會頻繁地跟vue的生命週期打交道

beforeCreate 和 created 函式都是在例項化 Vue 的階段
在vue.js原始碼中 beforeCreate 和 created 的鉤子呼叫是在 initState 的前後,initState 的作用是初始化 props、data、methods、watch、computed 等屬性

在執行 vm._render() 函式渲染 VNode 之前,執行了 beforeMount 鉤子函式,在執行完 vm._update() 把 VNode patch 到真實 DOM 後,執行 mouted 鉤子。
beforeUpdate 和 updated 的鉤子函式執行時機都應該是在資料更新的時候,比如雙向繫結等等

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  // ...
  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  // ...
}

可以看到這裡有一個vm._isMounted的判斷,也就是說元件在mounted後才會去執行這個鉤子函式
同時這裡例項化了一個watcher去監聽vm上的資料變化重新渲染

beforeDestroy 和 destroyed 鉤子函式的執行時機在元件銷燬的階段
注意mounted和destroyed的執行過程都是先子後父
從下圖可以看到初始化vue到最終渲染的整個過程
img

註冊元件

在開發一個元件的過程中往往會用到其他的元件
元件註冊的語法如下

Vue.component('my-component', {
  // 選項
})

import HelloWorld from './components/HelloWorld'
export default {
  components: {
    HelloWorld
  }
}

註冊元件實際上是一個合併的過程,合併option再建立vnode。

由於博主在這一部分學識尚淺,暫不做過多的描述

下載外掛

開發的過程中很可能需要一些其他的外掛如Element等使用
一般來說透過Vue.use()來下載外掛,並且會阻止多次註冊相同的外掛

export function initUse (Vue: GlobalAPI) {
 Vue.use = function (plugin: Function | Object) {
  const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
  if (installedPlugins.indexOf(plugin) > -1) {
   return this
  }
  const args = toArray(arguments, 1)
  args.unshift(this)
  if (typeof plugin.install === 'function') {
   plugin.install.apply(plugin, args)
  } else if (typeof plugin === 'function') {
   plugin.apply(null, args)
  }
  installedPlugins.push(plugin)
  return this
 }
}

這是use方法的原始碼,可以看到其引數只能是object或者function,然後判斷其是否被註冊過
然後再呼叫該外掛的install方法

可以看到 Vue 提供的外掛序號產生器制很簡單,每個外掛都需要實現一個靜態的 install 方法,當我們執行 Vue.use 註冊外掛的時候,就會執行這個 install 方法,並且在這個 install 方法的第一個引數我們可以拿到 Vue 物件,這樣的好處就是作為外掛的編寫方不需要再額外去import Vue 了。

路由

路由的主要作用是根據不同的路徑對映到不同的檢視,一般我們用官方外掛vue-router來解決路由的問題

Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })
  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })

在vue-router的類定義中有在vue原型上定義的 $router 和 $route 兩個屬性的get方法,這也是為什麼可以在元件例項上訪問 this.$router和this.$route

在new一個vueRouter後會返回它的例項,在beforecreate()中有這樣一段程式碼

beforeCreate() {
  if (isDef(this.$options.router)) {
    // ...
    this._router = this.$options.router
    this._router.init(this)
    // ...
  }
}  

所以在執行該鉤子函式時,如果有傳入router例項,則會執行router.init方法
匹配是利用matcher匹配,並且會生成使用者的路由表
(具體細節暫時不表)
當我們點選router-link的時候,會透過一系列函式找到完整的url,執行pushState方法

export function pushState (url?: string, replace?: boolean) {
  saveScrollPosition()
  const history = window.history
  try {
    if (replace) {
      history.replaceState({ key: _key }, '', url)
    } else {
      _key = genKey()
      history.pushState({ key: _key }, '', url)
    }
  } catch (e) {
    window.location[replace ? 'replace' : 'assign'](url)
  }
}

該方法會更新瀏覽器的url地址,並且把當前url壓入歷史棧中
有一個專門的監聽器會監聽歷史棧的變化情況

setupListeners () {
  const router = this.router
  const expectScroll = router.options.scrollBehavior
  const supportsScroll = supportsPushState && expectScroll
  if (supportsScroll) {
    setupScroll()
  }
  window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => {
    const current = this.current
    if (!ensureSlash()) {
      return
    }
    this.transitionTo(getHash(), route => {
      if (supportsScroll) {
        handleScroll(this.router, route, current, true)
      }
      if (!supportsPushState) {
        replaceHash(route.fullPath)
      }
    })
  })
}

當點選瀏覽器的返回按鈕時,會觸發popstate事件,透過同樣的方法拿到當前要跳轉的url並進行路徑轉換

在router-view中

data.routerView = true
// ...
while (parent && parent._routerRoot !== parent) {
  if (parent.$vnode && parent.$vnode.data.routerView) {
    depth++
  }
  if (parent._inactive) {
    inactive = true
  }
  parent = parent.$parent
}
const matched = route.matched[depth]
// ...
const component = cache[name] = matched.components[name]

這個迴圈就是從當前的的父節點向上找,一直找到根節點(vue例項),遍歷完成後,就根據當前遍歷的深度和路徑找到對應的元件並進行渲染
在router-view的最後有根據 component 渲染出對應的元件 vonde:

return h(component, data, children)
  • hash模式:單頁應用標配,hash發生變化的url都會被瀏覽器記錄下來
  • history模式:可以進行切換和修改(歷史狀態),使得路由配置更自由

其他

vuex

Vuex 是一個專為 Vue.js 應用程式開發的狀態管理模式。
它採用集中式儲存管理應用的所有元件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。
該狀態管理模式包含以下幾個部分:

  • state:驅動資料的應用源
  • view:以宣告方法將state對映到檢視
  • actions:響應在view上使用者的輸入導致的狀態變化
    以下是一個簡單的資料流模式
    img

需要注意以下兩點

  • Vuex 的狀態儲存是響應式的。當 Vue 元件從 store 中讀取狀態的時候,若 store 中的狀態發生變化,那麼相應的元件也會相應地得到高效更新。
  • 你不能直接改變 store 中的狀態。改變 store 中的狀態的唯一途徑就是顯式地提交 (commit) mutation。這樣使得我們可以方便地跟蹤每一個狀態的變化,從而讓我們能夠實現一些工具幫助我們更好地瞭解我們的應用。
    img

參考書籍《vue.js技術揭秘》

相關文章