Vue原理解析(二):快速搞懂new Vue()时到底做了什么?(上)

上一篇:Vue原理解析(一):Vue到底是什么?

上一章节我们知道了在new Vue()时,内部会执行一个this._init()方法,这个方法是在initMixin(Vue)内定义的:

export function initMixin(Vue) {
  Vue.prototype._init = function(options) {
    ...
  }
}

当执行new Vue()执行后,触发的一系列初始化都在_init方法中启动,它的实现如下:

let uid = 0

Vue.prototype._init = function(options) {

  const vm = this
  vm._uid = uid++  // 唯一标识
  
  vm.$options = mergeOptions(  // 合并options
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  )
  ...
  initLifecycle(vm) // 开始一系列的初始化
  initEvents(vm)
  initRender(vm)
  callHook(vm, 'beforeCreate')
  initInjections(vm)
  initState(vm)
  initProvide(vm)
  callHook(vm, 'created')
  ...
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}

先需要交代下,每一个组件都是一个Vue构造函数的子类,这个之后会说明为何如此。从上往下我们一步步看,首先会定义_uid属性,这是为每个组件每一次初始化时做的一个唯一的私有属性标识,有时候会有些作用。

有一个使用它的小例子,找到一个组件所有的兄弟组件并剔除自己:

<div>
  ...
  <child-components />
  <child-components />  // 找到它的兄弟组件
  ... 其他组件
  <child-components />
</div>

首先要找的组件需要定义name属性,当然定义name属性也是一个好的书写习惯。首先通过自己的父组件($parent)的所有子组件($children)过滤出相同name集合的组件,这个时候他们就是同一个组件了,虽然它们name相同,但是_uid不同,最后在集合内根据_uid剔除掉自己即可。

合并options配置

回到主线任务,接着会合并options并在实例上挂载一个$options属性。合并什么东西了?这里是分两种情况的:

  1. 初始化new Vue

在执行new Vue构造函数时,参数就是一个对象,也就是用户的自定义配置;会将它和vue之前定义的原型方法,全局API属性;还有全局的Vue.mixin内的参数,将这些都合并成为一个新的options,最后赋值给一个的新的属性$options

  1. 子组件初始化

如果是子组件初始化,除了合并以上那些外,还会将父组件的参数进行合并,如有父组件定义在子组件上的eventprops等等。

经过合并之后就可以通过this.$options.data访问到用户定义的data函数,this.$options.name访问到用户定义的组件名称,这个合并后的属性很重要,会被经常使用到。

接下里会顺序的执行一堆初始化方法,首先是这三个:

1. initLifecycle(vm)
2. initEvents(vm)
3. initRender(vm)

1. initLifecycle(vm): 主要作用是确认组件的父子关系和初始化某些实例属性。

export function initLifecycle(vm) {
  const options = vm.$options  // 之前合并的属性
  
  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  // 让每一个子组件的$root属性都是根组件
  
  vm.$children = []
  vm.$refs = {}
  
  vm._watcher = null
  ...
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}

vue是组件式开发的,所以当前实例可能会是其他组件的子组件的同时也可能是其他组件的父组件。

首先会找到当前组件第一个非抽象类型的父组件,所以如果当前组件有父级且当前组件不是抽象组件就一直向上查找,直至找到后将找到的父级赋值给实例属性vm.$parent,然后将当前实例push到找到的父级的$children实例属性内,从而建立组件的父子关系。接下来的一些_开头是私有实例属性我们记住是在这里定义的即可,具体意思也是以后用到的时候再做说明。

2. initEvents(vm): 主要作用是将父组件在使用v-on@注册的自定义事件添加到子组件的事件中心中。

首先看下这个方法定义的地方:

export function initEvents (vm) {
  vm._events = Object.create(null)  // 事件中心
  ...
  const listeners = vm.$options._parentListeners  // 经过合并options得到的
  if (listeners) {
    updateComponentListeners(vm, listeners) 
  }
}

我们首先要知道在vue中事件分为两种,他们的处理方式也各有不同:

2.1 原生事件

在执行initEvents之前的模板编译阶段,会判断遇到的是html标签还是组件名,如果是html标签会在转为真实dom之后使用addEventListener注册浏览器原生事件。绑定事件是挂载dom的最后阶段,这里只是初始化阶段,这里主要是处理自定义事件相关,也就是另外一种,这里声明下,大家不要理会错了执行顺序。

2.2 自定义事件

在经历过合并options阶段后,子组件就可以从vm.$options._parentListeners读取到父组件传过来的自定义事件:

<child-components @select='handleSelect' />

传过来的事件数据格式是{select:function(){}}这样的,在initEvents方法内定义vm._events用来存储传过来的事件集合。

内部执行的方法updateComponentListeners(vm, listeners)主要是执行updateListeners方法。这个方法有两个执行时机,首先是现在的初始化阶段,还一个就是最后patch时的原生事件也会用到。它的作用是比较新旧事件的列表来确定事件的添加和移除以及事件修饰符的处理,现在主要看自定义事件的添加,它的作用是借助之前定义的$on$emit方法,完成父子组件事件的通信,(详细的原理说明会在之后的全局API章节统一说明)。首先使用$onvm.events事件中心下创建一个自定义事件名的数组集合项,数组内的每一项都是对应事件名的回调函数,例如:

vm._events.select = [function handleSelect(){}, ...]  // 可以有多个

注册完成之后,使用$emit方法执行事件:

this.$emit('select')

首先会读取到事件中心内$emit方法第一个参数select的对象的数组集合,然后将数组内每个回调函数顺序执行一遍即完成了$emit做的事情。

不知道大家有没有注意到this.$emit这个方法是在当前组件实例触发的,所以事件的原理可能跟大部分人理解的不一样,并不是父组件监听,子组件往父组件去派发事件。

而是子组件往自身的实例上派发事件,只是因为回调函数是在父组件的作用域下定义的,所以执行了父组件内定义的方法,就造成了父子之间事件通信的假象。知道这个原理特性后,我们可以做一些更cool的事情,例如:

<div>
  <parent-component>  // $on添加事件
    <child-component-1>
      <child-component-2>
        <child-component-3 />  // $emit触发事件
      </child-component-2>
    </child-components-1>
  </parent-component>
</div>

我们可不可以在parent-component内使用$on添加事件到当前实例的事件中心,而在child-components-3内找到parent-component的组件实例并在它的事件中心触发对应的事件实现跨组件通信了,答案是可以了!这一原理发现再开发组件库时会有一定帮助。

3. initRender(vm): 主要作用是挂载可以将render函数转为vnode的方法。

export function initRender(vm) {
  vm._vnode = null
  ...
  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)  // 转化手写的
  ...
}

主要作用是挂载vm._cvm.$createElement两个方法,它们只是最后一个参数不同,这两个方法都可以将render函数转为vnode,从命名大家应该可以看出区别,vm._c转换的是通过编译器将template转换而来的render函数;而vm.$createElement转换的是用户自定义的render函数,比如:

new Vue({
  data: {
    msg: 'hello Vue!'
  },
  render(h) { // 这里的 h 就是vm.$createElement
    return h('span', this.msg);  
  }
}).$mount('#app');

render函数的参数h就是vm.$createElement方法,将内部定义的树形结构数据转为Vnode的实例。

4. callHook(vm, 'beforeCreate')

终于我们要执行实例的第一个生命周期钩子beforeCreate,这里callHook的原理是怎样的,我们之后的生命周期章节会说明,现在这里只需要知道它会执行用户自定义的生命周期方法,如果有mixin混入的也一并执行。

好吧,实例的第一个生命周期钩子阶段的初始化工作完成了,一句话来主要说明下他们做了什么事情:

  • initLifecycle(vm):确认组件(也是vue实例)的父子关系
  • initEvents(vm):将父组件的自定义事件传递给子组件
  • initRender(vm):提供将render函数转为vnode的方法
  • beforeCreate:执行组件的beforeCreate钩子函数

最后还是以一道vue容易被问道的面试题作为本章节的结束吧:

面试官微笑而又不失礼貌的问道:

  • 请问可以在beforeCreate钩子内通过this访问到data中定义的变量么,为什么以及请问这个钩子可以做什么?

怼回去:

  • 是不可以访问的,因为在vue初始化阶段,这个时候data中的变量还没有被挂载到this上,这个时候访问值会是undefinedbeforeCreate这个钩子在平时业务开发中用的比较少,而像插件内部的instanll方法通过Vue.use方法安装时一般会选在beforeCreate这个钩子内执行,vue-routervuex就是这么干的。

下一篇:Vue原理解析(三):快速搞懂new Vue()时到底做了什么?(下)

顺手点个赞或关注呗,找起来也方便~

分享一个笔者自己写的组件库,哪天可能会用的上了 ~ ↓

你可能会用的上的一个vue功能组件库,持续完善中...

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,923评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,154评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,775评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,960评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,976评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,972评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,893评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,709评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,159评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,400评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,552评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,265评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,876评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,528评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,701评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,552评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,451评论 2 352

推荐阅读更多精彩内容