Vue2核心原理(简易版)-组件初始化流程
什么是vue组件?
组件这部分内容十分复杂,单讲几句话是描述不清楚的。在我们这篇文章中你所需要知道的是:1. 组件是可复用的 Vue 实例;2. 组件的注册类型分为全局注册和局部注册。(全局注册的行为必须在根 Vue 实例 (通过 new Vue) 创建之前发生。)
示例:
// 全局注册
Vue.component('my-button1', {
template: `<button class="my-button">全局组件按钮</button>`
})
// 局部注册
// 不通过.vue文件创造的组件MyButton2
const MyButton2 = Vue.extend({
name: 'MyButton2',
template: `<button onclick="this.toggleMessage" class="my-button2">局部组件按钮</button>`,
})
let vm = new Vue({
el: '#app',
component: {
'my-button2': MyButton2
}
})
如图:
实现思路
组件初始化的过程
-
在之前,不论是我们的data,watch,computed...这些出现在options上的属性,我们都会习惯性的在initState中,对他们进行init操作的:
data是这样的:// 初始化data function initData() { const vm = this; let data = vm.$options.data; data = vm._data = typeof data === "function" ? data.call(vm) : data; // 代理到vm上 for (let key in data) { proxy(vm, "_data", key); } observe(data); }watch是这个姿势:
// 初始化watch function initWatch() { const vm = this; const watch = vm.$options.watch; Object.keys(watch).forEach((key) => { let handler = watch[key]; if (Array.isArray(handler)) { for (let i = 0; i < handler.length; i++) { createWatcher(vm, key, handler[i]); } } else { createWatcher(vm, key, handler); } }); }computed又是如此这般:
// 初始化computed function initComputed() { const vm = this; const computed = vm.$options.computed; vm._computedWatchers = {}; for (let key in computed) { const userDef = computed[key]; let getter = typeof userDef === "function" ? userDef : userDef.get; // 每一个计算属性实际就是一个watcher // 因为计算属性默认是不执行的,所以要给一个lazy true vm._computedWatchers[key] = new Watcher(vm, getter, () => {}, { lazy: true, }); // 将key定义在vm上,否则是取不到的 defineComputed(vm, key, userDef); } }- 那么我们的新朋友,
components呢,乍一看,局部注册的components和我们的老朋友们也一样出现在options配置当中,理论上我们也需要对其进行一步initComponent操作。 - 然而我们的确并没有,或者说,其实我们没有这个需要,为什么呢?因为components对象下,我们所定义的
'my-button2': MyButton2里面的MyButton2实质上已经完成了我们对components这个属性的预期,或者说是对'my-button2'这个新注册的组件的预期。因为我们仅仅是希望,在我们模版编译的过程中,能找到'my-button2'的VueComponent构造而已,嘿你说巧不巧,MyButton2就正好满足我们的要求,我们可以直接拿来用。
明确两点或许对你理解上面两段话有点儿帮助!
组件其实就是可复用的特殊的 Vue 实例
在生成真实节点的时候,遇到组件节点时,实际上就是把它看作一个新的Vue实例,对它再继续进行模版编译->构建vdom->生成真实dom。i. 怎么把我们写的这种配置转成你说的
VueComponent构造呢?
ii. 我们是怎么在生成真实节点的时候,找到VueComponent构造,生成vdom节点,并且对它进行挂载的?针对问题一,我们可以提供一个全局api方法,Vue.extend:
Vue.extend = function (opts) { // 原型继承 const Super = this; // 父亲大Vue const Sub = function VueComponent(options) { this._init(options); // 原型继承 this._init需要写入子类才会执行 }; Sub.prototype = Object.create(Super.prototype); Sub.prototype.constructor = Sub; // 构造函数指向自己 Sub.options = mergeOptions(Super.options, opts); // 先把extend接收到的options合并到Sub上去,后面实例化的时候还会再合并 return Sub; };看懂上面这个extend方法需要了解我们大名鼎鼎的JavaScript的原型继承机制,这部分内容不在我们今天的讨论范围,但是,我们可以粗略的理解为上面的代码就干了下面的事情。
- 继承大Vue,成为它的一个子类,具有大Vue原型上所有的功能。
- extend会首先接收一个对象options,这个对象options上的配置属性会首先被merge到我们所要构造的
VueComponent这个子类的options上去,随后当子类被实例化的过程中,新的配置又会再次merge到options上去,最后返回这个VueComponent构造。(this._init中有此操作,可以翻看代码)
所以我们的局部注册组件:
const MyButton2 = Vue.extend({ name: 'MyButton2', template: `<button onclick="this.toggleMessage" class="my-button2">局部组件按钮</button>`, })实际上是这个样子的:
OK,现在局部注册的时候我们用Vue.extend可以把那个对象变成一个
VueComponent构造了。但是全局注册怎么办呢?我们只需要也让对象被extend一下就好了!Vue.component = function (id, definition) { // definition是一个对象 // 通过this.options._base找到大Vue,保证具有全局api // 保证组件间的隔离,每个组件都会产生一个新的类,去继承父类 definition = this.options._base.extend(definition); this.options.components[id] = definition; };看到了吧,定义在Vue.component的那个对象(definition),最后还是免不了被Vue.extend生成
VueComponent构造,然后添加到this.options.components的命运。 - 那么我们的新朋友,
组件挂载的过程
-
首先,组件和普通节点的分水岭就在这里,createElement,就是生成虚拟节点的时候。
export function createElement(vm, tag, data = {}, ...children) { // 这里要开始区分原始标签还是组件标签了 if (!isReservedTag(tag)) { const Ctor = vm.$options.components[tag]; // 找到此组件名的构造函数,要么找到局部注册的,要么就是全局的 return createComponent(vm, tag, data, data.key, children, Ctor); } else { return vnode(vm, tag, data, data.key, children, undefined); } } function createComponent(vm, tag, data, key, children, Ctor) { // 组件的构造函数 if (isObject(Ctor)) { Ctor = vm.$options._base.extend(Ctor); // Vue.extend } data.hook = { // 等会渲染组件时 需要调用此初始化方法 init(vnode) { let vm = (vnode.componentInstance = new Ctor({ _isComponent: true })); // new Sub 会用此选项和组件的配置进行合并 vm.$mount(); // 组件挂载完成后 会在 vnode.componentInstance.$el => <button> }, }; return vnode(vm, `vue-component-${tag}`, data, key, undefined, undefined, { Ctor, children, }); }我们会把标签名变成这个样子
vue-component-${tag},当然,最重要的部分就是找到这个组件的Ctor(const Ctor = vm.$options.components[tag]),然后把它放到vnode中去。这样子,在我们后面的patch过程中,才可以生成组件的实例,并且执行$mount,生成真的$el(真实dom节点)。function createComponent(vnode) { let i = vnode.data; // vnode.data.hook.init if ((i = i.hook) && (i = i.init)) { i(vnode); // 调用init方法 } if (vnode.componentInstance) { // 有属性说明子组件new完毕了,并且组件对应的真实DOM挂载到了componentInstance.$el return true; } }大功告成!
小插曲-component的mergeOptions
-
如果真的细细品读, 你可能会发现,
const Ctor = vm.$options.components[tag];Ctor是在vm.$options.components上面找的,但是全局注册的时候,我们没有把全局的这个构造放到vm.$options.components上面去,那么这究竟是怎么一回事呢?// 让组件options的合并成为原型继承关系,局部组件没有就去找全局组件 strats.components = function (parentVal, childVal) { // Vue.options.components let options = Object.create(parentVal); // 根据父对象构造一个新对象 options.__proto__= parentVal if (childVal) { for (let key in childVal) { options[key] = childVal[key]; } } return options; };我们是在mergeOptions的时候动了手脚,让局部的components能沿着原型链找到全局定义在Vue.options上的组件构造!
所以,它们的关系就正如下图所示
原文链接
如果你在非掘金地址看到这篇文章,这里有原文传送门,您的点赞是对我最大的支持!