Vue源碼解析01

Vue源碼解析01

首先來一張Vue工作流程圖,作爲整個Vue源碼解析的基礎

Vue工作流程圖

初始化

new Vue() 初始化創建Vue實例,初始化data、props、events等

掛載

$mount 掛載執行編譯,首次渲染、創建和追加過程

編譯

compile() 編譯,該階段分爲三個階段parse、optimize、generate

渲染

render function 渲染函數,渲染函數執行時會觸發getter函數進行依賴收集,將來數據變化時會出發setter方法進行數據更新,這就是數據響應化

虛擬DOM

Virtual DOM 虛擬DOM,Vue2.0開始支持虛擬DOM,通過Js對象描述DOM,更新數據時映射爲DOM操作

更新視圖

patch 更新試圖,數據修改時Watcher(監聽器)會執行更新,對比新舊DOM,最小代價進行修改,就是patch

Vue源碼解析入口查找

首先,如果要分析源碼的化首先將Vue的項目遷移到本地,本文用的是2.1.10版本。
項目地址

  • 項目clone:git clone https://github.com/vuejs/vue.git

  • 配置運行環境這裏不過多贅述

♣記得在package.json中加入 –sourcemap,方便在瀏覽器中進行調試

    "scripts":{
        "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",
    }

直接運行 npm run dev創建測試文件vue.js

bundles /Users/songyanan/Desktop/前端學習/kkbNote/上課筆記/Vue191008/vue/src/platforms/web/entry-runtime-with-compiler.js → dist/vue.js...
created dist/vue.js in 4.5s

[2019-10-18 13:58:09] waiting for changes...

dist/vue.js是生成的測試文件,可以用來進行瀏覽器調試。

  • package.json中的dev命令指定了配置文件scripts/config.js 和打包的目標TARGET:web-full-dev

在scripts/config.js文件中存在打包的配置信息,其中代碼太多,只截取關鍵部分:

    // npm run dev命令中打包的目標文件描述,其中入口就是entry:resolve('web/entry-runtime-with-compiler.js')
    // umd格式的,對應package.json中的dev:web-full-dev
  // Runtime+compiler development build (Browser)
    'web-full-dev': {
        // 入口文件
        entry: resolve('web/entry-runtime-with-compiler.js'),
        // 打包生成的文件
        dest: resolve('dist/vue.js'),
        // 規定了輸出規範
        format: 'umd',
        // 環境變量 development 開發時
        env: 'development',
        alias: { he: './entity-decoder' },
        banner
    }

通過查看上面的resolove()方法中引入的aliases類,我們可以找到上面的入口文件的實際地址爲:
src/platforms/web/entry-runtime-with-compiler.js至此,找到入口文件

Vue的初始化過程

入口文件entry-runtime-with-compiler.js的主要作用

  • 擴展了mountmount方法,不同的平臺下,掛載的方法會有明顯的不同,通過擴展mount可以實現跨平臺的作用
    import Vue from " ./runtime/index";
    // 取出vue的$mount 重新覆蓋
    const mount = Vue.prototype.$mount
    // 擴展了$mount
    Vue.prototype.$mount = function (
    el?: string | Element,
    hydrating?: boolean
    ): Component {
        el = el && query(el)

        /* istanbul ignore if */
        if (el === document.body || el === document.documentElement) {
            process.env.NODE_ENV !== 'production' && warn(
            `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
            )
            return this
        }
        // 處理傳入vue的options選項:el和template
        // el和template都是掛載的時候用到的兩種方式 ?
        const options = this.$options
        // resolve template/el and convert to render function
        // 只有render選項不存在時 考慮el和template
        // 從這裏可以看出render的優先級是要高於template的,而tempelate的優先級高於el
        if (!options.render) {
            let template = options.template
            // 先判斷template是否存在
            if (template) {
            if (typeof template === 'string') {
                // template本身也可是一個選擇器
                if (template.charAt(0) === '#') {//首字母爲#號的話,看作是ID選擇器
                template = idToTemplate(template)
                /* istanbul ignore if */
                if (process.env.NODE_ENV !== 'production' && !template) {
                    warn(
                    `Template element not found or is empty: ${options.template}`,
                    this
                    )
                }
                }
            } else if (template.nodeType) {// template 是dom元素
                template = template.innerHTML
            } else {
                if (process.env.NODE_ENV !== 'production') {
                warn('invalid template option:' + template, this)
                }
                return this
            }
            } else if (el) {
            // 如果不存在template,存在el,則將el所在dom賦值給template
            template = getOuterHTML(el)
            }
            // 編譯過程
            if (template) {
            /* istanbul ignore if */
            if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
                mark('compile')
            }
            // 編譯,這個地方就是上面圖中的compile
            // 編譯的過程是將template轉換爲render函數
            const { render, staticRenderFns } = compileToFunctions(template, {
                outputSourceRange: process.env.NODE_ENV !== 'production',
                shouldDecodeNewlines,
                shouldDecodeNewlinesForHref,
                delimiters: options.delimiters,
                comments: options.comments
            }, this)
            // 不管是template還是el最終都會轉變爲render函數去渲染
            options.render = render
            options.staticRenderFns = staticRenderFns
            /* istanbul ignore if */
            if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
                mark('compile end')
                measure(`vue ${this._name} compile`, 'compile', 'compile end')
            }
            }
        }
        return mount.call(this, el, hydrating)
    }

./runtime/index 文件

該入口文件並沒有初始化Vue,它是通過引入的 ./runtime/index.js,所以我們來看一下該文件中實現了什麼功能

    import Vue from 'core/index'
    // 此處只粘貼核心功能相關的代碼
    //install platform patch function
    //1、 定義了patch方法,這是真正的打補丁函數
    Vue.prototype.__patch__ = inBrowser ? patch : noop
    // public mount method
    // 定義了$mount掛載方法,這就是上述圖片中的$mount掛載的部分
    Vue.prototype.$mount = function (
    el?: string | Element,
    hydrating?: boolean
    ): Component {
        // 找到宿主元素
        el = el && inBrowser ? query(el) : undefined
        // $mount的核心內容是執行mountComponent方法
        return mountComponent(this, el, hydrating)
    }

該文件依然沒有初始化定義Vue,所以我們接着找他引入的 core/index

core/index

core/index.js文件中代碼很簡單,核心功能是初始化了全局的API

    //只粘貼部分全局API
    import Vue from './instance/index'
    // 定義了全局的API,後面文章再分析
    initGlobalAPI(Vue)

繼續向下查找Vue的構造方法:./instance/index

./instance/index 定義了構造函數

    import { initMixin } from './init'
    import { stateMixin } from './state'
    import { renderMixin } from './render'
    import { eventsMixin } from './events'
    import { lifecycleMixin } from './lifecycle'
    import { warn } from '../util/index'

    // 定義了Vue的構造函數
    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)
    }

    initMixin(Vue)// 該方法實現了上面的_init()
    stateMixin(Vue)
    eventsMixin(Vue)
    lifecycleMixin(Vue)
    renderMixin(Vue)
    export default Vue

從上面的代碼可以看出,該文件實現了Vue的初始化。至此我們真正的找到了Vue項目的起點,new Vue()

  • Vue的構造方法的核心是**this._init(options)**方法,所以想進行下一步的源碼研究,我們需要看看initMixin(Vue)中是怎樣實現初始化的

初始化函數的實現 ./init

我們現在整體看一下initMinxin中的_init的代碼實現,忽略掉各種警告信息和選項初始化合並,查看其比較核心的功能:

    import { initState } from './state'
    import { initRender } from './render'
    import { initEvents } from './events'
    import { initLifecycle, callHook } from './lifecycle'
    import { initProvide, initInjections } from './inject'
    // 初始化聲明週期
    initLifecycle(vm)
    // 初始化事件,實現處理父組件傳遞的監聽事件的監聽器
    initEvents(vm)
    // 初始化渲染器$slots scopedSlots、_c、$createElement
    initRender(vm)
    // 調用生命週期鉤子函數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
    callHook(vm, 'created')

下面我們首先簡單看一下上述幾個方法,後面章節會有詳細解析

  • initLifecycle初始化生命週期的作用:
    export function initLifecycle (vm: Component) {
    const options = vm.$options

    // locate first non-abstract parent
    let parent = options.parent
    if (parent && !options.abstract) {
        while (parent.$options.abstract && parent.$parent) {
        parent = parent.$parent
        }
        // 通知父組件將當前實例加入父組件之中,創建的時候通知父組件
        parent.$children.push(vm)
    }

    // 初始化Vue實例中的一些屬性
    vm.$parent = parent
    vm.$root = parent ? parent.$root : vm
    vm.$children = []
    vm.$refs = {}
    vm._watcher = null
    vm._inactive = null
    vm._directInactive = false
    vm._isMounted = false
    vm._isDestroyed = false
    vm._isBeingDestroyed = false
}
  • initEvents(vm) 方法的作用
    // 這個方法的核心功能是初始化了Vue實例中的_events選項,並且將父組件的監聽方法加入到當前實例中
    export function initEvents (vm: Component) {
        vm._events = Object.create(null)
        vm._hasHookEvent = false
        // init parent attached events
        // 獲取父組件的監聽方法
        const listeners = vm.$options._parentListeners
        if (listeners) {
            // 將父組件的監聽方法加入到當前實例中進行處理
            // 這個地方解釋了關於 事件誰派發誰監聽 的問題,
            //雖然監聽事件實現是在父組件中,但是真正對事件進行監聽處理的是當前實例,也就是派發事件的子組件
            updateComponentListeners(vm, listeners)
        }
    }
  • 關於initRender(vm) 方法,看名稱能看出該方法是初始化了Vue的一些渲染函數相關
    export function initRender (vm: Component) {
        // 此處初始化了當前組件實例的Vnode,也就是虛擬dom
        vm._vnode = null // the root of the child tree
        vm._staticTrees = null // v-once cached trees
        const options = vm.$options
        const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
        const renderContext = parentVnode && parentVnode.context
        // 此處初始化了實例的$slots,插槽相關
        vm.$slots = resolveSlots(options._renderChildren, renderContext)
        vm.$scopedSlots = emptyObject
        // bind the createElement fn to this instance
        // so that we get proper render context inside it.
        // args order: tag, data, children, normalizationType, alwaysNormalize
        // internal version is used by render functions compiled from templates
        //createElement,給編譯器生成render函數使用
        vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
        // normalization is always applied for the public version, used in
        // user-written render functions.
        // 這是render(h)的h函數,給用戶編寫的render函數去使用
        vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

        // $attrs & $listeners are exposed for easier HOC creation.
        // they need to be reactive so that HOCs using them are always updated
        const parentData = parentVnode && parentVnode.data
        /* istanbul ignore else */
        if (process.env.NODE_ENV !== 'production') {
            defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
            !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
            }, true)
            defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
            !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
            }, true)
        } else {
            // 數據響應化處理相關
            defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
            defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
        }
    }
  • 關於initInjections(vm)方法,初始化數據注入相關
    // 主要功能是對
    export function initInjections (vm: Component) {
        // 對組件選項中的inject屬性中的各個key進行遍歷,通過父組件鏈一直向上查找provide()中和inject對應的屬性
        // 通俗點就是獲取父組件和祖先組件中提供的一些數據,然後注入到當前組件中
        const result = resolveInject(vm.$options.inject, vm)
        if (result) {
            toggleObserving(false)
            Object.keys(result).forEach(key => {
            /* istanbul ignore else */
            if (process.env.NODE_ENV !== 'production') {
                // 進行數據響應化的操作
                defineReactive(vm, key, result[key], () => {
                warn(
                    `Avoid mutating an injected value directly since the changes will be ` +
                    `overwritten whenever the provided component re-renders. ` +
                    `injection being mutated: "${key}"`,
                    vm
                )
                })
            } else {
                // 進行數據響應化的操作
                defineReactive(vm, key, result[key])
            }
            })
            toggleObserving(true)
        }
    }
  • 關於initState(vm)方法,初始化組件各種狀態相關
    export function initState (vm: Component) {
        // 初始化了實例中的_watchers數組
        vm._watchers = []
        const opts = vm.$options
        // 初始化props
        if (opts.props) initProps(vm, opts.props)
        // 初始化方法
        if (opts.methods) initMethods(vm, opts.methods)
        // data的處理,響應化處理
        if (opts.data) {
            // 初始化data
            initData(vm)
        } else {
            // 數據響應化
            observe(vm._data = {}, true /* asRootData */)
        }
        // 初始化computed
        if (opts.computed) initComputed(vm, opts.computed)
        //如果當前選項中國呢傳入了watch 且 watch不等於nativeWatch(細節處理,在Firefox瀏覽器下Object的原型上含有一個watch函數)
        if (opts.watch && opts.watch !== nativeWatch) {
            // 初始化watch
            initWatch(vm, opts.watch)
        }
    }
  • 關於initProvide(vm),初始化注入所需數據相關
// initProvide 是一個非常簡單的函數
//他的主要作用就是將當前選項中的provide數據放到當前實例的_provided屬性中
    export function initProvide (vm: Component) {
        const provide = vm.$options.provide
        if (provide) {
            vm._provided = typeof provide === 'function'
            ? provide.call(vm)
            : provide
        }
    }

總結

直接上一張思維導圖,後面會慢慢補充完整
思維導圖

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章