【vue源码工程学习2:跟踪入口文件】定义mount挂载函数

已被阅读 577 次 | 文章分类:日常随笔 | 2022-04-08 00:57

阅读源码基本步骤是找到入口文件,然后一步步往前追踪代码,看每个模块的导入导出文件;所以本文也按照这个逻辑记录;在记录过程中会尽量注释源码。 注:本节就先阅读如何找到Vue对象的导出及挂载函数的定义

1 入口脚本

目的:能找到源码中在哪里导出vue类;及如何定义$mount挂载函数

                                            
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
    </div>
    <script src="./vue.min.js"></script>
    </script>
    <script>
         const app = new Vue({   
            render: (h) => {
                return h('h', 'hello render')
            }
        }).$mount('#app')
    </script>
</body>
</html>
                                            
                                        

找到第一条脚本:

                                            
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",

// scripts/config.js:是入口文件;

// TARGET:web-full-dev"是目标 命令;
                                            
                                        

我们找到scripts文件夹下的config.js;查看他最终导出的对象是什么

如果target存在,导出对应的模块配置参数;如果不存在全部导出;

                                            
// 如果命令中 target参数有值
if (process.env.TARGET) {
  // 根据目标名称导出模块
  module.exports = genConfig(process.env.TARGET)
} else {
  exports.getBuild = genConfig
  exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}
                                            
                                        

genConfig函数:从builds根据name获取,最后返回config

                                            
function genConfig (name) {
  const opts = builds[name]
  const config = {
    input: opts.entry,
    external: opts.external,
    plugins: [
      flow(),
      alias(Object.assign({}, aliases, opts.alias))
    ].concat(opts.plugins || []),
    output: {
      file: opts.dest,
      format: opts.format,
      banner: opts.banner,
      name: opts.moduleName || 'Vue'
    },
    onwarn: (msg, warn) => {
      if (!/Circular/.test(msg)) {
        warn(msg)
      }
    }
  }
 ....省略
 return config

}
                                            
                                        

builds对象:根据name获取打包的参数配置;dev传入的target值是"web-full-dev";

                                            
const builds = {
  // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
  'web-runtime-cjs-dev': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.common.dev.js'),
    format: 'cjs',
    env: 'development',
    banner
  }, 
 // 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
  }
....省略
}
                                            
                                        

从上面可以看出返回的是roolup打包的必要参数;如入口文件、打包目标目录、format等等; 所以执行 yarn dev命令会在dist目录下生成一个vue.js的文件;

/net/upload/image/20220408/8064ddc8273147a7b97ec4fe2e6bf079.png

2 入口文件

上面了解了入口脚本怎么获取打包命令的参数;其中有个入口文件并没看如何获取;

上面的入口文件是通过下面的方式获取

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

第一步 看一下resolve函数怎么定义

                                            
// 获取别名模块
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)
  }
}
                                            
                                        

看到resovle函数就是根据传入的别名路径,找到别名对应的具体模块;别名和模块的对应关系就存在alias这个类中;

第二步 看一下alias类;仍然在scripts文件夹下

                                            
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别名对应的就是src/platforms/web目录下的entry-runtime-with-compiler.js模块;我们继续跟踪

第三步 打开entry-runtime-with-compiler.js文件,看到export default Vue;终于看见了熟悉的Vue对象;

/net/upload/image/20220408/6215d093f56f487680c65858b37c5dda.png

也就是说我们的Vue对象在该模块中定义并导出;所以该模块必须是第一个被引入的;后面的模块需要在该模块后面添加其他的vue内容;

3 定义挂载函数

查看entry-runtime-with-compiler.js,主要的一个作用定义了mount挂载函数,挂载函数内部主要获取了el元素,并定义了render渲染函数;代码如下

                                            
// 定义挂载函数 
Vue.prototype.$mount = function (){
...省略
 return mount.call(this, el, hydrating)
}
                                            
                                        
                                            
// 获取el模板并转成渲染函数
  // 1 优先看有没有render函数,如果有直接用
  if (!options.render) {
    let template = options.template
    //  2 如果没有render函数就看有没有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 = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      // 3 如果都没有就直接获取el的outerHTML作为渲染模板
      template = getOuterHTML(el)
    }
    // 如果template存在;用 template 生成 render 函数
    if (template) {
      /* 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')
      }
    }
    // 综上直接传render编译效率会高一些
  }
                                            
                                        

到这里el的获取,vue挂载函数的定义就结束了;

注:在定义render函数时,可以看到如果传入render函数,那么后面通过模板或者直接获取el内容的代码就不用执行;所以由此可见在创建vue对象时,传入render函数比template编译效率要高。

QQ:3410192267 | 技术支持 微信:popstarqqsmall

Copyright ©2017 xiaobaigis.com . 版权所有 鲁ICP备17027716号