[Vue源碼學習]1-從零跑通Vue實例化過程

目標:經過demo理解Vue的實例化過程,學習Vue的代碼組織結構html

Vue簡介

漸進式 - 便於學習

Vue (讀音 /vjuː/,相似於 view) 是一套用於構建用戶界面(View層)的漸進式框架 (與React相似) vue

image

Github star - 用戶數量足夠多(足夠成熟,代碼質量有保證) node

image

Debug環境搭建

方案一

  1. 切換node版本 (Expected version ">=4 <=9".)
nvm use v7
複製代碼
  1. clone vue 源碼
git clone --branch v2.5.17 git@github.com:vuejs/vue.git
複製代碼
  1. 生成源碼打包後的sourcemap
  2. 打開源碼package.json
  3. 修改dev命令
  4. 尾部加上 --sourcemap
  5. 運行yarn run dev
  6. dist目錄下的sourcemap就有了
  7. 經過引用vue.js進行調試

方案二

  1. 下載分享部分源碼 git clone git@github.com:PeterChen1997/vue2-analysis.git
  2. 進入vue-demo文件夾
  3. yarn dev便可斷點調試

類型檢查

Flow 是 facebook 出品的 JavaScript 靜態類型檢查工具。Vue.js 的源碼利用了 Flow 作了靜態類型檢查,因此瞭解 Flow 有助於咱們閱讀源碼webpack

在 Vue.js 的主目錄下有 .flowconfig 文件, 它是 Flow 的配置文件,感興趣的同窗能夠看官方文檔 這其中的 [libs] 部分用來描述包含指定庫定義的目錄,默認是名爲 flow-typed 的目錄git

// .flowconfig
[libs]
flow
複製代碼

這裏 [libs] 配置的是 flow,表示指定的庫定義都在 flow 文件夾內。咱們打開這個目錄,會發現文件以下:github

flow
├── compiler.js        # 編譯相關
├── component.js       # 組件數據結構
├── global-api.js      # Global API 結構
├── modules.js         # 第三方庫定義
├── options.js         # 選項相關
├── ssr.js             # 服務端渲染相關
├── vnode.js           # 虛擬 node 相關
├── weex.js            # weex 相關
複製代碼

flow主要有幾個比較明顯的問題:web

  1. 編輯器或 IDE 集成度低(與 TypeScript 相比)
  2. 社區力量較弱,所以庫的數量較少,並且庫的類型定義質量不高 3.Facebook Flow 團隊與社區之間缺少互動,並且沒有公共路線圖
  3. 高內存消耗和頻繁的內存泄漏
    image

React也是使用的flow,可是facebook也發現flow的問題也愈來愈明顯,fb的jest重構也選擇了ts,vue3也選擇了ts,不知道後續React是否會有所動做編程

源碼結構

咱們先只看source目錄下的結構json

src
├── compiler        # 編譯相關 
├── core            # 核心代碼 
├── platforms       # 不一樣平臺的支持
├── server          # 服務端渲染
├── sfc             # .vue 文件解析
├── shared          # 共享代碼
複製代碼

compiler

compiler 目錄包含 Vue.js 全部編譯相關的代碼。它包括把模板解析成 ast 語法樹,ast 語法樹優化,代碼生成等功能api

編譯的工做能夠在構建時作(藉助 webpack、vue-loader 等輔助插件);也能夠在運行時作,使用包含構建功能的 Vue.js。顯然,編譯是一項耗性能的工做,因此更推薦前者——離線編譯

core

core 目錄包含了 Vue.js 的核心代碼,包括內置組件(transition / slot等)、全局 API 封裝($set),Vue 實例化、觀察者、虛擬 DOM、工具函數等等 這裏的代碼可謂是 Vue.js 的靈魂,也是咱們以後須要重點分析的地方

platforms

Vue.js 是一個跨平臺的 MVVM 框架,它能夠跑在 web 上,也能夠配合 weex 跑在 native 客戶端上 platform 是 Vue.js 的入口,2 個目錄表明 2 個主要入口,分別打包成運行在 web 上和 weex 上的 Vue.js

咱們會重點分析 web 入口打包後的 Vue.js,對於 weex 入口打包的 Vue.js,感興趣的同窗能夠研究研究,做爲補充分享

server

Vue.js 2.x 支持服務端渲染,全部服務端渲染相關的邏輯都在這個目錄下 注意:這部分代碼是跑在服務端的 Node.js,不要和跑在瀏覽器端的 Vue.js 混爲一談

服務端渲染主要的工做是把組件渲染爲服務器端的 HTML 字符串,將它們直接發送到瀏覽器,最後合成爲客戶端的應用程序

sfc(single file component)

一般咱們開發 Vue.js 都會藉助 webpack 構建, 而後經過 .vue 單文件來編寫組件。 這個目錄下的代碼邏輯會把 .vue 文件內容解析成一個 JavaScript 的對象

shared

Vue.js 會定義一些工具方法,這裏定義的工具方法都是會被瀏覽器端的 Vue.js 和服務端的 Vue.js 所共享的

這樣的目錄設計讓代碼的閱讀性和可維護性都變強,是很是值得學習和推敲的

Vue實例化過程

簡單介紹了下Vue的相關內容後,如今開始講講今天的主題——Vue實例化過程

建立一個Vue實例

因爲咱們模板編譯部分在後面纔會講到,因此咱們實例化過程當中只使用render函數進行編寫,這和咱們常見的vue編寫方式不太同樣

var vm = new Vue({
    el: '.app',
    render: function(createElement) {
        return createElement(
            'div',
            {},
            'Hello World'
        )
    }
})
複製代碼

Vue 的設計雖然沒有徹底遵循 MVVM 模型,可是也受到了它的啓發。所以在文檔中常常會使用 vm (ViewModel 的縮寫) 這個變量名錶示 Vue 實例 (好比ref 屬性雖然爲父組件操做子組件大開了方便之門,可是它繞開了 ViewModel 來訪問 View)

(見DEMO 1)

從結果反向思考

若是隻是簡單的建立一個Hello World的div,咱們用js會怎麼實現:

  1. 找到element
  2. 調用函數生成HTML
  3. 替換HTML

最簡單的vue

寫成代碼應該是這個樣子

function createElement(type, options, text) {
    // createElement
    const element = document.createElement(type)

    // set content
    element.innerHTML = text

    return element
}

function Vue(options) {
    const { el, render } = options

    // 1. 找到element
    const targetDOM = document.querySelector(el)
    // 2. 調用函數生成Element
    const targetElement = render.call(this, createElement)
    // 3. 替換HTML
    targetDOM.outerHTML = targetElement.outerHTML
}

new Vue({
    el: '.app',
    render: function(createElement) {
        return createElement(
            'div',
            {},
            'Hello World'
        )
    }
})
複製代碼

(DEMO 2)

固然,Vue做爲一個視圖框架,並非只提供了一個簡單渲染div的能力。若是咱們但願在上面加一點細節,好比說加上個 vdom ,應該怎麼加

加上vdom

vdom簡介

Vue.js 中 Virtual DOM 是借鑑了一個開源庫 snabbdom 的實現,而後加入了一些 Vue.js 特點的東西,相比於Vue的vdom,它更加簡單和純粹

VNode 是對真實 DOM 的一種抽象描述,它的核心定義無非就幾個關鍵屬性,標籤名、數據、子節點、鍵值等,其它屬性都是用來擴展 VNode 的靈活性以及實現一些特殊 feature 的

因爲 VNode 只是用來映射到真實 DOM 的渲染,不須要包含操做 DOM 的方法,所以它是很是輕量和簡單的

下面是snabbdom的官網例子:

var snabbdom = require('snabbdom');var patch = snabbdom.init([ // Init patch function with chosen modules
  require('snabbdom/modules/class').default, // makes it easy to toggle classes
  require('snabbdom/modules/props').default, // for setting properties on DOM elements
  require('snabbdom/modules/style').default, // handles styling on elements with support for animations
  require('snabbdom/modules/eventlisteners').default, // attaches event listeners
]);

var h = require('snabbdom/h').default; // helper function for creating vnodes
var container = document.getElementById('container');
var vnode = h('div#container.two.classes', {on: {click: someFn}}, [
  h('span', {style: {fontWeight: 'bold'}}, 'This is bold'),
  ' and this is just normal text',
  h('a', {props: {href: '/foo'}}, 'I\'ll take you places!')
]);// Patch into empty DOM element – this modifies the DOM as a side effect
patch(container, vnode);

var newVnode = h('div#container.two.classes', {on: {click: anotherEventHandler}}, [
  h('span', {style: {fontWeight: 'normal', fontStyle: 'italic'}}, 'This is now italic type'),
  ' and this is still just normal text',
  h('a', {props: {href: '/bar'}}, 'I\'ll take you places!')
]);// Second `patch` invocation
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
複製代碼

咱們觀察後能夠發現,sanbbdom的createElement(h)函數執行後,返回的並非咱們想象的一個真實的targetElement,而是一個 vnode 。再經過 patch 函數掛載到真實的DOM元素上

Vue內的Virtual DOM 的實現基本相似:

  • Vue先經過createElement函數生成vnode
  • 再經過_update函數把vnode掛載到真實元素上

咱們先看看Vue是怎麼生成vnode的

Vue.js 的 createElement 方法,定義在 src/core/vdom/create-elemenet.js 中

// wrapper function for providing a more flexible interface
// without getting yelled at by flowexport
function createElement ( context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean ): VNode | Array<VNode> {
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }

  // normalizationType 表示子節點規範的類型,類型不一樣規範的方法也就不同,它主要是參考 render 函數是編譯生成的仍是用戶手寫的
  return _createElement(context, tag, data, children, normalizationType)
}
複製代碼

這裏實際調用的是_createElement函數,_createElement 方法有 5 個參數:

  • context 表示 VNode 的上下文環境,它是 Component 類型;
  • tag 表示標籤,它能夠是一個字符串,也能夠是一個 Component;
  • data 表示 VNode 的數據,它是一個 VNodeData 類型,能夠在 flow/vnode.js 中找到它的定義
  • children 表示當前 VNode 的子節點,它是任意類型的,它接下來須要被規範爲標準的 VNode 數組
  • normalizationType 表示子節點規範的類型,類型不一樣規範的方法也就不同,它主要是參考 render 函數是編譯生成的仍是用戶手寫的

createElement 函數的主要作了兩個工做,咱們主要分析第二個:

  • children 的規範化
  • VNode 的建立

vnode的建立

下列代碼摘選自_createElement 方法,定義在 src/core/vdom/create-elemenet.js 中

let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
複製代碼
  • 這裏先對 tag 作判斷
  • 若是是 string 類型
    • 則接着判斷若是是內置的一些節點,則直接建立一個普通 VNode
    • 若是是爲已註冊的組件名,則經過 createComponent 建立一個組件類型的 VNode
    • 不然建立一個未知的標籤的 VNode
  • 若是是 tag 一個 Component 類型
  • 則直接調用 createComponent 建立一個組件類型的 VNode 節點

對於 createComponent 建立組件類型的 VNode 的過程,咱們在以後的組件化分享部分會詳細介紹,這裏先直接跳過

vnode的掛載

咱們已經知道 Vue 是如何建立了一個 VNode,接下來就是要把這個 VNode 渲染成一個真實的 DOM 並渲染出來,這個過程是經過 _update函數 完成的,它被調用的時機有 2 個,一個是首次渲染,一個是數據更新的時候

咱們這一次只分析首次渲染部分,數據更新部分會在以後分析響應式原理的時候涉及接下來分析一下這個過程

_update 方法定義在 src/core/instance/lifecycle.js

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    // ...
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      )
      // no need for the ref nodes after initial patch
      // this prevents keeping a detached DOM tree in memory (#5851)
      vm.$options._parentElm = vm.$options._refElm = null
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    // ...
  }
複製代碼

_update 的核心就是調用 vm.patch 方法,這個方法實際上在不一樣的平臺,好比 web 和 weex 上的定義是不同的,在瀏覽器非服務端渲染狀況下,他會指向到

src/platforms/web/runtime/patch.js

/* @flow */

import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'

// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)

export const patch: Function = createPatchFunction({ nodeOps, modules })
該方法的定義是調用 createPatchFunction 方法的返回值,這裏傳入了一個對象,包含 nodeOps 參數和 modules 參數

其中,nodeOps 封裝了一系列 DOM 操做的方法,modules 定義了一些模塊的鉤子函數的實現,咱們這裏先不詳細介紹,來看一下 createPatchFunction 的實現,它定義在 src/core/vdom/patch.js 中
export function createPatchFunction (backend) {
    // ...
    return function patch (oldVnode, vnode, hydrating, removeOnly) {
        // create diff patch
        // ...
    }
}
複製代碼

createPatchFunction 內部定義了一系列的輔助方法,最終返回了一個 patch 方法,這個方法就賦值給了 vm._update 函數裏調用的 vm.patch

思考一下

在介紹 patch 的方法實現以前,咱們能夠思考一下

爲什麼 Vue.js 源碼繞了這麼一大圈,把相關代碼分散到各個目錄

由於前面介紹過,patch 是平臺相關的,在 Web 和 Weex 環境,它們把虛擬 DOM 映射到 「平臺 DOM」 的方法是不一樣的,而且對 「DOM」 包括的屬性模塊建立和更新也不盡相同。所以每一個平臺都有各自的 nodeOps 和 modules,它們的代碼須要託管在 src/platforms 這個大目錄下

而不一樣平臺的 patch 的主要邏輯部分是相同的,因此這部分公共的部分託管在 core 這個大目錄下。差別化部分只須要經過參數來區別,這裏用到了一個函數柯里化的技巧,經過 createPatchFunction 把差別化參數提早固化,這樣不用每次調用 patch 的時候都傳遞 nodeOps 和 modules 了,這種編程技巧也很是值得學習

在計算機科學中,柯里化(英語:Currying),又譯爲卡瑞化或加里化,是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下的參數並且返回結果的新函數的技術

在這裏,nodeOps 表示對 「平臺 DOM」 的一些操做方法,modules 表示平臺的一些模塊,它們會在整個 patch 過程的不一樣階段執行相應的鉤子函數,這些代碼的具體實現會在以後的分享介紹

回到 patch 方法自己,它接收 4個參數:

  • oldVnode 表示舊的 VNode 節點,它也能夠不存在或者是一個 DOM 對象
  • vnode 表示執行 _render 後返回的 VNode 的節點
  • hydrating 表示是不是服務端渲染
  • removeOnly 是給 transition-group 用的

patch 的邏輯看上去相對複雜,由於它有着很是多的分支邏輯,爲了方便理解,咱們並不會在這裏介紹全部的邏輯,僅會針對咱們以前的例子分析它的執行邏輯

結合咱們的例子,咱們的場景是首次渲染,因此在執行 patch 函數的時候,傳入的 vm.el 對應的是例子中 id 爲 app 的 DOM 對象,這個也就是咱們在 index.html 模板中寫的 <div class="app">,
// initial render
vm.el = vm.patch(vm.$el, vnode, hydrating, false /* removeOnly */)

在__patch__函數內,調用了createElm。 createElm 的做用是經過虛擬節點建立真實的 DOM 並插入到它的父節點中,而後還有一些相似遍歷生成子節點的過程,這裏就不詳細介紹了

最後在patch內,將以前的node替換。那麼至此咱們從主線上把模板和數據如何渲染成最終的 DOM 的過程分析完畢了,那咱們能夠經過代碼實現一遍基本的邏輯

(demo3)

思惟導圖

生命週期

image

Vue實例化過程的關鍵步驟

image

源碼調試

DEBUG DEMO1

總結

思考點

  • 跨平臺代碼組織形式
  • 函數科裏化減小參數傳遞
  • ...
相關文章
相關標籤/搜索