美文网首页
Vue 源码解析 - 主线流程

Vue 源码解析 - 主线流程

作者: Whyn | 来源:发表于2020-04-12 11:10 被阅读0次

    [TOC]

    前言

    前面我们对 Vue 的基本使用进行了归纳:Vue 学习笔记,接下来我们对 Vue 的源码进行解读。

    :本文所使用的 Vue 源码版本为:Vue 2.6.10

    源码调试环境构建

    在进行源码阅读之前,可以先搭建下 Vue 的源码调试环境:

    • Vue 调试环境搭建:搭建 Vue 调试环境,可以让我们在浏览器中对 Vue 源码进行单步调试,方便阅读理解源码,具体搭建步骤如下:
    1. 下载 Vue 源码,这里使用当前最新源码:Vue 2.6.10
    2. 在源码package.json中的dev脚本最后添加--sourcemap,用于生成.map文件:
    "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev --sourcemap",
    
    1. 在源码根目录下,执行npm install,安装package.json指定的依赖。
    2. 执行npm run dev, 这一步 将在dist目录下生成vue.jsvue.js.map两个文件。
    3. 找到样例项目,随便打开一个项目,比如:examples/commits/index.html,将vue.min.js修改为vue.js
    <script src="../../dist/vue.js"></script>
    
    1. 浏览器打开examples/commits/index.html,运行项目。
    2. F12 打开浏览器控制台,点击Sources标签,跳转到源码界面。
    3. Vue 源码的入口位置为:src/core/instance/index.js,随意在该文件内打上一个断点,刷新即可进入调试模式,如下图所示:
    src/core/instance/index.js

    Vue 源码目录设计

    Vue 的源码都存放在src目录下:

    vue/src
    ├─compiler
    │  ├─codegen
    │  ├─directives
    │  └─parser
    ├─core
    │  ├─components
    │  ├─global-api
    │  ├─instance
    │  │  └─render-helpers
    │  ├─observer
    │  ├─util
    │  └─vdom
    │      ├─helpers
    │      └─modules
    ├─platforms
    │  ├─web
    │  │  ├─compiler
    │  │  │  ├─directives
    │  │  │  └─modules
    │  │  ├─runtime
    │  │  │  ├─components
    │  │  │  ├─directives
    │  │  │  └─modules
    │  │  ├─server
    │  │  │  ├─directives
    │  │  │  └─modules
    │  │  └─util
    │  └─weex
    │      ├─compiler
    │      │  ├─directives
    │      │  └─modules
    │      │      └─recycle-list
    │      ├─runtime
    │      │  ├─components
    │      │  ├─directives
    │      │  ├─modules
    │      │  └─recycle-list
    │      └─util
    ├─server
    │  ├─bundle-renderer
    │  ├─optimizing-compiler
    │  ├─template-renderer
    │  └─webpack-plugin
    ├─sfc
    └─shared
    

    src目录主要包含 6 大类功能代码:

    目录 描述
    compiler 模板编译(即template转化成render函数)
    core 存放通用的,平台无关的运行时代码(核心代码)
    server 服务端渲染代码
    platforms 平台相关代码
    sfc 单文件组件(*.vue)编译解析代码
    shared 通用的工具方法

    Vue 源码构建

    Vue 源码是基于 Rollup 进行构建的,下面我们对其构建过程进行分析。

    RollupWebpack 都是前端构建工具,Webpack 功能相对更强大,但 Rollup 更轻量。Vue 之所以采用 Rollup 构建发布文件,据尤雨溪本人的回答,是因为最终打包出来的文件会更小,且初始化速度更快。

    首先,查看下package.json设置的构建命令:

    "scripts": {
        ...
        "build": "node scripts/build.js",
        "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
        "build:weex": "npm run build -- weex",
        ...
    }
    

    可以看到,当我们执行构建命令npm run build的时候,其实就是执行node scripts/build.js文件,因此,我们来看下构建脚本scripts/build.js的具体内容:

    ...
    // 读取构建配置文件
    let builds = require('./config').getAllBuilds()
    
    // 过滤构建版本
    // filter builds via command line arg
    if (process.argv[2]) {
      const filters = process.argv[2].split(',')
      builds = builds.filter(b => {
        return filters.some(f => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1)
      })
    } else {
      // filter out weex builds by default
      builds = builds.filter(b => {
        return b.output.file.indexOf('weex') === -1
      })
    }
    
    // 进行构建
    build(builds)
    
    function build (builds) {
        ...
        buildEntry(builds[built])
        ...
    }
    
    function buildEntry (config) {
        ...
        return rollup.rollup(config)
        ...
        return write(file, code)
        ...
    }
    
    function write (dest, code, zip) {
      return new Promise((resolve, reject) => {
          ...
        fs.writeFile(dest, code, err => {
            ...
        })
    ...
    }
    

    scripts/build.js构建脚本主要做了 3 件事:

    1. 读取配置文件:具体代码为:
    // build.js
    let builds = require('./config').getAllBuilds()
    
    // config.js
    exports.getAllBuilds = () => Object.keys(builds).map(genConfig) 
    

    通过config.js提供的getAllBuilds函数,即可获取到配置信息。而getAllBuilds获取的信息来自builds,我们看下builds的具体内容:

    const builds = {
      // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
      'web-runtime-cjs-dev': {
        entry: resolve('web/entry-runtime.js'),
        dest: resolve('dist/vue.runtime.common.dev.js'),
        format: 'cjs',
        env: 'development',
        banner
      },
      'web-runtime-cjs-prod': {
        entry: resolve('web/entry-runtime.js'),
        dest: resolve('dist/vue.runtime.common.prod.js'),
        format: 'cjs',
        env: 'production',
        banner
      },
      // Runtime+compiler CommonJS build (CommonJS)
      'web-full-cjs-dev': {
        entry: resolve('web/entry-runtime-with-compiler.js'),
        dest: resolve('dist/vue.common.dev.js'),
          ...
      },
      'web-full-cjs-prod': {
        entry: resolve('web/entry-runtime-with-compiler.js'),
        dest: resolve('dist/vue.common.prod.js'),
          ...
      },
      // Runtime only ES modules build (for bundlers)
      'web-runtime-esm': {
        entry: resolve('web/entry-runtime.js'),
        dest: resolve('dist/vue.runtime.esm.js'),
          ...
      },
      // Runtime+compiler ES modules build (for bundlers)
      'web-full-esm': {
        entry: resolve('web/entry-runtime-with-compiler.js'),
        dest: resolve('dist/vue.esm.js'),
          ...
      },
      // Runtime+compiler ES modules build (for direct import in browser)
      'web-full-esm-browser-dev': {
        entry: resolve('web/entry-runtime-with-compiler.js'),
        dest: resolve('dist/vue.esm.browser.js'),
          ...
      },
      // Runtime+compiler ES modules build (for direct import in browser)
      'web-full-esm-browser-prod': {
        entry: resolve('web/entry-runtime-with-compiler.js'),
        dest: resolve('dist/vue.esm.browser.min.js'),
          ...
      },
      // runtime-only build (Browser)
      'web-runtime-dev': {
        entry: resolve('web/entry-runtime.js'),
        dest: resolve('dist/vue.runtime.js'),
          ...
      },
      // runtime-only production build (Browser)
      'web-runtime-prod': {
        entry: resolve('web/entry-runtime.js'),
        dest: resolve('dist/vue.runtime.min.js'),
          ...
      },
      // Runtime+compiler development build (Browser)
      'web-full-dev': {
        entry: resolve('web/entry-runtime-with-compiler.js'),
        dest: resolve('dist/vue.js'),
          ...
      },
      // Runtime+compiler production build  (Browser)
      'web-full-prod': {
        entry: resolve('web/entry-runtime-with-compiler.js'),
        dest: resolve('dist/vue.min.js'),
          ...
      },
      // Web compiler (CommonJS).
      'web-compiler': {
        entry: resolve('web/entry-compiler.js'),
        dest: resolve('packages/vue-template-compiler/build.js'),
          ...
      },
      // Web compiler (UMD for in-browser use).
      'web-compiler-browser': {
        entry: resolve('web/entry-compiler.js'),
        dest: resolve('packages/vue-template-compiler/browser.js'),
          ...
      },
      // Web server renderer (CommonJS).
      'web-server-renderer-dev': {
        entry: resolve('web/entry-server-renderer.js'),
        dest: resolve('packages/vue-server-renderer/build.dev.js'),
          ...
      },
      'web-server-renderer-prod': {
        entry: resolve('web/entry-server-renderer.js'),
        dest: resolve('packages/vue-server-renderer/build.prod.js'),
          ...
      },
      'web-server-renderer-basic': {
        entry: resolve('web/entry-server-basic-renderer.js'),
        dest: resolve('packages/vue-server-renderer/basic.js'),
          ...
      },
      'web-server-renderer-webpack-server-plugin': {
        entry: resolve('server/webpack-plugin/server.js'),
        dest: resolve('packages/vue-server-renderer/server-plugin.js'),
          ...
      },
      'web-server-renderer-webpack-client-plugin': {
        entry: resolve('server/webpack-plugin/client.js'),
        dest: resolve('packages/vue-server-renderer/client-plugin.js'),
          ...
      },
      // Weex runtime factory
      'weex-factory': {
        weex: true,
        entry: resolve('weex/entry-runtime-factory.js'),
        dest: resolve('packages/weex-vue-framework/factory.js'),
          ...
      },
      // Weex runtime framework (CommonJS).
      'weex-framework': {
        weex: true,
        entry: resolve('weex/entry-framework.js'),
        dest: resolve('packages/weex-vue-framework/index.js'),
          ...
      },
      // Weex compiler (CommonJS). Used by Weex's Webpack loader.
      'weex-compiler': {
        weex: true,
        entry: resolve('weex/entry-compiler.js'),
        dest: resolve('packages/weex-template-compiler/build.js'),
          ...
      }
    }
    

    可以看到,builds提供了所有版本的构建信息,而genConfig函数只是对builds提供的构建信息转换成 Rollup 所需的参数格式而已。

    :上述配置信息build中的entry字段表示构建入口的JS文件路径,dest字段表示构建完成的JS文件路径,字段format表示构建文件的格式,其值有如下可选:

    • cjs:表示构建出来的文件遵循 CommonJS 规范。
    • es:表示构建文件遵循 ES Module 规范。
    • umd:表示构建文件遵循 UMD 规范。
    1. 过滤构建文件:可以通过命令行传入参数指定构建版本,否则默认构建除weex以外的所有版本。
    2. 进行构建:使用 Rollup 进行构建,最终构建的版本存放于dist目录和package目录中。
    npm run build

    以上,就是 Vue 的整个构建过程。

    Vue 源码入口文件

    Vue 构建完成后,会生成两种 Vue.js 版本:Runtime OnlyRuntime + Compiler

    • Runtime Only:在编译阶段,将.vue等文件编译成.js文件的时候,通常借助如 webpackvue-loader工具进行操作,因此,Runtime Only 版本的 Vue.js 无须包含编译部分代码,其体积会更轻量。

    • Runtime + Compiler:如果没有对模板进行预编译,但代码中又使用如template等需要进行编译的模板,则需要在运行时进行编译(在线编译)。因此,Runtime + Compiler 版本的 Vue.js 不仅包含了必须的运行时代码,也包含了编译代码。

    // 需要编译器的版本:Runtime + Compiler
    new Vue({
        template: '<div>{{ hi }}</div>'
    })
    
    // 不需要运行时编译:Runtime Only
    new Vue({
        render (h) {
            return h('div', this.hi)
        }
    })
    

    以下我们的分析采用的是 Runtime + Compiler 版本。

    在上面讲解 Vue 的构建过程中,可以知道,对于 Web 应用,其 Runtime + Compiler 构建出来的 Vue.js 的入口文件为:src/platforms/web/entry-runtime-with-compiler.js,具体代码如下:

    // scripts/alias.js
    const path = require('path')
    
    const resolve = p => path.resolve(__dirname, '../', p)
    
    module.exports = {
      vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
      compiler: resolve('src/compiler'),
      core: resolve('src/core'),
      shared: resolve('src/shared'),
      web: resolve('src/platforms/web'),
      weex: resolve('src/platforms/weex'),
      server: resolve('src/server'),
      sfc: resolve('src/sfc')
    }
    
    // scripts/config.js
    const aliases = require('./alias')
    const resolve = p => {
      const base = p.split('/')[0]
      if (aliases[base]) {
        return path.resolve(aliases[base], p.slice(base.length + 1))
      } else {
        return path.resolve(__dirname, '../', p)
      }
    }
    const builds = {
        ...
        // Runtime+compiler development build (Browser)
        'web-full-dev': {
            entry: resolve('web/entry-runtime-with-compiler.js'),
            dest: resolve('dist/vue.js'),
            format: 'umd',
            env: 'development',
            alias: { he: './entity-decoder' },
            banner
        },
        // Runtime+compiler production build  (Browser)
        'web-full-prod': {
            entry: resolve('web/entry-runtime-with-compiler.js'),
            dest: resolve('dist/vue.min.js'),
            format: 'umd',
            env: 'production',
            alias: { he: './entity-decoder' },
            banner
        },
        ...
    }
    

    scripts/alias.js文件对一些路径进行了映射,比如web对应真实路径为src/platforms/web,这样在其他文件中可简化路径书写。

    下面来看下src/platforms/web/entry-runtime-with-compiler.js源码内容:

    // src/platforms/web/entry-runtime-with-compiler.js
    import Vue from './runtime/index'
    import { compileToFunctions } from './compiler/index'
    
    ...
    // 缓存 Vue 原型上的 mount 函数
    const mount = Vue.prototype.$mount
    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
    ): Component {
        ...
        // 对模板进行编译
        const { render, staticRenderFns } = compileToFunctions(template, {...}
        ...
        return mount.call(this, el, hydrating)
    }
    
    function getOuterHTML (el: Element): string {
        ...
    }
    
    Vue.compile = compileToFunctions
    
    export default Vue
    

    entry-runtime-with-compiler.js主要做了以下三件事:

    • 导入Vue
    • Vue原型上重新定义了一个$mount函数,用于对模板进行编译,最后会重新调用原先定义的$mount函数,进行组件挂载操作。
    • Vue.compile指向函数compileToFunctions

    下面依次对上述事件进行阐述。

    Vue 源码整体流程

    entry-runtime-with-compiler.js首先会导入Vue实例:

    import Vue from './runtime/index'
    

    我们循着代码进入./runtime/index.js进行查看:

    import Vue from 'core/index'
    ...
    // install platform specific utils
    Vue.config.mustUseProp = mustUseProp
    Vue.config.isReservedTag = isReservedTag
    Vue.config.isReservedAttr = isReservedAttr
    Vue.config.getTagNamespace = getTagNamespace
    Vue.config.isUnknownElement = isUnknownElement
    
    // install platform runtime directives & components
    extend(Vue.options.directives, platformDirectives)
    extend(Vue.options.components, platformComponents)
    
    // install platform patch function
    Vue.prototype.__patch__ = inBrowser ? patch : noop
    
    // public mount method
    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
    ): Component {
      el = el && inBrowser ? query(el) : undefined
      return mountComponent(this, el, hydrating)
    }
    
    // devtools global hook
    /* istanbul ignore next */
    if (inBrowser) {
        ...
        if (config.devtools) {
          if (devtools) {
              ...
    }
    
    export default Vue
    

    代码很清晰,主要就是做了以下几件事:

    • 安装一些平台相关的工具函数
    • 安装一些平台相关的运行时指令和组件
    • 为浏览器安装patch函数
    • Vue定义挂载函数:这里是第一次在Vue的原型上定义$mount函数,其功能就是进行组件挂载mountComponent。该函数会在entry-runtime-with-compiler.js中被重新定义,增加模板编译功能,成功编译出render函数后,再经由该函数进行组件挂载。
    • 浏览器环境下hook测试工具devtools

    ./runtime/index.js最重要的功能就是定义了$mount函数进行组件挂载功能:mountComponent,其具体详情参见:Vue 源码解析 - 组件挂载

    回到开头,我们可以看到,Vue实例由core/index导入而来,其具体路径为src/core/index.js,我们进入src/core/index.js进行查看:

    import Vue from './instance/index'
    import { initGlobalAPI } from './global-api/index'
    import { isServerRendering } from 'core/util/env'
    import { FunctionalRenderContext } from 'core/vdom/create-functional-component'
    
    initGlobalAPI(Vue)
    
    Object.defineProperty(Vue.prototype, '$isServer', {
      get: isServerRendering
    })
    
    Object.defineProperty(Vue.prototype, '$ssrContext', {
      get () {
        /* istanbul ignore next */
        return this.$vnode && this.$vnode.ssrContext
      }
    })
    
    // expose FunctionalRenderContext for ssr runtime helper installation
    Object.defineProperty(Vue, 'FunctionalRenderContext', {
      value: FunctionalRenderContext
    })
    
    Vue.version = '__VERSION__'
    
    export default Vue
    

    这里主要是对Vue进行了一些全局初始化操作:主要包含以下两方面初始化内容:

    • Vue定义全局静态方法:查看initGlobalAPI源码即可看到定义的静态方法,这里就不深入源码,只列举出initGlobalAPI总共定义的静态方法:
      Vue.util = {
          warn,
          extend,
          mergeOptions,
          defineReactive
      }
      Vue.set = set
      Vue.delete = del
      Vue.nextTick = nextTick
      Vue.observable = <T>(obj: T): T => {...}
      Vue.use = function (plugin: Function | Object) {...}
      Vue.mixin = function (mixin: Object) {...}
      Vue.extend = function (extendOptions: Object): Function {..}
      
    • Vue原型上定义了$isServer$ssrContextFunctionalRenderContext等属性。

    再次回到开头,可以看到src/core/index.js./instance/index导入Vue,那我们继续查看./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'
    
    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)
    stateMixin(Vue)
    eventsMixin(Vue)
    lifecycleMixin(Vue)
    renderMixin(Vue)
    
    export default Vue
    

    直到这里,我们才终于寻找到Vue的源码定义,并且./instance/index中除了定义Vue之外,还做了很多其他事情:initMixin(Vue)stateMixin(Vue)eventsMixin(Vue)lifecycleMixin(Vue)renderMixin(Vue)。这些方法都以Mixin结尾,表明其都是通过 Mixin 方式为Vue添加扩展功能(这也是为什么Vue采用函数定义,而不是class的原因),其实质就是在Vue的原型上添加扩展方法,如下所示:

    // initMixin(Vue)
    Vue.prototype._init = function (...) {...}
    // stateMixin(Vue)
    Vue.prototype.$set = set
    Vue.prototype.$delete = del
    Vue.prototype.$watch = function (...){...}
    // eventsMixin(Vue)
    Vue.prototype.$on = function (...): Component {...}
    Vue.prototype.$once = function (...): Component {...}
    Vue.prototype.$off = function (...): Component {...}
    Vue.prototype.$emit = function (...): Component {...}
    // lifecycleMixin(Vue)
    Vue.prototype._update = function (...) {...}
    Vue.prototype.$forceUpdate = function () {...}
    Vue.prototype.$destroy = function () {...}
    // renderMixin(Vue)
    Vue.prototype.$nextTick = function (...) {...}
    Vue.prototype._render = function (): VNode {...}
    

    到这里,Vue源码的粗略完整流程已经分析完毕。其总体流程如下图所示:

    Vue 总体流程

    以下将对 Vue 整体流程中相对重要的模块依序进行源码分析。

    Vue 实例创建流程

    当我们使用new Vue()时,我们来看下整个Vue实例的创建过程:

    // src/core/instance/index.js
    function Vue(options) {
        ...
        this._init(options)
    }
    

    new Vue()时,Vue 内部就只进行了this._init(options)操作,前面我们分析过,this._init函数是在initMixin(Vue)中定义的:

    // src/core/instance/init.js
    export function initMixin (Vue: Class<Component>) {
      Vue.prototype._init = function (options?: Object) {
        const vm: Component = this
        ...
        // a flag to avoid this being observed
        vm._isVue = true
        // merge options
        if (options && options._isComponent) { // 组件对象
          initInternalComponent(vm, options)
        } else { // Vue 实例
            // 将传递进来的选项和 Vue 自带的系统相关的选项进行合并
          vm.$options = mergeOptions(
              // Vue 的内置选项,定义于 web/runtime/index.js 中,Vue.options.directives,Vue.options.components
            resolveConstructorOptions(vm.constructor),
            options || {},
            vm
          )
        }
        ...
        // expose real self
        vm._self = vm
        // 初始化生命周期
        initLifecycle(vm)
        // 初始化事件处理 
        initEvents(vm)
        // 初始化 render
        initRender(vm)
        // 触发 beforeCreate 钩子函数
        callHook(vm, 'beforeCreate')
        // 解析 options.inject 注入
        initInjections(vm) // resolve injections before data/props
        // 初始化 props、methods、data、computed 与 watch
        initState(vm)
        // 解析 options.provide
        initProvide(vm) // resolve provide after data/props
        // 触发 created 钩子函数
        callHook(vm, 'created')
        ...
        // 如果有传入 el,则进行挂载
        if (vm.$options.el) {
          vm.$mount(vm.$options.el)
        }
      }
    }
    

    可以看到,_init函数内部主要是对传递进来的Options对象和Vue自带的指令directives和组件components(这些指令和组件的定义位于web/runtime/index.js中)进行合并,以及很多的初始化操作与钩子函数触发,最后还进行了挂载操作。

    _init函数做了很多的事情,我们主要对以下事件进行分析:

    1. initLifecycle(vm):见名知意,该函数用于初始化Vue的生命周期,其源码如下所示:
    // core/instance/lifecycle.js
    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)
        }
    
        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
    }
    

    可以看到,initLifecycle函数就是对Vue$parent$root$children$refs_watcher_inactive_directInactive_isMounted_isDestroyed_isBeingDestroyed属性进行了复位操作。

    1. initEvents(vm):见名知意,该函数用于初始化Vue的事件,其源码如下所示:
    // core/instance/event.js
    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)
            }
    }
    

    主要是对Vue组件的_events_hasHookEvent$options._parentListeners进行复位操作。

    1. initRender(vm):初始化Vue组件的渲染功能。其源码如下所示:
    export function initRender (vm: Component) {
      ...
      // 将子元素解析到一个 slot 对象中
      vm.$slots = resolveSlots(options._renderChildren, renderContext)
      ...
      vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
      ...
      vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
      ...
    }
    

    可以看到,initRender函数主要是为Vue组件设置了_c$createElement函数,可用于创建虚拟节点,其中:

    • vm._c是内部版本,主要用于渲染模板代码。
    • vm.$createElement是公有版本,用于用户自定义渲染函数。

    这两个函数的底层实现均为createElement函数,该函数主要用于创建虚拟节点,关于该函数相关内容,请参考:Vue 源码解析 - 组件挂载

    1. callHook(vm, 'beforeCreate'):见名知意,该函数会触发beforeCreate钩子。其源码如下所示:
    // core/instance/lifecycle.js
    export function callHook(vm: Component, hook: string) {
      // #7573 disable dep collection when invoking lifecycle hooks
      pushTarget();
      const handlers = vm.$options[hook];
      const info = `${hook} hook`;
      if (handlers) {
        for (let i = 0, j = handlers.length; i < j; i++) {
          invokeWithErrorHandling(handlers[i], vm, null, vm, info);
        }
      }
      if (vm._hasHookEvent) {
        vm.$emit("hook:" + hook);
      }
      popTarget();
        popTarget()
    }
    
    // core/util/error.js
    export function invokeWithErrorHandling(
      handler: Function,
      context: any,
      args: null | any[],
      vm: any,
      info: string
    ) {
      let res;
      try {
        res = args ? handler.apply(context, args) : handler.call(context);
        ...
    }
    

    从源码中可以看到,callHook(vm,"beforeCreate")就是通过vm.$option["beforeCreate"]取出我们设置的beforeCreate钩子函数,最后在invokeWithErrorHandling中进行回调,这样,我们的钩子函数就生效了。

    1. initInjections(vm):初始化注入inject事件。其源码如下:
    // core/instance/inject.js
    export function initInjections(vm: Component) {
      const result = resolveInject(vm.$options.inject, vm);
      if (result) {
        toggleObserving(false);
        Object.keys(result).forEach(key => {
          ...
          defineReactive(vm, key, result[key]);
        });
        toggleObserving(true);
      }
    }
    
    export function resolveInject (inject: any, vm: Component): ?Object {
      if (inject) {
          ...
        for (let i = 0; i < keys.length; i++) {
          const key = keys[i]
          ...
          let source = vm
          while (source) {
              ...
              result[key] = source._provided[provideKey]
              ...
            }
            source = source.$parent
          }
          ...
        }
        return result
      }
    }
    

    Vue 提供provide/inject事件,允许祖先组件向其子孙组件传递数据,无论子孙组件嵌套多深,都能进行传递。而initInjections就是完成这个功能的。

    从源码中可以看到,initInjections首先会取出options.inject字段,然后取出该字段的键值Object.get(key),依次从祖先的_provided字段中取出该键值对应的值,存储到一个新的对象中result,直至遍历结束。最后还会为这些inject的键值进行响应式设置defineReactive,如此便完成了provide/inject功能。

    :响应式设置defineReactive的具体详情请参考:Vue 源码解析 - 数据驱动与响应式原理

    1. initState(vm):对一些状态的初始化。其源码如下:
    // core/instance/state.js
    export function initState(vm: Component) {
      vm._watchers = [];
      const opts = vm.$options;
      //   初始化 options.props
      if (opts.props) initProps(vm, opts.props);
      //   初始化 options.methods
      if (opts.methods) initMethods(vm, opts.methods);
      if (opts.data) {
        // 初始化 options.data
        initData(vm);
      } else {
        // 没有 options.data 时,绑定为一个空对象
        observe((vm._data = {}), true /* asRootData */);
      }
      //   初始化 options.computed
      if (opts.computed) initComputed(vm, opts.computed);
      if (opts.watch && opts.watch !== nativeWatch) {
        // 初始化 options.watcher
        initWatch(vm, opts.watch);
      }
    }
    

    从源码中可以看到,initState函数主要对Vue组件的props,methods,data,computedwatch等状态进行初始化,各初始化具体内容如下:

    • props:由函数initProps进行初始化,其源码如下所示:
    // src/core/instance/state.js
    function initProps(vm: Component, propsOptions: Object) {
        ...
        // propsOptions 就是 Vue.$options.props
      for (const key in propsOptions) {
        ...
        // 对键进行检测,并返回其对应的值
        const value = validateProp(key, propsOptions, propsData, vm);
        ...
        defineReactive(props, key, value);
        }
        ...
    }
    
    // src/core/util/props.js
    export function validateProp (
      key: string,
      propOptions: Object,
      propsData: Object,
      vm?: Component
    ): any {
      const prop = propOptions[key]
      ...
      // boolean casting
      // 检测是否为 Boolean 类型
      const booleanIndex = getTypeIndex(Boolean, prop.type)
      if (booleanIndex > -1) {
          ...
          value = true
          ...
        }
      }
      // check default value
      if (value === undefined) {
          // 获取 default 值
        value = getPropDefaultValue(vm, prop, key)
          ...
      }
      ...
      return value
    }
    

    initProps其实就是对Vue.$options.props进行解析并设置到Vue的实例属性上,且这些属性具备响应式功能defineReactive

    具体的实现步骤就是对Vue.$options.props进行遍历,获取其每个键值,对每个键值的值进行检测validateProp并返回其值,然后进行响应式设置。

    • methods:由函数initMethods进行初始化,其源码如下所示:
    // core/instance/state.js
    function initMethods(vm: Component, methods: Object) {
       ...
      for (const key in methods) {
        ...
        // 绑定 methods 中的函数到 vm 中
        vm[key] = typeof methods[key] !== "function" ? noop : bind(methods[key], vm);
      }
    }
    
    // src/shared/util.js
    export function noop (a?: any, b?: any, c?: any) {}
    
    function nativeBind (fn: Function, ctx: Object): Function {
        return fn.bind(ctx)
    }
    
    export const bind = Function.prototype.bind ? nativeBind : polyfillBind
    

    initMethods源码比较好读,就是遍历Vue.$options.methods,将每个方法都绑定到Vue实例上(如果methods内的键对应不是一个函数,就绑定到一个空函数noop,否则,就绑定到Vue上:bind(methods[key],vm))。

    • data:由函数initData进行初始化,其内部主要是对Options.data进行了代理(使得Vue实例具备与Options.data相同的键值)和对Options.data的键值进行了响应式设置,具体详情请参考:Vue 源码解析 - 数据驱动与响应式原理
    1. initProvide(vm):解析options.provide,其源码如下所示:
    // src/core/instance/inject.js
    export function initProvide(vm: Component) {
        const provide = vm.$options.provide
        if (provide) {
            vm._provided = typeof provide === 'function'
                ? provide.call(vm)
                : provide
        }
    }
    

    其实就是如果定义了Options.provide遍历,就将其赋值到vm._provided上。

    1. callHook(vm, 'created'):触发created钩子函数。

    2. vm.$mount(vm.$options.el):前面我们讲过,Vue 有两种版本:Runtime OnlyRuntime + Compiler

      对于不同的 Vue 版本,mount函数有不同的实现:

      • 对于 Runtime Only 版本,mount函数只提供 组件挂载 功能
      • 对于 Runtime + Compiler 版本,mount函数提供 模板编译 + 组件挂载 功能

      更多详细内容,请参考:Vue 源码解析 - 模板编译Vue 源码解析 - 组件挂载

    到此,整个Vue实例的创建过程就简略

    参考

    相关文章

      网友评论

          本文标题:Vue 源码解析 - 主线流程

          本文链接:https://www.haomeiwen.com/subject/jvcvmhtx.html