Vue源码解读1-Vue初始化

前言

自己学习Vue也有不少时间了,特别是喜欢研究一些原理,一直以来都是看别人的博客学习,这次想分享学习的心得,而且这也是我第一次发博客,写的不好请见谅,文章随缘更新(最近有时间),感谢观看

注:一定要搭配源码看,版本为:2.6.10

在new Vue之前的初始化

我们先来到`node_modules\vue\src\core\instance\index.js`, 这里是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'

// 这里是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)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

接下来会一步步分析, initMixin(Vue), stateMixin(Vue), eventsMixin(Vue), lifecycleMixin(Vue), renderMixin(Vue)

  • initMixin(Vue)

    文件位置: node_modules\vue\src\core\instance\init.js

    export function initMixin (Vue: Class<Component>) {
      Vue.prototype._init = function (options?: Object) {
          // ...具体逻辑后续讲解
      }
    }
    

    很明显,在Vue.prototype上初始化了一个_init方法, 它是在我们new Vue后进行了调用, 至于具体实现后面详解。

  • stateMixin(Vue)

    文件位置: node_modules\vue\src\core\instance\state.js

    export function stateMixin (Vue: Class<Component>) {
      // flow somehow has problems with directly declared definition object
      // when using Object.defineProperty, so we have to procedurally build up
      // the object here.
      const dataDef = {}  // 定义一个data空对象, 后面就会知道,这是对$data属性的特性设置
      dataDef.get = function () { return this._data }  // 定义data的getter方法
      const propsDef = {}
      propsDef.get = function () { return this._props }// 定义props的getter方法
      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)
        }
      }
      Object.defineProperty(Vue.prototype, '$data', dataDef)  
      Object.defineProperty(Vue.prototype, '$props', propsDef)
    
      Vue.prototype.$set = set
      Vue.prototype.$delete = del
    
      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 unwatchFn () {
          watcher.teardown()
        }
      }
    }
    

    接下来我们一句句分析这个函数做了什么

    首先创建了两个dataDef,propsDef对象,并且设置了get属性访问器,返回了_data_props,至于具体作用在下面会有说明

      const dataDef = {}
      dataDef.get = function () { return this._data }
      const propsDef = {}
      propsDef.get = function () { return this._props }

    接下来是这段代码,如果不是生产环境,也就是开发环境,我们是不能直接赋值给$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)
            }
        }
     // this.$data = ... ,this.$props  这样都是会报警告的

    在原型上添加属性访问器,也就是我们可以在组件内直接使用this.$data访问

      // 在原型中添加$data属性,并设置访问器,返回的是_data
      Object.defineProperty(Vue.prototype, '$data', dataDef)
       // 在原型中添加$props属性,并设置访问器,返回的是_props
      Object.defineProperty(Vue.prototype, '$props', propsDef)

    我们来看下这个$data到底是什么

    <template>
      <div class="about">
        <h1>{{ appName }}</h1>
        <h1>{{ username }}</h1>
      </div>
    </template>
    <script>
    export default {
      data() {
        return {
          count: 1
        }
      },
      mounted() {
        // 打印了this
        console.log(this.$data);
      }
    };
    </script>
    

    打印结果很明显,就是vue实例的data方法返回的对象,并且包含__ob__属性,表示它被观测了,这些我们以后会在数据响应会详细讲解

    {__ob__: Observer}
    count: 1
    __ob__: Observer {value: {…}, dep: Dep, vmCount: 1}
    get count: ƒ reactiveGetter()
    set count: ƒ reactiveSetter(newVal)
    __proto__:

    接下来我们继续进行,它在Vue原型上添加了两个方法,set和del,那么他们到底是用来做什么的呢,我们直接定位到这两个方法的实现,并且下面两个代码实现我将以注释的形式讲解,请跳转到响应的源代码

      // 原型上添加 $set和$delete方法
      Vue.prototype.$set = set
      Vue.prototype.$delete = del
    // src/core/observer/index.js
    
    
     // @param {*} target 目标对象
    // @param {*} key 要添加的键
     // @param {*} val 要添加的值
    
    export function set (target: Array<any> | Object, key: any, val: any): any {
        // 如果目标对象是undefined或者null 或者是原始类型就警告
      if (process.env.NODE_ENV !== 'production' &&
        (isUndef(target) || isPrimitive(target))
      ) {
        warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
      }
      // 如果目标是数组, 并且key是有效的索引,也就是说必须是整数
      // 整个if所干的事情就是,删除原来的值,并替换新的值,然后直接返回新的值
      if (Array.isArray(target) && isValidArrayIndex(key)) {
        // 就找到最大的那个长度
        target.length = Math.max(target.length, key)
        // 如果key对应的下标有值,就删掉数组下标对应的值,然后添加新的值,否则不会删除,添加新的值
        target.splice(key, 1, val) // 注意这里的splice是变异后的方法,会触发响应
        return val // 直接返回属性的值
      }
    
      // 如果key是对象的key,并且不是系统原生的属性,比如说一些toString,之类的存在于Object.prototype原型的属性
      if (key in target && !(key in Object.prototype)) {
        // 直接改变目标对象的属性值,并返回,注意改变值会触发响应,如果页面有使用到这个属性,就会自动改变页面的这个值
        target[key] = val
        return val 
      }
    
      // 获取__ob__对象
      const ob = (target: any).__ob__
      // 这句话的意思就是,避免给Vue实例添加响应式的属性或者给他的$data
      if (target._isVue || (ob && ob.vmCount)) {
        process.env.NODE_ENV !== 'production' && warn(
          'Avoid adding reactive properties to a Vue instance or its root $data ' +
          'at runtime - declare it upfront in the data option.'
        )
        return val
      }
      // 如果key属性不在目标对象上,也就是说本来就不存在于目标对象上,也就意味着,不是响应式的
      // 当我们赋值后,会自动添加响应式,并触发页面更新
      if (!ob) {
        target[key] = val
        return val
      }
      // ob.value设置为响应式
      defineReactive(ob.value, key, val)
      // 通知页面更新
      ob.dep.notify()
      return val
    }
    

    总结下 Vue.prototype.$set所做的事情,其实就是向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新属性,因为 Vue 无法探测普通的新增属性(官方文档原话)。

    接下来我们再来看看$del实现

    export function del (target: Array<any> | Object, key: any) {
      if (process.env.NODE_ENV !== 'production' &&
        (isUndef(target) || isPrimitive(target))
      ) {
        warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
      }
      if (Array.isArray(target) && isValidArrayIndex(key)) {
        target.splice(key, 1)
        return
      }
      const ob = (target: any).__ob__
      if (target._isVue || (ob && ob.vmCount)) {
        process.env.NODE_ENV !== 'production' && warn(
          'Avoid deleting properties on a Vue instance or its root $data ' +
          '- just set it to null.'
        )
        return
      }
      if (!hasOwn(target, key)) {
        return
      }
      delete target[key]
      if (!ob) {
        return
      }
      ob.dep.notify()
    }

    我想不用多说,根据上面的下面不难看出,就是删除响应式数据中的一个属性,并且更新视图。

    官方文档介绍:删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开 Vue 不能检测到属性被删除的限制,但是你应该很少会使用它。

    然后我们回到初始化时候的代码

      Vue.prototype.$set = set
      Vue.prototype.$delete = del
      // 添加$watch方法
      Vue.prototype.$watch = function (
        expOrFn: string | Function,
        cb: any,
        options?: Object
      ): Function {
          // ...后续
      }

    以上代码又是在原型上添加了一个$watch方法,简单说一下,其实就是监听一个数据的变化,在以后的数据响应会详细讲解

    回到最开始我们看到的四个初始化方法,我们继续分析

    initMixin(Vue)
    stateMixin(Vue)
    eventsMixin(Vue)
    lifecycleMixin(Vue)
    renderMixin(Vue)
  • eventsMixin(Vue)

    文件位置:node_modules\vue\src\core\instance\events.js

    export function eventsMixin (Vue: Class<Component>) {
      Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
          // ...
        return vm
      }
    
      Vue.prototype.$once = function (event: string, fn: Function): Component {
        // ...
        return vm
      }
    
      Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
        // ...
        return vm
      }
    
      Vue.prototype.$emit = function (event: string): Component {
        // ...
        return vm
      }
    }

    分别在Vue.prototype, 初始化了四个方法$on,$once, $off, $emit,这些代码的实现我们暂且不看,以后都会有讲解,不过我们还是讲一下具体作用

    • vm.$on: 监听当前实例上的自定义事件。事件可以由vm.$emit触发。回调函数会接收所有传入事件触发函数的额外参数。
    • vm.$emit: 监听一个自定义事件,但是只触发一次,在第一次触发之后移除监听器。
    • vm.$off: 移除自定义事件监听器。
    • vm.$once: 监听一个自定义事件,但是只触发一次,在第一次触发之后移除监听器。

      以上方法,有个常见的使用场景:eventBus,我们可以再次new Vue()一个实例,专门用来父子组件直接的传值

lifecycleMixin(Vue)

文件位置: node_modules\vue\src\core\instance\lifecycle.js


  export function lifecycleMixin (Vue: Class<Component>) {
    Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
      // ..
    }
  
    Vue.prototype.$forceUpdate = function () {
      const vm: Component = this
      if (vm._watcher) {
        vm._watcher.update()
      }
    }
  
    Vue.prototype.$destroy = function () {
     // ..
    }
  }

Vue.prototype上初始化_update, $forceUpdate, $destroy三个方法

  • _update:用于渲染dom或者更新dom到视图
  • $forceUpdate:迫使 Vue 实例重新渲染。注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。
  • $destroy:完全销毁一个实例。清理它与其它实例的连接,解绑它的全部指令及事件监听器。

renderMixin(Vue)

文件位置:node_modules\vue\src\core\instance\render.js

export function renderMixin (Vue: Class<Component>) {
  // install runtime convenience helpers
  installRenderHelpers(Vue.prototype)

  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }

  Vue.prototype.$nextTick = function (): VNode {
    // ...
    return vnode
  }
}

Vue.prototype上初始化$nextTick

  • $nextTick: 将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。它跟全局方法 Vue.nextTick 一样,不同的是回调的 this 自动绑定到调用它的实例上。

initGlobalAPI

我们定位到src\core\global-api\index.js文件,Vue除了初始化上面的一些实例方法,也添加了下面这些静态方法

export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  const configDef = {}
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  Object.defineProperty(Vue, 'config', configDef)

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  // 2.6 explicit observable API
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }

  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue
  // 这里把keep-alive组件注册到全局
  extend(Vue.options.components, builtInComponents)

  initUse(Vue)
  initMixin(Vue)
  initExtend(Vue)
  initAssetRegisters(Vue)
}

我们来看下具体做了哪些事情

  const configDef = {}
  
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
      // 无法更改config,否则警告
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
// 添加配置对象
  Object.defineProperty(Vue, 'config', configDef)


 // 添加了`util`对象,里面有一些方法,`warn:用于警告`,`extend:合并两个对象`,`mergeOptions:合并选项`,`defineReactive:设置响应式数据`
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick
  // 这三个不必多说和之前的初始化一样
Vue.observable = <T>(obj: T): T => {
   observe(obj)
   return obj
}
// 深度观察对象,并设置为响应式
  
    // ASSET_TYPES
    // export const ASSET_TYPES = [
    //   'component',
    //   'directive',
    //   'filter'
    // ]
    Vue.options = Object.create(null)
    ASSET_TYPES.forEach(type => {
      Vue.options[type + 's'] = Object.create(null)
    })
  // 在Vue的options属性上添加一些属性,初始为空对象,包括我们常见的components,directives,filter
  // 是不是很眼熟
 Vue.options._base = Vue
 //  Vue.options添加了_base属性指向Vue,此时的options如下
  /*
  Vue.options = {
      components:{},
      directives:{},
      filter:{},
      _base: Vue
  }
  */
extend(Vue.options.components, builtInComponents)

builtInComponents这个又是什么鬼?我们来看看是什么

// src/core/components/index.js
import KeepAlive from './keep-alive'
export default {
  KeepAlive
}

原来是把KeepAlive组件添加到了components

接下来又是各种初始化,注意千万别把之前的初始化混淆

  initUse(Vue)               添加了use方法,一般用来注册插件
  initMixin(Vue)           添加了mixin方法,混入全局的钩子函数
  initExtend(Vue)           使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象
  initAssetRegisters(Vue)   注册全局组件,指令或过滤器
  • initUse: 位置src/core/global-api/use.js,添加了use方法,一般用来注册插件
  • initMixin:位置src/core/global-api/mixin.js,添加了mixin方法,混入全局的钩子函数
  • initExtend:位置src/core/global-api/extend.js,使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象

    // 样例
    var Profile = Vue.extend({
      template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
      data: function () {
        return {
          firstName: 'Walter',
          lastName: 'White',
          alias: 'Heisenberg'
        }
      }
    })
    
  • initAssetRegisters:位置src/core/global-api/assets.js,注册全局组件,指令或过滤器

    Vue.component('my-component', { /* ... */ })
    Vue.directive('my-directive', {
      bind: function () {},
      inserted: function () {},
      update: function () {},
      componentUpdated: function () {},
      unbind: function () {}
    })
    Vue.filter('my-filter', function (value) {
      // 返回处理后的值
    })

经过了initGlobalAPI(Vue)最终返回了Vue,其他代码暂且不看,主要用来处理服务端渲染

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
  }
})
Object.defineProperty(Vue, 'FunctionalRenderContext', {
  value: FunctionalRenderContext
})
Vue.version = '__VERSION__'
export default Vue

总结

经过了initMixin(Vue)stateMixin(Vue)eventsMixin(Vue)lifecycleMixin(Vue)renderMixin(Vue), Vue.prototype有哪些方法呢

Vue.prototype = {
    _init: f,  // new Vue初始化时调用
    $data: get => _data  
    $props: get => _props,  
    $set: f, 
    $delete: f,
    $watch: f,  // 监听data数据变化的方法
    $on: f,  // 事件方法
    $once: f, // 事件方法
    $emit: f, // 手动触发事件方法
    _update: f, // 初始化或者更新,把VNode渲染为真正的DOM节点
    $forceUpdate: f, // 
    $destroy: f
}

经过initGlobalAPI添加了许多静态方法和对象

  1.Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }
  2.Vue.set = set
  3.Vue.delete = del
  4.Vue.nextTick = nextTick
  5.Vue.observable = <T>(obj: T): T => {
   observe(obj)
   return obj
  }
  6.Vue.options = Object.create(null)
  7.Vue.options[type + 's'] = Object.create(null)
  8.创建全局的keep-alive组件
  9.initUse(Vue)               添加了use方法,一般用来注册插件
  10.initMixin(Vue)           添加了mixin方法,混入全局的钩子函数
  11.initExtend(Vue)           使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象
  12.initAssetRegisters(Vue)   注册全局组件,指令或过滤器
  

下一篇:new Vue()后发生了什么

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理