简介

本文以vue2.x框架作为分析,简单记录整个源码编译的过程。https://zhuanlan.zhihu.com/p/552685329

源码目录

src
├── compiler        # 编译相关
├── core            # 核心代码
├── platforms       # 不同平台的支持
├── server          # 服务端渲染
├── sfc             # .vue 文件解析
├── shared          # 共享代码

compiler

目录包含 Vue.js 所有编译相关的代码。它包括把模板解析成 ast 语法树,ast 语法树优化,代码生成等功能。

core

core 目录包含了 Vue.js 的核心代码,包括内置组件、全局 API 封装,Vue 实例化、观察者、虚拟 DOM、工具函数等等。

platform

Vue.js 是一个跨平台的 MVVM 框架,它可以跑在 web 上,也可以配合 weex 跑在 native 客户端上。platform 是 Vue.js 的入口,2 个目录代表 2 个主要入口,分别打包成运行在 web 上和 weex 上的 Vue.js。

server

Vue.js 2.0 支持了服务端渲染,所有服务端渲染相关的逻辑都在这个目录下。注意:这部分代码是跑在服务端的 Node.js.

sfc

这个目录下只有一个parser.js文件 会把 .vue 文件内容解析成一个 JavaScript 的对象。

shared

Vue.js 会定义一些工具方法,这里定义的工具方法都是会被浏览器端的 Vue.js 和服务端的 Vue.js 所共享的

构建

Vue.js 源码是基于 Rollup 构建的,它的构建相关配置都在 scripts 目录下。

构建的入口文件就是scripts/build.js

通过命令行的区分,找到config.js里面的不同环境和情况下的build集合来构建出不同用途的 Vue.js

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},'web-runtime-cjs-prod': {entry: resolve('web/entry-runtime.js'),dest: resolve('dist/vue.runtime.common.prod.js'),format: 'cjs',env: 'production',banner},// Runtime+compiler CommonJS build (CommonJS)'web-full-cjs-dev': {entry: resolve('web/entry-runtime-with-compiler.js'),dest: resolve('dist/vue.common.dev.js'),format: 'cjs',env: 'development',alias: { he: './entity-decoder' },banner},'web-full-cjs-prod': {entry: resolve('web/entry-runtime-with-compiler.js'),dest: resolve('dist/vue.common.prod.js'),format: 'cjs',env: 'production',alias: { he: './entity-decoder' },banner},// Runtime only ES modules build (for bundlers)'web-runtime-esm': {entry: resolve('web/entry-runtime.js'),dest: resolve('dist/vue.runtime.esm.js'),format: 'es',banner}..................................
}

其实通过别名路径查找alias的都是src目录下面的文件

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

Runtime Only 和 Runtime + Compiler

这两种方式的构建方式都是生成vue项目常见的。分别查找的是src/platforms/web下面的文件

  • Runtime Only

entry-runtime.js里面

import Vue from './runtime/index'export default Vue

我们在使用 Runtime Only 版本的 Vue.js 的时候,通常需要借助如 webpack 的 vue-loader 工具把 .vue 文件编译成 JavaScript,因为是在编译阶段做的,所以它只包含运行时的 Vue.js 代码,因此代码体积也会更轻量。

  • Runtime + Compiler

在entry-runtime-with-compiler.js中,可以发现

import config from 'core/config'
import { warn, cached } from 'core/util/index'
import { mark, measure } from 'core/util/perf'import Vue from './runtime/index'
import { query } from './util/index'
import { compileToFunctions } from './compiler/index'
import { shouldDecodeNewlines, shouldDecodeNewlinesForHref } from './util/compat'
...................//

他的vue来源本质上和Runtime Only是一样的 在这里多做了一层编译的处理而已。

我们如果没有对代码做预编译,但又使用了 Vue 的 template 属性并传入一个字符串,则需要在客户端编译模板,如下所示:

// 需要编译器的版本
new Vue({template: '<div>{{ hi }}</div>'
})// 这种情况不需要
new Vue({render (h) {return h('div', this.hi)}
})

因为在 Vue.js 2.0 中,最终渲染都是通过 render 函数,如果写 template 属性,则需要编译成 render 函数,那么这个编译过程会发生运行时,所以需要带有编译器的版本。显然是Runtime Only性能更佳。

new Vue()的初始化

入口文件

上面分析的vue通过不同构建方式去生成的vue项目,可以追溯到源头vue来源都是在src/core/index.js文件中

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'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

vue构造函数的生成(import Vue from ‘./instance/index’)

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')}this._init(options)
}initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)export default Vue

它实际上就是一个用 Function 实现的类,我们只能通过 new Vue 去实例化它。采用es5的方式是因为需要在vue原型上做许多操作,并且扩展分散到许多模块,便于维护,class类难以实现这个效果。

initGlobalAPI

Vue.js 在整个初始化过程中,除了给它的原型 prototype 上扩展方法,还会给 Vue 这个对象本身扩展全局的静态方法,它的定义在 src/core/global-api/index.js 中:

export function initGlobalAPI (Vue: GlobalAPI) {// configconst configDef = {}configDef.get = () => configif (process.env.NODE_ENV !== 'production') {configDef.set = () => {warn('Do not replace the Vue.config object, set individual fields instead.')}}Object.defineProperty(Vue, 'config', configDef)// exposed util methods.// NOTE: these are not considered part of the public API - avoid relying on// them unless you are aware of the risk.Vue.util = {warn,extend,mergeOptions,defineReactive}Vue.set = setVue.delete = delVue.nextTick = nextTickVue.options = Object.create(null)ASSET_TYPES.forEach(type => {Vue.options[type + 's'] = Object.create(null)})// this is used to identify the "base" constructor to extend all plain-object// components with in Weex's multi-instance scenarios.Vue.options._base = Vueextend(Vue.options.components, builtInComponents)initUse(Vue)initMixin(Vue)initExtend(Vue)initAssetRegisters(Vue)
}

这里就是在 Vue 上扩展的一些全局方法的定义,如平时常见的Vue.set、 Vue.delete 、 Vue.nextTick等方法,同时生成了初始化的options配置项,包含 ‘component’,‘directive’, ‘filter’。

new Vue 发生了什么

通过上面vue构造函数生成的时候,可以发现调用了一个this._init方法,这个方法在src/core/instance/init.js

Vue.prototype._init = function (options?: Object) {const vm: Component = this// a uidvm._uid = uid++let startTag, endTag/* istanbul ignore if */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 observedvm._isVue = true// merge optionsif (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 selfvm._self = vminitLifecycle(vm)initEvents(vm)initRender(vm)callHook(vm, 'beforeCreate')initInjections(vm) // resolve injections before data/propsinitState(vm)initProvide(vm) // resolve provide after data/propscallHook(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) {//如果有el模板 就自动$mount方法挂载dom,如果没有可以手动挂载vm.$mount(vm.$options.el)}
}

总结就是初始化的时候会调用init方法,然后做了以下几件事;

  • 合并配置(这里是值options配置,这就是为什么vue.use()需要在初始化之前配置,因为需要在合并之前把三方插件配置合并到options里面再去初始化vue)
  • 初始化生命周期
  • 初始化事件中心
  • 初始化渲染
  • 初始化 data、props、computed、watcher

Vue 的挂载

可以看到init的最后一步就是挂载,mount方法是在vue原型上的方法,mount方法是在vue原型上的方法,mount方法是在vue原型上的方法,mount是和平台、构建方式都有关,所以多个文件都有不同的挂载方式。以带 compiler 版本的 $mount 实现,就是带template字符串模板的方式构建。src/platform/web/entry-runtime-with-compiler.js 文件中

const mount = Vue.prototype.$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 <html> or <body> - mount to normal elements instead.`)return this}const options = this.$options// resolve template/el and convert to render functionif (!options.render) {let template = options.templateif (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) {template = getOuterHTML(el)}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 = renderoptions.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')}}}return mount.call(this, el, hydrating)
}
  • 首先缓存了原型上的 $mount 方法,再重新定义该方法。

  • 限制了边界不能挂载到bodyhtml 这样的根节点上。

  • 如果没有options配置项里面没有定义 render 方法,则会把 el 或者 template 字符串转换成 render 方法

所以vue2.x版本都是通过render方法渲染的,它是调用 compileToFunctions 方法实现的,最终通过原先原型上的 $mount挂载到dom树。

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

这就是原型上的$mount原始方法,这个runtime only 版本的 Vue 是可以直接使用的。

mountComponent 方法就是挂载组件

export function mountComponent (vm: Component,el: ?Element,hydrating?: boolean
): Component {vm.$el = elif (!vm.$options.render) {vm.$options.render = createEmptyVNodeif (process.env.NODE_ENV !== 'production') {/* istanbul ignore if */if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||vm.$options.el || el) {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._nameconst id = vm._uidconst 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 definednew Watcher(vm, updateComponent, noop, {before () {if (vm._isMounted && !vm._isDestroyed) {//监听器监听,触发beforeUpdate勾子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 hookif (vm.$vnode == null) {vm._isMounted = true//生命周期勾子mounted,此时已经挂载完成callHook(vm, 'mounted')}return vm
}
  • 这个方法先判断 vm.$options.render 是否存在,如果不存在的话就让它等于 createEmptyVNode(生成虚拟dom)
  • 接着定义了 updateComponent 函数
  • 主核心的部分就是通过core/observer/watcher.js里面的监听者,实例化一个监听器 new Watcher(),updateComponent函数作为第二个参数传给Watcher类,那么updateComponent函数中读取的所有数据都将被watcher所监控;在此方法中调用 vm._render 方法生成虚拟 节点,最终调用 vm._update 更新 DOM。
  • 判断vm._isMountedtrue时候,说明已经挂载完成,此时生命周期勾子mounted调用。

关于vm._rendervm._update

关于挂载阶段的updateComponent函数,调用 vm._update(vm.__render(), hydrating)这个方法。

vm._render定义在src/core/instance/render.js 文件中

export function renderMixin (Vue: Class<Component>) {// install runtime convenience helpersinstallRenderHelpers(Vue.prototype)Vue.prototype.$nextTick = function (fn: Function) {return nextTick(fn, this)}Vue.prototype._render = function (): VNode {const vm: Component = thisconst { render, _parentVnode } = vm.$optionsif (_parentVnode) {vm.$scopedSlots = normalizeScopedSlots(_parentVnode.data.scopedSlots,vm.$slots,vm.$scopedSlots)}// set parent vnode. this allows render functions to have access// to the data on the placeholder node.vm.$vnode = _parentVnode// render selflet vnodetry {// There's no need to maintain a stack because all render fns are called// separately from one another. Nested component's render fns are called// when parent component is patched.currentRenderingInstance = vmvnode = render.call(vm._renderProxy, vm.$createElement)} catch (e) {handleError(e, vm, `render`)// return error render result,// or previous vnode to prevent render error causing blank component/* istanbul ignore else */if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {try {vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)} catch (e) {handleError(e, vm, `renderError`)vnode = vm._vnode}} else {vnode = vm._vnode}} finally {currentRenderingInstance = null}// if the returned array contains only a single node, allow itif (Array.isArray(vnode) && vnode.length === 1) {vnode = vnode[0]}// return empty vnode in case the render function errored outif (!(vnode instanceof VNode)) {if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {warn('Multiple root nodes returned from render function. Render function ' +'should return a single root node.',vm)}vnode = createEmptyVNode()}// set parentvnode.parent = _parentVnodereturn vnode}
}

可以看出vm._render返回值就是一个vnode,本质还是在initRender初始化的时候,调用了vm.$createElement方法,也就是createElement()方法,并返回的一个 vnode,这里就用到了vue的虚拟dom技术,再其他篇章有专门介绍。

vm._update

Vue 的 _update 是实例的一个私有方法,它被调用的时机有 2 个,一个是首次渲染,一个是数据更新的时候;这方法的作用就是作用是把 VNode 渲染成真实的 DOM。src/core/instance/lifecycle.js

  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {const vm: Component = thisconst prevEl = vm.$elconst prevVnode = vm._vnodeconst restoreActiveInstance = setActiveInstance(vm)vm._vnode = vnode// Vue.prototype.__patch__ is injected in entry points// based on the rendering backend used.if (!prevVnode) {// initial rendervm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)} else {// updatesvm.$el = vm.__patch__(prevVnode, vnode)}restoreActiveInstance()// update __vue__ referenceif (prevEl) {prevEl.__vue__ = null}if (vm.$el) {vm.$el.__vue__ = vm}// if parent is an HOC, update its $el as wellif (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {vm.$parent.$el = vm.$el}// updated hook is called by the scheduler to ensure that children are// updated in a parent's updated hook.}

核心就是调用vm.__patch__ 方法,这个方法在不同平台如 web 和 weex上写法是不同的,在常规浏览器环境下,他指向了虚拟dom的它指向了 patch 方法,虚拟dom篇章有仔细说明,通过patch函数比对节点生成新的真实dom。

小结

以上就是vue源码结构,构建,初始化,和挂载的源码说明。


大概就是以上几个过程。

vue2.x源码解析(一)相关推荐

  1. Vue2.0源码解析——编译原理

    Vue2.0源码解析--编译原理 前言:本篇文章主要对Vue2.0源码的编译原理进行一个粗浅的分析,其中涉及到正则.高阶函数等知识点,对js的考察是非常的深的,因此我们来好好啃一下这个编译原理的部分. ...

  2. Vue2.0源码解析 - 知其然知其所以然之Vue.use

    前言 小伙伴们大家好.用过Vue的小伙伴都知道,在我们进行Vue开发时,避免不了会使用一些第三方的库,比如说ElementUI组件库.当我们导入好这些组件库后会执行一个Vue.use函数,然后把导进来 ...

  3. vue2.0源码解析(一)

    1.先下载vue源码(当前版本为:2.6.11) 地址: git clone https://github.com/vuejs/vue.git 2.切换到package.json dev脚本中 -c ...

  4. Vue2.0源码解析 - 知其然知其所以然之keep-alive

    前言 [一天一个小知识,每天进步一点点]小伙伴们大家好,今天将要给大家分享是Vue中关于组件缓存的一个内置组件 - keep-alive 不知道小伙伴们有没有遇到这样一种情况,在我们的项目开发中,有时 ...

  5. Vue2源码解析 解析器

    目录 1  解析器的作用 2  解析器内部运行原理 3  html解析器 3.1  运行原理 3.2  截取开始标签 3.3  截取结束标签 3.4  截取注释 3.5  截取条件注释 3.6  截取 ...

  6. 前端进阶-手写Vue2.0源码(三)|技术点评

    前言 今天是个特别的日子 祝各位女神女神节快乐哈 封面我就放一张杀殿的帅照表达我的祝福 哈哈 此篇主要手写 Vue2.0 源码-初始渲染原理 上一篇咱们主要介绍了 Vue 模板编译原理 它是 Vue ...

  7. Vue源码解析:虚拟dom比较原理

    通过对 Vue2.0 源码阅读,想写一写自己的理解,能力有限故从尤大佬2016.4.11第一次提交开始读,准备陆续写: 模版字符串转AST语法树 AST语法树转render函数 Vue双向绑定原理 V ...

  8. 【Vue3】源码解析

    [Vue3]源码解析 首先得知道 Proxy Reflect Symbol Map和Set diff算法 patchChildren diff算法具体做了什么(重点)? patchKeyedChild ...

  9. 【手写 Vue2.x 源码】第二十八篇 - diff 算法-问题分析与 patch 优化

    一,前言 首先,对 6 月的更文内容做一下简单回顾: Vue2.x 源码环境的搭建 Vue2.x 初始化流程介绍 对象的单层.深层劫持 数组的单层.深层劫持 数据代理的实现 对象.数组数据变化的观测 ...

最新文章

  1. linux硬盘满了问题排查
  2. 嵌入式系统学习笔记之五-uboot常用命令之补充
  3. 50万+Python 开发者的选择,这本书对零基础真是太太太友好了
  4. Save a tree as XML using XmlSerializer
  5. 衔接上一学期:排球积分规则
  6. 关于response格式转换
  7. 待人真诚p2psearcher2013源码下载
  8. 力扣226-翻转二叉树(C++,附思路)
  9. 特斯拉Autopilot系统被评为中国最佳驾驶辅助系统
  10. 微信小程序css篇----flex模型
  11. 封装BackgroundWorker控件(提供源代码下载,F5即可见效果)
  12. CreatePipe 函数
  13. blackberry 7290 关于电子书阅读的几个注意事项
  14. AndroidX App Startup 介绍及使用
  15. 人脸识别打卡机怎么调sj_人脸通怎么使用_人脸通考勤机怎么设置
  16. 手机1520 win8.1升级win10
  17. 浅谈老妈的QQ号被盗之后
  18. 微信游戏奇迹暖暖选取服务器失败,奇迹暖暖微信登录授权失败
  19. 多级下料问题的建模 翻译
  20. linux防火墙关闭开放的端口,Linux关闭防火墙,开放端口

热门文章

  1. html模板 信,HTML5 响应式求职信网页模板 - 踏得网
  2. 谈谈做反向跟单交易误区:跟随数据源越多越好?
  3. 蚌埠学院计算机网络,蚌埠学院计算机科学与技术系介绍
  4. 把QQ炫铃变为本机系统提示音
  5. 别急着撤离北京,这里有最好的机会
  6. 强者都在示弱,弱者都在逞强!
  7. TOP20精选议题引领 | K+峰会深圳站圆满收官!
  8. 远力集团,到底是如何成为中国城市产业发展新引擎的?
  9. 元旦文案‖适合2021年跨年发的句子
  10. 曙光计算机能玩游戏吗,怎么用手机模拟器玩曙光英雄,曙光英雄手游电脑版教程...