首先这篇文章是读 vue.js
源代码的梳理性文章,文章分块梳理,记录着本身的一些理解及大体过程;更重要的一点是但愿在 vue.js 3.0
发布前深刻的了解其原理。html
若是你从未看过或者接触过 vue.js
源代码,建议你参考如下列出的 vue.js
解析的相关文章,由于这些文章更细致的讲解了这个工程,本文只是以一些 demo
演示某一功能点或 API
实现,力求简要梳理过程。vue
若是搞清楚了工程目录及入口,建议直接去看代码,这样比较高效 ( 遇到难以理解对应着回来看看别人的讲解,加以理解便可 )node
文章所涉及到的代码,基本都是缩减版,具体还请参阅 vue.js - 2.5.17。git
若有任何疏漏和错误之处欢迎指正、交流。github
/** * Vue构造函数 * * @param {*} options 选项参数 */ function Vue(options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) { warn('Vue是一个构造函数,应该用“new”关键字调用'); } this._init(options); } 复制代码
咱们知道 new Vue()
将执行 Vue
构造函数, 进而执行 _init()
, 那 _init
方法从何处而来?答案是Vue
在初始化时添加了该方法,若是你对初始化还不是很清楚,建议你参考上文对初始化过程的梳理性文章:「试着读读 Vue 源代码」初始化先后作了哪些事情❓。数组
_init()
import config from '../config'; import { initProxy } from './proxy'; import { initState } from './state'; import { initRender } from './render'; import { initEvents } from './events'; import { mark, measure } from '../util/perf'; import { initLifecycle, callHook } from './lifecycle'; import { initProvide, initInjections } from './inject'; import { extend, mergeOptions, formatComponentName } from '../util/index'; let uid = 0; export function initMixin(Vue: Class<Component>) { Vue.prototype._init = function(options?: Object) { const vm: Component = this; // 当前 Vue 实例 vm._uid = uid++; // 当前 Vue 实例惟一标识 /**************************** 非生产环境下进行性能监控 --- start ****************************/ let startTag, endTag; if (process.env.NODE_ENV !== 'production' && config.performance && mark) { startTag = `vue-perf-start:${vm._uid}`; endTag = `vue-perf-end:${vm._uid}`; mark(startTag); } vm._isVue = true; // 一个标志,避免该对象被响应系统观测 /****************** 对 Vue 提供的 props、data、methods等选项进行合并处理 ******************/ // _isComponent 内部选项:在 Vue 建立组件的时候才会生成 if (options && options._isComponent) { initInternalComponent(vm, options); // 优化内部组件实例化,由于动态选项合并不是常慢,并且没有一个内部组件选项须要特殊处理。 } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), // parentVal options || {}, // childVal vm ); } // 设置渲染函数的做用域代理,其目的是提供更好的提示信息(如:在模板内访问实例不存在的属性,则会在非生产环境下提供准确的报错信息) if (process.env.NODE_ENV !== 'production') { initProxy(vm); } else { vm._renderProxy = vm; } vm._self = vm; // 暴露真实的实例自己 /**************************** 执行相关初始化程序及调用初期生命周期函数 ****************************/ initLifecycle(vm); // 初始化生命周期 initEvents(vm); // 初始化事件 initRender(vm); // 初始化渲染 callHook(vm, 'beforeCreate'); // 调用生命周期钩子函数 -- beforeCreate initInjections(vm); // resolve injections before data/props initState(vm); // 初始化 initProps、initMethods、initData、initComputed、initWatch initProvide(vm); // resolve provide after data/props callHook(vm, 'created'); // 此时尚未任何挂载的操做,因此在 created 中是不能访问DOM的,即不能访问 $el /**************************** 非生产环境下进行性能监控 --- end ****************************/ 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) { vm.$mount(vm.$options.el); } }; } 复制代码
_init
方法所作的事情可大概梳理出如下要点:
注:性能监控:利用
Web Performance API
容许网页访问某些函数来测量网页和Web
应用程序的性能; 这里是Vue - mark、measure
具体代码实现,就不过多赘述了; 接下来着重看被监控的几个步骤主要作了什么?markdown
若是就单单看代码,可能就不太直观且不易理解;不如直接用 Demo 代入断点调试看看每一步是如何作的,那将会使你对代码的运行有更直观的理解与认识。app
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>vue.js DEMO</title> <script src="../../dist/vue.js"></script> </head> <body> <div id="app"> <p>计算属性:{{messageTo}}</p> <p>数据属性:{{ message }}</p> <button @click="update">更新</button> <item v-for="item in list" :msg="item" :key="item" @rm="remove(item)" /> </div> <script> new Vue({ el: '#app', components: { item: { props: ['msg'], template: `<div style="margin-top: 20px;">{{ msg }} <button @click="$emit('rm')">x</button></div>`, created() { console.log('---componentA - 组件生命周期钩子执行 created---'); } } }, mixins: [ { created() { console.log('---created - mixins---'); }, methods: { remove(item) { console.log('响应移除:', item); } } } ], data: { message: 'hello vue.js', list: ['hello,', 'the updated', 'vue.js'], obj: { a: 1, b: { c: 2, d: 3 } } }, computed: { messageTo() { return `${this.message} !;`; } }, watch: { message(val, oldVal) { console.log(val, oldVal, 'message - 改变了'); } }, methods: { update() { this.message = `${this.list.join(' ')} ---- ${Math.random()}`; } } }); </script> </body> </html> 复制代码
根据上述 demo
断点进入 Vue
构造函数 options
参数以下断点图所:dom
根据上述 Demo
咱们着重分析执行代码即 mergeOptions
函数,根据代码可知该函数是对咱们传入的options
作了一层处理,而后赋值给实例属性$options
。ide
resolveConstructorOptions
, 该函数主要判断构造函数是否存在父类,若存在父类须要对 vm.constructor.options
进行处理返回,若不存在直接返回vm.constructor.options
; 根据上述Demo
直接返回 vm.constructor.options
。
注:在上文初始化过程对 vm.constructor.options
进行处理,其结果为:
Vue.options = { components: { KeepAlive, Transition, TransitionGroup }, directives: { model, show }, filters: Object.create(null), _base: Vue }; 复制代码
// _isComponent 内部选项:在 Vue 建立组件的时候才会生成 if (options && options._isComponent) { initInternalComponent(vm, options); // 优化内部组件实例化,由于动态选项合并不是常慢,并且没有一个内部组件选项须要特殊处理。 } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), // parentVal options || {}, // childVal vm ); } 复制代码
根据上述分析,程序进入 mergeOptions
函数内部,下面断点图展现了该函数的入参:
mergeOptions
将两个 option
对象合并到一个新的 options
,用于实例化和继承的核心实用程序中。
export function mergeOptions( parent: Object, child: Object, vm?: Component ): Object { // 校验组件的名字是否符合要求: // 限定组件的名字由普通的字符和中横线(-)组成,且必须以字母开头。 // 检测是不是内置的标签(如:slot) || 检测是不是保留标签(html、svg等)。 if (process.env.NODE_ENV !== 'production') { checkComponents(child); } // 若是 child 是一个函数的话,去其静态属性 options 重写 child; if (typeof child === 'function') { child = child.options; } /************************ 规范化处理 ************************/ normalizeProps(child, vm); normalizeInject(child, vm); normalizeDirectives(child); /************************ extends/mixins 递归处理合并 ************************/ const extendsFrom = child.extends; if (extendsFrom) { parent = mergeOptions(parent, extendsFrom, vm); } if (child.mixins) { for (let i = 0, l = child.mixins.length; i < l; i++) { parent = mergeOptions(parent, child.mixins[i], vm); } } /************************ 合并阶段 ************************/ const options = {}; let key; for (key in parent) { mergeField(key); } for (key in child) { if (!hasOwn(parent, key)) { mergeField(key); } } function mergeField(key) { const strat = strats[key] || defaultStrat; options[key] = strat(parent[key], child[key], vm, key); } return options; } 复制代码
normalizeProps(child, vm);
normalizeInject(child, vm);
normalizeDirectives(child);
复制代码
上述代码主要对 Vue 选项进行规范化处理,咱们知道 Vue 的选项支持多种写法,但最终都须要化为统一格式,进行处理。 下面所列出的是各类写法与规范化以后的对比; 上述代码实现就不过多论述了,可直接根据上述导航到代码段去看便可。
Props:
props: ['size', 'myMessage']
props: { height: Number }
props: { height: { type: Number, default: 0 } }
props: { size: { type: null }, myMessage: { type: null } }
props: { height: { type: Number } }
props: { height: { type: Number, default: 0 } }
Inject:
inject: ['foo']
,inject: { bar: 'foo' }
inject: { foo: { from: 'foo' } }
inject: { bar: { from: 'foo' } }
Directives:
directives: { foo: function() { console.log('自定义指令: v-foo') }
directives: { foo: { bind: function() { console.log('v-foo'), update: function() { console.log('v-foo') } } }
代码到执行到这里,将开始真正的合并了,最终返回合并以后的options
。
const options = {}; let key; for (key in parent) { mergeField(key); } for (key in child) { if (!hasOwn(parent, key)) { mergeField(key); } } function mergeField(key) { const strat = strats[key] || defaultStrat; options[key] = strat(parent[key], child[key], vm, key); } return options; 复制代码
这里特别说明一下,Vue
为每个选项合并都提供了选项合并的策略函数,strats
变量存放着这些函数。这里就不分别对每一个策略函数进行展开论述了。
const defaultStrat = function(parentVal: any, childVal: any): any { return childVal === undefined ? parentVal : childVal; }; export function mergeDataOrFn( parentVal: any, childVal: any, vm?: Component ): ?Function { // ... } // optionMergeStrategies: Object.create(null), const strats = config.optionMergeStrategies; // el / propsData 合并策略函数 if (process.env.NODE_ENV !== 'production') { strats.el = strats.propsData = function(parent, child, vm, key) { // ... }; } // data 合并策略函数 strats.data = function( parentVal: any, childVal: any, vm?: Component ): ?Function { // ... }; // watch 合并策略函数 strats.watch = function( parentVal: ?Object, childVal: ?Object, vm?: Component, key: string ): ?Object { // ... }; // props、methods、inject、computed 合并策略函数 strats.props = strats.methods = strats.inject = strats.computed = function( parentVal: ?Object, childVal: ?Object, vm?: Component, key: string ): ?Object { // ... }; // provide 合并策略函数 strats.provide = mergeDataOrFn; 复制代码
根据上述分析, mergeOptions
函数将返回规范化,且合并以后options
,下面断点图展现了合并以后的options
:
initLifecycle(vm); // 初始化生命周期 initEvents(vm); // 初始化事件 initRender(vm); // 初始化渲染 callHook(vm, 'beforeCreate'); // 调用生命周期钩子函数 -- beforeCreate initInjections(vm); // resolve injections before data/props initState(vm); // 初始化 initProps、initMethods、initData、initComputed、initWatch initProvide(vm); // resolve provide after data/props callHook(vm, 'created'); // 此时尚未任何挂载的操做,因此在 created 中是不能访问DOM的,即不能访问 $el 复制代码
initLifecycle
$children
属性里$parent
为父实例export function initLifecycle(vm: Component) { const options = vm.$options; /** * abstract - 是不是抽象组件 * 抽象组件: 它自身不会渲染一个 DOM 元素,也不会出如今父组件链中。(如 keep-alive transition ) */ let parent = options.parent; if (parent && !options.abstract) { // 循环查找第一个非抽象的父组件 while (parent.$options.abstract && parent.$parent) { parent = parent.$parent; } parent.$children.push(vm); } vm.$parent = parent; vm.$root = parent ? parent.$root : vm; vm.$children = []; vm.$refs = {}; vm._watcher = null; vm._inactive = null; vm._directInactive = false; vm._isMounted = false; vm._isDestroyed = false; vm._isBeingDestroyed = false; } 复制代码
initEvents
export function initEvents(vm: Component) { // 在当前实例添加 `_events` `_hasHookEvent` 属性 vm._events = Object.create(null); vm._hasHookEvent = false; // 用于判断是否存在生命周期钩子的事件侦听器 const listeners = vm.$options._parentListeners; // 初始化父附加事件 if (listeners) { updateComponentListeners(vm, listeners); } } 复制代码
initRender
export function initRender(vm: Component) { vm._vnode = null; // the root of the child tree vm._staticTrees = null; // v-once cached trees /*************************** 解析并处理 slot **************************/ const options = vm.$options; const parentVnode = (vm.$vnode = options._parentVnode); // the placeholder node in parent tree const renderContext = parentVnode && parentVnode.context; vm.$slots = resolveSlots(options._renderChildren, renderContext); vm.$scopedSlots = emptyObject; /*************************** 包装 createElement() **************************/ // render: (createElement: () => VNode) => VNode createElement // 将createElement fn绑定到这个实例,以便在其中得到适当的呈现上下文。 // args顺序:标签、数据、子元素、normalizationType、alwaysNormalize内部版本由模板编译的呈现函数使用 vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false); // 规范化老是应用于公共版本,用于用户编写的呈现函数。 vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true); /*************************** 在实例添加 $attrs/$listeners **************************/ // $attrs和$listeners 用于更容易的临时建立。它们须要是反应性的,以便使用它们的 HOC 老是被更新 const parentData = parentVnode && parentVnode.data; if (process.env.NODE_ENV !== 'production') { // 定义响应式的属性 defineReactive( vm, '$attrs', (parentData && parentData.attrs) || emptyObject, () => { !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm); }, true ); defineReactive( vm, '$listeners', options._parentListeners || emptyObject, () => { !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm); }, true ); } else { defineReactive( vm, '$attrs', (parentData && parentData.attrs) || emptyObject, null, true ); defineReactive( vm, '$listeners', options._parentListeners || emptyObject, null, true ); } /*************************** 在实例添加 $attrs/$listeners **************************/ } 复制代码
callHook
export function callHook(vm: Component, hook: string) { pushTarget(); // 为了不在某些生命周期钩子中使用 props 数据致使收集冗余的依赖 #7573 const handlers = vm.$options[hook]; if (handlers) { // 在合并选项处理时:生命周期钩子选项会被合并处理成一个数组 for (let i = 0, j = handlers.length; i < j; i++) { try { handlers[i].call(vm); } catch (e) { // 捕获生命周期函数执行过程当中可能抛出的异常 handleError(e, vm, `${hook} hook`); } } } // 判断是否存在生命周期钩子的事件侦听器,在 initEvents 中初始化,若存在触发响应钩子函数 if (vm._hasHookEvent) { vm.$emit('hook:' + hook); } popTarget(); } 复制代码
这里额外提一下: 可使用 hook: 加 生命周期钩子名称 的方式来监听组件相应的生命周期
<child @hook:beforeCreate="handleChildBeforeCreate" @hook:created="handleChildCreated" @hook:mounted="handleChildMounted" @hook:生命周期钩子名称 /> 复制代码
initInjections
export function initInjections(vm: Component) { const result = resolveInject(vm.$options.inject, vm); // 做用:寻找父代组件提供的数据 if (result) { // provide 和 inject 绑定并非可响应的。 // 这是刻意为之的。然而,若是你传入了一个可监听的对象,那么其对象的属性仍是可响应的。 toggleObserving(false); // 关闭响应式检测 Object.keys(result).forEach(key => { // 对每一个属性定义响应式属性,并在非生产环境下,提供警告程序。 if (process.env.NODE_ENV !== 'production') { defineReactive(vm, key, result[key], () => { warn( `避免直接修改注入的值,由于当提供的组件从新呈现时,更改将被覆盖。正在修改的注入:“${key}”`, vm ); }); } else { defineReactive(vm, key, result[key]); } }); toggleObserving(true); // 开启响应式检测 } } 复制代码
initState
/** * 初始化 props/ methods/ data/ computed/ watch/ 等选项。 */ export function initState(vm: Component) { vm._watchers = []; const opts = vm.$options; if (opts.props) initProps(vm, opts.props); if (opts.methods) initMethods(vm, opts.methods); if (opts.data) { initData(vm); } else { observe((vm._data = {}), true /* asRootData */); } if (opts.computed) initComputed(vm, opts.computed); if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch); } } 复制代码
注: 这里只是简单展现了其初始化顺序,其内部各个初始化方法将在
构建响应式系统
深挖。 这里只须要明白一点,即初始化顺序:props
=>methods
=>data
=>computed
=>watch
(根据上述顺序,天然也就知道,为何能够在data
选项中使用props
去初始化值)
initProvide
export function initProvide(vm: Component) { const provide = vm.$options.provide; if (provide) { vm._provided = typeof provide === 'function' ? provide.call(vm) : provide; } } 复制代码
上述初始化部分的分析,只是简单的梳理了其执行过程,若是想对其内部实现作更为细致的认识,能够自行去看看代码实现或上述说明提到的源码解析的相关文章。
若存在挂载点,则执行挂载函数,渲染组件。挂载函数如何执行,实现机制如何,将在后文慢慢梳理出来。
if (vm.$options.el) { vm.$mount(vm.$options.el); } 复制代码
总结:全文梳理了执行 new Vue()
调用 _init()
方法,接着又跟着代码执行过程探讨了内部实现。
承接上文 - 「试着读读 Vue 源代码」初始化先后作了哪些事❓
承接下文 - 「试着读读Vue源代码」响应式系统是如何构建的❓待续...