新人看Vue2源码(一)

1,557 阅读4分钟

前言

转眼一过去,从实习到转正已经过去七八个月了,工作的内容感觉越来越枯燥,每天的搬砖让自己的身体被掏空 自己的思维僵化,继续这样下去我感觉我就废了,所以出这么一篇文章是为了一边强迫自己去思考,一边去调整自己的状态。

另外,新人理解源码不一定正确,还希望各位大佬指点一下。

准备工作

项目地址: https://github.com/vuejs/vue.git

版本: 2.6.14

项目结构:

image.png

image.png

稍微提一嘴:

我们知道.vue文件经过webpackvue-loader打包后会变成js对象,这个对象里会多出一个render的属性,这个属性来自于.vue文件的template。编译器就是干的这种事。

再来看package.json:

我们会发现 vue 是用 rollup 进行打包的,所以我们需要全局安装 rollup,接着再安装依赖,最后运行npm run dev即可。 [注: 我在dev中添加了--sourcemap, 为了之后debug可以看到在源文件的哪个位置]

{
  "name": "vue",
  "version": "2.6.14",
  "main": "dist/vue.runtime.common.js",
  "module": "dist/vue.runtime.esm.js",
  "sideEffects": false,
  "scripts": {
    "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",
    "dev:cjs": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-cjs-dev",
    "dev:esm": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-esm",
    ...
  },
}

可以看到,npm run dev会执行scripts/config.js的文件,而且还指定了参数web-full-dev

scripts/config.js

const aliases = require('./alias')
const resolve = p => {
  const base = p.split('/')[0]
  if (aliases[base]) {
    return path.resolve(aliases[base], p.slice(base.length + 1))
  } else {
    return path.resolve(__dirname, '../', p)
  }
}
const builds = [
  ...
  // Runtime+compiler development build (Browser)
  'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.js'),
    format: 'umd',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },
  ...
]

alias.js

const path = require('path')

const resolve = p => path.resolve(__dirname, '../', p)

module.exports = {
  vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
  compiler: resolve('src/compiler'),
  core: resolve('src/core'),
  shared: resolve('src/shared'),
  web: resolve('src/platforms/web'),
  weex: resolve('src/platforms/weex'),
  server: resolve('src/server'),
  sfc: resolve('src/sfc')
}

很明显,这个 web/entry-runtime-with-compiler.js 就是入口文件了,可是回顾一下我们的目录,好像也没 web 这个文件夹。于是我们注意到 resolve 这个函数应该是有做相关的处理,然后分析一下代码:

resolve('web/entry-runtime-with-compiler.js')

-> base = 'web'; 

-> alias[web] = 'src/platforms/web';

-> path.resolve('src/platforms/web/entry-runtime-with-compiler.js')

image.png

Ohhhhh!!!终于找到入口了!!终于可以结束这漫长的准备工作了。

entry-runtime-with-compiler.js

一进来就看到 mount 这个函数被改写了,what's up??

冷静的一波分析发现,这原来是跟 render 有关,不知道大家会不会好奇如果传入的 options 中有 el, render, el 这几个选项的时候,会先处理哪个?代码中就清晰的表明了顺序。

/* @flow */

import Vue from "./runtime/index";

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

//  扩展$mount方法
const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el);

  const options = this.$options;
  // resolve template/el and convert to render function

  //  先查看是否有render选项,有的话直接mount
  //    没有的话,查看是否有template选项
  //      有且template是以#开头(说明是id选择器),取其innerHTML
  //      有且template是一个元素,取其innerHTML
  //      否则直接return 实例
  //
  //    没有的话,查看是否有el选项
  //      有且el有outerHTML,取其outerHTML
  //      有且el无outterHTML,则创建一个div容器包裹它,取容器的innerHTML
  //    处理完之后template = getHTML(el)

  //    通过以上步骤,存在template选项则编译它,目的是获取render函数

  if (!options.render) {
    let template = options.template;
    if (template) {
      if (typeof template === "string") {
        if (template.charAt(0) === "#") {
          template = idToTemplate(template);
        }
      } else if (template.nodeType) {
        template = template.innerHTML;
      } else {
        return this;
      }
    } else if (el) {
      template = getOuterHTML(el);
    }
    if (template) {
      //  编译,将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);
};

/**
 * Get outerHTML of elements, taking care
 * of SVG elements in IE as well.
 */
function getOuterHTML(el: Element): string {
  if (el.outerHTML) {
    return el.outerHTML;
  } else {
    const container = document.createElement("div");
    container.appendChild(el.cloneNode(true));
    return container.innerHTML;
  }
}

Vue.compile = compileToFunctions;

export default Vue;

总结:

  • 打包的入口
  • 处理编译

src/platforms/web/runtime/index.js

上面我们留意到 Vue 是从本文件中被引入的,所以这里应该有 Vue 的相关信息,我们接着来看一下吧。

/* @flow */

import Vue from 'core/index'
import config from 'core/config'
import { mountComponent } from 'core/instance/lifecycle'


// install platform patch function
// 安装更新函数,它的作用是将vdom转换为dom 
Vue.prototype.__patch__ = inBrowser ? patch : noop

// public mount method
//  实现一个$mount方法,调用mountComponent
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}


export default Vue

总结:

  • 安装补丁函数,实现vdom -> dom
  • 实现$mount

src/core/index

上面文件的 Vue 是从本文件中引入的,这次都翻到 core 里面了,总该到头了吧,快让我们康康!

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'

//  初始化全局的api: Vue.use/component/set...
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

总结:

  • 初始化全局api

src/core/instance/index

淦,都翻到 instance 了,总该到头了吧。。

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')
  }
  //  初始化,不过这_init是哪来的???
  this._init(options)
}

//  初始化实例方法和属性
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

疑惑:

  • 构造函数中的 _init 方法是哪来的?

总结:

  • Vue构造函数的声明
  • 初始化实例的属性和方法

src/core/instance/init

抱着疑问我点开了 initMixin 定义的地方,在这里找到了答案。

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


    // 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;

    //  声明周期相关的属性初始化$parent
    initLifecycle(vm);

    //  自定义组件事件的监听
    initEvents(vm);

    //  插槽处理,$createElement
    initRender(vm);

    //  调用声明周期的钩子函数
    callHook(vm, "beforeCreate");

    //  下面是组件数据和状态初始化

    initInjections(vm); // resolve injections before data/props

    //  data/props/methods/computed/watch
    initState(vm);

    initProvide(vm); // resolve provide after data/props

    callHook(vm, "created");

    //  有el选项执行挂载
    if (vm.$options.el) {
      vm.$mount(vm.$options.el);
    }
  };
}

总结:

  • 定义了Vue实例的初始化过程
  • 按顺序执行了初始化操作
    • initLifecycle - 初始化了与生命周期相关的属性,$parent/$root/$ref/$children
    • initEvents - 初始化自定义事件监听的相关属性以及函数
    • initRender - 插槽以及$createElement的处理
    • callHook(vm, "beforeCreate"); - 调用beforeVreate
    • initInjections - 注入Injection
    • initState - 初始化data/props/methods/computed/watch
    • initProvide - 处理Provide选项
    • callHook(vm, "created") - 调用created
    • 如果选项中有el,调用$mount

小结

通过上面一连串的追踪,我们基本了解到了 new Vue 的过程,这时候就能了解到一些常见的问题的答案:


问:new Vue({el: "#app"}) 的时候做了什么?

答:执行初始化操作 -> 得到一个根实例 -> 执行挂载函数 -> 得到渲染函数并执行 -> 生成vdom -> 通过patch函数将vdom 转成dom -> 将dom append#el


问: 当选项中同时出现elrendertemplate选项的时候,会优先执行哪一个?

答:render > template > el,优先执行render函数


问:beforeCreatecreated中,能访问 injectiondata吗?

答:beforeCreate中两个都无法访问,created中都能访问