Vue源码(一)

目录

今天开始,我将和大家一起探索vue源码,大家一起学习!
首先去git下载一份vue,我这里是v2.6.9版本的,如果要跟着一起分析的话,推荐使用同一版本!
刚下载的vue的目录结构是这样的
在这里插入图片描述

  • dist是打包生成的各种版本的vue,分为AMD,UMD,CMD,以及CommonJS四种打包格式
  • examples是一些测试用例
  • flow 是vue2用来进行静态类型检查的,这里文件是对flow语法的支持
  • types 是对ts的支持
  • packages是一些单独切出来的包,像weex
  • scripts 是关于打包的一些配置文件
  • src 就是源代码了

关于入口

很多人想看源码,但是不知道从哪一块下手,所以这里我说一下我是怎么找入口的。
首先dist文件夹中有vue.js,这是已经被打包好的js文件,src中的所有js代码都合并到了这里,我们直接去看这个vue.js肯定是会懵逼的,1w多行跳来跳去的,所以我们是不是要找到打包的入口?
那么让我们进入到scripts文件夹,因为这是打包的相关配置文件夹
在这里插入图片描述

我们这里先看build.js
看10行左右的这里 可以看到引入了config.js中所有的配置

// 把同级目录下config的所有创建配置导入
let builds = require('./config').getAllBuilds()

27行左右开始递归打包

// 开始打包
build(builds)

既然引入了配置,那我们就要进入到config.js看配置了
看38行左右

// dist目录中各种打包方式的定义
const builds = {} //里面是各种打包方式配置

我们这里可以直接ctrl+f搜索vue.js,找到dest中为vue.js的那个打包配置
这样就来到了120行左右

 // 浏览器要跑的版本
  // Runtime+compiler development build (Browser)
  'web-full-dev': {
    // entry-runtime-with-compiler.js入口函数
    entry: resolve('web/entry-runtime-with-compiler.js'),
    // 最后输出为vue.js
    dest: resolve('dist/vue.js'),
    format: 'umd',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },

可以看到输出为dist下面的vue.js,那么入口函数当然就是web/entry-runtime-with-compiler.js
显然,这个web是一个别名,我们可以通过alias.js去查找这个别名对应的目录

进入alias.js
可以在第10行看到

 web: resolve('src/platforms/web'),

web别名对应着src/platforms/web
那么入口函数的路径已经出来了src/platforms/web/entry-runtime-with-compiler.js

接下来我们根据路径打开这个js文件

路径:vue-2.6.9\src\platforms\web\entry-runtime-with-compiler.js

这次分享就分析这一个js文件
将其几个代码块折叠一下,可以看到这文件其实也就3个函数:

  1. idTotemplate
  2. $mount
  3. getOuterHTML

在这里插入图片描述

$mount逐行分析

这里是不是就遇到了我们平时见到的$mount,那我们就从这个$mount开始分析

// 扩展$mount 保存老的$mount 老的$mount也会执行以前的操作
const mount = Vue.prototype.$mount
// 进行的新的mount操作
Vue.prototype.$mount = function (
  // 传入el,也就是挂载的元素节点
  el?: string | Element,
  // todo: 等待分析 涉及服务端渲染
  hydrating?: boolean
): Component {
  // 获取el节点
  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 <html> or <body> - mount to normal elements instead.`
    )
    return this
  }
  // 获取配置的一些选项,也就是render template el那些
  // 可从代码中得到一些选项的优先级:render>template>el
  const options = this.$options
  // resolve template/el and convert to render function
  // 如果不存在render函数,就将template/el的设置转换为render函数
  // render优先级非常高了,这些操作都是在没有render的情况下进行的
  if (!options.render) {
    // 获取template
    let template = options.template
    // 如果有template
    if (template) {
      // string "#app"这类的
      if (typeof template === 'string') {
        // #开头
        if (template.charAt(0) === '#') {
          // 将进入idToTemplate idToTemplate是接收#app这类,返回对应节点的innerHTML
          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元素 document.querySelector()
        // 获取到这段内容,也就获取到了节点
        template = template.innerHTML
      } else {
        // 忽略
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
      // 如果template不存在,获取el
    } else if (el) {
      // <div id="app"></div>
      // 调用getOuterHTML,获取包括标签的内容
      template = getOuterHTML(el)
    }
    // 这里对拿到的template进行编译
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }
      // 如果是模板字符串,需要编译器去编译  也就是进入compileToFunctions这个函数
      // 可以通过这个函数查看编译器的工作机制,也就是把template转换为render:todo
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      // 赋值给当前选项的render
      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')
      }
    }
  }
  // 执行老mount的操作 正常的挂载渲染过程
  return mount.call(this, el, hydrating)
}

很多解释已经在代码里敲好了,这里总结一下:

  1. 首先会保存老的$mount,然后进入函数,获取el和options
  2. 判断options.render是否存在,如果存在直接执行老$mount,中途那些东西都是在options.render不存在时执行的,因此可以看出render的优先级非常高了
  3. 然后当options.render不存在时,就会先判断template,再判断el,优先级由此可见:render>template>el
  4. template会检测#app,document.querySelector(’#app’),分别进行对应操作
  5. el会检测 <div id=“app>”</div>,进行对应操作
  6. 当然template还会被compile解析,这里先留个todo,之后再去看compile是怎么将template转化为render函数的

这是我画的一个贼丑的图,描述了一些过程
在这里插入图片描述

idToTemplate函数

路径:当前目录

// 根据id查询到el 并返回innerHTML
const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})

getOuterHTML函数

路径:当前目录

// 获取包括标签的内容
function getOuterHTML (el: Element): string {
  // 如果存在,直接使用outerHTML
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    // 不存在就创建div
    const container = document.createElement('div')
    // 将el深复制一份加入div
    container.appendChild(el.cloneNode(true))
    // 返回这个div
    return container.innerHTML
  }
}

query函数

路径:vue-2.6.9\src\platforms\web\util\index.js

/* @flow */

import { warn } from 'core/util/index'

export * from './attrs'
export * from './class'
export * from './element'

/**
 * Query an element selector if it's not an element already.
 */
// 获取形式为“#app”的el的节点元素
export function query (el: string | Element): Element {
  // 如果为字符串 “#app”
  if (typeof el === 'string') {
    // 通过#app 获取到元素节点
    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 {
    // 如果不是字符串 "#app"形式,直接返回el
    return el
  }
}

总结

总结一下:这个入口函数,最主要的还是实现了$mount这一个函数。
下一次分享,我们将进入到vue初始化的那里去探索,当然也是通过这个入口文件去找到。怎么去找到vue初始化那里,先留个悬念在这。

猜你喜欢

转载自blog.csdn.net/qq_46299172/article/details/107532084