最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Vue源码解读(入口到构造函数整体流程)

    正文概述 掘金(Husky-Yellow)   2021-02-14   563

    整体流程

    在之前的介绍中,我们知道Vue.js内部会根据Web浏览器Weex跨平台和SSR服务端渲染不同的环境寻找不同的入口文件,但其核心代码是在src/core目录下,我们这一篇文章的主要目的是为了搞清楚从入口文件到Vue构造函数执行,这期间的整体流程。

    在分析完从入口到构造函数的各个部分的流程后,我们可以得到一份大的流程图:

    Vue源码解读(入口到构造函数整体流程)

    initGlobalAPI流程

    我们会在src/core/index.js文件中看到如下精简代码:

    import Vue from './instance/index'
    import { initGlobalAPI } from './global-api/index'
    initGlobalAPI(Vue)
    
    export default Vue
    

    在以上代码中,我们发现它引入了Vue随后调用了initGlobalAPI()函数,此函数的作用是挂载一些全局API方法。

    Vue源码解读(入口到构造函数整体流程)

    我们首先能在src/core/global-api文件夹下看到如下目录结构:

    |-- global-api        
    |   |-- index.js      # 入口文件
    |   |-- assets.js     # 挂载filter、component和directive
    |   |-- extend.js     # 挂载extend方法
    |   |-- mixin.js      # 挂载mixin方法
    |   |-- use.js        # 挂载use方法
    

    随后在index.js入口文件中,我们能看到如下精简代码:

    import { initUse } from './use'
    import { initMixin } from './mixin'
    import { initExtend } from './extend'
    import { initAssetRegisters } from './assets'
    import { set, del } from '../observer/index'
    import { observe } from 'core/observer/index'
    import { extend, nextTick } from '../util/index'
    
    export function initGlobalAPI (Vue: GlobalAPI) {
      Vue.set = set
      Vue.delete = del
      Vue.nextTick = nextTick
    
      Vue.observable = (obj) => {
        observe(obj)
        return obj
      }
    
      initUse(Vue)
      initMixin(Vue)
      initExtend(Vue)
      initAssetRegisters(Vue)
    }
    

    我们能从以上代码很清晰的看到在index.js入口文件中,会在Vue构造函数上挂载各种全局API函数,其中setdeletenextTickobservable直接赋值为一个函数,而其他几种API则是调用了一个以init开头的方法,我们以initAssetRegisters()方法为例,它的精简代码如下:

    // ['component','directive', 'filter']
    import { ASSET_TYPES } from 'shared/constants'
    
    export function initAssetRegisters (Vue: GlobalAPI) {
      ASSET_TYPES.forEach(type => {
        Vue[type] = function () {
          // 省略了函数的参数和函数实现代码
        }
      })
    }
    

    其中ASSET_TYPES是一个定义在src/shared/constants.js中的一个数组,然后在initAssetRegisters()方法中遍历这个数组,依次在Vue构造函数上挂载Vue.component()Vue.directive()Vue.filter()方法,另外三种init开头的方法调用挂载对应的全局API是一样的道理:

    // initUse
    export function initUse(Vue) {
      Vue.use = function () {}
    }
    
    // initMixin
    export function initMixin(Vue) {
      Vue.mixin = function () {}
    }
    
    // initExtend
    export function initExtend(Vue) {
      Vue.extend = function () {}
    }
    

    最后,我们发现还差一个Vue.compile()方法,它其实是在runtime+compile版本才会有的一个全局方法,因此它在src/platforms/web/entry-runtime-with-compile.js中被定义:

    import Vue from './runtime/index'
    import { compileToFunctions } from './compiler/index'
    Vue.compile = compileToFunctions
    export default Vue
    

    因此我们根据initGlobalAPI()方法的逻辑,可以得到如下流程图: Vue源码解读(入口到构造函数整体流程)

    initMixin流程

    在上一目录我们讲到了initGlobalAPI的整体流程,这一,我们来介绍initMixin的整体流程。首选,我们把目光回到src/core/index.js文件中:

    源码地址

    import Vue from './instance/index'
    import { initGlobalAPI } from './global-api/index'
    initGlobalAPI(Vue)
    
    export default Vue
    

    我们发现,它从别的模块中引入了大Vue,那么接下来我们的首要任务就是揭开Vue构造函数的神秘面纱。

    在看src/core/instance/index.js代码之前,我们发现instance目录结构如下:

    |-- instance
    |   |-- render-helpers      # render渲染相关的工具函数目录
    |   |-- events.js           # 事件处理相关
    |   |-- init.js             # _init等方法相关
    |   |-- inject.js           # inject和provide相关
    |   |-- lifecycle.js        # 生命周期相关
    |   |-- proxy.js            # 代理相关
    |   |-- render.js           # 渲染相关
    |   |-- state.js            # 数据状态相关
    |   |-- index.js            # 入口文件
    

    可以看到,目录结构文件有很多,而且包含的面也非常杂,但我们现在只需要对我们最关心的几个部分做介绍:

    • events.js:处理事件相关,例如:$on$off$emit以及$once等方法的实现。
    • init.js:此部分代码逻辑包含了Vue从创建实例到实例挂载阶段的所有主要逻辑。
    • lifecycle.js:生命周期相关,例如:$destroy$activated$deactivated
    • state.js:数据状态相关,例如:dataprops以及computed等。
    • render.js:渲染相关,其中最值得关注的是Vue.prototype._render渲染函数的定义。

    在介绍了instance目录结构的及其各自的作用以后,我们再来看入口文件,其实入口文件这里才是Vue构造函数庐山真面目:

    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构造函数其实就是一个普通的函数,我们只能通过new操作符进行访问,既new Vue()的形式,Vue函数内部也使用了instanceof操作符来判断实例的父类是否为Vue构造函数,不是的话则在开发环境下输出一个警告信息。
    • 除了声明Vue构造函数,这部分的代码也调用了几种mixin方法,其中每种mixin方法各司其职,处理不同的内容。

    从以上代码中,我们能得到src/core/instance/index.js文件非常直观的代码逻辑流程图:

    Vue源码解读(入口到构造函数整体流程)

    接下来我们的首要任务是弄清楚_init()函数的代码逻辑以及initMixin的整体流程。我们从上面的代码发现,在构造函数内部会调用this._init()方法,也就是说:

    // 实例化时,会调用this._init()方法。
    new Vue({
      data: {
        msg: 'Hello, Vue.js'
      }
    })
    

    然后,我们在init.js中来看initMixin()方法是如何被定义的:

    export function initMixin (Vue) {
      Vue.prototype._init = function (options) {
        // 省略代码
      }
    }
    

    我们可以发现,initMixin()方法的主要作用就是在Vue.prototype上定义一个_init()实例方法,接下来我们来看一下_init()函数的具体实现逻辑:

    Vue.prototype._init = function (options) {
        const vm = this
        // 1. 合并配置
        if (options && options._isComponent) {
          initInternalComponent(vm, options)
        } else {
          vm.$options = mergeOptions(
            resolveConstructorOptions(vm.constructor),
            options || {},
            vm
          )
        }
    
        // 2.render代理
        if (process.env.NODE_ENV !== 'production') {
          initProxy(vm)
        } else {
          vm._renderProxy = vm
        }
    
        // 3.初始化生命周期、初始化事件中心、初始化inject,
        //   初始化state、初始化provide、调用生命周期
        vm._self = vm
        initLifecycle(vm)
        initEvents(vm)
        initRender(vm)
        callHook(vm, 'beforeCreate')
        initInjections(vm)
        initState(vm)
        initProvide(vm)
        callHook(vm, 'created')
    
        // 4.挂载
        if (vm.$options.el) {
          vm.$mount(vm.$options.el)
        }
      }
    

    因为我们是要分析initMixin整体流程,对于其中某些方法的具体实现逻辑会在后续进行详细的说明,因此我们可以从以上代码得到initMixin的整体流程图。

    Vue源码解读(入口到构造函数整体流程)

    stateMixin流程

    stateMixin主要是处理跟实例相关的属性和方法,它会在Vue.prototype上定义实例会使用到的属性或者方法,这一节我们主要任务是弄清楚stateMixin的主要流程。在src/core/instance/state.js代码中,它精简后如下所示:

    import { set, del } from '../observer/index'
    export function stateMixin (Vue) {
      // 定义$data, $props
      const dataDef = {}
      dataDef.get = function () { return this._data }
      const propsDef = {}
      propsDef.get = function () { return this._props }
      Object.defineProperty(Vue.prototype, '$data', dataDef)
      Object.defineProperty(Vue.prototype, '$props', propsDef)
    
      // 定义$set, $delete, $watch
      Vue.prototype.$set = set
      Vue.prototype.$delete = del
      Vue.prototype.$watch = function() {}
    }
    

    我们可以从上面代码中发现,stateMixin()方法中在Vue.prototype上定义的几个属性或者方法,全部都是和响应式相关的,我们来简要分析一下以上代码:

    • $data和$props:根据以上代码,我们发现$data$props分别是_data_props的访问代理,从命名中我们可以推测,以下划线开头的变量,我们一般认为是私有变量,然后通过$data$props来提供一个对外的访问接口,虽然可以通过属性的get()方法去取,但对于这两个私有变量来说是并不能随意set,对于data来说不能替换根实例,而对于props来说它是只读的。因此在原版源码中,还劫持了set()方法,当设置$data或者$props时会报错:
    if (process.env.NODE_ENV !== 'production') {
      dataDef.set = function () {
        warn(
          'Avoid replacing instance root $data. ' +
          'Use nested data properties instead.',
          this
        )
      }
      propsDef.set = function () {
        warn(`$props is readonly.`, this)
      }
    }
    
    • $set$deletesetdelete这两个方法被定义在跟instance目录平级的observer目录下,在stateMixin()中,它们分别赋值给了$set$delete方法,而在initGlobalAPI中,也同样使用到了这两个方法,只不过一个是全局方法,一个是实例方法。
    • $watch:在stateMixin()方法中,详细实现了$watch()方法,此方法实现的核心是通过一个watcher实例来监听。当取消监听时,同样是使用watcher实例相关的方法,关于watcher我们会在后续响应式章节详细介绍。
    Vue.prototype.$watch = function (
        expOrFn: string | Function,
        cb: any,
        options?: Object
      ): Function {
        const vm: Component = this
        if (isPlainObject(cb)) {
          return createWatcher(vm, expOrFn, cb, options)
        }
        options = options || {}
        options.user = true
        const watcher = new Watcher(vm, expOrFn, cb, options)
        if (options.immediate) {
          try {
            cb.call(vm, watcher.value)
          } catch (error) {
            handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
          }
        }
        return function  () {
          watcher.teardownunwatchFn()
        }
      }
    

    在以上代码分析完毕后,我们可以得到stateMixin如下流程图:

    Vue源码解读(入口到构造函数整体流程)

    eventsMixin流程

    在使用Vue做开发的时候,我们一定经常使用到$emit$on$off$once等几个实例方法,eventsMixin主要做的就是在Vue.prototype上定义这四个实例方法:

    export function eventsMixin (Vue) {
      // 定义$on
      Vue.prototype.$on = function (event, fn) {}
    
      // 定义$once
      Vue.prototype.$once = function (event, fn) {}
    
      // 定义$off
      Vue.prototype.$off = function (event, fn) {}
    
      // 定义$emit
      Vue.prototype.$emit = function (event) {}
    }
    

    通过以上代码,我们发现eventsMixin()所做的事情就是使用发布-订阅模式来处理事件,接下来让我们先使用发布-订阅实现自己的事件中心,随后再来回顾源码。

    $on的实现

    $on方法的实现比较简单,我们先来实现一个基础版本的:

    function Vue () {
      this._events = Object.create(null)
    }
    
    Vue.prototype.$on = function (event, fn) {
      if (!this._events[event]) {
        this._events[event] = []
      }
      this._events[event].push(fn)
      return this
    }
    

    接下来对比一下Vue源码中,关于$on的实现:

    Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
      const vm: Component = this
      if (Array.isArray(event)) {
        for (let i = 0, l = event.length; i < l; i++) {
          vm.$on(event[i], fn)
        }
      } else {
        (vm._events[event] || (vm._events[event] = [])).push(fn)
        // optimize hook:event cost by using a boolean flag marked at registration
        // instead of a hash lookup
        if (hookRE.test(event)) {
          vm._hasHookEvent = true
        }
      }
      return vm
    }
    

    代码分析:

    1. 我们发现在Vue源码中,$on方法还接受一个数组event,这其实是在Vue2.2.0版本以后才有的,当传递一个event数组时,会通过遍历数组的形式递归调用$on方法。
    2. 我们还发现,所有$on的事件全部绑定在_events私有属性上,这个属性其实是在我们上面已经提到过的initEvents()方法中被定义的。
    export function initEvents (vm) {
      vm._events = Object.create(null)
    }
    

    $emit的实现

    我们先来实现一个简单的$emit方法:

    Vue.prototype.$emit = function (event) {
      const cbs = this._events[event]
      if (cbs) {
        const args = Array.prototype.slice.call(arguments, 1)
        for (let i = 0; i < cbs.length; i++) {
          const cb = cbs[i]
          cb && cb.apply(this, args)
        }
      }
      return this
    }
    

    接下来,我们使用$emit$on来配合测试事件的监听和触发:

    const app = new Vue()
    app.$on('eat', (food) => {
      console.log(`eating ${food}!`)
    })
    app.$emit('eat', 'orange')
    // eating orange!
    

    最后我们来看Vue源码中关于$emit的实现:

    Vue.prototype.$emit = function (event: string): Component {
      const vm: Component = this
      // ...省略处理边界代码
      let cbs = vm._events[event]
      if (cbs) {
        cbs = cbs.length > 1 ? toArray(cbs) : cbs
        const args = toArray(arguments, 1)
        const info = `event handler for "${event}"`
        for (let i = 0, l = cbs.length; i < l; i++) {
          invokeWithErrorHandling(cbs[i], vm, args, vm, info)
        }
      }
      return vm
    }
    

    代码分析:

    1. 从整体上看,$emit实现方法非常简单,第一步从_events对象中取出对应的cbs,接着一个个遍历cbs数组、调用并传参。
    2. invokeWithErrorHandling代码中会使用try/catch把我们函数调用并执行的地方包裹起来,当函数调用出错时,会执行VuehandleError()方法,这种做法不仅更加友好,而且对错误处理也非常有用。

    $off的实现

    $off方法的实现,相对来说比较复杂一点,因为它需要根据不同的传参做不同的事情:

    • 当没有提供任何参数时,移除全部事件监听。
    • 当只提供event参数时,只移除此event对应的监听器。
    • 同时提供event参数和fn回调,则只移除此event对应的fn这个监听器。

    在了解了以上功能点后,我们来实现一个简单的$off方法:

    Vue.prototype.$off = function (event, fn) {
      // 没有传递任何参数
      if (!arguments.length) {
        this._events = Object.create(null)
        return this
      }
      // 传递了未监听的event
      const cbs = this._events[event]
      if (!cbs) {
        return this
      }
      // 没有传递fn
      if (!fn) {
        this._events[event] = null
        return this
      }
      // event和fn都传递了
      let i = cbs.length
      let cb
      while (i--) {
        cb = cbs[i]
        if (cb === fn) {
          cbs.splice(i, 1)
          break
        }
      }
      return this
    }
    

    接下来,我们撰写测试代码:

    const app = new Vue()
    function eatFood (food) {
      console.log(`eating ${food}!`)
    }
    app.$on('eat', eatFood)
    app.$emit('eat', 'orange')
    app.$off('eat', eatFood)
    // 不执行回调
    app.$emit('eat', 'orange')
    

    最后我们来看Vue源码中关于$off的实现:

    Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
      const vm: Component = this
      // all
      if (!arguments.length) {
        vm._events = Object.create(null)
        return vm
      }
      // array of events
      if (Array.isArray(event)) {
        for (let i = 0, l = event.length; i < l; i++) {
          vm.$off(event[i], fn)
        }
        return vm
      }
      // specific event
      const cbs = vm._events[event]
      if (!cbs) {
        return vm
      }
      if (!fn) {
        vm._events[event] = null
        return vm
      }
      // specific handler
      let cb
      let i = cbs.length
      while (i--) {
        cb = cbs[i]
        if (cb === fn || cb.fn === fn) {
          cbs.splice(i, 1)
          break
        }
      }
      return vm
    }
    

    $once的实现

    关于$once方法的实现比较简单,可以简单的理解为在回调之后立马调用$off,因此我们来实现一个简单的$once方法:

    Vue.prototype.$once = function (event, fn) {
      function onFn () {
        this.$off(event, onFn)
        fn.apply(this, arguments)
      }
      this.$on(event, onFn)
      return this
    }
    

    接着我们对比一下Vue源码中的$once方法:

    Vue.prototype.$once = function (event: string, fn: Function): Component {
      const vm: Component = this
      function on () {
        vm.$off(event, on)
        fn.apply(vm, arguments)
      }
      on.fn = fn
      vm.$on(event, on)
      return vm
    }
    

    注意:在源码中$once的实现是在回调函数中使用fn绑定了原回调函数的引用,在上面已经提到过的$off方法中也同样进行了cb.fn === fn的判断。

    在实现完以上几种方法后,我们可以得到eventsMixin如下流程图:

    Vue源码解读(入口到构造函数整体流程)

    lifecycleMixin流程

    和以上其它几种方法一样,lifecycleMixin主要是定义实例方法和生命周期,例如:$forceUpdate()$destroy,另外它还定义一个_update的私有方法,其中$forceUpdate()方法会调用它,因此lifecycleMixin精简代码如下:

    export function lifecycleMixin (Vue) {
      // 私有方法
      Vue.prototype._update = function () {}
    
      // 实例方法
      Vue.prototype.$forceUpdate = function () {
        if (this._watcher) {
          this._watcher.update()
        }
      }
      Vue.prototype.$destroy = function () {}
    }
    

    代码分析:

    • _update()会在组件渲染的时候调用,其具体的实现我们会在组件章节详细介绍
    • $forceUpdate()为一个强制Vue实例重新渲染的方法,它的内部调用了_update,也就是强制组件重选编译挂载。
    • $destroy()为组件销毁方法,在其具体的实现中,会处理父子组件的关系,事件监听,触发生命周期等操作。

    lifecycleMixin()方法的代码不是很多,我们也能很容易的得到如下流程图:

    Vue源码解读(入口到构造函数整体流程)

    renderMixin流程

    相比于以上几种方法,renderMixin是最简单的,它主要在Vue.prototype上定义各种私有方法和一个非常重要的实例方法:$nextTick,其精简代码如下:

    export function renderMixin (Vue) {
      // 挂载各种私有方法,例如this._c,this._v等
      installRenderHelpers(Vue.prototype)
      Vue.prototype._render = function () {}
    
      // 实例方法
      Vue.prototype.$nextTick = function (fn) {
        return nextTick(fn, this)
      }
    }
    

    代码分析:

    • installRenderHelpers:它会在Vue.prototype上挂载各种私有方法,例如this._n = toNumberthis._s = toStringthis._v = createTextVNodethis._e = createEmptyVNode
    • _render()_render()方法会把模板编译成VNode,我们会在其后的编译章节详细介绍。
    • nextTick:就像我们之前介绍过的,nextTick会在Vue构造函数上挂载一个全局的nextTick()方法,而此处为实例方法,本质上引用的是同一个nextTick

    在以上代码分析完毕后,我们可以得到renderMixin如下流程图:

    Vue源码解读(入口到构造函数整体流程)


    起源地下载网 » Vue源码解读(入口到构造函数整体流程)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元