首先这篇文章是读 vue.js
源代码的梳理性文章,文章分块梳理,记录着本身的一些理解及大体过程;更重要的一点是但愿在 vue.js 3.0
发布前深刻的了解其原理。css
若是你从未看过或者接触过 vue.js
源代码,建议你参考如下列出的 vue.js
解析的相关文章,由于这些文章更细致的讲解了这个工程,本文只是以一些 demo
演示某一功能点或 API
实现,力求简要梳理过程。html
若是搞清楚了工程目录及入口,建议直接去看代码,这样比较高效 ( 遇到难以理解对应着回来看看别人的讲解,加以理解便可 )vue
文章所涉及到的代码,基本都是缩减版,具体还请参阅 vue.js - 2.5.17。node
JavaScript
自己是一种直译式脚本语言,在找到入口后,主要须要理清其调用关系? 找出 Vue 构造函数的在哪定义了?按照这个逻辑,跟着程序一步一步走便可。github
首先src/platforms/web/entry-runtime-with-compiler.js
web
这个文件最开始,引入一些方法与配置,并导入了 Vue
进而程序去执行 ./runtime/index
文件算法
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'; 如下代码省略, 将在分析初始化时展开... 复制代码
接着 src/platforms/web/runtime/index.js
api
这个文件也是引入一些方法与配置,并导入了 Vue
, 程序继续走到 core/index
缓存
import Vue from 'core/index'; import config from 'core/config'; import { extend, noop } from 'shared/util'; import { mountComponent } from 'core/instance/lifecycle'; import { devtools, inBrowser, isChrome } from 'core/util/index'; import { query, mustUseProp, isReservedTag, isReservedAttr, getTagNamespace, isUnknownElement } from 'web/util/index'; import { patch } from './patch'; import platformDirectives from './directives/index'; import platformComponents from './components/index'; 如下代码省略, 将在分析初始化时展开... 复制代码
来到核心代码 src/core/index.js
该文件仍然也是从外部文件导入了 Vue
, 程序来到 ./instance/index
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'; 如下代码省略, 将在分析初始化时展开... 复制代码
import { initMixin } from './init'; ... /** * Vue构造函数 * * @param {*} options 选项参数 */ function Vue(options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) { warn('Vue是一个构造函数,应该用“new”关键字调用'); } this._init(options); } export default Vue; 如下代码省略, 将在分析初始化时展开... 复制代码
综上:
src/core/instance/index.js
( 定义 Vue
构造函数 ) =>src/core/index.js
( 在 Vue 构造函数上添加全局的 API ) =>web/runtime/index.js
( 安装特定于平台的 utils & 运行时指令和组件 & 定义公用的挂载方法 & 配置 devtools 全局钩子 ) =>web/entry-runtime-with-compiler.js
( 重写 根据上述调用关系一步一步走,首先看到最初定义 Vue 构造函数的文件到底作了哪些事情
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是一个构造函数,应该用“new”关键字调用'); } this._init(options); } initMixin(Vue); stateMixin(Vue); eventsMixin(Vue); lifecycleMixin(Vue); renderMixin(Vue); export default Vue; 复制代码
该方法就作了一件事,在 Vue.prototype
添加 _init
方法。
export function initMixin(Vue: Class<Component>) { Vue.prototype._init = function(options?: Object) { // 代码省略,在初始化会细致分析 }; } 复制代码
import { set, del, observe, defineReactive, toggleObserving } from '../observer/index'; ... export function stateMixin(Vue: Class<Component>) { // 在使用object.defineproperty时,flow在直接声明定义对象方面存在一些问题,所以咱们必须在这里以程序的方式构建对象。 const dataDef = {}; dataDef.get = function() { return this._data; }; const propsDef = {}; propsDef.get = function() { return this._props; }; // 在非生产环境下 设置 $data $props 为只读属性 if (process.env.NODE_ENV !== 'production') { dataDef.set = function(newData: Object) { warn('避免替换实例根$data。 而是使用嵌套数据属性。', this); }; propsDef.set = function() { warn(`$props 是只读的。`, this); }; } // 在Vue原型上定义两个属性,并分别代理了 _data _props 的实例属性 Object.defineProperty(Vue.prototype, '$data', dataDef); Object.defineProperty(Vue.prototype, '$props', propsDef); // 在 vue 原型上添加 实例方法 / 数据相关: $set/$delete/$watch Vue.prototype.$set = set; // 向响应式对象中添加一个属性,并确保这个新属性一样是响应式的,且触发视图更新 Vue.prototype.$delete = del; // 删除对象的属性。若是对象是响应式的,确保删除能触发更新视图。 Vue.prototype.$watch = function( // 观察 Vue 实例变化的一个表达式或计算属性函数。回调函数获得的参数为新值和旧值。 expOrFn: string | Function, cb: any, options?: Object ): Function { // 代码省略,在初始化会细致分析 }; ... } 复制代码
在 Vue.prototype
添加实例方法 / 事件相关:$on
/$once
/$off
/$emit
export function eventsMixin(Vue: Class<Component>) { // 做用:监听当前实例上的自定义事件。事件能够由vm.$emit触发。回调函数会接收全部传入事件触发函数的额外参数。 Vue.prototype.$on = function( event: string | Array<string>, fn: Function ): Component { // ... }; // 做用:监听一个自定义事件,可是只触发一次,在第一次触发以后移除监听器 Vue.prototype.$once = function(event: string, fn: Function): Component { // ... }; // 做用:移除自定义事件监听器。 Vue.prototype.$off = function( event?: string | Array<string>, fn?: Function ): Component { // ... }; // 做用:触发当前实例上的事件。附加参数都会传给监听器回调。 Vue.prototype.$emit = function(event: string): Component { // ... }; } 复制代码
在 Vue.prototype
添加实例方法 / 生命周期相关:_update
/$forceUpdate
/$destroy
export function lifecycleMixin(Vue: Class<Component>) { Vue.prototype._update = function(vnode: VNode, hydrating?: boolean) { // ... } // 做用:迫使 Vue 实例从新渲染。注意它仅仅影响实例自己和插入插槽内容的子组件,而不是全部子组件。 Vue.prototype.$forceUpdate = function() { // ... } // 做用:彻底销毁一个实例。清理它与其它实例的链接,解绑它的所有指令及事件监听器。 Vue.prototype.$destroy = function() { // ... } 复制代码
在 Vue.prototype
添加实例方法:$nextTick
/_render
/_o
/_n
等。
export function installRenderHelpers(target: any) { target._o = markOnce; target._n = toNumber; target._s = toString; target._l = renderList; target._t = renderSlot; target._q = looseEqual; target._i = looseIndexOf; target._m = renderStatic; target._f = resolveFilter; target._k = checkKeyCodes; target._b = bindObjectProps; target._v = createTextVNode; target._e = createEmptyVNode; target._u = resolveScopedSlots; target._g = bindObjectListeners; } 复制代码
import { warn, nextTick, emptyObject, handleError, defineReactive } from '../util/index'; import { installRenderHelpers } from './render-helpers/index'; export function renderMixin(Vue: Class<Component>) { installRenderHelpers(Vue.prototype); // 安装运行时方便助手 Vue.prototype.$nextTick = function(fn: Function) { return nextTick(fn, this); }; Vue.prototype._render = function(): VNode { // ... }; } 复制代码
断点调试
综上所述该文件主要作了两件事:定义 Vue
构造函数、包装 Vue.prototype
。
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); // 在 Vue 构造函数上添加全局的API // 在 Vue.prototype 上添加 $isServer 只读属性,该属性代理了 isServerRendering 方法 Object.defineProperty(Vue.prototype, '$isServer', { get: isServerRendering }); // 在 Vue.prototype 上添加 $ssrContext 只读属性,该属性代理了 $vnode.ssrContext Object.defineProperty(Vue.prototype, '$ssrContext', { get() { return this.$vnode && this.$vnode.ssrContext; } }); // 为 ssr 运行时助手安装公开 FunctionalRenderContext Object.defineProperty(Vue, 'FunctionalRenderContext', { value: FunctionalRenderContext }); Vue.version = '__VERSION__'; // 在 Vue 上添加静态属性 version export default Vue; 复制代码
初始化全局 API
/* @flow */ import config from '../config'; import { initUse } from './use'; import { initMixin } from './mixin'; import { initExtend } from './extend'; import { initAssetRegisters } from './assets'; import { set, del } from '../observer/index'; import { ASSET_TYPES } from 'shared/constants'; import builtInComponents from '../components/index'; import { warn, extend, nextTick, mergeOptions, defineReactive } from '../util/index'; // 全局API以静态属性和方法的形式被添加到 Vue 构造函数 export function initGlobalAPI(Vue: GlobalAPI) { const configDef = {}; configDef.get = () => config; if (process.env.NODE_ENV !== 'production') { configDef.set = () => { warn('不要替换 Vue.config 对象,请设置单独的字段代替。'); }; } Object.defineProperty(Vue, 'config', configDef); // 在 Vue 上添加 config 只读属性,该属性代理了 config // 暴露 util 的方法。注意:这些不被认为是公共API的一部分——除非您意识到了风险,不然请避免依赖它们。 Vue.util = { warn, extend, mergeOptions, defineReactive }; // 在 Vue 上添加 set/delete/nextTick/options 属性 Vue.set = set; Vue.delete = del; Vue.nextTick = nextTick; Vue.options = Object.create(null); // 在 Vue.options 添加 components, directives, filters 属性 // ASSET_TYPES = [ 'component', 'directive', 'filter' ] ASSET_TYPES.forEach(type => { Vue.options[type + 's'] = Object.create(null); }); // 这用于标识“基本”构造函数,以便在Weex的多实例场景中扩展全部纯对象组件。 Vue.options._base = Vue; // 将 builtInComponents 的属性混入到 Vue.options.components 中 extend(Vue.options.components, builtInComponents); // extend() 将属性混合到目标对象中 /* 包装以后 Vue.options 结果以下: Vue.options = { components: { KeepAlive }, directives: Object.create(null), filters: Object.create(null), _base: Vue } */ // 在 Vue 构造函数上添加 use 静态方法,全局API Vue.use initUse(Vue); // 在 Vue 构造函数上添加 mixins 静态方法,全局API Vue.mixins initMixin(Vue); // 在 Vue 构造函数上添加 Vue.cid 静态属性 extend 静态方法,全局API Vue.extend initExtend(Vue); // 在 Vue 构造函数上添加 三个 静态方法,分别用来全局注册组件,指令和过滤器 initAssetRegisters(Vue); } 复制代码
接下来就其中细节部分分别展开讨论
来自 ../components/index
的 builtInComponents
实际只是导出了包含内置组件(keep-alive
)属性的对象
import KeepAlive from './keep-alive'; export default { KeepAlive }; 复制代码
keep-alive
内容以下:
export default { name: 'keep-alive', abstract: true, // 是不是抽象组件 props: { // ... }, created() { // ... }, destroyed() { // ... }, mounted() { // ... }, render() { // ... } }; 复制代码
export function initUse(Vue: GlobalAPI) { // 做用:安装 Vue.js 插件。若是插件是一个对象,必须提供 install 方法。 // 若是插件是一个函数,它会被做为 install 方法。install 方法调用时,会将 Vue 做为参数传入。 Vue.use = function(plugin: Function | Object) { // ... }; } 复制代码
export function initMixin(Vue: GlobalAPI) { // 做用:全局注册一个混入,影响注册以后全部建立的每一个 Vue 实例。 // 插件做者可使用混入,向组件注入自定义的行为。不推荐在应用代码中使用。 Vue.mixin = function(mixin: Object) { // ... }; } 复制代码
export function initExtend(Vue: GlobalAPI) { // 每一个实例构造函数,包括Vue,都有一个唯一的cid。这使咱们可以为原型继承建立包装的“子构造函数”并缓存它们。 Vue.cid = 0 let cid = 1 // 做用:使用基础 Vue 构造器,建立一个“子类”。参数是一个包含组件选项的对象。 Vue.extend = function (extendOptions: Object): Function { // ... } 复制代码
export function initAssetRegisters(Vue: GlobalAPI) { // 建立 asset 注册方法 // ASSET_TYPES = [ 'component', 'directive', 'filter' ] ASSET_TYPES.forEach(type => { Vue[type] = function( id: string, definition: Function | Object ): Function | Object | void { // ... } // Vue.component( id, [definition] ) 注册或获取全局组件。注册还会自动使用给定的id设置组件的名称 // Vue.directive( id, [definition] ) 注册或获取全局指令。 // Vue.filter( id, [definition] ) 注册或获取全局过滤器。 } 复制代码
断点调试
综上所述该文件主要作了一件事:包装 Vue
构造函数。
/* @flow */ import Vue from 'core/index'; import config from 'core/config'; import { extend, noop } from 'shared/util'; import { mountComponent } from 'core/instance/lifecycle'; import { devtools, inBrowser, isChrome } from 'core/util/index'; import { query, mustUseProp, isReservedTag, isReservedAttr, getTagNamespace, isUnknownElement } from 'web/util/index'; import { patch } from './patch'; import platformDirectives from './directives/index'; import platformComponents from './components/index'; /********* 安装特定于平台的utils **********/ Vue.config.mustUseProp = mustUseProp; // 检查属性是否必须使用属性绑定,例如,值与平台相关。 Vue.config.isReservedTag = isReservedTag; // 检查是不是保留标签,以便不能将其注册为组件。这是平台相关的,可能会被覆盖。 Vue.config.isReservedAttr = isReservedAttr; // 检查是不是保留属性,使其不能用做组件 prop。这是平台相关的,可能会被覆盖。 Vue.config.getTagNamespace = getTagNamespace; // 获取元素的名称空间 Vue.config.isUnknownElement = isUnknownElement; // 检查标记是否为未知元素。平台相关的。 /********* 安装特定于平台的utils **********/ /********* 安装平台运行时指令和组件 **********/ extend(Vue.options.directives, platformDirectives); extend(Vue.options.components, platformComponents); /* 对 Vue.options.directives/components 合并包装以后: Vue.options = { components: { KeepAlive, Transition, TransitionGroup }, directives: { model, show }, filters: Object.create(null), _base: Vue } */ /********* 安装平台运行时指令和组件 **********/ Vue.prototype.__patch__ = inBrowser ? patch : noop; // 安装平台补丁功能 /** * 公用的挂载方法 * * @param {String | Element} el 挂载元素 * @param {Boolean} hydrating 用于 Virtual DOM 的补丁算法 * @returns {Function} 真正的挂载组件的方法 */ Vue.prototype.$mount = function( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined; return mountComponent(this, el, hydrating); }; /************** 配置 devtools 全局钩子函数 与 开发提示 **************/ if (inBrowser) { setTimeout(() => { if (config.devtools) { if (devtools) { devtools.emit('init', Vue); } else if ( process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test' && isChrome ) { console[console.info ? 'info' : 'log']( '下载Vue Devtools扩展以得到更好的开发体验:\n' + 'https://github.com/vuejs/vue-devtools' ); } } if ( process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test' && config.productionTip !== false && typeof console !== 'undefined' ) { console[console.info ? 'info' : 'log']( `您正在以开发模式运行Vue。\n` + `在部署生产时,请确保打开生产模式。\n` + `详情请浏览 https://vuejs.org/guide/deployment.html` ); } }, 0); } /************** 配置 devtools 全局钩子函数 与 开发提示 **************/ export default Vue; 复制代码
import model from './model'; import show from './show'; export default { model, show }; 复制代码
model
实现:
const directive = { inserted (el, binding, vnode, oldVnode) { // ... } componentUpdated (el, binding, vnode) { // ... } }; export default directive; 复制代码
show
实现:
export default { bind(el: any, { value }: VNodeDirective, vnode: VNodeWithData) { // ... }, update(el: any, { value, oldValue }: VNodeDirective, vnode: VNodeWithData) { // ... }, unbind( el: any, binding: VNodeDirective, vnode: VNodeWithData, oldVnode: VNodeWithData, isDestroy: boolean ) { // ... } }; 复制代码
import Transition from './transition'; import TransitionGroup from './transition-group'; export default { Transition, TransitionGroup }; 复制代码
Transition
实现:
export const transitionProps = { name: String, appear: Boolean, css: Boolean, mode: String, type: String, enterClass: String, leaveClass: String, enterToClass: String, leaveToClass: String, enterActiveClass: String, leaveActiveClass: String, appearClass: String, appearActiveClass: String, appearToClass: String, duration: [Number, String, Object] }; export default { name: 'transition', props: transitionProps, abstract: true, render(h: Function) { // ... } }; 复制代码
const props = extend( { tag: String, moveClass: String }, transitionProps ); export default { props, beforeMount() { // ... }, render(h: Function) { // ... }, updated() { // ... }, methods: { hasMove(el: any, moveClass: string): boolean { // ... } } }; 复制代码
断点调试
综上所述该文件主要对 Vue.config
进行扩展、 对 Vue.options.directives/components
进行合并包装、添加公用的挂载方法 $mount
、配置 devtools
全局钩子函数。
$mount
函数,给运行时版的 $mount
函数增长编译模板的能力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'; const mount = Vue.prototype.$mount; // 缓存运行时版的 $mount 函数 // 重写 $mount 函数,给运行时版的 $mount 函数增长编译模板的能力 Vue.prototype.$mount = function( el?: string | Element, hydrating?: boolean ): Component { el = el && query(el); // 处理 挂载点 // 过滤 body html if (el === document.body || el === document.documentElement /*html*/) { process.env.NODE_ENV !== 'production' && warn( `Do not mount Vue to <html> or <body> - mount to normal elements instead.` ); return this; } /*************** 解析模板/el并转换为render函数 ***************/ const options = this.$options; if (!options.render) { let template = options.template; // 获取合适的内容做为模板(template) if (template) { if (typeof template === 'string') { if (template.charAt(0) === '#') { // 把该字符串做为 css 选择符去选中对应的元素,并把该元素的 innerHTML 做为模板 template = idToTemplate(template); if (process.env.NODE_ENV !== 'production' && !template) { warn(`模板元素未找到或为空: ${options.template}`, this); } } } else if (template.nodeType) { // 元素节点 template = template.innerHTML; } else { if (process.env.NODE_ENV !== 'production') { warn('无效的模板选项:' + template, this); } return this; } } else if (el) { template = getOuterHTML(el); // el 选项指定的挂载点将被做为组件模板 } if (template) { if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile'); } /*************** 将模板(template)字符串编译为渲染函数 ***************/ const { render, staticRenderFns } = compileToFunctions( template, { shouldDecodeNewlines, shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this ); options.render = render; options.staticRenderFns = staticRenderFns; /*************** 将模板(template)字符串编译为渲染函数 ***************/ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile end'); measure(`vue ${this._name} compile`, 'compile', 'compile end'); } } } /*************** 解析模板/el并转换为render函数 ***************/ return mount.call(this, el, hydrating); }; /** * 获取元素的outerHTML,并在IE中处理SVG元素。 */ function getOuterHTML(el: Element): string { // IE9-11 中 SVG 标签元素是没有 innerHTML 和 outerHTML 这两个属性 if (el.outerHTML) { return el.outerHTML; } else { const container = document.createElement('div'); container.appendChild(el.cloneNode(true)); // 返回调用该方法的节点的一个副本(是否深度克隆) return container.innerHTML; } } /** * 根据 ID 获取或替换 HTML 元素的内容 */ const idToTemplate = cached(id => { const el = query(id); return el && el.innerHTML; }); Vue.compile = compileToFunctions; export default Vue; 复制代码
总结: 跟着程序执行过程看下来,整个初始化的过程就是对 Vue 构造函数的包装与丰富。
本部份内容旨在梳理初始化的全过程,对其中全局 API 及方法实现并未细化。
承接上文 - 「试着读读Vue源代码」工程目录及本地运行(断点调试)