Vue2.6.0源码阅读(六):组件基础

举报
街角小林 发表于 2022/10/28 21:26:55 2022/10/28
【摘要】 以下面这个十分简单的示例:Vue.component('my-component', { template: ` <span>{{text}}</span> `, data() { return { text: '我是子组件' } }})new Vue({ el: '#app', template: ` <div> <my-compon...

以下面这个十分简单的示例:

Vue.component('my-component', {
  template: `
    <span>{{text}}</span>
  `,
  data() {
    return {
      text: '我是子组件'
    }
  }
})
new Vue({
  el: '#app',
  template: `
    <div>
      <my-component></my-component>
    </div>
  `
})

我们来看一下组件是如何渲染出来的。

extend方法详解

首先来看Vue.component方法,这个方法用来注册或获取全局组件,它的定义如下:

const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]
ASSET_TYPES.forEach(type => {
    Vue[type] = function (
    id,
     definition
    ) {
        // 只传了一个参数代表是获取组件
        if (!definition) {
            return this.options[type + 's'][id]
        } else {
            // 定义组件
            if (type === 'component' && isPlainObject(definition)) {
                // 组件名称
                definition.name = definition.name || id
                definition = this.options._base.extend(definition)
            }
            // ...
            this.options[type + 's'][id] = definition
            return definition
        }
    }
})

_base其实就是Vue构造函数,所以当我们调用component方法,其实执行的是Vue.extend方法,这个方法Vue也暴露出来了,官方的描述是使用基础Vue构造器,创建一个子类

Vue.extend = function (extendOptions) {
    extendOptions = extendOptions || {}
    const Super = this
    const SuperId = Super.cid
    // 检测是否存在缓存
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
        return cachedCtors[SuperId]
    }
    // 组件名称
    const name = extendOptions.name || Super.options.name
    // ...
}

每个实例构造器,包括Vue,都有一个唯一的cid,用来支持缓存。

// ...
// 定义一个子类构造函数
const Sub = function VueComponent (options) {
    // 构造函数简洁的优势,不用向传统那样调用父类的构造函数super.call(this)
    this._init(options)
}
// 关联原型链
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
// 合并参数,将我们传入的组件选项保存为默认选项
Sub.options = mergeOptions(
    Super.options,
    extendOptions
)
Sub['super'] = Super
// ...

接下来定义了一个子类的构造函数,因为Vue的构造函数里就调用了一个方法,所以这里就没有call父类的构造器。

根据前面的文章我们可以知道Vueoptions选项就componentsdirectivesfilters_base几个属性,这里会把我们传入的扩展选项和这个合并后作为子类构造器的默认选项:

Snipaste_2021-11-15_20-15-51.jpg

// ...
if (Sub.options.props) {
    initProps(Sub)
}
if (Sub.options.computed) {
    initComputed(Sub)
}
// ...

接下来对于存在propscomputed两个属性时做了一点初始化处理,可以看看具体都做了什么:

function initProps (Comp) {
  const props = Comp.options.props
  for (const key in props) {
    proxy(Comp.prototype, `_props`, key)
  }
}

proxy方法之前已经介绍过,这里就是把this._props上的属性代理到Comp.prototype对象上,比如我们创建了一个这个子类构造函数的实例,存在一个属性type,然后当我们访问this.type时,实际访问的是Sub.prototype.type,最终指向的实际是this._props.type,代理到原型对象上的好处是只要代理一次,不用每次实例时都做一遍这个操作。

function initComputed (Comp) {
  const computed = Comp.options.computed
  for (const key in computed) {
    defineComputed(Comp.prototype, key, computed[key])
  }
}

计算属性和普通属性差不多,只不过调用的是defineComputed方法,这个方法之前我们跳过了,因为涉及到计算属性缓存的内容,所以这里我们也跳过,后面再说,反正这里和proxy差不多,也是将计算属性代理到原型对象上。

继续extend函数:

// ...
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// ...

将父类的静态方法添加到子类上。

// ...
ASSET_TYPES.forEach(function (type) {
    Sub[type] = Super[type]
})
// ...

添加componentdirectivefilter三个静态方法。

// ...
if (name) {
    Sub.options.components[name] = Sub
}
// ...

允许递归查找自己。

// ...
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
// ...

保存对一些选项的引用。

// ...
cachedCtors[SuperId] = Sub
return Sub

最后通过父类的id来进行一个缓存。

可以看到extend函数主要就是创建了一个子类,对于我们开头的示例来说,父类是Vue构造器,如果我们创建了一个子类MyComponent,也可以通过MyComponent.extend()再创建一个子类,那么这个子类的父类当然就是MyComponent了。

最后我们创建出来的子类如下:

Snipaste_2021-11-16_09-51-37.jpg

回到开头我们执行的Vue.component方法,这个方法执行后的结果就是在Vue.options.components对象上添加了my-component的构造器。

Snipaste_2021-11-26_11-18-48.jpg

组件对应的VNode

虽然我们不深入看编译过程,但我们可以看一下我们开头示例的模板产出的渲染函数的内容是怎样的:

with(this){return _c('div',[_c('my-component')],1)}

_c就是createElement方法的简写:

Snipaste_2021-11-26_11-33-33.jpg

接下来我们来看看这个createElement方法。

createElement方法详解

export function createElement (
  context,
  tag,
  data,
  children,
  normalizationType,
  alwaysNormalize
) {
  // 如果data选项是数组或原始值,那么认为它是子节点
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  // _c方法alwaysNormalize为false
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}

这个方法其实就是_createElement方法的一个包装函数,处理了一下data为数组和原始值的情况,接下来看_createElement方法:

export function _createElement (
 context,
 tag,
 data,
 children,
 normalizationType
) {
    // data不允许使用响应式数据
    if (isDef(data) && isDef((data).__ob__)) {
        return createEmptyVNode()
    }
    // 存在:is,那么标签为该指令的值
    if (isDef(data) && isDef(data.is)) {
        tag = data.is
    }
    // 标签不存在,可能是:is的值为falsy的情况
    if (!tag) {
        return createEmptyVNode()
    }
    // 支持单个函数子节点作为默认的作用域插槽
    if (Array.isArray(children) &&
        typeof children[0] === 'function'
       ) {
        data = data || {}
        data.scopedSlots = { default: children[0] }
        children.length = 0
    }
    // 规范化处理子节点
    if (normalizationType === ALWAYS_NORMALIZE) {
        children = normalizeChildren(children)
    } else if (normalizationType === SIMPLE_NORMALIZE) {
        children = simpleNormalizeChildren(children)
    }
    // ...
}

先是对一些情况做了判断和相应的处理。

// ...
let vnode, ns
// 标签是字符串类型
if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    // 判断是否是保留标签
    if (config.isReservedTag(tag)) {
        vnode = new VNode(
            config.parsePlatformTagName(tag), data, children,
            undefined, undefined, context
        )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {// 是注册的组件
        vnode = createComponent(Ctor, data, context, children, tag)
    } else {
        // 未知或未列出的命名空间元素,在运行时检查,因为当其父项规范化子项时,可能会为其分配命名空间
        vnode = new VNode(
            tag, data, children,
            undefined, undefined, context
        )
    }
} else {
    // 直接是组件选项或构造器
    vnode = createComponent(tag, data, context, children)
}
// ...

紧接着根据标签名的类型来判断是普通元素还是组件。

// ...
if (Array.isArray(vnode)) {
    return vnode
} else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
} else {
    return createEmptyVNode()
}

最后根据vnode的类型来返回不同的虚拟节点数据。

中间的一些细节我们并没有看,因为没有遇到具体的情况也看不明白,所以接下来我们以前面的模板渲染函数为例来看看具体过程,忘了没关系,就是这样的:

with(this){return _c('div',[_c('my-component')],1)}

创建组件虚拟节点

_c('my-component'),即createElement(vm, 'my-component', undefined, undefined, undefined, false)vm即我们通过new Vue创建的实例,

Snipaste_2021-11-26_14-21-40.jpg

上面两个条件分支都不会进入,直接到_createElement函数。

Snipaste_2021-11-26_14-23-12.jpg

因为datachildren都不存在,所以也会一路跳过来到了下面:

Snipaste_2021-11-26_14-26-53.jpg

config对象里的方法都是平台相关的,webweex环境下是不一样的,我们看web平台下的getTagNamespace方法:

export function getTagNamespace (tag) {
  if (isSVG(tag)) {
    return 'svg'
  }
  // 对MathML的基本支持,注意,它不支持其他MathML元素作为组件根的节点
  if (tag === 'math') {
    return 'math'
  }
}

// 在下面列表中则返回true
export const isSVG = makeMap(
  'svg,animate,circle,clippath,cursor,defs,desc,ellipse,filter,font-face,' +
  'foreignObject,g,glyph,image,line,marker,mask,missing-glyph,path,pattern,' +
  'polygon,polyline,rect,switch,symbol,text,textpath,tspan,use,view',
  true
)

my-component是我们自定义的组件,并不存在命名空间,继续往下:

Snipaste_2021-11-26_14-35-26.jpg

接下来判断是否是内置标签,看看isReservedTag方法:

export const isReservedTag = (tag) => {
  return isHTMLTag(tag) || isSVG(tag)
}

export const isHTMLTag = makeMap(
  'html,body,base,head,link,meta,style,title,' +
  'address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,' +
  'div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,' +
  'a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,' +
  's,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,' +
  'embed,object,param,source,canvas,script,noscript,del,ins,' +
  'caption,col,colgroup,table,thead,tbody,td,th,tr,' +
  'button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,' +
  'output,progress,select,textarea,' +
  'details,dialog,menu,menuitem,summary,' +
  'content,element,shadow,template,blockquote,iframe,tfoot'
)

列出了html的所有标签名。这里显然也不是,继续往下:

Snipaste_2021-11-26_14-50-24.jpg

data不存在,所以会执行resolveAsset方法:

Snipaste_2021-11-26_15-02-32.jpg

这个实例本身的options.components对象是空的,但是在第二篇【new Vue时做了什么】中我们介绍了实例化时选项合并的过程,对于components选项来说,会把构造函数的该选项以原型的方式挂载到实例的该选项上,可以看到图上的右边是存在我们注册的全局组件my-component的,所以最后会在原型链上找到我们组件的构造函数。继续往下:

Snipaste_2021-11-26_15-04-28.jpg

接下来会执行createComponent方法,顾名思义,创建组件,这个方法也很长,但是对于我们这里的情况来说大部分逻辑都不会进入,精简后代码如下:

export function createComponent (
 Ctor,
 data,
 context,
 children,
 tag
) {
    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
}

可以看到也是创建了一个VNode实例,标签名是根据组件的cidname拼接成的,其实就是创建了一个组件的占位节点。

回到_createElement方法,最后对于my-component组件来说,创建的VNode如下:

Snipaste_2021-11-26_15-47-15.jpg

创建html虚拟节点

接下来看_c('div',[_c('my-component')],1)的过程,和my-component差不多,但是可以看到第三个参数传的是1,也就是normalizationType的值为1,所以会进入simpleNormalizeChildren分支:

Snipaste_2021-11-26_15-52-02.jpg

export function simpleNormalizeChildren (children) {
  for (let i = 0; i < children.length; i++) {
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children)
    }
  }
  return children
}

这个函数也很简单,就是判断子节点中是否存在数组,是的话就把整个数组拍平。继续:

Snipaste_2021-11-26_16-01-08.jpg

div显然是保留标签,所以直接创建一个对应的vnode

所以最终上面的渲染函数的产出就是一个虚拟DOM树,根节点是div,子节点是my-component

Snipaste_2021-11-26_16-04-29.jpg

根据上一篇文章的介绍,执行渲染函数是通过vm._render()方法,产出的虚拟DOM树会传递给vm._update()方法来生成实际的DOM节点。这部分的内容我们后面有机会再探讨。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。