Vue源码学习 - 组件化一 createComponent

  • 组件化
    • createComponent
      • 构造子类构造函数
      • 安装组件钩子函数
      • 实例化 VNode
      • 总结

学习内容和文章内容来自 黄轶老师
黄轶老师的慕课网视频教程地址:《Vue.js2.0 源码揭秘》、
黄轶老师拉钩教育教程地址:《Vue.js 3.0 核心源码解析》
这里分析的源码是Runtime + Compiler 的 Vue.js
调试代码在:node_modules\vue\dist\vue.esm.js 里添加
vue版本:Vue.js 2.5.17-beta

你越是认真生活,你的生活就会越美好——弗兰克·劳埃德·莱特
《人生果实》经典语录

点击回到 Vue源码学习完整目录

组件化

Vue.js另一个核心思想组件化。所谓组件化,就是把页面拆分成多个组件 (component),每个组件依赖的CSS、JavaScript、模板、图片等资源放在一起开发和维护。组件资源独立,组件在系统内部可复用,组件和组件之间可以嵌套

我们在用 Vue.js开发实际项目的时候,就是像搭积木一样,编写一堆组件拼装生成页面。在 Vue.js 的官网中,也是花了大篇幅来介绍什么是组件,如何编写组件以及组件拥有的属性和特性。

那么在这一章节,我们将从源码的角度来分析 Vue 的组件内部是如何工作的,只有了解了内部的工作原理,才能让我们使用它的时候更加得心应手。

接下来我们会用 Vue-cli 初始化的代码为例,来分析一下 Vue 组件初始化的一个过程。

// src\main.js
import Vue from 'vue'
import App from './App.vue'var app = new Vue({el: '#app',// 这里的 h 是 createElement 方法render: h => h(App)
})

这段代码相信很多同学都很熟悉,它和我们上一章相同的点也是通过 render 函数去渲染的,不同的是这次通过createElement传的参数是一个组件而不是一个原生的标签,那么接下来我们就开始分析这一过程。

createComponent

上一章我们在分析createElement的实现的时候,它最终会调用 _createElement方法,其中有一段逻辑是对参数 tag 的判断,如果是一个普通的 html 标签,像上一章的例子那样是一个普通的 div,则会实例化一个普通 VNode 节点,否则通过 createComponent 方法创建一个组件 VNode

// 这里的tag 就是src\main.js文件里 render: h => h(App) 传的App
if (typeof tag === 'string') {let Ctorns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)if (config.isReservedTag(tag)) {// platform built-in elementsvnode = new VNode(config.parsePlatformTagName(tag), data, children,undefined, undefined, context)} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {// componentvnode = createComponent(Ctor, data, context, children, tag)} else {// unknown or unlisted namespaced elements// check at runtime because it may get assigned a namespace when its// parent normalizes childrenvnode = new VNode(tag, data, children,undefined, undefined, context)}
} else {// 可以在这里加个debugger 单步调试// direct component options / constructorvnode = createComponent(tag, data, context, children)
}

在我们这一章传入的是一个 App 对象,它本质上是一个 Component 类型,那么它会走到上述代码的else逻辑,直接通过 createComponent方法来创建vnode

所以接下来我们来看一下createComponent方法的实现,它定义在 src/core/vdom/create-component.js文件中:

export function createComponent (Ctor: Class<Component> | Function | Object | void,data: ?VNodeData,context: Component,children: ?Array<VNode>,tag?: string
): VNode | Array<VNode> | void {// Ctor参数就是前面 createComponent(tag, data, context, children)传入的tag参数
// context就是Vueif (isUndef(Ctor)) {return}// 这里baseCtor相当于Vueconst baseCtor = context.$options._base// plain options object: turn it into a constructorif (isObject(Ctor)) {// extend方法这里重点看 // 主要将baseCtor 也就是Vue相关属性方法继承给Ctor 也就是传入的App组件Ctor = baseCtor.extend(Ctor)}// if at this stage it's not a constructor or an async component factory,// reject.if (typeof Ctor !== 'function') {if (process.env.NODE_ENV !== 'production') {warn(`Invalid Component definition: ${String(Ctor)}`, context)}return}// 这里是异步组件相关内容 这里先跳过// async componentlet asyncFactoryif (isUndef(Ctor.cid)) {asyncFactory = CtorCtor = resolveAsyncComponent(asyncFactory, baseCtor, context)if (Ctor === undefined) {// return a placeholder node for async component, which is rendered// as a comment node but preserves all the raw information for the node.// the information will be used for async server-rendering and hydration.return createAsyncPlaceholder(asyncFactory,data,context,children,tag)}}data = data || {}// resolve constructor options in case global mixins are applied after// component constructor creationresolveConstructorOptions(Ctor)// transform component v-model data into props & eventsif (isDef(data.model)) {transformModel(Ctor.options, data)}// extract propsconst propsData = extractPropsFromVNodeData(data, Ctor, tag)// functional componentif (isTrue(Ctor.options.functional)) {return createFunctionalComponent(Ctor, propsData, data, context, children)}// extract listeners, since these needs to be treated as// child component listeners instead of DOM listenersconst listeners = data.on// replace with listeners with .native modifier// so it gets processed during parent component patch.data.on = data.nativeOnif (isTrue(Ctor.options.abstract)) {// abstract components do not keep anything// other than props & listeners & slot// work around flowconst slot = data.slotdata = {}if (slot) {data.slot = slot}}// 这里可以关注下 主要是添加一些组件钩子函数// install component management hooks onto the placeholder nodeinstallComponentHooks(data)// return a placeholder vnodeconst name = Ctor.options.name || tag// 留意这里new VNode() 第三第四第五个参数为undefinedconst vnode = new VNode(`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,data, undefined, undefined, undefined, context,{ Ctor, propsData, listeners, tag, children },asyncFactory)// Weex specific: invoke recycle-list optimized @render function for// extracting cell-slot template.// https://github.com/Hanks10100/weex-native-directive/tree/master/component/* istanbul ignore if */if (__WEEX__ && isRecyclableComponent(vnode)) {return renderRecyclableComponentTemplate(vnode)}return vnode
}

可以看到,createComponent 的逻辑也会有一些复杂,但是分析源码比较推荐的是只分析核心流程,分支流程可以之后针对性的看,所以这里针对组件渲染这个 case 主要就 3 个关键步骤

构造子类构造函数
安装组件钩子函数
实例化 vnode

构造子类构造函数

const baseCtor = context.$options._base// plain options object: turn it into a constructor
if (isObject(Ctor)) {Ctor = baseCtor.extend(Ctor)
}


我们在编写一个组件的时候,通常都是创建一个普通对象,还是以我们的App.vue 为例,代码如下:

import HelloWorld from './components/HelloWorld'export default {name: 'app',components: {HelloWorld}
}

这里export的是一个对象,所以 createComponent里的代码逻辑会执行到 baseCtor.extend(Ctor),在这里 baseCtor 实际上就是Vue,这个的定义是在最开始初始化 Vue的阶段,在src/core/global-api/index.js 中的initGlobalAPI函数有这么一段逻辑:

// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base = Vue

细心的同学会发现,这里定义的是 Vue.options,而我们的 createComponent取的是 context.$options,实际上在src/core/instance/init.js 里 Vue 原型上的 _init 函数中有这么一段逻辑:

vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor),options || {},vm
)

这样就把 Vue 上的一些 option 扩展到了 vm.$options上,所以我们也就能通过 vm.$options._base 拿到 Vue 这个构造函数了。
mergeOptions 的实现我们会在后续章节中具体分析,现在只需要理解它的功能把 Vue 构造函数的 options 和用户传入的 options 做一层合并,到 vm.$options 上。

在了解了 baseCtor 指向了 Vue 之后,我们来看一下 Vue.extend 函数的定义,在 src/core/global-api/extend.js 中。

/*** Class inheritance*/
Vue.extend = function (extendOptions: Object): Function {extendOptions = extendOptions || {}const Super = thisconst SuperId = Super.cid// 缓存用 同一个组件只会初始化一次 后续复用 直接返回缓存 而不用重新初始化const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})if (cachedCtors[SuperId]) {return cachedCtors[SuperId]}const name = extendOptions.name || Super.options.nameif (process.env.NODE_ENV !== 'production' && name) {// 校验组件名称validateComponentName(name)}const Sub = function VueComponent (options) {this._init(options)}// Sub继承Super 也就是VueSub.prototype = Object.create(Super.prototype)Sub.prototype.constructor = SubSub.cid = cid++Sub.options = mergeOptions(Super.options,extendOptions)Sub['super'] = Super// For props and computed properties, we define the proxy getters on// the Vue instances at extension time, on the extended prototype. This// avoids Object.defineProperty calls for each instance created.if (Sub.options.props) {initProps(Sub)}if (Sub.options.computed) {initComputed(Sub)}// allow further extension/mixin/plugin usageSub.extend = Super.extendSub.mixin = Super.mixinSub.use = Super.use// create asset registers, so extended classes// can have their private assets too.ASSET_TYPES.forEach(function (type) {Sub[type] = Super[type]})// enable recursive self-lookupif (name) {Sub.options.components[name] = Sub}// keep a reference to the super options at extension time.// later at instantiation we can check if Super's options have// been updated.Sub.superOptions = Super.optionsSub.extendOptions = extendOptionsSub.sealedOptions = extend({}, Sub.options)// cache constructorcachedCtors[SuperId] = Subreturn Sub
}

Vue.extend 的作用就是构造一个 Vue 的子类,它使用一种非常经典的原型继承的方式把一个纯对象转换一个继承于 Vue 的构造器 Sub 并返回,然后对 Sub 这个对象本身扩展了一些属性,
如扩展options、添加全局 API等;
并且对配置中的propscomputed 做了初始化工作;
最后对于这个Sub 构造函数做了缓存,避免多次执行 Vue.extend的时候对同一个子组件重复构造。

这样当我们去实例化 Sub的时候,就会执行 this._init逻辑再次走到了Vue 实例的初始化逻辑,实例化子组件的逻辑在之后的章节会介绍。

const Sub = function VueComponent (options) {this._init(options)
}

安装组件钩子函数

// install component management hooks onto the placeholder node
installComponentHooks(data)

我们之前提到 Vue.js 使用的Virtual DOM 参考的是开源库 snabbdom,它的一个特点是在 VNode 的 patch 流程中对外暴露了各种时机的钩子函数,方便我们做一些额外的事情,Vue.js 也是充分利用这一点,在初始化一个 Component 类型的 VNode 的过程中实现了几个钩子函数

const componentVNodeHooks = {init (vnode: VNodeWithData, hydrating: boolean): ?boolean {if (vnode.componentInstance &&!vnode.componentInstance._isDestroyed &&vnode.data.keepAlive) {// kept-alive components, treat as a patchconst mountedNode: any = vnode // work around flowcomponentVNodeHooks.prepatch(mountedNode, mountedNode)} else {const child = vnode.componentInstance = createComponentInstanceForVnode(vnode,activeInstance)child.$mount(hydrating ? vnode.elm : undefined, hydrating)}},prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {const options = vnode.componentOptionsconst child = vnode.componentInstance = oldVnode.componentInstanceupdateChildComponent(child,options.propsData, // updated propsoptions.listeners, // updated listenersvnode, // new parent vnodeoptions.children // new children)},insert (vnode: MountedComponentVNode) {const { context, componentInstance } = vnodeif (!componentInstance._isMounted) {componentInstance._isMounted = truecallHook(componentInstance, 'mounted')}if (vnode.data.keepAlive) {if (context._isMounted) {// vue-router#1212// During updates, a kept-alive component's child components may// change, so directly walking the tree here may call activated hooks// on incorrect children. Instead we push them into a queue which will// be processed after the whole patch process ended.queueActivatedComponent(componentInstance)} else {activateChildComponent(componentInstance, true /* direct */)}}},destroy (vnode: MountedComponentVNode) {const { componentInstance } = vnodeif (!componentInstance._isDestroyed) {if (!vnode.data.keepAlive) {componentInstance.$destroy()} else {deactivateChildComponent(componentInstance, true /* direct */)}}}
}const hooksToMerge = Object.keys(componentVNodeHooks)function installComponentHooks (data: VNodeData) {const hooks = data.hook || (data.hook = {})for (let i = 0; i < hooksToMerge.length; i++) {const key = hooksToMerge[i]const existing = hooks[key]const toMerge = componentVNodeHooks[key]if (existing !== toMerge && !(existing && existing._merged)) {hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge}}
}function mergeHook (f1: any, f2: any): Function {const merged = (a, b) => {// flow complains about extra args which is why we use anyf1(a, b)f2(a, b)}merged._merged = truereturn merged
}

整个installComponentHooks的过程就是把 componentVNodeHooks 的钩子函数合并到 data.hook 中,在 VNode 执行 patch 的过程中执行相关的钩子函数,具体的执行我们稍后在介绍 patch 过程中会详细介绍。
这里要注意的是合并策略,在合并过程中,如果某个时机的钩子已经存在 data.hook中,那么通过执行mergeHook函数做合并,这个逻辑很简单,就是在最终执行的时候,依次执行这两个钩子函数即可。

实例化 VNode

const name = Ctor.options.name || tag
const vnode = new VNode(`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,data, undefined, undefined, undefined, context,{ Ctor, propsData, listeners, tag, children },asyncFactory
)
return vnode

最后一步非常简单,通过 new VNode 实例化一个vnode 并返回。需要注意的是和普通元素节点的 vnode 不同组件的 vnode没有 children 的,这点很关键,在之后的 patch 过程中我们会再提。

总结

这一节我们分析了 createComponent 的实现,了解到它在渲染一个组件的时候的 3 个关键逻辑
构造子类构造函数
安装组件钩子函数
实例化 vnode
createComponent 后返回的是组件 vnode,它也一样走到 vm._update 方法,进而执行了patch 函数,我们在上一章对patch 函数做了简单的分析,那么下一节我们会对它做进一步的分析。

Vue源码学习 - 组件化(二)

点击回到 Vue源码学习完整目录


谢谢你阅读到了最后~
期待你关注、收藏、评论、点赞~
让我们一起 变得更强

Vue源码学习 - 组件化一 createComponent相关推荐

  1. Vue源码学习 - 组件化(三) 合并配置

    Vue源码学习 - 组件化(三) 合并配置 合并配置 外部调用场景 组件场景 总结 学习内容和文章内容来自 黄轶老师 黄轶老师的慕课网视频教程地址:<Vue.js2.0 源码揭秘>. 黄轶 ...

  2. VUE源码学习第一篇--前言

    一.目的 前端技术的发展,现在以vue,react,angular为代表的MVVM模式以成为主流,这三个框架大有三分天下之势.react和angular有facebook与谷歌背书,而vue是以一己之 ...

  3. Vue源码学习 - 准备工作

    Vue源码学习 - 准备工作 准备工作 认识Flow 为什么用 Flow Flow 的工作方式 类型推断 类型注释 数组 类和对象 null Flow 在 Vue.js 源码中的应用 flow实践 总 ...

  4. vue源码学习--vue源码学习入门

    本文为开始学习vue源码的思路整理.在拿到vue项目源码的之后看到那些项目中的文件夹,会很困惑,不知道每个文件夹内的世界,怎么变换,怎样的魔力,最后产生了vue框架.学习源码也无从学起.我解决了这些困 ...

  5. Vue源码学习之Computed与Watcher原理

    前言  computed与watch是我们在vue中常用的操作,computed是一个惰性求值观察者,具有缓存性,只有当依赖发生变化,第一次访问computed属性,才会计算新的值.而watch则是当 ...

  6. Vue源码学习之initInjections和initProvide

    Vue源码学习之initInjections和initProvide 在进行源码阅读之前先让我们了解一个概念:provide/inject,这个是Vue在2.2.0版本新增的一个属性,按照Vue官网的 ...

  7. vue实例没有挂载到html上,vue 源码学习 - 实例挂载

    前言 在学习vue源码之前需要先了解源码目录设计(了解各个模块的功能)丶Flow语法. src ├── compiler # 把模板解析成 ast 语法树,ast 语法树优化,代码生成等功能. ├── ...

  8. Vue源码学习(一):源码的入口在哪里

    Vue源码解读系列 文章目录 Vue源码解读系列 前言 一.源码下载 二.目录解读 三.找到打包入口文件 四.如何进行代码调试 总结 前言   如何设计API和如何使用元编程思想(元编程,简单说是指框 ...

  9. vue源码学习(八)patch

    vue源码版本为2.6.11(cdn地址为: https://lib.baomitu.com/vue/2.6.11/vue.js) 虚拟DOM最核心的部分是patch,它可以将vnode渲染成真实的D ...

最新文章

  1. 这门国产语言终于要发布 1.0 版本了
  2. 如何:创建公钥/私钥对
  3. 有问有答 | 分布式服务框架精华问答
  4. 点这里,关注计算机视觉技术最前沿~
  5. Eclipse启动SpringCloud微服务集群的方法
  6. 云端之战:Thomas Kurian离职,Java 11趋向收费,Ellison豪赌ERP和云数据库
  7. eclipse+mysql+tomcat配置JNDI
  8. matlab 离散积分器设置,MATLABSIULINK积分器相关操作.docx
  9. IE 11中 onpropertychange失效
  10. H5游戏忆童年—承载梦想的纸飞机回来了吗?
  11. 微软Win8Server2012各版本安装密匙序列号
  12. 编译原理学习之:上下文无关文法(Context-free Grammar)和下推自动机(Push-down automata)
  13. 逻辑回归 自由度_回归自由度的官方定义
  14. linux下ssd4k对齐,linux查看硬盘4K对齐方法
  15. 360全景拍摄为什么要使用鱼眼镜头,与超广角镜头区别?
  16. python微信朋友圈刷图_10分钟用Python做个微信朋友圈抽奖九宫格
  17. 推荐一款好用的解压缩应用软件-BANDIZIP
  18. SQL注入——SQL注入具体防御方案
  19. 云图雅集—优美的文章段落
  20. 第十章 集合类 总结

热门文章

  1. 互联网周刊:腾讯能否灭UC
  2. 如何完整干净卸载JDK,有疑惑的兄弟进来看看吧!
  3. Java项目源码下载S2SH基于WEB的网上购物系统的设计与实现|电商购物商城
  4. 制作一个简单的Android版的音乐播放器
  5. vue学习,制作扫雷游戏
  6. 颜色渐变丶渲染效果类---(Unity自学笔记)
  7. mac 软件分享平台
  8. 计算机考研845大纲,2017年西北工业大学845电路基础考研大纲
  9. r、s sm2签名值_用Openssl计算ECDSA签名
  10. Quartus同Modelsim的联合仿真