渣新一枚,近期打算离职,去好点的城市发展,而后发现写了很多业务代码,真正有用的又一问三不知。 虽然一直都在用Vue,但总以为好像只是背了背文档,成了一个Api调用师。基于职业发展焦虑, 打算好好学习学习,把看到的东西总结下,即使快过期了,面试也可能会用到。既方便之后复习,也分享给你们,有错还请指正,写的很差望海涵。。vue
咱们再来看看源码中是如何一步一步建立一个Vue实例的node
new Vue(options)
显示建立一个Vue实例,传入实例配置属性options,咱们就来看看Vue构造函数作了什么?react
function Vue ( options ) {
if ( !( this instanceof Vue ) ) { warn( 'Vue is a constructor and should be called with the `new` keyword' ); } this._init( options ); } 复制代码
构造函数就只作了一件事,调用内部方法_init()
, _init
是混入Vue
原型对象上的一个方法,咱们继续往下看。web
_init简要代码以下(已省略于本文无关代码)面试
Vue.prototype._init = function ( options ) {
var vm = this; vm.$options = mergeOptions( resolveConstructorOptions( vm.constructor ), options || {}, vm ); vm._self = vm; initLifecycle( vm ); initEvents( vm ); initRender( vm ); callHook( vm, 'beforeCreate' ); initInjections( vm ); // resolve injections before data/props initState( vm ); // init props methods data computed watch initProvide( vm ); // resolve provide after data/props callHook( vm, 'created' ); if ( vm.$options.el ) { vm.$mount( vm.$options.el ); } } 复制代码
_init
函数中,mergeOptions
将传入的配置和诸如Mixin
配置按照默认或用户自定义的合并策略进行合并。 配置项合并完成后,就能够开始初始化组件数据了。缓存
function initLifecycle ( vm ) {
var options = vm.$options; // locate first non-abstract parent var 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; } 复制代码
initLifecycle
中首先循环查找当前组件的父级(非抽象),并挂在到$parent
属性上,而后初始化了其余的一些属性的默认值。 说明beforeCreate
此时已经能够访问$parent
和$root
编辑器
function initEvents ( vm ) {
vm._events = Object.create( null ); vm._hasHookEvent = false; // init parent attached events var listeners = vm.$options._parentListeners; if ( listeners ) { updateComponentListeners( vm, listeners ); } } 复制代码
initEvents
也不复杂,初始化内部属性并获取父组件注册到该组件上的事件并初始化(updateComponentListeners)。 其中_parentListeners
是父组件传入的事件监听,_parentListeners
存在时将会在子组件内进行事件注册。ide
「这里须要注意的是,initEvents
初始化的是父组件传入的事件监听。」函数
function initRender ( vm ) {
var options = vm.$options; var parentVnode = vm.$vnode = options._parentVnode; // the placeholder node in parent tree var renderContext = parentVnode && parentVnode.context; vm.$slots = resolveSlots( options._renderChildren, renderContext ); vm.$scopedSlots = emptyObject; vm._c = function ( a, b, c, d ) { return createElement( vm, a, b, c, d, false ); }; vm.$createElement = function ( a, b, c, d ) { return createElement( vm, a, b, c, d, true ); }; // $attrs & $listeners are exposed for easier HOC creation. // they need to be reactive so that HOCs using them are always updated var parentData = parentVnode && parentVnode.data; { defineReactive$$1( vm, '$attrs', parentData && parentData.attrs || emptyObject, function () { !isUpdatingChildComponent && warn( "$attrs is readonly.", vm ); }, true ); defineReactive$$1( vm, '$listeners', options._parentListeners || emptyObject, function () { !isUpdatingChildComponent && warn( "$listeners is readonly.", vm ); }, true ); } } 复制代码
这里能够看到,initRender
初始化一些属性,其中主要的函数$createElemen
t已经能在实例中访问。代码的最后几行中, 还将父组件的attrs
和listeners
(都作了响应式处理)挂载到了子组件实例中,主要是为了高阶组件的使用。oop
在调用beforeCreate
前,vue实例已经能访问$parent
, $root
, $listeners
, $attrs
, $createElement
, $slots
属性了。 这些属性大部分都是从父组件中传入的。
function initInjections ( vm ) {
var result = resolveInject( vm.$options.inject, vm ); if ( result ) { toggleObserving( false ); Object.keys( result ).forEach( function ( key ) { /* istanbul ignore else */ { defineReactive$1( vm, key, result[key], function () { warn( "Avoid mutating an injected value directly since the changes will be " + "overwritten whenever the provided component re-renders. " + "injection being mutated: \"" + key + "\"", vm ); } ); } } ); toggleObserving( true ); } } 复制代码
initInjections
仅仅对inject
选项作处理,resolveInject
函数内部会层层遍历父节点,查找全部注入的属性并将inject
相关属性转换成键值对的形式。 拿到键值对后,再逐一将这些注入属性挂载到当前实例下。
须要注意的是,挂载前执行了toggleObserving
函数,传入false
时,后续绑定的属性将不会主动设置为响应式,也就是说,inject
属性一般都并不是响应的(除非它自己就是响应式)。 inject
初始化后再恢复响应式绑定=>toggleObserving( true )
。
function initState ( vm ) {
vm._watchers = []; var 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 ); } } 复制代码
从代码中就能够看出来initState
都作了什么,看到这里,咱们能够记住created
前已经初始化了props methods data computed watch
属性。
function initProvide ( vm ) {
var provide = vm.$options.provide; if ( provide ) { vm._provided = typeof provide === 'function' ? provide.call( vm ) : provide; } } 复制代码
initProvide只作了一件事,将组件提供的Provide选项保存到私有属性_provided中。
至此,咱们已经能访问绝大部分的组件属性了,如data, props, methods, inject
等
_init
函数最后一步就是挂在实例到DOM中,用到的就是$mount
函数。
$mount
涉及到的代码可能有些多,篇幅问题,咱们就看看主要步骤。
Vue.prototype.$mount = function (el, hydrating) {
if ( !options.render ) { var template = options.template; if ( template ) { // 对template模板进行校验处理 } else if ( el ) { template = getOuterHTML( el ); } if ( template ) { var ref = compileToFunctions( template, { outputSourceRange: "development" !== 'production', shouldDecodeNewlines: shouldDecodeNewlines, shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this ); var render = ref.render; var staticRenderFns = ref.staticRenderFns; options.render = render; options.staticRenderFns = staticRenderFns; } } return mount.call( this, el, hydrating ) }; 复制代码
$mount
根据手写render函数
或template
选项和el
选项决定如何渲染挂在实例。 其中比较主要的就是compileToFunctions
函数。 咱们看到该函数传入了template
并返回了render
,staticRenderFns
等函数。 很容易猜到Vue在这里对模板进行了解析。返回的render也就是咱们常说的渲染函数。
模板转换为渲染函数有三个步骤:
有渲染函数后,咱们就能挂在到真实节点上了mount.call( this, el, hydrating )
。
mount function ( el, hydrating ) {
el = el && inBrowser ? query( el ) : undefined; return mountComponent( this, el, hydrating ) }; function mountComponent ( vm, el, hydrating ) { vm.$el = el; if ( !vm.$options.render ) { vm.$options.render = createEmptyVNode; } callHook( vm, 'beforeMount' ); ... } 复制代码
mountComponent
函数中,确保了render函数存在。紧随其后调用了beforeMount
生命周期。 咱们回顾如下created
和beforeMount
,它们之间进行了模板编译,优化,转换为渲染函数。
// 紧接刚才的代码
var updateComponent; updateComponent = function () { vm._update( vm._render(), hydrating ); }; new Watcher( vm, updateComponent, noop, { before: function before () { if ( vm._isMounted && !vm._isDestroyed ) { callHook( vm, 'beforeUpdate' ); } } }, true /* isRenderWatcher */ ); hydrating = false; if ( vm.$vnode == null ) { vm._isMounted = true; callHook( vm, 'mounted' ); } return vm 复制代码
这里主要是初始化了Watcher
实例,实例中传入了updateComponent
函数。 该函数后续会对比虚拟Dom并更新视图。初始化了Watcher
时就会执行一次updateComponent
。 而后callHook( vm, 'mounted' )
。 因此mounted阶段已经能访问真实DOM了。
new Watcher( vm, updateComponent, noop, {
before: function before () { if ( vm._isMounted && !vm._isDestroyed ) { callHook( vm, 'beforeUpdate' ); } } }, true /* isRenderWatcher */ ); 复制代码
new Watche 的before中调用了beforeUpdate,顺着before,我找到了flushSchedulerQueue函数。
function flushSchedulerQueue () {
currentFlushTimestamp = getNow(); flushing = true; var watcher, id; ... // do not cache length because more watchers might be pushed // as we run existing watchers for ( index = 0; index < queue.length; index++ ) { watcher = queue[index]; if ( watcher.before ) { watcher.before(); } ... watcher.run(); ... } ... // call component updated and activated hooks callActivatedHooks( activatedQueue ); callUpdatedHooks( updatedQueue ); ... } 复制代码
当须要更新依赖/状态时($forceUpdate
, 数据读取变更),flushSchedulerQueue
就会被触发。
flushSchedulerQueue
先遍历了watcher
,调用了before
(也就是生命周期beforeUpdate), 而后执行callActivatedHook
函数,该函数调用其「子组件」的activated
钩子, 最后再调用callUpdatedHooks
也就是updated
钩子。
来看看activated, keep-alive缓存的组件激活时调用
componentVNodeHooks: {
insert: function insert ( vnode ) { var context = vnode.context; var componentInstance = vnode.componentInstance; if ( !componentInstance._isMounted ) { componentInstance._isMounted = true; callHook( componentInstance, 'mounted' ); } if ( vnode.data.keepAlive ) { if ( context._isMounted ) { queueActivatedComponent( componentInstance ); } else { activateChildComponent( componentInstance, true /* direct */ ); } } }, ... } 复制代码
以上代码能够看到,mounted调用后会判断是否为缓存组件并调用activated。
调用$destory
时触发beforeDestroy
或deactivated
,完成数据销毁后调用destoryed
。
好记性不如烂笔头,看了很快就忘了,仍是记一记能有个好的思路,也方便复习。
本文使用 mdnice 排版