Vue2.0源码学习(3) - 组件的创建和patch过程
组件化
组件化是vue的另一个核心思想,所谓的组件化就,就是说把页面拆分成多个组件(component),每个组件依赖的css、js、图片等资源放在一起开发和维护。组件是资源独立的,在内部系统中是可以多次复用的,组间之间也是可以互相嵌套的。
接下来我们用vue-cli为例,来分析一下Vue组件是如何工作的,还是它的创建及其工作原理。
import Vue from 'vue'
import App from './App.vue'
var app = new Vue({
el: '#app',
// 这里的 h 是 createElement 方法
render: h => h(App)
})
创建组件 - createComponent
在分析createComponent函数前,我们得先知道vue的源码执行过程中是怎么调用到createComponent的。其实我们在上一章就有所提及,具体流程如下:
①:Vue.prototype.$mount;(src\platforms\web\entry-runtime-with-compiler.js和src\platforms\web\runtime\index.js)
②:mountComponent;(src\core\instance\lifecycle.js)
③:vm._update(vm._render());(src\core\instance\lifecycle.js)
④:render.call(vm._renderProxy, vm.$createElement);(src\core\instance\render.js)
⑤:vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true);(src\core\instance\render.js)
⑥:createElement;(src\core\vdom\create-element.js)
⑦:_createElement;(src\core\vdom\create-element.js)
⑧:createComponent;
//src\core\vdom\create-element.js
// part 3
export function _createElement (
context: Component, //上下文环境,一般就是vm
tag?: string | Class<Component> | Function | Object, //标签(element)
data?: VNodeData, //VNode数据,VnodeData类型,详见flow\vnode.js
children?: any, //Vnode子节点
normalizationType?: number //子节点规范类型
): VNode | Array<VNode> {
...
//这次这个tag就是Class<Component>了
if (typeof tag === 'string') {
...
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
...
}
这次我们进入到_createElement函数走的就是createComponent的流程了。
// src\core\vdom\create-component.js
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component, //当前vm实例
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
}
// 标注①
const baseCtor = context.$options._base //实际上就是Vue
if (isObject(Ctor)) {
// 标注②
Ctor = baseCtor.extend(Ctor) //即Vue.extend(src\core\global-api\extend.js)
}
...
// 钩子函数挂载到data对象,详情查阅源码
installComponentHooks(data)
}
①:baseCtor其实就是Vue,流程如下
// src\core\global-api\index.js
// 初始化时候定义了Vue.options._base = Vue
Vue.options._base = Vue
// src\core\instance\init.js
Vue.prototype._init = function (options?: Object) {
// 在这里吧Vue.options合并到vm.$options上
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
而context其实就是vm,所以baseCtor =context.$options._base = vm.$options._base = Vue.options._base = Vue;
②:分析完baseCtor的由来,那么baseCtor.extend显然就是Vue.extend了,把Ctor对象转换成新的构造器,我们下面来详细看看Vue.extend。
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this //vue
const SuperId = Super.cid
//添加了一个_Ctor空对象属性
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
//后面会缓存cachedCtors[SuperId],防止多次生成相同构造器
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' && name) {
//名称校验,防止你们整些花里胡哨的关键字段。
validateComponentName(name)
}
const Sub = function VueComponent (options) {
this._init(options) //vue._init
}
Sub.prototype = Object.create(Super.prototype) //子构造器原型指向父构造器原型
Sub.prototype.constructor = Sub
Sub.cid = cid++
//入参配置和vue配置合并
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 usage
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.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-lookup
if (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.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
// cache constructor 缓存起来
cachedCtors[SuperId] = Sub
return Sub
}
Vue.extend的作用其实就是构建一个Vue的子类,把对象转换成继承于Vue的构造器Sub并返回,然后对Sub本身扩展了option、全局API等,并对配置中的props和computed做了初始化工作,最后对Sub做了缓存,防止多次生成相同构造器。
// src\core\vdom\create-component.js
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component, //当前vm实例
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
...
// 钩子函数挂载到data对象
installComponentHooks(data)
...
}
我们继续看createComponent函数,中间忽略了一些代码块,后续涉及到的时候再分析,现在我们先看看installComponentHooks函数。
// src\core\vdom\create-component.js
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
}
}
}
const hooksToMerge = Object.keys(componentVNodeHooks)
//默认钩子
const componentVNodeHooks = {
init(){},
prepatch(){},
insert(){},
destroy(){},
}
//合并钩子
function mergeHook (f1: any, f2: any): Function {
const merged = (a, b) => {
// flow complains about extra args which is why we use any
f1(a, b)
f2(a, b)
}
merged._merged = true
return merged
}
installComponentHooks其实就遍历了hooksToMerge,其实就是遍历了componentVNodeHooks的钩子然后和data.hook合并。
接下来我们继续往下看createComponent函数
// src\core\vdom\create-component.js
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component, //当前vm实例
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
...
// return a placeholder 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的入参分别是什么?
// src\core\vdom\vnode.js
export default class VNode {
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
){},
}
主要有三个需要关注的点:
tag:会有一个'vue-component-'标识这个是个组件;
children:入参是undefined,记住组件的children是空的,这个到时候再patch遍历时候会用到;
componentOptions:组件的很多数据都存放在这里虽然chilrend入参为空,但是这里有传入;
new Vnode之后把vnode return到createComponent,到此我们createComponent的流程就跑完了。接下来我们又回到了vm._update(vm._render(), hydrating)中,开始了vm._update之旅了,其实也就是回到了把vnode转换成真实dom的patch函数。
patch函数 - 组件处理
又回到最初的起点,呆呆的站在patch前。
// src\core\vdom\patch.js
return function patch (oldVnode, vnode, hydrating, removeOnly) {
...
// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
}
patch的流程和配置el或者template一样,会走到createElm
// src\core\vdom\patch.js
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
...
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
...
}
与之前最大的不同一就是跑到了createComponent函数时候的处理了,这边需要注意的是这个createComponent函数是patch.js中的,而不是我们上文提及到的create-component.js中的,这里要区分开来,不要混淆了。
下面我们来看看我们调用到的patch.js中的pcreateComponent函数
// src\core\vdom\patch.js
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
//keepalive逻辑,先不解读
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
// i.hook = data.hook,再判断是否有init方法,都成立是运行init。
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
createComponent一次赋值且判断vnode.data、vnode.data.hook、vnode.data.init是否为空,都成立是则运行init方法。那么这个init方法又是哪个呢?我们还记得上文生成vnode时候在create-component.js中的调用createComponent函数的时候有个installComponentHooks方法吗?在那里我们插入了init钩子,忘了的可以回顾一下前文,下面我们看看init的代码。
// src\core\vdom\create-component.js
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
// componentInstance是undefined,进入else逻辑
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
// 运行的代码块
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
}
在vnode.componentInstance和vnode.data.keepAlive都是undefined的情况下我们进入了else逻辑。
其中执行了createComponentInstanceForVnode函数,它返回的其实就是一个vm实例,下面我们具体看看createComponentInstanceForVnode函数。
// src\core\vdom\create-component.js
export function createComponentInstanceForVnode (
// we know it's MountedComponentVNode but flow doesn't
vnode: any,
// activeInstance in lifecycle state
parent: any //vm实例
): Component {
const options: InternalComponentOptions = {
_isComponent: true, //重新进入vue._init时候做判断用到
_parentVnode: vnode,
parent
}
// check inline-template render functions
const inlineTemplate = vnode.data.inlineTemplate //undefined
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
return new vnode.componentOptions.Ctor(options)
}
上述函数一开始配置了options,inlineTemplate是undefined,直接忽略,然后到了return new vnode.componentOptions.Ctor(options);
vnode.componentOptions.Ctor究竟是什么?这个我们得回到上诉的componentOptions,还记得那里new vnode传入的参数吗?其实倒数第二个就是componentOptions,入参是{ Ctor, propsData, listeners, tag, children},
所以知道这个怎么来了吧,vnode.componentOptions.Ctor其实就是入参的Ctor,而Ctor忘记了的,自己回顾一下Ctor。
所以其实它运行的就是extend中的Sub构造器:
// src\core\global-api\extend.js
const Sub = function VueComponent (options) {
this._init(options)
}
因为Sub构造器继承的是Vue,因此this._init又回来Vue._init这个初始化操作。
那么组件进入_init和普通节点有什么不一样呢?我们来重新进入vue._init,下面只选择性展示不一样的地方。
// src\core\instance\init.js
Vue.prototype._init = function (options?: Object) {
...
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)
}
...
}
_isComponent在createComponentInstanceForVnode函数是已经配置好是true了,所以会进入到initInternalComponent方法,下面看看它的定义:
// src\core\instance\init.js
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
主要就是把options的配置给到vm.$options上。
然后接下来看其他的差异:
// src\core\instance\init.js
Vue.prototype._init = function (options?: Object) {
...
initLifecycle(vm)
...
}
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的关联,下面主要分析一下parent是什么?看字面意思也知道这是父级的东西,没错就是父级vm实例,他是在哪里定义的呢,这时候我们的回顾一下createComponentInstanceForVnode的方法,其中有配置options.parent = parent,而这个入参parent再往上追溯,其实就在componentVNodeHooks中的init方法中传递过来的,它传递的activeInstance其实是一个全局变量,那么这个又是在什么时候定义的?其实它在src\core\instance\lifecycle.js中做了定义,运行Vue._update的时候已经做了赋值,下面我们看一下代码。
// src\core\instance\lifecycle.js
//全局定义
export let activeInstance: any = null
// activeInstance的赋值
export function setActiveInstance(vm: Component) {
const prevActiveInstance = activeInstance
activeInstance = vm
return () => {
activeInstance = prevActiveInstance
}
}
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
// 调用activeInstance赋值方法
const restoreActiveInstance = setActiveInstance(vm)
}
因此在每次运行到_update的时候,在进入下个阶段patch函数之前,它都会缓存住生成真实dom之前的vm,因此在patch做递归进入一下个阶段的时候,activeInstance就是它的父vm实例。知道了parent的来源,那么这父子关联的代码块的逻辑也就明了了。
这时候patch也进入了递归阶段,递归方法还是和之前相似,在回顾一下流程:
①:Vue.prototype._init;(src\core\instance\init.js)
②:Vue.prototype.$mount;(src\platforms\web\entry-runtime-with-compiler.js)
②:Vue.prototype.$mount;(src\platforms\web\runtime\index.js)
③:mountComponent;(src\core\instance\lifecycle.js)
④:Vue.prototype._render;(src\core\instance\render.js)
⑤:Vue.prototype._update;(src\core\instance\lifecycle.js)
⑥:vm.__patch__;(src\platforms\web\runtime\index.js)
⑦:createPatchFunction;(src\core\vdom\patch.js)
⑧:patch;(src\core\vdom\patch.js)
⑨:createElm;(src\core\vdom\patch.js)
⑩:createComponent;(src\core\vdom\patch.js)
在createComponent中的i.init开始了新一轮的初始化,当递归结束后我们还有接下来的流程,我们继续看createComponent方法。
// src\core\vdom\patch.js
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
//这里会完成整个patch的递归流程
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it.
// 在初始化hook之后,如果vnode是一个子组件,那么它应该创建一个子实例并挂载它。
// the child component also has set the placeholder vnode's elm.
// 子组件还设置了占位符vnode的elm
// in that case we can just return the element and be done.
// 在这种情况下,我们只需返回element就可以了
// 只有在patch结束后才进入了这里
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
递归调用i.init结束后,我们进入下一个逻辑。vnode.componentInstance,在i.int中得到了赋值,就是一个vm实例,详情回顾componentVNodeHooks,因此我们进入了initComponent函数,下面看看initComponent是做什么的。
// src\core\vdom\patch.js
function initComponent (vnode, insertedVnodeQueue) {
...
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
// 创建一些钩子,后续再分析
invokeCreateHooks(vnode, insertedVnodeQueue)
setScope(vnode)
} else {
...
}
}
initComponent主要是给vnode.elm赋值,vnode.componentInstance.$el即vm.$el,__patch__方法有做返回。我们继续回到initComponent的下一步insert,insert其作用就是根据判断插入dom(insertBefore/appendChild),之前有讲述过,至此,子组件的真实dom就生成了。由于这是递归插入的模式,因此dom的插入顺序是先子后父。
到此组件的patch的过程就到此结束了,建议配合代码运行调试,反复几次理解运行逻辑,及参数传递与缓存。
Vue2.0源码学习(3) - 组件的创建和patch过程的更多相关文章
- Vue2.0源码学习(6) - 组件注册
组件注册 前言 在 Vue.js 中,除了它内置的组件如 keep-alive.component.transition.transition-group 等,其它用户自定义组件在使用前必须注册.在开 ...
- Vue2.0源码学习(4) - 合并配置
合并配置 通过之前的源码学习,我们已经了解到了new Vue主要有两种场景,第一种就是在外部主动调用new Vue创建一个实例,第二个就是代码内部创建子组件的时候自行创建一个new Vue实例.但是无 ...
- Vue2.0源码学习(2) - 数据和模板的渲染(下)
vm._render是怎么实现的 上述updateComponent方法调用是运行了一个函数: // src\core\instance\lifecycle.js updateComponent = ...
- Vue2.0源码学习(1) - 数据和模板的渲染(上)
准备 一.首先去GitHub上把vue源码download下来,传送门:https://github.com/vuejs/vue 二.搭建一个vue-cli跑起来,用于代码调试,不看着代码动起来只看源 ...
- 【Spark2.0源码学习】-1.概述
Spark作为当前主流的分布式计算框架,其高效性.通用性.易用性使其得到广泛的关注,本系列博客不会介绍其原理.安装与使用相关知识,将会从源码角度进行深度分析,理解其背后的设计精髓,以便后续 ...
- [Android FrameWork 6.0源码学习] View的重绘过程之WindowManager的addView方法
博客首页:http://www.cnblogs.com/kezhuang/p/关于Activity的contentView的构建过程,我在我的博客中已经分析过了,不了解的可以去看一下<[Andr ...
- spark2.0源码学习
[Spark2.0源码学习]-1.概述 [Spark2.0源码学习]-2.一切从脚本说起 [Spark2.0源码学习]-3.Endpoint模型介绍 [Spark2.0源码学习]-4.Master启动 ...
- Spring5.0源码学习系列之浅谈BeanFactory创建
Spring5.0源码学习系列之浅谈BeanFactory创建过程 系列文章目录 提示:Spring源码学习专栏链接 @ 目录 系列文章目录 博客前言介绍 一.获取BeanFactory主流程 二.r ...
- Spring5.0源码学习系列之事务管理概述
Spring5.0源码学习系列之事务管理概述(十一),在学习事务管理的源码之前,需要对事务的基本理论比较熟悉,所以本章节会对事务管理的基本理论进行描述 1.什么是事务? 事务就是一组原子性的SQL操作 ...
随机推荐
- Maven+ajax+SSM实现新增
转载自:https://www.cnblogs.com/kebibuluan/p/9014986.html 20.尚硅谷_SSM高级整合_新增_创建员工新增的模态框.avi 1.接下来当我们点击增加按 ...
- 用户注册调优 及Connection对象
调优的方法: (1)减少Connection对象的销毁与创建 我们可以在服务器启动时 预先创建好二十个Connection对象 因为每次Coonection对象的创建与销毁会浪费大量的时间 我们需要 ...
- SYCOJ2100摆动序列
题目-摆动序列 (shiyancang.cn) 直接分成两部分,插入即可.只有一个地方不对,那就是符号.两个大的放一个小的,两个小的放一个大的.那么每次的大的放最大的,每次的小的放其次小的,用完就不用 ...
- 只需两步在Linux系统安装百度网盘--Ubuntu20
Linux Ubuntu系统安装百度网盘 百度网盘已支持Linux系统下载和使用.使用Linux系统下载并安装一个百度网盘是非常简单的,只需要以下两个步骤: 第一步 进入官网下载.deb类型的百度网盘 ...
- java下拉框转换公共方法
1. 下拉框实例类 import org.apache.commons.beanutils.PropertyUtils; import org.apache.commons.lang3.Boolean ...
- office 下载(免费使用
https://otp.landian.vip/zh-cn/download.html
- 【源码阅读】vm-insert与vm-storage之间的通讯
先说结论 vm-insert与vm-storage之间采用极其简单的通讯协议 对于简单的场景,越简单性能越高 vm-insert连接到vm-storage后,先发送字符串vminsert.02,vm- ...
- golang中自定义实现0当做除数的错误异常处理接口
package main import "fmt" type ZeroDivisor struct { // 定义一个0当做除数的结构体 divisor int // 被除数 di ...
- java 坐标练习
定义一个三维空间的点,有三个坐标 实现以下目标: 1.可以生成特定坐标的点对象 2.提供可以设置三个坐标的方法 3.提供可以计算该点到特定点距离的平方的方法 class Point { double ...
- python30day
内容回顾 tcp协议的多人多次通信 和一个人通信多说句话 和一个人聊完再和其他人聊 bind 绑定一个id和端口 socket()tcp协议的server listen 监听,代表socket服务的开 ...