Vue源码阅读(一):Vue构造函数与初始化过程

--本文采自本人公众号【猴哥别瞎说】vue

接着前文的准备。咱们知道:在 Vue2.6.10 的源码结构中,入口文件是在 src/platforms/web/entry-runtime-with-compiler.js。那么咱们就具体来看看里面的代码吧。node

在初次看源码的时候,有两个值得注意的点:web

  1. 不要抠细节,把握总体的方向,以囫囵吞枣的方式看便可。
  2. 制定阅读的目标,有重点地去看

浏览器

咱们先来看看咱们这次的目标:Vue的构造函数到底在哪里?具体的初始化过程又是怎样的?bash

追踪Vue构造函数

咱们先来看 entry-runtime-with-compiler.js 文件的核心部分:并发

//将$mount函数进行拓展,对用户输入的$options的template/el/render进行解析、处理
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  //处理用户自定义选项,可知执行顺序是 render > template > el
  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.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) {
        //传进来的是dom
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      //若是只设置el,那么就会直接获取其中的元素
      //值得注意的是,会将元素自己覆盖
      template = getOuterHTML(el)
    }
    if (template) {
      //获取render函数
      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
    }
  }
  //执行原先的挂载函数
  return mount.call(this, el, hydrating)
}
复制代码

看代码知道,这个文件仅仅是对已有的 mount 函数作了加强:将用户输入的自定义模板选项,进行处理并发挥render函数。dom

从这里能够得出的结论是:ide

1.若是参数中同时存在el、render、template变量,其优先级顺序为:render > template > el。函数

2.编译发生的时间段(compileToFunctions,将模板转化为render函数)在整个初始化过程当中很是早的,在执行具体mount函数以前。oop

这里并非真正的 Vue 构造函数所在。咱们接着看 ./runtime/index.js 文件:

// install platform patch function
//定义补丁函数,这是将虚拟DOM转换为真实DOM的操做,很是重要
Vue.prototype.__patch__ = inBrowser ? patch : noop

// public mount method
//定义挂载函数
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  //这才是真正的挂载函数
  return mountComponent(this, el, hydrating)
}
复制代码

结果发现,该文件仅仅是定义了原型链上的__patch__函数以及$mount函数。而详细的挂载过程还不在这里。具体细节咱们先不看。

继续找:core/index.js

//初始化全局API
initGlobalAPI(Vue)
复制代码

结果依然仍是失望的。不由感慨:这个俄罗斯套娃也太多层了吧。在core/index.js内部,也仅仅只是初始化了全局API而已。这个并非今天咱们关注的重点,仍是继续找:src/core/instance/index.js :

function Vue (options) {
  this._init(options)
}

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

在这里,终于找到了Vue的构造函数!!!与此同时,咱们也发现:该文件执行了许多 Mixin 操做。这些详细的部分咱们先不理会,咱们先来看看构造函数中的 this._init() 到底作了什么事情呢?

能够经过浏览器调试的方式,也能够经过全局搜索prototype._init的方式,找到_init()的所在文件:src/core/instance/init.js。

//初始化顺序:生命周期->事件监听->渲染->beforeCreate->注入->state初始化->provide->created
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm) // 初始化 props/data/watch/methods, 此处会是研究数据响应化的重点
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    //若是存在el元素,则会自动执行$mount,这也是必需要理解的
    //也就是说,在写法上若是有el元素,能够省略$mount
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
复制代码

从这里能够得出的结论是:

1.在_init()函数中,咱们看到vue实例初始化时候的执行顺序:生命周期->事件监听->渲染->beforeCreate->注入数据inject->组件状态初始化->提供数据provide->created。

2.若是存在el元素,则会自动执行挂载。

若是咱们想要了解数据响应化的细节,那就应该详细去看initState()函数。它会是咱们下节课的重点。

在这个时候,咱们找到了主要的文件脉络。至于具体的初始化过程,咱们还须要深刻去看$mount过程当中作了什么。因而咱们回到真正的挂载函数mountComponent()函数里面去看看,其中发生的挂载细节:

//这才是真正的mount函数
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  ...
  callHook(vm, 'beforeMount')
  //核心代码逻辑
  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    ...
  } else {
    updateComponent = () => {
      //更新Component,主要作了两个事情:render(生成vdom)、update(转换vdom为dom)
      vm._update(vm._render(), hydrating)
    }
  }

  // 在此处定义Watcher(一个Vue实例对应的是一个Watcher),而且与updateComponent关联起来
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false
  
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}
复制代码

在上面的代码中,咱们看到,在vue实例挂载的过程当中,会新建一个 Watcher。这个 Watcher 的做用是相似于一个观察者,它若是收到数据发生了变化的消息,那么就会执行 updateComponent 函数。而这个 updateComponent 函数,主要作了两个事情:render(生成 vdom)、update(转换 vdom 为 dom)。

综上,梳理为如下的流程图:

屏幕快照 2019-11-22 下午10.29.06.png-58.9kB

那么,Watcher 的工做原理是怎样的,它是如何在整个数据响应化的过程当中发挥做用的?下篇文章将会给你答案!

相关文章
相关标签/搜索