探一探源码/new Vue发生了什么(一)

发布于 / Front End / 1 条评论

一、数据的初始化

这部分涉及到data数据的初始化的过程,以及后续涉及到的双向绑定的过程。

首先我们知道Vue的源头实际上是一个用Function实现的类。源码地址:vue/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)
}

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

export default Vue

当我们new Vue时,最后实际上执行的是this._init(options),而_init这个方法实际上是initMinin(Vue)初始化的,我们来看看这个函数。

vue/src/core/instance/init.js

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

这个函数中我们把目光对焦到initstate函数上面,这个函数实际上处理的就是数据绑定相关的逻辑。

vue/src/core/instance/state.js

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

一层一层下来我们终于看到了这个最关键的函数,initData

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}          //这一段代码就解释了为什么data是一个function,这里要注意vm._data就是在此时被赋值的
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {   //这段循环确保了methods和props中不会有和data中重名的逻辑
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)    //进行了代理劫持
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

相关逻辑我已经注释在代码上,总而言之,initData解释了一些现象。

1、访问this.somedata时实际上访问的是this._data.somedata亦或者说是vm._data.somedata。

2、methods和props中的名字为什么不能和data中的变量名重名的问题。

3、访问data时被Vue自己的proxy劫持getter/setter。(use defineProperty,后续会提到)

ok,那么我们现在重点看一下这个proxy。

函数调用了proxy(vm, `_data`, key)

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

代码含义实际上很简单,当访问_data中的变量时就被sharedPropertyDefinition自己的get和set劫持了。

二、Vue实例挂载的实现

回到最初vue/src/core/instance/index.js中声明Vue函数的时候最后一句

if (vm.$options.el) {
      vm.$mount(vm.$options.el)
 }

这句就是挂载的入口了。mount方法实际上在Vue Runtime版本中有事先的定义

vue/src/platforms/web/entry-runtime-with-compiler.js

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,   //传入的可以是字符串或者dom
  hydrating?: boolean
): Component {
  el = el && query(el)   //query方法实际上就是一个dom元素的selector

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) { //确保不是挂载到body或者html标签下,否则覆盖。。
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {   //如果没有render函数的分支
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {    //传入的template如果是一个dom的话那就拿innerHTMl
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {   //如果没有template则给他套一层div之后在返回
      template = getOuterHTML(el)
    }
    if (template) {    //后续就是template编译成render函数的过程
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

相关解释我也注释在代码上了,根据这段代码我们可以解释官网LifeCycle的图了:

1、Vue会判断是否直接传入了render函数,如果没有则判断是否有template。

2、有template的情况下会直接将template编译成render函数。否则则会在外层套一个div作为template返回。当然,如果传入的template是一个dom的话则取innerHTMl

接下来我们仔细观察一下这个mount函数是怎么运作的。

上面这个函数的最初有这么一句话

const mount = Vue.prototype.$mount

这里实际上是对$mount做了一个缓存。$mount 最初的定义实际上在vue/src/platforms/web/runtime/index.js里,这里附上代码

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

而这段代码中的核心函数mountComponent则在lifecycle.js这个文件里,这里附上注释代码。

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {                      //如果template没有编译成render函数则走入这个warning分支
    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) {        //这种情况实际上代表着检测到template但未被编译,可能是Runtime-only的仅运行版本的问题
        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的hook函数

  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,后续需要用到
    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, {              //new了一个渲染Watcher,
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        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
}

上述代码我觉得需要特别关注的是new了一个Watcher,这个watcher的具体构造函数可以去src\core\observer\watcher.js查看,实际上这个就是所谓的观察者模式了,传入的第二个参数updateComponent函数并不是只有在初始化mount的时候执行,就像他的名字一样,一旦数据发生了改变,就会执行这个更新函数,也就是这个背后是有一个依赖收集的逻辑的,具体的我们后续分析。我们现在聚焦到这里最重要的一行代码。

vm._update(vm._render(), hydrating)

篇幅原因, render函数看下一章节。

转载原创文章请注明,转载自: 静沐暖阳 » 探一探源码/new Vue发生了什么(一)
  1. avatar

    大佬