vue2.x源码解析(一)
简介
本文以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
方法,再重新定义该方法。限制了边界不能挂载到
body
、html
这样的根节点上。如果没有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._isMounted
为
true时候,说明已经挂载完成,此时生命周期勾子mounted调用。
关于vm._render
和 vm._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源码解析(一)相关推荐
- Vue2.0源码解析——编译原理
Vue2.0源码解析--编译原理 前言:本篇文章主要对Vue2.0源码的编译原理进行一个粗浅的分析,其中涉及到正则.高阶函数等知识点,对js的考察是非常的深的,因此我们来好好啃一下这个编译原理的部分. ...
- Vue2.0源码解析 - 知其然知其所以然之Vue.use
前言 小伙伴们大家好.用过Vue的小伙伴都知道,在我们进行Vue开发时,避免不了会使用一些第三方的库,比如说ElementUI组件库.当我们导入好这些组件库后会执行一个Vue.use函数,然后把导进来 ...
- vue2.0源码解析(一)
1.先下载vue源码(当前版本为:2.6.11) 地址: git clone https://github.com/vuejs/vue.git 2.切换到package.json dev脚本中 -c ...
- Vue2.0源码解析 - 知其然知其所以然之keep-alive
前言 [一天一个小知识,每天进步一点点]小伙伴们大家好,今天将要给大家分享是Vue中关于组件缓存的一个内置组件 - keep-alive 不知道小伙伴们有没有遇到这样一种情况,在我们的项目开发中,有时 ...
- Vue2源码解析 解析器
目录 1 解析器的作用 2 解析器内部运行原理 3 html解析器 3.1 运行原理 3.2 截取开始标签 3.3 截取结束标签 3.4 截取注释 3.5 截取条件注释 3.6 截取 ...
- 前端进阶-手写Vue2.0源码(三)|技术点评
前言 今天是个特别的日子 祝各位女神女神节快乐哈 封面我就放一张杀殿的帅照表达我的祝福 哈哈 此篇主要手写 Vue2.0 源码-初始渲染原理 上一篇咱们主要介绍了 Vue 模板编译原理 它是 Vue ...
- Vue源码解析:虚拟dom比较原理
通过对 Vue2.0 源码阅读,想写一写自己的理解,能力有限故从尤大佬2016.4.11第一次提交开始读,准备陆续写: 模版字符串转AST语法树 AST语法树转render函数 Vue双向绑定原理 V ...
- 【Vue3】源码解析
[Vue3]源码解析 首先得知道 Proxy Reflect Symbol Map和Set diff算法 patchChildren diff算法具体做了什么(重点)? patchKeyedChild ...
- 【手写 Vue2.x 源码】第二十八篇 - diff 算法-问题分析与 patch 优化
一,前言 首先,对 6 月的更文内容做一下简单回顾: Vue2.x 源码环境的搭建 Vue2.x 初始化流程介绍 对象的单层.深层劫持 数组的单层.深层劫持 数据代理的实现 对象.数组数据变化的观测 ...
最新文章
- linux硬盘满了问题排查
- 嵌入式系统学习笔记之五-uboot常用命令之补充
- 50万+Python 开发者的选择,这本书对零基础真是太太太友好了
- Save a tree as XML using XmlSerializer
- 衔接上一学期:排球积分规则
- 关于response格式转换
- 待人真诚p2psearcher2013源码下载
- 力扣226-翻转二叉树(C++,附思路)
- 特斯拉Autopilot系统被评为中国最佳驾驶辅助系统
- 微信小程序css篇----flex模型
- 封装BackgroundWorker控件(提供源代码下载,F5即可见效果)
- CreatePipe 函数
- blackberry 7290 关于电子书阅读的几个注意事项
- AndroidX App Startup 介绍及使用
- 人脸识别打卡机怎么调sj_人脸通怎么使用_人脸通考勤机怎么设置
- 手机1520 win8.1升级win10
- 浅谈老妈的QQ号被盗之后
- 微信游戏奇迹暖暖选取服务器失败,奇迹暖暖微信登录授权失败
- 多级下料问题的建模 翻译
- linux防火墙关闭开放的端口,Linux关闭防火墙,开放端口