内容简介:作者:小土豆biubiubiu博客园:掘金:
作者:小土豆biubiubiu
博客园: https://www.cnblogs.com/HouJiao/
掘金: https://juejin.im/user/58c61b4361ff4b005d9e894d
简书: https://www.jianshu.com/u/cb1c3884e6d5
微信公众号:土豆妈的碎碎念(扫码关注,一起吸猫,一起听故事,一起学习前端技术)
欢迎大家扫描微信二维码进入群聊讨论(若二维码失效可添加微信JEmbrace拉你进群):
码字不易,点赞鼓励哟~
温馨提示
本篇文章内容过长,一次看完会有些乏味,建议大家可以先收藏,分多次进行阅读,这样更好理解。
前言
相信很多人和我一样,在刚开始了解和学习 Vue
生命明周期的时候,会做下面一系列的总结和学习。
总结1
Vue
的实例在创建时会经过一系列的初始化:
设置数据监听、编译模板、将实例挂载到DOM并在数据变化时更新DOM等
总结2
在这个初始化的过程中会运行一些叫做"生命周期钩子"的函数:
beforeCreate:组件创建前 created:组件创建完毕 beforeMount:组件挂载前 mounted:组件挂载完毕 beforeUpdate:组件更新之前 updated:组件更新完毕 beforeDestroy:组件销毁前 destroyed:组件销毁完毕
示例1
关于每个钩子函数里组件的状态示例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Vue的生命周期</title> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> </head> <body> <div id="app"> <h3>{{info}}</h3> <button v-on:click='updateInfo'>修改数据</button> <button v-on:click='destoryComponent'>销毁组件</button> </div> <script> var vm = new Vue({ el: '#app', data: { info: 'Vue的生命周期' }, beforeCreate: function(){ console.log("beforeCreated-组件创建前"); console.log("el:"); console.log(this.$el); console.log("data:"); console.log(this.$data); }, created: function(){ console.log("created-组件创建完毕"); console.log("el:"); console.log(this.$el); console.log("data:"); console.log(this.$data); console.log("info:"); console.log(this.$data.info); }, beforeMount: function(){ console.log("beforeMounted-组件挂载前"); console.log("el:"); console.log(this.$el); console.log("data:"); console.log(this.$data); console.log("info:"); console.log(this.$data.info); }, mounted: function(){ console.log("mounted-组件挂载完毕"); console.log("el:"); console.log(this.$el); console.log("data:"); console.log(this.$data); console.log("info:"); console.log(this.$data.info); }, beforeUpdate: function(){ console.log("beforeUpdate-组件更新前"); console.log("el:"); console.log(this.$el); console.log("data:"); console.log(this.$data); console.log("info:"); console.log(this.$data.info); }, updated: function(){ console.log("updated-组件更新完毕"); console.log("el:"); console.log(this.$el); console.log("data:"); console.log(this.$data); console.log("info:"); console.log(this.$data.info); }, beforeDestroy: function(){ console.log("beforeDestory-组件销毁前"); //在组件销毁前尝试修改data中的数据 this.info="组件销毁前"; console.log("el:"); console.log(this.$el); console.log("data:"); console.log(this.$data); console.log("info:"); console.log(this.$data.info); }, destroyed: function(){ console.log("destoryed-组件销毁完毕"); //在组件销毁完毕后尝试修改data中的数据 this.info="组件已销毁"; console.log("el:"); console.log(this.$el); console.log("data:"); console.log(this.$data); console.log("info:"); console.log(this.$data.info); }, methods: { updateInfo: function(){ // 修改data数据 this.info = '我发生变化了' }, destoryComponent: function(){ //手动调用销毁组件 this.$destroy(); } } }); </script> </body> </html>
总结3:
结合前面示例1的运行结果会有如下的总结。
组件创建前(beforeCreate)
组件创建前,组件需要挂载的DOM元素el和组件的数据data都未被创建。
组件创建完毕(created)
创建创建完毕后,组件的数据已经创建成功,但是DOM元素el还没被创建。
组件挂载前(beforeMount):
组件挂载前,DOM元素已经被创建,只是data中的数据还没有应用到DOM元素上。
组件挂载完毕(mounted)
组件挂载完毕后,data中的数据已经成功应用到DOM元素上。
组件更新前(beforeUpdate)
组件更新前,data数据已经更新,组件挂载的DOM元素的内容也已经同步更新。
组件更新完毕(updated)
组件更新完毕后,data数据已经更新,组件挂载的DOM元素的内容也已经同步更新。 (感觉和beforeUpdate的状态基本相同)
组件销毁前(beforeDestroy)
组件销毁前,组件已经不再受vue管理,我们可以继续更新数据,但是模板已经不再更新。
组件销毁完毕(destroyed)
组件销毁完毕,组件已经不再受vue管理,我们可以继续更新数据,但是模板已经不再更新。
组件生命周期图示
最后的总结,就是来自 Vue
官网的生命周期图示。
那到这里,前期对 Vue
生命周期的学习基本就足够了。那今天,我将带大家从 Vue源码
了解 Vue2.x的生命周期的初始化阶段
,开启 Vue生命周期
的进阶学习。
Vue官网的这张生命周期图示非常关键和实用,后面我们的学习和总结都会基于这个图示。
创建组件实例
对于一个组件, Vue
框架要做的第一步就是创建一个 Vue
实例:即 new Vue()
。那 new Vue()
都做了什么事情呢,我们来看一下 Vue
构造函数的源码实现。
//源码位置备注:/vue/src/core/instance/index.js 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
从 Vue构造函数
的源码可以看到有两个重要的内容: if条件判断逻辑
和 _init方法的调用
。那下面我们就这两个点进行抽丝破茧,看一看它们的源码实现。
在这里需要说明的是 index.js
文件的引入会早于 new Vue
代码的执行,因此在 new Vue
之前会先执行 initMixin
、 stateMixin
、 eventsMixin
、 lifecycleMixin
、 renderMixin
。这些方法内部大致就是在为组件实例定义一些属性和实例方法,并且会为属性赋初值。
我不会详细去解读这几个方法内部的实现,因为本篇主要是分析学习 new Vue
的源码实现。那我在这里说明这个是想让大家大致了解一下和这部分相关的源码的执行顺序,因为在 Vue
构造函数中调用的 _init
方法内部有很多实例属性的访问、赋值以及很多实例方法的调用,那这些实例属性和实例方法就是在 index.js
引入的时候通过执行 initMixin
、 stateMixin
、 eventsMixin
、 lifecycleMixin
、 renderMixin
这几个方法定义的。
创建组件实例 - if条件判断逻辑
if条件判断逻辑如下:
if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) { warn('Vue is a constructor and should be called with the `new` keyword') }
我们先看一下 &&
前半段的逻辑。
process
是 node
环境内置的一个 全局变量
,它提供有关当前 Node.js
进程的信息并对其进行控制。如果本机安装了 node
环境,我们就可以直接在命令行输入一下这个全局变量。
这个全局变量包含的信息非常多,这里只截出了部分属性。
对于 process的evn属性 它返回当前用户环境信息。但是这个信息不是直接访问就能获取到值,而是需要通过设置才能获取。
可以看到我没有设置这个属性,所以访问获得的结果是 undefined
。
然后我们在看一下 Vue
项目中的 webpack
对 process.evn.NODE_EVN
的设置说明:
执行 npm run dev
时会将 process.env.NODE_MODE
设置为 'development'
执行 npm run build
时会将 process.env.NODE_MODE
设置为 'production'
该配置在Vue项目根目录下的 package.json scripts
中设置
所以设置 process.evn.NODE_EVN
的作用就是为了区分当前 Vue
项目的运行环境是 开发环境
还是 生产环境
,针对不同的环境 webpack
在打包时会启用不同的 Plugin
。
&&
前半段的逻辑说完了,在看下 &&
后半段的逻辑: this instanceof Vue
。
这个逻辑我决定用一个示例来解释一下,这样会非常容易理解。
我们先写一个 function
。
function Person(name,age){ this.name = name; this.age = age; this.printThis = function(){ console.log(this); } //调用函数时,打印函数内部的this this.printThis(); }
关于 JavaScript
的函数有两种调用方式:以 普通函数
方式调用和以 构造函数
方式调用。我们分别以两种方式调用一下 Person
函数,看看函数内部的 this
是什么。
// 以普通函数方式调用 Person('小土豆biubiubiu',18); // 以构造函数方式创建 var pIns = new Person('小土豆biubiubiu');
上面这段代码在浏览器的执行结果如下:
从结果我们可以总结:
以普通函数方式调用Person,Person内部的this对象指向的是浏览器全局的window对象 以构造函数方式调用Person,Person内部的this对象指向的是创建出来的实例对象
这里其实是JavaScript语言中this指向的知识点。
那我们可以得出这样的结论:当以 构造函数
方式调用某个函数 Fn
时,函数内部 this instanceof Fn
逻辑的结果就是 true
。
啰嗦了这么多, if条件判断的逻辑
已经很明了了:
如果当前是非生产环境且没有使用new Vue的方式来调用Vue方法,就会有一个警告: Vue is a constructor and should be called with the `new`keyword 即Vue是一个构造函数应该使用关键字new来调用Vue
创建组件实例 - _init方法的调用
_init
方法是定义在Vue原型上的一个方法:
//源码位置备注:/vue/src/core/instance/init.js export function initMixin (Vue: Class<Component>) { Vue.prototype._init = function (options?: Object) { const vm: Component = this // a uid vm._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 observed vm._isVue = true // merge options if (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 self vm._self = vm initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props initState(vm) initProvide(vm) // resolve provide after data/props callHook(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) { vm.$mount(vm.$options.el) } } }
Vue
的构造函数所在的源文件路径为 /vue/src/core/instance/index.js
,在该文件中有一行代码 initMixin(Vue)
,该方法调用后就会将 _init
方法添加到Vue的原型对象上。这个我在前面提说过 index.js
和 new Vue
的执行顺序,相信大家已经能理解。
那这个 _init
方法中都干了写什么呢?
vm.$options
大致浏览一下 _init
内部的代码实现,可以看到第一个就是为组件实例设置了一个 $options
属性。
//源码位置备注:/vue/src/core/instance/init.js // merge options if (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 ) }
首先 if
分支的 options
变量是 new Vue
时传递的选项。
那满足 if
分支的逻辑就是如果 options
存在且是一个组件。那在 new Vue
的时候显然不满足 if
分支的逻辑,所以会执行 else
分支的逻辑。
使用 Vue.extend
方法创建组件的时候会满足 if
分支的逻辑。
在else分支中, resolveConstructorOptions
的作用就是通过组件实例的构造函数获取当前组件的选项和父组件的选项,在通过 mergeOptions
方法将这两个选项进行合并。
这里的父组件不是指组件之间引用产生的父子关系,还是跟 Vue.extend
相关的父子关系。目前我也不太了解 Vue.extend
的相关内容,所以就不多说了。
vm._renderProxy
接着就是为组件实例的 _renderProxy
赋值。
//源码位置备注:/vue/src/core/instance/init.js /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm }
如果是非生产环境,调用 initProxy
方法,生成 vm
的代理对象 _renderProxy
;否则 _renderProxy
的值就是当前组件的实例。
然后我们看一下非生产环境中调用的 initProxy
方法是如何为 vm._renderProxy
赋值的。
//源码位置备注:/vue/src/core/instance/proxy.js const hasProxy = typeof Proxy !== 'undefined' && isNative(Proxy) initProxy = function initProxy (vm) { if (hasProxy) { // determine which proxy handler to use const options = vm.$options const handlers = options.render && options.render._withStripped ? getHandler : hasHandler vm._renderProxy = new Proxy(vm, handlers) } else { vm._renderProxy = vm } }
在 initProxy
方法内部实际上是利用 ES6
中 Proxy
对象为将组件实例vm进行包装,然后赋值给 vm._renderProxy
。
关于 Proxy
的用法如下:
那我们简单的写一个关于 Proxy
的用法示例。
let obj = { 'name': '小土豆biubiubiu', 'age': 18 }; let handler = { get: function(target, property){ if(target[property]){ return target[property]; }else{ console.log(property + "属性不存在,无法访问"); return null; } }, set: function(target, property, value){ if(target[property]){ target[property] = value; }else{ console.log(property + "属性不存在,无法赋值"); } } } obj._renderProxy = null; obj._renderProxy = new Proxy(obj, handler);
这个写法呢,仿照源码给 vm
设置 Proxy
的写法,我们给 obj
这个对象设置了 Proxy
。
根据 handler
函数的实现,当我们访问代理对象 _renderProxy
的某个属性时,如果属性存在,则直接返回对应的值;如果属性不存在则打印 '属性不存在,无法访问'
,并且返回 null
。
当我们修改代理对象 _renderProxy
的某个属性时,如果属性存在,则为其赋新值;如果不存在则打印 '属性不存在,无法赋值'
。
接着我们把上面这段代码放入浏览器的控制台运行,然后访问代理对象的属性:
然后在修改代理对象的属性:
结果和我们前面描述一致。然后我们在说回 initProxy
,它实际上也就是在访问 vm
上的某个属性时做一些验证,比如该属性是否在vm上,访问的属性名称是否合法等。
总结这块的作用,实际上就是在非生产环境中为我们的代码编写的代码做出一些错误提示。
连续多个函数调用
最后就是看到有连续多个函数被调用。
initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created')
我们把最后这几个函数的调用顺序和 Vue
官网的 生命周期图示
对比一下:
可以发现代码和这个图示基本上是一一对应的,所以 _init
方法被称为是 Vue实例的初始化方法
。下面我们将逐个解读 _init
内部按顺序调用的那些方法。
initLifecycle-初始化生命周期
//源码位置备注:/vue/src/core/instance/lifecycle.js export function initLifecycle (vm: Component) { const options = vm.$options // locate first non-abstract parent 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 }
在初始化生命周期这个函数中, vm
是当前 Vue
组件的实例对象。我们看到函数内部大多数都是给 vm
这个实例对象的属性赋值。
以 $
开头的属性称为组件的 实例属性
,在 Vue
官网中都会有明确的解释。
$parent
属性表示的是当前组件的父组件,可以看到在 while
循环中会一直递归寻找第一个非抽象的父级组件: parent.$options.abstract && parent.$parent
。
非抽象类型的父级组件这里不是很理解,有伙伴知道的可以在评论区指导一下。
$root
属性表示的是当前组件的 跟组件
。如果当前组件存在 父组件
,那当前组件的 根组件
会继承父组件的 $root
属性,因此直接访问 parent.$root
就能获取到当前组件的根组件;如果当前组件实例不存在父组件,那当前组件的跟组件就是它自己。
$children
属性表示的是当前组件实例的 直接子组件
。在前面 $parent
属性赋值的时候有这样的操作: parent.$children.push(vm)
,即将当前组件的实例对象添加到到父组件的 $children
属性中。所以 $children
数据的添加规则为:当前组件为父组件的 $children
属性赋值,那当前组件的 $children
则由其子组件来负责添加。
$refs
属性表示的是模板中注册了 ref
属性的 DOM
元素或者组件实例。
initEvents-初始化事件
//源码位置备注:/vue/src/core/instance/events.js export function initEvents (vm: Component) { // Object.create(null):创建一个原型为null的空对象 vm._events = Object.create(null) vm._hasHookEvent = false // init parent attached events const listeners = vm.$options._parentListeners if (listeners) { updateComponentListeners(vm, listeners) } }
vm._events
在初始化事件函数中,首先给 vm
定义了一个 _events
属性,并给其赋值一个空对象。那 _events
表示的是什么呢?我们写一段代码验证一下。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Vue的生命周期</title> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script> var ChildComponent = Vue.component('child', { mounted() { console.log(this); }, methods: { triggerSelf(){ console.log("triggerSelf"); }, triggerParent(){ this.$emit('updateinfo'); } }, template: `<div id="child"> <h3>这里是子组件child</h3> <p> <button v-on:click="triggerSelf">触发本组件事件 </button> </p> <p> <button v-on:click="triggerParent">触发父组件事件 </button> </p> </div>` }) </script> </head> <body> <div id="app"> <h3>这里是父组件App</h3> <button v-on:click='destoryComponent'>销毁组件</button> <child v-on:updateinfo='updateInfo'> </child> </div> <script> var vm = new Vue({ el: '#app', mounted() { console.log(this); }, methods: { updateInfo: function() { }, destoryComponent: function(){ }, } }); </script> </body> </html>
我们将这段代码的逻辑简单梳理一下。
首先是 child
组件。
创建一个名为child组件的组件,在该组件中使用v-on声明了两个事件。 一个事件为triggerSelf,内部逻辑打印字符串'triggerSelf'。 另一个事件为triggetParent,内部逻辑是使用$emit触发父组件updateinfo事件。 我们还在组件的mounted钩子函数中打印了组件实例this的值。
接着是 App
组件的逻辑。
App组件中定义了一个名为destoryComponent的事件。 同时App组件还引用了child组件,并且在子组件上绑定了一个为updateinfo的native DOM事件。 App组件的mounted钩子函数也打印了组件实例this的值。
因为在 App
组件中引用了 child
组件,因此 App
组件和 child
组件构成了父子关系,且 App
组件为父组件, child
组件为子组件。
逻辑梳理完成后,我们运行这份代码,查看一下两个组件实例中 _events
属性的打印结果。
从打印的结果可以看到,当前组件实例的 _events
属性保存的只是父组件绑定在当前组件上的事件,而不是组件中所有的事件。
vm._hasHookEvent
_hasHookEvent
属性表示的是父组件是否通过 v-hook:钩子函数名称
把钩子函数绑定到当前组件上。
updateComponentListeners(vm, listeners)
对于这个函数,我们首先需要关注的是 listeners
这个参数。我们看一下它是怎么来的。
// init parent attached events const listeners = vm.$options._parentListeners
从注释翻译过来的意思就是 初始化父组件添加的事件
。到这里不知道大家是否有和我相同的疑惑,我们前面说 _events
属性保存的是父组件绑定在当前组件上的事件。这里又说 _parentListeners
也是父组件添加的事件。这两个属性到底有什么区别呢?
我们将上面的示例稍作修改,添加一条打印信息 (这里只将修改的部分贴出来)
。
<script> // 修改子组件child的mounted方法:打印属性 var ChildComponent = Vue.component('child', { mounted() { console.log("this._events:"); console.log(this._events); console.log("this.$options._parentListeners:"); console.log(this.$options._parentListeners); }, }) </script> <!--修改引用子组件的代码:增加两个事件绑定(并且带有事件修饰符) --> <child v-on:updateinfo='updateInfo' v-on:sayHello.once='sayHello' v-on:SayBye.capture='SayBye'> </child> <script> // 修改App组件的methods方法:增加两个方法sayHello和sayBye var vm = new Vue({ methods: { sayHello: function(){ }, SayBye: function(){ }, } }); </script>
接着我们在浏览器中运行代码,查看结果。
从这个结果我们其实可以看到, _events
和 _parentListeners
保存的内容实际上都是父组件绑定在当前组件上的事件。只是保存的键值稍微有一些区别:
区别一: 前者事件名称这个key直接是事件名称 后者事件名称这个key保存的是一个字符串和事件名称的拼接,这个字符串是对修饰符的一个转化(.once修饰符会转化为~;.capture修饰符会转化为!) 区别二: 前者事件名称对应的value是一个数组,数组里面才是对应的事件回调 后者事件名称对应的vaule直接就是回调函数
Ok,继续我们的分析。
接着就是判断这个 listeners
:假如 listeners
存在的话,就执行 updateComponentListeners(vm, listeners)
方法。我们看一下这个方法内部实现。
//源码位置备注:/vue/src/core/instance/events.js export function updateComponentListeners ( vm: Component, listeners: Object, oldListeners: ?Object ) { target = vm updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm) target = undefined }
可以看到在该方法内部又调用到了 updateListeners
,先看一下这个函数的参数吧。
listeners
:这个参数我们刚说过,是父组件中添加的事件。
oldListeners
:这参数根据变量名翻译就是旧的事件,具体是什么目前还不太清楚。但是在初始化事件的整个过程中,调用到 updateComponentListeners
时传递的 oldListeners
参数值是一个空值。所以这个值我们暂时不用关注。(在 /vue/src/
目录下全局搜索 updateComponentListeners
这个函数,会发现该函数在其他地方有调用,所以该参数应该是在别的地方有用到)。
add
: add是一个函数,函数内部逻辑代码为:
function add (event, fn) { target.$on(event, fn) }
remove
: remove也是一个函数,函数内部逻辑代码为:
function remove (event, fn) { target.$off(event, fn) }
createOnceHandler
:
vm
:这个参数就不用多说了,就是当前组件的实例。
这里我们主要说一下add函数和remove函数中的两个重要代码: target.$on
和 target.$off
。
首先 target
是在 event.js
文件中定义的一个全局变量:
//源码位置备注:/vue/src/core/instance/events.js let target: any
在 updateComponentListeners
函数内部,我们能看到将组件实例赋值给了 target
:
//源码位置备注:/vue/src/core/instance/events.js target = vm
所以 target
就是组件实例。当然熟悉 Vue
的同学应该很快能反应上来 $on
、 $off
方法本身就是定义在组件实例上和事件相关的方法。那组件实例上有关事件的方法除了 $on
和 $off
方法之外,还有两个方法: $once
和 $emit
。
在这里呢,我们暂时不详细去解读这四个事件方法的源码实现,只截图贴出 Vue
官网对这个四个实例方法的用法描述。
vm.$on
vm.$once
vm.$emit
vm.$emit的用法在 Vue父子组件通信 一文中有详细的示例。
vm.$off
updateListeners
函数的参数基本解释完了,接着我们在回归到 updateListeners
函数的内部实现。
//源码位置备注:/vue/src/vdom/helpers/update-listener.js export function updateListeners ( on: Object, oldOn: Object, add: Function, remove: Function, createOnceHandler: Function, vm: Component ) { let name, def, cur, old, event // 循环断当前组件的父组件上的事件 for (name in on) { // 根据事件名称获取事件回调函数 def = cur = on[name] // oldOn参数对应的是oldListeners,前面说过这个参数在初始化的过程中是一个空对象{},所以old的值为undefined old = oldOn[name] event = normalizeEvent(name) if (isUndef(old)) { if (isUndef(cur.fns)) { cur = on[name] = createFnInvoker(cur, vm) } if (isTrue(event.once)) { cur = on[name] = createOnceHandler(event.name, cur, event.capture) } // 将父级的事件添加到当前组件的实例中 add(event.name, cur, event.capture, event.passive, event.params) } } }
首先是 normalizeEvent
这个函数,该函数就是对事件名称进行一个分解。假如事件名称 name='updateinfo.once'
,那经过该函数分解后返回的 event
对象为:
{ name: 'updateinfo', once: true, capture: false, passive: false }
关于 normalizeEvent
函数内部的实现也非常简单,这里就直接将结论整理出来。感兴趣的同学可以去看下源码实现,源码所在位置: /vue/src/vdom/helpers/update-listener.js
。
接下来就是在循环父组件事件的时候做一些 if/else
的条件判断,将父组件绑定在当前组件上的事件添加到当前组件实例的 _events
属性中;或者从当前组件实例的 _events
属性中移除对应的事件。
将父组件绑定在当前组件上的事件添加到当前组件的_events属性中
这个逻辑就是 add
方法内部调用 vm.$on
实现的。详细可以去看下 vm.$on
的源码实现,这里不再多说。而且从 vm.$on
函数的实现,也能看出 _events
和 _parentListener
之间的关联和差异。
initRender-初始化模板
//源码位置备注:/vue/src/core/instance/render.js export function initRender (vm: Component) { vm._vnode = null // the root of the child tree vm._staticTrees = null // v-once cached trees 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 fn绑定到组件实例上 vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) // normalization is always applied for the public version, used in // user-written render functions. vm.$createElement = (a, b, c, d) => 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 const parentData = parentVnode && parentVnode.data /* istanbul ignore else */ 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) } }
initRender
函数中,基本上是在为组件实例vm上的属性赋值: $slots
、 $scopeSlots
、 $createElement
、 $attrs
、 $listeners
。
那接下来就一一分析一下这些属性就知道 initRender
在执行的过程的逻辑了。
vm.$slots
这是来自官网对 vm.$slots
的解释,那为了方便,我还是写一个示例。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Vue的生命周期</title> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script> var ChildComponent = Vue.component('child', { mounted() { console.log("Clild组件,this.$slots:"); console.log(this.$slots); }, template:'<div id="child">子组件Child</div>' }) </script> </head> <body> <div id="app"> <h1 slot='root'>App组件,slot='root'</h1> <child> <h3 slot='first'>这里是slot=first</h3> <h3 slot='first'>这里是slot=first</h3> <h3>这里没有设置slot</h3> <h3 slot='last'>这里是slot=last</h3> </child> </div> <script> var vm = new Vue({ el: '#app', mounted() { console.log("App组件,this.$slots:"); console.log(this.$slots); } }); </script> </body> </html>
运行代码,看一下结果。
可以看到, child
组件的 vm.$slots
打印结果是一个包含三个键值对的对象。其中 key
为 first
的值保存了两个 VNode
对象,这两个 Vnode
对象就是我们在引用 child
组件时写的 slot=first
的两个 h3
元素。那 key
为 last
的值也是同样的道理。
key
为 default
的值保存了四个 Vnode
,其中有一个是引用 child
组件时写没有设置 slot
的那个 h3
元素,另外三个 Vnode
实际上是四个 h3
元素之间的换行,假如把 child
内部的 h3
这样写:
<child> <h3 slot='first'>这里是slot=first</h3><h3 slot='first'>这里是slot=first</h3><h3>这里没有设置slot</h3><h3 slot='last'>这里是slot=last</h3> </child>
那最终打印 key
为 default
对应的值就只包含我们没有设置 slot
的 h1
元素。
所以源代码中的 resolveSlots
函数就是解析模板中父组件传递给当前组件的 slot
元素,并且转化为 Vnode
赋值给当前组件实例的 $slots
对象。
vm.$scopeSlots
vm.$scopeSlots
是 Vue
中作用域插槽的内容,和 vm.$slot
查不多的原理,就不多说了。
在这里暂时给 vm.$scopeSlots
赋值了一个空对象,后续会在挂载组件调用 vm.$mount
时为其赋值。
vm.$createElement
vm.$createElement
是一个函数,该函数可以接收两个参数:
第一个参数:HTML元素标签名 第二个参数:一个包含Vnode对象的数组
vm.$createElement
会将 Vnode
对象数组中的 Vnode
元素编译成为 html
节点,并且放入第一个参数指定的 HTML
元素中。
那前面我们讲过 vm.$slots
会将父组件传递给当前组件的 slot
节点保存起来,且对应的 slot
保存的是包含多个 Vnode
对象的数组,因此我们就借助 vm.$slots
来写一个示例演示一下 vm.$createElement
的用法。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Vue的生命周期</title> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script> var ChildComponent = Vue.component('child', { render:function(){ return this.$createElement('p',this.$slots.first); } }) </script> </head> <body> <div id="app"> <h1 slot='root'>App组件,slot='root'</h1> <child> <h3 slot='first'>这里是slot=first</h3> <h3 slot='first'>这里是slot=first</h3> <h3>这里没有设置slot</h3> <h3 slot='last'>这里是slot=last</h3> </child> </div> <script> var vm = new Vue({ el: '#app' }); </script> </body> </html>
这个示例代码和前面介绍 vm.$slots
的代码差不多,就是在创建子组件时编写了 render
函数,并且使用了 vm.$createElement
返回模板的内容。那我们浏览器中的结果。
可以看到,正如我们所说, vm.$createElement
将 $slots
中 frist
对应的 包含两个Vnode对象的数组
编译成为两个 h3
元素,并且放入第一个参数指定的 p
元素中,在经过子组件的 render
函数将 vm.$createElement
的返回值进行处理,就看到了浏览器中展示的效果。
vm.$createElement
内部实现暂时不深入探究,因为牵扯到 Vue
中 Vnode
的内容,后面了解 Vnode
后在学习其内部实现。
vm.$attr和vm.$listener
这两个属性是有关组件通信的实例属性,赋值方式也非常简单,不在多说。
callHook(beforeCreate)-调用生命周期钩子函数
callhook
函数执行的目的就是调用 Vue
的生命周期钩子函数,函数的第二个参数是一个 字符串
,具体指定调用哪个钩子函数。那在初始化阶段,顺序执行完 initLifecycle
、 initState
、 initRender
后就会调用 beforeCreate
钩子函数。
接下来看下源码实现。
//源码位置备注:/vue/src/core/instance/lifecycle.js export function callHook (vm: Component, hook: string) { // #7573 disable dep collection when invoking lifecycle hooks pushTarget() // 根据钩子函数的名称从组件实例中获取组件的钩子函数 const handlers = vm.$options[hook] const info = `${hook} hook` if (handlers) { for (let i = 0, j = handlers.length; i < j; i++) { invokeWithErrorHandling(handlers[i], vm, null, vm, info) } } if (vm._hasHookEvent) { vm.$emit('hook:' + hook) } popTarget() }
首先根据钩子函数的名称从组件实例中获取组件的钩子函数,接着调用 invokeWithErrorHandling
, invokeWithErrorHandling
函数的第三个参数为null,所以 invokeWithErrorHandling
内部就是通过apply方法实现钩子函数的调用。
我们应该看到源码中是循环 handlers
然后调用 invokeWithErrorHandling
函数。那实际上,我们在编写组件的时候是可以 写多个名称相同的钩子
,但是实际上 Vue
在处理的时候只会在实例上保留最后一个重名的钩子函数,那这个循环的意义何在呢?
为了求证,我在 beforeCrated
这个钩子中打印了 this.$options['before']
,然后发现这个结果是一个数组,而且只有一个元素。
这样想来就能理解这个循环的写法了。
initInjections-初始化注入
initInjections这个函数是个Vue中的inject相关的内容。所以我们先看一下 官方文档度对inject的解释 。
官方文档中说 inject
和 provide
通常是一起使用的,它的作用实际上也是父子组件之间的通信,但是会建议大家在开发高阶组件时使用。
provide
是下文中 initProvide
的内容。
关于 inject
和 provide
的用法会有一个特点:只要父组件使用 provide
注册了一个数据,那不管有多深的子组件嵌套,子组件中都能通过 inject
获取到父组件上注册的数据。
大致了解 inject
和 provide
的用法后,就能猜想到 initInjections
函数内部是如何处理 inject
的了:解析获取当前组件中 inject
的值,需要查找父组件中的 provide
中是否注册了某个值,如果有就返回,如果没有则需要继续向上查找父组件。
下面看一下 initInjections
函数的源码实现。
// 源码位置备注:/vue/src/core/instance/inject.js export function initInjections (vm: Component) { const result = resolveInject(vm.$options.inject, vm) if (result) { toggleObserving(false) Object.keys(result).forEach(key => { /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { defineReactive(vm, key, result[key], () => { warn( `Avoid mutating an injected value directly since the changes will be ` + `overwritten whenever the provided component re-renders. ` + `injection being mutated: "${key}"`, vm ) }) } else { defineReactive(vm, key, result[key]) } }) toggleObserving(true) } }
源码中第一行就调用了 resolveInject
这个函数,并且传递了当前组件的inject配置和组件实例。那这个函数就是我们说的递归向上查找父组件的 provide
,其核心代码如下:
// source为当前组件实例 let source = vm while (source) { if (source._provided && hasOwn(source._provided, provideKey)) { result[key] = source._provided[provideKey] break } // 继续向上查找父组件 source = source.$parent }
需要说明的是当前组件的 _provided
保存的是父组件使用 provide
注册的数据,所以在 while
循环里会先判断 source._provided
是否存在,如果该值为 true
,则表示父组件中包含使用 provide
注册的数据,那么就需要进一步判断父组件 provide
注册的数据是否存在当前组件中 inject
中的属性。
递归查找的过程中,对弈查找成功的数据, resolveInject
函数会将inject中的元素对应的值放入一个字典中作为返回值返回。
例如当前组件中的 inject
设置为: inject: ['name','age','height']
,那经过 resolveInject
函数处理后会得到这样的返回结果:
{ 'name': '小土豆biubiubiu', 'age': 18, 'height': '180' }
最后在回到 initInjections
函数,后面的代码就是在非生产环境下,将inject中的数据变成响应式的,利用的也是双向数据绑定的那一套原理。
initState-初始化状态
//源码位置备注:/vue/src/core/instance/state.js 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) } }
初始化状态这个函数中主要会初始化 Vue
组件定义的一些属性: props
、 methods
、 data
、 computed
、 Watch
。
我们主要看一下 data
数据的初始化,即 initData
函数的实现。
//源码位置备注:/vue/src/core/instance/state.js function initData (vm: Component) { let data = vm.$options.data // 省略部分代码······ // observe data observe(data, true /* asRootData */) }
在 initData
函数里面,我们看到了一行熟悉系的代码: observe(data)
。这个 data
参数就是 Vue
组件中定义的 data
数据。正如注释所说,这行代码的作用就是 将对象变得可观测
。
在往 observe
函数内部追踪的话,就能追到之前 [1W字长文+多图,带你了解vue2.x的双向数据绑定源码实现] 里面的 Observer
的实现和调用。
所以现在我们就知道将对象变得可观测就是在 Vue
实例初始化阶段的 initData
这一步中完成的。
initProvide-初始化
//源码位置备注:/vue/src/core/instance/inject.js export function initProvide (vm: Component) { const provide = vm.$options.provide if (provide) { vm._provided = typeof provide === 'function' ? provide.call(vm) : provide } }
这个函数就是我们在总结 initInjections
函数时提到的 provide
。那该函数也非常简单,就是为当前组件实例设置 _provide
。
callHook(created)-调用生命周期钩子函数
到这个阶段已经顺序执行完 initLifecycle
、 initState
、 initRender
、 callhook('beforeCreate')
、 initInjections
、 initProvide
这些方法,然后就会调用 created
钩子函数。
callHook
内部实现在前面已经说过,这里也是一样的,所以不再重复说明。
总结
到这里,Vue2.x的生命周期的 初始化阶段
就解读完毕了。这里我们将初始化阶段做一个简单的总结。
源码还是很强大的,学习的过程还是比较艰难枯燥的,但是会发现很多有意思的写法,还有我们经常看过的一些理论内容在源码中的真实实践,所以一定要坚持下去。期待下一篇文章 [你还不知道Vue的生命周期吗?带你从Vue源码了解Vue2.x的生命周期(模板编译阶段)]
。
作者:小土豆biubiubiu
博客园: https://www.cnblogs.com/HouJiao/
掘金: https://juejin.im/user/58c61b4361ff4b005d9e894d
简书: https://www.jianshu.com/u/cb1c3884e6d5
微信公众号:土豆妈的碎碎念(扫码关注,一起吸猫,一起听故事,一起学习前端技术)
欢迎大家扫描微信二维码进入群聊讨论(若二维码失效可添加微信JEmbrace拉你进群):
码字不易,点赞鼓励哟~
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- nodejs源码—初始化
- Kratos 初始化源码分析
- Mybatis源码解读-初始化过程详解
- Vue源码探究-类初始化函数详情
- Spring MVC 源码解析(二)— 容器初始化
- Swoole 源码分析——Server 模块之初始化
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Programming Ruby
Dave Thomas、Chad Fowler、Andy Hunt / Pragmatic Bookshelf / 2004-10-8 / USD 44.95
Ruby is an increasingly popular, fully object-oriented dynamic programming language, hailed by many practitioners as the finest and most useful language available today. When Ruby first burst onto the......一起来看看 《Programming Ruby》 这本书的介绍吧!