手把手教你读Vue2源码-1

vue源码:https://github.com/vuejs/

这里,我的调试环境为:
window10 x64
vue2.5

今天的目标:搭建调试环境,找入口文件

要学习源码,就要先学会如何调试源码,所以我们第一步可以先拉取vue源码,然后在本地配置下调试环境:

cd ./vue
npm i
npm npm i -g rollup
// 修改package.json中script中dev运行脚本,添加--sourcemap
"dev": "rollup -w -c build/config.js --sourcemap --environment TARGET:web-full-dev",

npm run dev

// 之后就会在dist目录下生成vue.js,方便我们用于测试和调试了~

配制调试环境的坑

  1. 安装过程可能提示找不到PhantomJS:
    PhantomJS not found on PATH
    Downloading https://github.com/Medium/phantomjs/releases/download/v2.1.1/phantomjs-2.1.1-windows.zip Saving to C:\Users\ADMINI~1\AppData\Local\Temp\phantomjs\phantomjs-2.1.1-windows.zip Receiving...
    根据提示,去下载压缩包,放到对应位置就好

  2. 提示以下错误
    Error: Could not load D:\vue-source\vue\src\core/config (imported by D:\vue-source\vue\src\platforms\web\entry-runtime-with-compiler.js): ENOENT: no such file or directory, open 'D:\vue-source\vue\src\core/config'
    查了下资料,说是rollup-plugin-alias插件中解析路径的问题,有人提PR了(https://github.com/vuejs/vue/issues/2771),尤大大说是没有针对window10做处理造成的,解决方法是将 node_modules/rollup-plugin-alias/dist/rollup-plugin-alias.js 改为

// var entry = options[toReplace]
// 81行,上面那句,改为:
var entry = normalizeId(options[toReplace]);

打包后dist输出的文件一些后缀说明

dist目录.png
  • 有runtime字样的:说明只能在运行时运行,不包含编译器(也就是如果我们直接使用template模板,是不能正常编译识别的)
  • common:commonjs规范,用于webpack1
  • esm:es模块,用于webpack2+
  • 没带以上字样的: 使用umd,统一模块标准,兼容cjs和amd,用于浏览器,也是之后我们要用于测试的文件

我们可以在test文件夹下,创建我们的测试文件test1.html:


创建测试文件test1.html


  


  

初始化流程

{{msg}}

找入口文件

  1. 在package.json中,dev运行脚本中找配置文件(-c 指向配置文件):rollup -w -c build/config.js --sourcemap --environment TARGET:web-full-dev
  2. 进入配置文件中,根据TARGET找到对应的配置文件TARGET:web-full-dev,搜索这个环境,让到对应的entry入口文件
// 1. build/config.js根据target环境,来找entry入口
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
  },
}

// 2. 查看resolve解析方法,从中看出web是在别名文件中有对应地址
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)
  }
}

// 3. 根据aliases找到alias.js文件,从中找到web对应的相应地址
module.exports = {
  web: resolve('src/platforms/web'),
}

// 4. 最后根据拼接规则,我们终于找到真正的对应入口
src/platforms/web/entry-runtime-with-compiler.js

查看入口文件

带个问题去看源码,以下这个vue实例中,最终挂载起作用是的哪个?

// render,template,el哪个的优先级高?
const app = new Vue({
  el: "#demo",
  template: "
template
", render(h) { return h('div', 'render') }, data: { foo: 'foo' } }) // 答案:render > template > el

可以从源码找答案,在主要的地方我已添加中文注释(英文注释是源码本身的),可查看对应注释地方:

// 保存原来的$mount
const mount = Vue.prototype.$mount
// 覆盖默认的$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  // 如果看到有以下这样注释的,一般用于调试阶段输出一些警告信息,我们在学习时为了简单点,可以直接忽略的部分
  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to  or  - mount to normal elements instead.`
    )
    return this
  }

  // 从这里开始,解析我们的配置
  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)
          //...
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }

    // 如果存在模板,执行编译
    if (template) {
      // ...

      // 编译得到渲染函数
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

    }
  }
  // 最后执行挂载,可以看到使用的是父级原来的mount方式挂载
  return mount.call(this, el, hydrating)
}

从以上1,2,3步骤中,我们就可以得出刚刚的答案了。

src/platforms/web/entry-runtime-with-compiler.js文件作用:入口文件,覆盖$mount,执行模板解析和编译工作

找Vue的构造函数

这里主要找Vue的构造函数,中间路过一些文件会写一些大概的作用,但主线不会偏离我们的目标

  1. 在入口文件entry-runtime-with-compiler.js中,可以查看Vue引入文件
import Vue from './runtime/index'
  1. /runtime/index.js文件
import Vue from 'core/index'

// 定义了patch补丁:将虚拟dom转为真实dom
Vue.prototype.__patch__ = inBrowser ? patch : noop

// 定义$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)
}
  1. core/index.js文件
import Vue from './instance/index'

// 定义了全局API
initGlobalAPI(Vue)
  1. src/core/instance/index.js文件
    终于找到了Vue的构造函数,它只做了一件事,就是初始化,这个初始化方法是通过minxin传送到这个文件的,所以我们接下来是要去查看Init的方法,这也是我们以后要常看的一个文件
// 构造函数
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)  // _init方法是通过mixin传入的,从这里可以找到初始化方法
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

初始化方法定义的文件 src/core/instance/init.js

划重点,比较重要,可以看出初始化操作主要做以下事件:

initLifecycle(vm) // 初始化生命周期,声明$parten,$root,$children(空的),$refs
initEvents(vm)  // 对父组件传入的事件添加监听
initRender(vm)  // 声明$slot,$createElement()
callHook(vm, 'beforeCreate') // 调用beforeCreate钩子
initInjections(vm) // 注入数据 resolve injections before data/props
initState(vm) // 重中之重:数据初始化,响应式
initProvide(vm) // 提供数据 resolve provide after data/props
callHook(vm, 'created') // 调用created钩子
// 定义初始化方法
export function initMixin (Vue: Class) {
  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

    // 合并选项,将用户设置的options和vue默认设置的options,做一个合并处理
    // 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
      )
    }
    //...
    
    // 重点在这里,初始化的一堆操作!
    // expose real self
    vm._self = vm
    initLifecycle(vm) // 初始化生命周期,声明$parten,$root,$children(空的),$refs,这里说明创建组件是自上而下的
    initEvents(vm)  // 对父组件传入的事件添加监听
    initRender(vm)  // 声明$slot,$createElement()
    callHook(vm, 'beforeCreate') // 调用beforeCreate钩子
    initInjections(vm) // 注入数据 resolve injections before data/props
    initState(vm) // 重中之重:数据初始化,响应式
    initProvide(vm) // 提供数据 resolve provide after data/props
    callHook(vm, 'created') // 调用created钩子

    // ...

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

如有错误之处,还望指出哈

你可能感兴趣的:(手把手教你读Vue2源码-1)