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

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

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


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几个属性,这里会把我们传入的扩展选项和这个合并后作为子类构造器的默认选项:


image.png


// ...
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了。


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


image.png


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


image.png


组件对应的VNode


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


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


_c就是createElement方法的简写:


image.png


接下来我们来看看这个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创建的实例,


image.png


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



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


image.png


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是我们自定义的组件,并不存在命名空间,继续往下:


image.png


接下来判断是否是内置标签,看看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的所有标签名。这里显然也不是,继续往下:


image.png


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


image.png


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


image.png


接下来会执行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如下:


image.png

创建html虚拟节点


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


image.png


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
}


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


image.png


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


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


image.png


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



相关文章
|
2月前
|
JavaScript
在 Vue 中处理组件选项与 Mixin 选项冲突的详细解决方案
【10月更文挑战第18天】通过以上的分析和探讨,相信你对在 Vue 中使用 Mixin 时遇到组件选项与 Mixin 选项冲突的解决方法有了更深入的理解。在实际开发中,要根据具体情况灵活选择合适的解决方案,以确保代码的质量和可维护性。
90 7
|
3天前
|
前端开发 JavaScript 测试技术
Vue3中v-model在处理自定义组件双向数据绑定时,如何避免循环引用?
Web 组件化是一种有效的开发方法,可以提高项目的质量、效率和可维护性。在实际项目中,要结合项目的具体情况,合理应用 Web 组件化的理念和技术,实现项目的成功实施和交付。通过不断地探索和实践,将 Web 组件化的优势充分发挥出来,为前端开发领域的发展做出贡献。
17 8
|
3天前
|
JavaScript
在 Vue 3 中,如何使用 v-model 来处理自定义组件的双向数据绑定?
需要注意的是,在实际开发中,根据具体的业务需求和组件设计,可能需要对上述步骤进行适当的调整和优化,以确保双向数据绑定的正确性和稳定性。同时,深入理解 Vue 3 的响应式机制和组件通信原理,将有助于更好地运用 `v-model` 实现自定义组件的双向数据绑定。
|
16天前
|
存储 JavaScript 开发者
Vue 组件间通信的最佳实践
本文总结了 Vue.js 中组件间通信的多种方法,包括 props、事件、Vuex 状态管理等,帮助开发者选择最适合项目需求的通信方式,提高开发效率和代码可维护性。
|
16天前
|
存储 JavaScript
Vue 组件间如何通信
Vue组件间通信是指在Vue应用中,不同组件之间传递数据和事件的方法。常用的方式有:props、自定义事件、$emit、$attrs、$refs、provide/inject、Vuex等。掌握这些方法可以实现父子组件、兄弟组件及跨级组件间的高效通信。
|
2月前
|
缓存 JavaScript UED
Vue 的动态组件与 keep-alive
【10月更文挑战第19天】总的来说,动态组件和 `keep-alive` 是 Vue.js 中非常实用的特性,它们为我们提供了更灵活和高效的组件管理方式,使我们能够更好地构建复杂的应用界面。深入理解和掌握它们,以便在实际开发中能够充分发挥它们的优势,提升我们的开发效率和应用性能。
45 18
|
28天前
|
缓存 JavaScript UED
Vue 中实现组件的懒加载
【10月更文挑战第23天】组件的懒加载是 Vue 应用中提高性能的重要手段之一。通过合理运用动态导入、路由配置等方式,可以实现组件的按需加载,减少资源浪费,提高应用的响应速度和用户体验。在实际应用中,需要根据具体情况选择合适的懒加载方式,并结合性能优化的其他措施,以打造更高效、更优质的 Vue 应用。
|
2月前
|
前端开发 UED
vue3知识点:Suspense组件
vue3知识点:Suspense组件
34 4
|
2月前
|
JavaScript 前端开发 测试技术
组件化开发:创建可重用的Vue组件
【10月更文挑战第21天】组件化开发:创建可重用的Vue组件
26 1
|
2月前
|
JavaScript 前端开发 Java
《vue3第五章》新的组件,包含:Fragment、Teleport、Suspense
《vue3第五章》新的组件,包含:Fragment、Teleport、Suspense
34 2