Vue源码解读1-Vue初始化

14次阅读

共计 11895 个字符,预计需要花费 30 分钟才能阅读完成。

前言

自己学习 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()后发生了什么

正文完
 0