Vue源码解析:Vue实例

Vue源码解析:Vue实例(一)

为什么要写这个


用了很久的JS,也用了很久的Vue.js,从最初的Angular到React到现在在用的Vue。学习路径基本上是:

  • 别人告诉我,这个地方你要用ng-module,那么我就用ng-module,至于ng-modul的功能是什么,我不知道
  • 带我的大佬不厌其烦了,教授了我查阅API的方法(找到官网,一般都有),自从开始阅读API以后,我会的方法越来越多,心情非常激动的使用一个又一个新功能
  • 开始去思考每一个框架的实现细节原理

所以就有现在我想要去研究Vue的源码,研究的方法是跟着Vue官网的教程,一步步的找到教程中功能的实现代码分析实现的代码细节,并且会详细解释代码中涉及的JS(ES6)知识。即使是前端新人也可以轻松阅读

你能得到什么


你可以得到以下知识:

  • Vue.js 源码知识
  • ES5、ES6基础知识

面对对象


  • 前端新人
  • 不想花大量时间阅读源码但是想快速知道Vue.js实现细节的人
  • 我自己

话不多说,下面就开始我的第一节笔记,对应官网教程中的Vue实例

Vue实例

Vue实例包含

  • 创建一个Vue实例

      var vm = new Vue({
        // 选项 options
      })
    复制代码
  • 数据与方法

      // 该对象被加入到一个 Vue 实例中
      var vm = new Vue({
          data: data
      })
    复制代码
  • 实例生命周期钩子

      new Vue({
      data: {
          a: 1
      },
      created: function () {
          // `this` 指向 vm 实例
          console.log('a is: ' + this.a)
      }
      })
      // => "a is: 1"
    复制代码
  • 生命周期图示

创建一个Vue实例

每个Vue应用都是通过Vue函数创建一个新的Vue实例开始:

var vm = new Vue({
  // 选项
 })
复制代码

我们从Github下载到Vue.js源码后解压打开,探索new Vue创建了一个什么东西

打开下载的vue-dev,找到vue-dev/src/core/index.js

// vue-dev/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-dev/src/core/index.js的头部找到

import Vue from './instance/index'
复制代码

打开vue-dev/src/core/instance/index.js

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)
}



export default Vue
复制代码

Vue实例在这里得到了各种初始化(init),在这里申明了一个构造器(Constructor)Vue,在构造器里调用了_init方法,并向_init方法中传入了options

 this._init(options)
复制代码

显然_init不是Function原型链中的方法,必定是在某处得到定义。紧接着后面看到一系列的Mixin函数调用

   initMixin(Vue)
   stateMixin(Vue)
   eventsMixin(Vue)
   lifecycleMixin(Vue)
   renderMixin(Vue)
复制代码

显然是这一堆Mixin方法赋予了Vue实例一个_init方法(之后会有单独的一篇笔记讲述Mixin是怎样的一种设计思维,相关知识会从原型链讲起)

顾名思义,根据函数名字猜测_init是来自于initMixin方法,根据

import { initMixin } from './init'
复制代码

找到vue-dev/src/core/instance/init.js(由于实在是太长了全粘贴过来不方便阅读,故根据需要粘贴相应的节选,如果想要全览的小伙伴可以去下载源码来看完整的)

在vue-dev/src/core/instance/init.js中我们搜索_init,找到下面这个方法

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}
复制代码

其中

export function initMixin (Vue: Class<Component>) {}
复制代码

里的

Vue: Class<Component>
复制代码

来自于flow语法,一个不错的静态类型检测器

这个initMixin方法里只干了一件事,就是给Vue.prototype._init赋值,即在所有Vue实例的原型链中添加了_init方法。这个_init方法又做了些什么呢?

  • 它给Vue实例添加了很多的属性,比如$options

  • 它给vm初始化了代理

      initProxy(vm)
    复制代码
  • 它给vm初始化了很多

      initLifecycle(vm)
      initEvents(vm)
      initRender(vm)
      callHook(vm, 'beforeCreate')
      initInjections(vm) // resolve injections before data/props
      initState(vm)
      initProvide(vm) // resolve provide after data/props
      callHook(vm, 'created')
    复制代码
  • 它甚至偷偷的唤起了钩子函数

      callHook(vm, 'beforeCreate')
      callHook(vm, 'created')
    复制代码

实例生命周期钩子 & 生命周期图示

所谓的唤起钩子函数callHook是做什么的呢?我们找到

import { initLifecycle, callHook } from './lifecycle'
复制代码

打开这个文件lifecycle.js

export function callHook (vm: Component, hook: string) {
  // #7573 disable dep collection when invoking lifecycle hooks
  pushTarget()
  const handlers = vm.$options[hook]
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      try {
        handlers[i].call(vm)
      } catch (e) {
        handleError(e, vm, `${hook} hook`)
      }
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget()
}
复制代码

可以看到,callHook函数的作用是,调用option里用户设定的生命周期函数。例如

new Vue({
  data: {
    a: 1
  },
  created: function () {
    // `this` 指向 vm 实例
    console.log('a is: ' + this.a)
  }
})
// => "a is: 1"
复制代码
new Vue() 到 beforeCreate 到 created

它在'beforeCreate'和'created'之间干了什么呢?

 callHook(vm, 'beforeCreate')
 initInjections(vm) // resolve injections before data/props
 initState(vm)
 initProvide(vm) // resolve provide after data/props
 callHook(vm, 'created')
复制代码

对应生命周期图示来看代码

    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
复制代码

在new Vue()之后,调用了_init(),在_init()内,调用了

    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
复制代码

这点正好对应官网生命周期图示中new Vue()与生命周期钩子'beforeCreate'之间的Init Events & Lifecycle,也就是说我们在option中设置的钩子函数,会在这个生命周期节点得到调用,是因为这个callHook(vm, 'beforeCreate')(vue-dev/src/core/instance/init.js),而在这个时间节点之前完成Init Events & Lifecycle的正是

    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
复制代码

除了官方提到的Events和Lifecycle的Init之外,还在这个生命周期节点完成了Render的Init

之后是Init injections & reactivity,对应的函数调用是

initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
复制代码

这段函数调用之后_init()还没有结束,后面有

if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}
复制代码

对应生命周期示意图中的

依据options中是否包含el来决定是否mount(挂载)这个el

毫无疑问,$mount函数必定是完成下一步的关键,在src文件夹中搜索$mount的定义,在/vue-dev/src/platforms/web/runtime/index.js中找到了

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
复制代码

Vue.$mount函数内包含两个重要的函数

  • query()
  • mountComponent()

其中

// src/platforms/web/util/index.js

export function query (el: string | Element): Element {
  if (typeof el === 'string') {
    const selected = document.querySelector(el)
    if (!selected) {
      process.env.NODE_ENV !== 'production' && warn(
        'Cannot find element: ' + el
      )
      return document.createElement('div')
    }
    return selected
  } else {
    return el
  }
}
复制代码

可以看到,query()是对document.querySelector()的一个包装,作用是依据new Vue(options)optionsel设定的元素选择器进行DOM内元素的选取,并设定了相应的容错、报错方法

created 到 beforeMount 到 mounted

// src/core/instance/lifecycle.js

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}
复制代码

当options里不存在render函数的时候,会执行createEmptyVNode,添加到vm.$options.render,之后执行生命周期钩子函数callHook(vm, 'beforeMount'),即对应的生命周期为

 if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
复制代码

如果render function存在,则直接调用beforeMount生命周期钩子函数,如果不存在,则通过createEmptyVNodeCompile template into render function Or compile el's outerHTML as template。

下一步就是看createEmptyVNode是如何做到compile something into render function的。

// src/core/vdom/vnode.js

export const createEmptyVNode = (text: string = '') => {
  const node = new VNode()
  node.text = text
  node.isComment = true
  return node
}
复制代码

createEmptyVNode通过new VNode()返回了VNode实例

VNode是一个很长的class,这里只放VNode的constructor作参考

constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }
复制代码

事实上VNode是Vue虚拟的DOM节点,最后这个虚拟DOM节点被挂载到vm.$options.render,到这里

 vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  callHook(vm, 'beforeMount')
复制代码

唤起生命周期钩子函数beforeMount,正式进入beforeMount to mounted的阶段

现在要做的是Create vm.$el and replace "el" with it

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
复制代码

倒着来找callHook(vm, 'mounted')的触发,在这之前,做了这么几件事

  • 定义updateComponent,后被Watcher使用
  • 调用构造函数Watcher产生新实例
  • 判断vm.$vnode 是否为null,如果是,则callHook(vm, 'mounted')

src/core/observer/watcher.js里可以找到Watcher的定义,这里展示它的constructor

    constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.computed = !!options.computed
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.computed = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.computed // for computed watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = function () {}
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    if (this.computed) {
      this.value = undefined
      this.dep = new Dep()
    } else {
      this.value = this.get()
    }
  }
复制代码

(累死我了,要休息一哈,第一次写,对于部分细节是否要深入把握不好。深入的话太深了一个知识点要讲好多好多好多,可能一天都说不完。讲太浅了又觉得啥干活都没有,不好把握各位谅解。要是有错误的望各位提出来,我也算是抛砖引玉了)

转载于:https://juejin.im/post/5aeab3b5518825672565b469

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值