亲宝软件园·资讯

展开

一起学习vue源码 - Vue2.x的生命周期(初始化阶段)

小土豆biubiubiu 人气:0
> 作者:小土豆biubiubiu > > 博客园:https://www.cnblogs.com/HouJiao/ > > 掘金:https://juejin.im/user/58c61b4361ff4b005d9e894d > > 简书:https://www.jianshu.com/u/cb1c3884e6d5 > > 微信公众号:土豆妈的碎碎念(扫码关注,一起吸猫,一起听故事,一起学习前端技术) > > 欢迎大家扫描微信二维码进入群聊讨论(若二维码失效可添加微信JEmbrace拉你进群): > > ![](https://user-gold-cdn.xitu.io/2020/3/31/1712fabb576f52e4?w=168&h=168&f=png&s=10818) > 码字不易,点赞鼓励哟~ # 温馨提示 本篇文章内容过长,一次看完会有些乏味,建议大家可以先收藏,分多次进行阅读,这样更好理解。 # 前言 相信很多人和我一样,在刚开始了解和学习`Vue`生命明周期的时候,会做下面一系列的总结和学习。 ### 总结1 `Vue`的实例在创建时会经过一系列的初始化: 设置数据监听、编译模板、将实例挂载到DOM并在数据变化时更新DOM等 ### 总结2 在这个初始化的过程中会运行一些叫做"生命周期钩子"的函数: beforeCreate:组件创建前 created:组件创建完毕 beforeMount:组件挂载前 mounted:组件挂载完毕 beforeUpdate:组件更新之前 updated:组件更新完毕 beforeDestroy:组件销毁前 destroyed:组件销毁完毕 ### 示例1 关于每个钩子函数里组件的状态示例: ```html Vue的生命周期

{{info}}

``` ### 总结3: 结合前面示例1的运行结果会有如下的总结。 ##### 组件创建前(beforeCreate) ![](https://user-gold-cdn.xitu.io/2020/3/23/171061381bd35710?w=370&h=127&f=png&s=2525) 组件创建前,组件需要挂载的DOM元素el和组件的数据data都未被创建。 ##### 组件创建完毕(created) ![](https://user-gold-cdn.xitu.io/2020/3/23/1710625a35847c04?w=487&h=282&f=png&s=10846) 创建创建完毕后,组件的数据已经创建成功,但是DOM元素el还没被创建。 ##### 组件挂载前(beforeMount): ![](https://user-gold-cdn.xitu.io/2020/3/23/17106271481df1bf?w=544&h=370&f=png&s=18533) 组件挂载前,DOM元素已经被创建,只是data中的数据还没有应用到DOM元素上。 ##### 组件挂载完毕(mounted) ![](https://user-gold-cdn.xitu.io/2020/3/23/171062919bec4076?w=502&h=369&f=png&s=15751) 组件挂载完毕后,data中的数据已经成功应用到DOM元素上。 ##### 组件更新前(beforeUpdate) ![](https://user-gold-cdn.xitu.io/2020/3/23/171062ee2a428d7b?w=714&h=370&f=png&s=25396) 组件更新前,data数据已经更新,组件挂载的DOM元素的内容也已经同步更新。 ##### 组件更新完毕(updated) ![](https://user-gold-cdn.xitu.io/2020/3/23/171062f0bdcbb925?w=733&h=369&f=png&s=24996) 组件更新完毕后,data数据已经更新,组件挂载的DOM元素的内容也已经同步更新。 (感觉和beforeUpdate的状态基本相同) ##### 组件销毁前(beforeDestroy) ![](https://user-gold-cdn.xitu.io/2020/3/23/171063700615891d?w=764&h=372&f=png&s=29450) 组件销毁前,组件已经不再受vue管理,我们可以继续更新数据,但是模板已经不再更新。 ##### 组件销毁完毕(destroyed) ![](https://user-gold-cdn.xitu.io/2020/3/23/171063913c6df20f?w=749&h=372&f=png&s=30296) 组件销毁完毕,组件已经不再受vue管理,我们可以继续更新数据,但是模板已经不再更新。 ### 组件生命周期图示 最后的总结,就是来自`Vue`官网的生命周期图示。 ![](https://user-gold-cdn.xitu.io/2020/3/23/171063deee776547?w=1200&h=3039&f=png&s=77677) 那到这里,前期对`Vue`生命周期的学习基本就足够了。那今天,我将带大家从`Vue源码`了解`Vue2.x的生命周期的初始化阶段`,开启`Vue生命周期`的进阶学习。 > Vue官网的这张生命周期图示非常关键和实用,后面我们的学习和总结都会基于这个图示。 # 创建组件实例 对于一个组件,`Vue`框架要做的第一步就是创建一个`Vue`实例:即`new Vue()`。那`new Vue()`都做了什么事情呢,我们来看一下`Vue`构造函数的源码实现。 ```javascript //源码位置备注:/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条件判断逻辑如下: ```javascript 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`环境,我们就可以直接在命令行输入一下这个全局变量。 ![](https://user-gold-cdn.xitu.io/2020/3/23/17106770b40423fb?w=623&h=541&f=png&s=15969) > 这个全局变量包含的信息非常多,这里只截出了部分属性。 对于[process的evn属性](http://nodejs.cn/api/process.html#process_process_env) 它返回当前用户环境信息。但是这个信息不是直接访问就能获取到值,而是需要通过设置才能获取。 ![](https://user-gold-cdn.xitu.io/2020/3/23/171067fe815e21a7?w=246&h=46&f=png&s=1044) 可以看到我没有设置这个属性,所以访问获得的结果是`undefined`。 然后我们在看一下`Vue`项目中的`webpack`对`process.evn.NODE_EVN`的设置说明: ![](https://user-gold-cdn.xitu.io/2020/3/23/17106859d6fbe5a4?w=717&h=229&f=png&s=18149) > 执行`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`。 ```javascript function Person(name,age){ this.name = name; this.age = age; this.printThis = function(){ console.log(this); } //调用函数时,打印函数内部的this this.printThis(); } ``` 关于`JavaScript`的函数有两种调用方式:以`普通函数`方式调用和以`构造函数`方式调用。我们分别以两种方式调用一下`Person`函数,看看函数内部的`this`是什么。 ```javascript // 以普通函数方式调用 Person('小土豆biubiubiu',18); // 以构造函数方式创建 var pIns = new Person('小土豆biubiubiu'); ``` 上面这段代码在浏览器的执行结果如下: ![](https://user-gold-cdn.xitu.io/2020/3/23/17106ee03aeb89a9?w=709&h=57&f=png&s=6418) 从结果我们可以总结: 以普通函数方式调用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原型上的一个方法: ```javascript //源码位置备注:/vue/src/core/instance/init.js export function initMixin (Vue: Class) { 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`属性。 ```javascript //源码位置备注:/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`时传递的选项。 ![](https://user-gold-cdn.xitu.io/2020/3/31/1712e961b159cdec?w=288&h=120&f=png&s=2963) 那满足`if`分支的逻辑就是如果`options`存在且是一个组件。那在`new Vue`的时候显然不满足`if`分支的逻辑,所以会执行`else`分支的逻辑。 > 使用`Vue.extend`方法创建组件的时候会满足`if`分支的逻辑。 在else分支中,`resolveConstructorOptions`的作用就是通过组件实例的构造函数获取当前组件的选项和父组件的选项,在通过`mergeOptions`方法将这两个选项进行合并。 > 这里的父组件不是指组件之间引用产生的父子关系,还是跟`Vue.extend`相关的父子关系。目前我也不太了解`Vue.extend`的相关内容,所以就不多说了。 ### vm._renderProxy 接着就是为组件实例的`_renderProxy`赋值。 ```javascript //源码位置备注:/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`赋值的。 ```javascript //源码位置备注:/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`的用法如下: ![](https://user-gold-cdn.xitu.io/2020/3/31/1712f633a4679a6f?w=700&h=406&f=png&s=20257) 那我们简单的写一个关于`Proxy`的用法示例。 ```javascript 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`的某个属性时,如果属性存在,则为其赋新值;如果不存在则打印`'属性不存在,无法赋值'`。 接着我们把上面这段代码放入浏览器的控制台运行,然后访问代理对象的属性: ![](https://user-gold-cdn.xitu.io/2020/3/31/1712f815c89bebb7?w=420&h=160&f=png&s=9002) 然后在修改代理对象的属性: ![](https://user-gold-cdn.xitu.io/2020/3/31/1712f826166a4f2a?w=454&h=119&f=png&s=5691) 结果和我们前面描述一致。然后我们在说回`initProxy`,它实际上也就是在访问`vm`上的某个属性时做一些验证,比如该属性是否在vm上,访问的属性名称是否合法等。 总结这块的作用,实际上就是在非生产环境中为我们的代码编写的代码做出一些错误提示。 ### 连续多个函数调用 最后就是看到有连续多个函数被调用。 ```javascript 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`官网的`生命周期图示`对比一下: ![](https://user-gold-cdn.xitu.io/2020/3/24/1710a8365dd21156?w=589&h=338&f=png&s=36734) 可以发现代码和这个图示基本上是一一对应的,所以`_init`方法被称为是`Vue实例的初始化方法`。下面我们将逐个解读`_init`内部按顺序调用的那些方法。 # initLifecycle-初始化生命周期 ```javascript //源码位置备注:/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-初始化事件 ```javascript //源码位置备注:/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`表示的是什么呢?我们写一段代码验证一下。 ```html Vue的生命周期

这里是父组件App

``` 我们将这段代码的逻辑简单梳理一下。 首先是`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`属性的打印结果。 ![](https://user-gold-cdn.xitu.io/2020/3/26/17114c92fd0c64e5?w=409&h=43&f=png&s=3016) ![](https://user-gold-cdn.xitu.io/2020/3/26/17114cac1271b4cf?w=400&h=50&f=png&s=2974) 从打印的结果可以看到,当前组件实例的`_events`属性保存的只是父组件绑定在当前组件上的事件,而不是组件中所有的事件。 ### vm._hasHookEvent `_hasHookEvent`属性表示的是父组件是否通过`v-hook:钩子函数名称`把钩子函数绑定到当前组件上。 ### updateComponentListeners(vm, listeners) 对于这个函数,我们首先需要关注的是`listeners`这个参数。我们看一下它是怎么来的。 ```javascript // init parent attached events const listeners = vm.$options._parentListeners ``` 从注释翻译过来的意思就是`初始化父组件添加的事件`。到这里不知道大家是否有和我相同的疑惑,我们前面说`_events`属性保存的是父组件绑定在当前组件上的事件。这里又说`_parentListeners`也是父组件添加的事件。这两个属性到底有什么区别呢? 我们将上面的示例稍作修改,添加一条打印信息`(这里只将修改的部分贴出来)`。 ```html ``` 接着我们在浏览器中运行代码,查看结果。 ![](https://user-gold-cdn.xitu.io/2020/3/27/1711ac7c0733adf6?w=681&h=250&f=png&s=14739) 从这个结果我们其实可以看到,`_events`和`_parentListeners`保存的内容实际上都是父组件绑定在当前组件上的事件。只是保存的键值稍微有一些区别: 区别一: 前者事件名称这个key直接是事件名称 后者事件名称这个key保存的是一个字符串和事件名称的拼接,这个字符串是对修饰符的一个转化(.once修饰符会转化为~;.capture修饰符会转化为!) 区别二: 前者事件名称对应的value是一个数组,数组里面才是对应的事件回调 后者事件名称对应的vaule直接就是回调函数 Ok,继续我们的分析。 接着就是判断这个`listeners`:假如`listeners`存在的话,就执行`updateComponentListeners(vm, listeners)`方法。我们看一下这个方法内部实现。 ```javascript //源码位置备注:/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是一个函数,函数内部逻辑代码为: ```javascript function add (event, fn) { target.$on(event, fn) } ``` `remove`: remove也是一个函数,函数内部逻辑代码为: ```javascript function remove (event, fn) { target.$off(event, fn) } ``` `createOnceHandler`: `vm`:这个参数就不用多说了,就是当前组件的实例。 这里我们主要说一下add函数和remove函数中的两个重要代码:`target.$on`和`target.$off`。 首先`target`是在`event.js`文件中定义的一个全局变量: ```javascript //源码位置备注:/vue/src/core/instance/events.js let target: any ``` 在`updateComponentListeners`函数内部,我们能看到将组件实例赋值给了`target`: ```javascript //源码位置备注:/vue/src/core/instance/events.js target = vm ``` 所以`target`就是组件实例。当然熟悉`Vue`的同学应该很快能反应上来`$on`、`$off`方法本身就是定义在组件实例上和事件相关的方法。那组件实例上有关事件的方法除了`$on`和`$off`方法之外,还有两个方法:`$once`和`$emit`。 在这里呢,我们暂时不详细去解读这四个事件方法的源码实现,只截图贴出`Vue`官网对这个四个实例方法的用法描述。 ##### vm.$on ![](https://user-gold-cdn.xitu.io/2020/3/26/17116041de20ab03?w=734&h=493&f=png&s=26499) ##### vm.$once ![](https://user-gold-cdn.xitu.io/2020/3/26/1711604d34990221?w=625&h=254&f=png&s=11077) ##### vm.$emit ![](https://user-gold-cdn.xitu.io/2020/3/26/17116057fdce415d?w=501&h=212&f=png&s=9858) > vm.$emit的用法在 [Vue父子组件通信](https://juejin.im/post/5e61c014e51d45270f52c9e6) 一文中有详细的示例。 ##### vm.$off ![](https://user-gold-cdn.xitu.io/2020/3/26/17116055d27e51e4?w=528&h=387&f=png&s=20597) `updateListeners`函数的参数基本解释完了,接着我们在回归到`updateListeners`函数的内部实现。 ```javascript //源码位置备注:/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`对象为: ```javascript { 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-初始化模板 ```javascript //源码位置备注:/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 ![](https://user-gold-cdn.xitu.io/2020/3/27/1711b28dd23c2471?w=726&h=379&f=png&s=28020) 这是来自官网对`vm.$slots`的解释,那为了方便,我还是写一个示例。 ```html Vue的生命周期

App组件,slot='root'

这里是slot=first

这里是slot=first

这里没有设置slot

这里是slot=last

``` 运行代码,看一下结果。 ![](https://user-gold-cdn.xitu.io/2020/3/27/1711b605912ca6ca?w=572&h=409&f=png&s=20667) 可以看到,`child`组件的`vm.$slots`打印结果是一个包含三个键值对的对象。其中`key`为`first`的值保存了两个`VNode`对象,这两个`Vnode`对象就是我们在引用`child`组件时写的`slot=first`的两个`h3`元素。那`key`为`last`的值也是同样的道理。 > `key`为`default`的值保存了四个`Vnode`,其中有一个是引用`child`组件时写没有设置`slot`的那个`h3`元素,另外三个`Vnode`实际上是四个`h3`元素之间的换行,假如把`child`内部的`h3`这样写: ```html

这里是slot=first

这里是slot=first

这里没有设置slot

这里是slot=last

``` > 那最终打印`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`的用法。 ```html Vue的生命周期

App组件,slot='root'

这里是slot=first

这里是slot=first

这里没有设置slot

这里是slot=last

``` 这个示例代码和前面介绍`vm.$slots`的代码差不多,就是在创建子组件时编写了`render`函数,并且使用了`vm.$createElement`返回模板的内容。那我们浏览器中的结果。 ![](https://user-gold-cdn.xitu.io/2020/3/27/1711ba6d350762fb?w=502&h=496&f=png&s=23585) 可以看到,正如我们所说,`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`钩子函数。 接下来看下源码实现。 ```javascript //源码位置备注:/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']`,然后发现这个结果是一个数组,而且只有一个元素。 > ![](https://user-gold-cdn.xitu.io/2020/3/30/17129634749300c3?w=335&h=90&f=png&s=3434) > 这样想来就能理解这个循环的写法了。 # initInjections-初始化注入 initInjections这个函数是个Vue中的inject相关的内容。所以我们先看一下[官方文档度对inject的解释](https://cn.vuejs.org/v2/api/#provide-inject)。 ![](https://user-gold-cdn.xitu.io/2020/3/30/171297502c34d0cc?w=761&h=491&f=png&s=28800) 官方文档中说`inject`和`provide`通常是一起使用的,它的作用实际上也是父子组件之间的通信,但是会建议大家在开发高阶组件时使用。 > `provide` 是下文中`initProvide`的内容。 关于`inject`和`provide`的用法会有一个特点:只要父组件使用`provide`注册了一个数据,那不管有多深的子组件嵌套,子组件中都能通过`inject`获取到父组件上注册的数据。 ![](https://user-gold-cdn.xitu.io/2020/3/30/17129858beba264b?w=603&h=528&f=png&s=24127) 大致了解`inject`和`provide`的用法后,就能猜想到`initInjections`函数内部是如何处理`inject`的了:解析获取当前组件中`inject`的值,需要查找父组件中的`provide`中是否注册了某个值,如果有就返回,如果没有则需要继续向上查找父组件。 下面看一下`initInjections`函数的源码实现。 ```javascript // 源码位置备注:/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`,其核心代码如下: ```javascript // 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`函数处理后会得到这样的返回结果: ```javascript { 'name': '小土豆biubiubiu', 'age': 18, 'height': '180' } ``` 最后在回到`initInjections`函数,后面的代码就是在非生产环境下,将inject中的数据变成响应式的,利用的也是双向数据绑定的那一套原理。 # initState-初始化状态 ```javascript //源码位置备注:/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`。 ![](https://user-gold-cdn.xitu.io/2020/3/24/1710b8edaf1bf3c4?w=763&h=730&f=png&s=54803) 我们主要看一下`data`数据的初始化,即`initData`函数的实现。 ```javascript //源码位置备注:/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的双向数据绑定源码实现]](https://juejin.im/post/5e71e7066fb9a07cab3ab804) 里面的`Observer`的实现和调用。 所以现在我们就知道将对象变得可观测就是在`Vue`实例初始化阶段的`initData`这一步中完成的。 # initProvide-初始化 ```javascript //源码位置备注:/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的生命周期的`初始化阶段`就解读完毕了。这里我们将初始化阶段做一个简单的总结。 ![](https://user-gold-cdn.xitu.io/2020/3/31/1712f98b87f33baa?w=821&h=708&f=png&s=85376) 源码还是很强大的,学习的过程还是比较艰难枯燥的,但是会发现很多有意思的写法,还有我们经常看过的一些理论内容在源码中的真实实践,所以一定要坚持下去。期待下一篇文章`[你还不知道Vue的生命周期吗?带你从Vue源码了解Vue2.x的生命周期(模板编译阶段)]`。 > 作者:小土豆biubiubiu > > 博客园:https://www.cnblogs.com/HouJiao/ > > 掘金:https://juejin.im/user/58c61b4361ff4b005d9e894d > > 简书:https://www.jianshu.com/u/cb1c3884e6d5 > > 微信公众号:土豆妈的碎碎念(扫码关注,一起吸猫,一起听故事,一起学习前端技术) > > 欢迎大家扫描微信二维码进入群聊讨论(若二维码失效可添加微信JEmbrace拉你进群): > > ![](https://user-gold-cdn.xitu.io/2020/3/31/1712fabb576f52e4?w=168&h=168&f=png&s=10818) > 码字不易,点赞鼓励哟~

加载全部内容

相关教程
猜你喜欢
用户评论