2021年9月15日

vue2 源码解读

作者 theluyuan

从今天开始将自己读一下vue的源码,也当做是学习了,完全读完之后会重新整理,所以现在看不懂也不要怕,当整完vue2之后我会重新整vue3。

为什么不直接弄vue3呢,因为我考虑了一下,现在vue2还是主流,所以还是用2吧,当我读完的时候3应该就是主流了,哈哈哈

克隆代码

git clone https://github.com/vuejs/vue.git

打开

目录结构目前我们还是不清楚,所以我们从构造函数开始,一直到运行结束。

入口文件

根据build中config.js我们得知 入口文件是在 src\platforms\web\entry-runtime-with-compiler.js

然后最初的是在 src\core\instance\index.js

function Vue (options) {
    // 判断是不是生产环境 和判断是不是通过new 调用的 如果不是发出警告
  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) 文件地址在 src\core\instance\init.js

主要作用就是给vue原型上添加 _init方法

export function initMixin (Vue: Class<Component>) {

  Vue.prototype._init = function (options?: Object) {
    // 将vm 指向 this
    const vm: Component = this
    // a uid
    // 将vm的_uid = uid++
    // 这个uid 是一个全局变量
    // 应该是每执行一次就有一个唯一的_uid 这个目前作用是性能检测使用的
    vm._uid = uid++

    // 作为性能检测的开始标签 和 结束标签
    let startTag, endTag
    /* istanbul ignore if */
    // 判断是不是生产环境 
    // 判断 config 中的 performance 这个默认是false
    // 设置为 true 以在浏览器开发工具的性能/时间线面板中启用对组件初始化、编译、渲染和打补丁的性能追踪。只适用于开发模式和支持 performance.mark API 的浏览器上。
    // 然后判断是不是有make 这个判断了浏览器支不支持性能检测 如果不支持是空
    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

    // 合并option
    // 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)
    }
  }
}

目前下面先停一下 里面好多参数都是不知道的 还是从 new Vue开始

然后我们就准备开始最开始做了什么东西

const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})

最开始执行了一下这个

这个函数在src\shared\util.js

/**
 * Create a cached version of a pure function.
 */
export function cached<F: Function> (fn: F): F {
  const cache = Object.create(null)
  return (function cachedFn (str: string) {
    const hit = cache[str]
    return hit || (cache[str] = fn(str))
  }: any)
}

这个详细看这篇文章 缓存函数起到什么/多大的作用?

我理解的这个作用是缓存传入的id 如果之前获取过就直接返回,如果没有获取过就运行获取然后缓存返回

然后接下来是

const mount = Vue.prototype.$mount

这个是将现在的$mount保存到mount这个变量中

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

这是原始的$mount

inBrowser这个变量在src\core\util\env.js

export const inBrowser = typeof window !== 'undefined'

判断了一下window是不是存在 存在就是在浏览器中

然后看一下query这个方法

文件位置在src\platforms\web\util\index.js

/**
 * Query an element selector if it's not an element already.
 */
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
      )
      // 然后返回一个新的div
      return document.createElement('div')
    }
    // 选择到了就会返回选择到的
    return selected
  } else {
    // 如果不是字符串那么直接返回那个元素
    return el
  }
}

这个函数主要作用就是找到传入的元素

然后再看一下 mountComponent

这个稍微有点长 文件位置src\core\instance\lifecycle.js


export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  // 把传进来的vm里面的$el = el
  // 就是把组件的$el属性指向组件的标签
  vm.$el = el
  // 判断一下有没有 render这个
  // render 是vue的渲染函数
  if (!vm.$options.render) {
    // 没有的话将render 赋值 createEmptyVNode 貌似就是通过文本生成渲染函数 他目前在 src\core\vdom\vnode.js
    vm.$options.render = createEmptyVNode
    // 判断一下当前环境
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      // 判断一下template,如果值以 # 开始,则它将被用作选择符,并使用匹配元素的 innerHTML 作为模板。常用的技巧是用 <script type="x-template"> 包含模板。 这是官网解释
      // 或者判断有没有el则个属性 再或者有传入的节点
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        // 警告 您使用的是Vue的仅运行时版本,其中模板编译器不可用。请将模板预编译为渲染函数,或使用包含编译器的版本。
        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
        )
      }
    }
  }
  // 运行 beforeMount 钩子
  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 && !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
}