Vue3 源码解读系列(三)——组件渲染

组件渲染

vnode 本质是用来描述 DOM 的 JavaScript 对象,它在 Vue 中可以描述不同类型的节点,比如:普通元素节点、组件节点等。

vnode 的优点:

  1. 抽象:引入 vnode,可以把渲染过程抽象化,从而使得组件的抽象能力也得到提升

  2. 跨平台:因为 patch vnode 的过程不同平台可以有自己的实现,基于 vnode 再做服务端渲染、weex 平台、小程序平台的渲染

组件的渲染流程:

在这里插入图片描述

创建 vnode

createVNode 主要做了四件事:

  1. 处理 props,标准化 class 和 style
  2. 对 vnode 类型信息编码
  3. 创建 vnode 对象
  4. 标准化子节点
/**
 * 创建 vnode
 */
function createVNode(type, props = null, children = null) {
  // 1、处理 props,标准化 class 和 style
  if (props) {
    // ...
  }

  // 2、对 vnode 类型信息编码
  const shapeFlag = isString(type)
    ? 1 /* ELEMENT */
    : isSuspense(type)
      ? 128 /* SUSPENSE */
      : isTeleport(type)
        ? 64 /* TELEPORT */
        : isObject(type)
          ? 4 /* STATEFUL_COMPONENT */
          : isFunction(type)
            ? 2 /* FUNCTIONAL_COMPONENT */
            : 0

  // 3、创建 vnode 对象
  const vnode = {
    type,
    props,
    shapeFlag,
    // 一些其他属性
  }

  // 4、标准化子节点,把不同数据类型的 children 转成数组或者文本类型
  normalizeChildren(vnode, children)
  return vnode
}

渲染 vnode

组件自身通过 render 进行渲染,子组件通过 patch 进行渲染。

render 主要做了几件事:

  1. 检查是否存在 vnode
    • 如果之前有,现在没有,则销毁
    • 如果现在有,则创建或更新
  2. 缓存 vnode,用于判断是否已经渲染
/**
 * 渲染 vnode
 */
const render = (vnode, container) => {
  // vnode 为 null,则销毁组件
  if (vnode == null) {
    if (container._vnode) {
      unmount(container._vnode, null, null, true)
    }
  }
  // 否则创建或者更新组件
  else {
    patch(container._vnode || null, vnode, container)
  }

  // 缓存 vnode 节点,表示已经渲染
  container._vnode = vnode
}

patch 主要做了两件事:

  1. 判断是否销毁节点
  2. 挂载新节点
/**
 * 更新 DOM
 * @param {vnode} n1 - 旧的 vnode(为 null 时表示第一次挂载)
 * @param {vnode} n2 - 新的 vnode
 * @param {DOM} container - DOM 容器,vnode 渲染生成 DOM 后,会挂载到 container 下面
 */
const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false) => {
  // 如果存在新旧节点,且新旧节点类型不同,则销毁旧节点
  if (n1 && !isSameVNodeType(n1, n2)) {
    anchor = getNextHostNode(n1)
    unmount(n1, parentComponent, parentSuspense, true)
    n1 = null
  }

  // 挂载新 vnode
  const { type, shapeFlag } = n2
  switch (type) {
    case Text:
      // 处理文本节点
      break
    case Comment:
      // 处理注释节点
      break
    case Static:
      // 处理静态节点
      break
    case Fragment:
      // 处理 Fragment 元素
      break
    default:
      if (shapeFlag & 1/* ELEMENT */) {
        // 处理普通 DOM 元素
        processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
      } else if (shapeFlag & 6/* COMPONENT */) {
        // 处理 COMPONENT
        processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
      } else if (shapeFlag & 64/* TELEPORT */) {
        // 处理 TELEPORT
      } else if (shapeFlag & 128/* SUSPENSE */) {
        // 处理 SUSPENSE
      }
  }
}
处理组件

在 props、data、methods、computed 等 options 中定义一些变量,在组件初始化阶段,Vue3 内部会处理这些 options,即把定义的变量添加到了组件实例上等模板编译成 render 函数的时候,内部通过 with(this){} 的语法去访问在组件实例中的变量。

组件的渲染流程:

在这里插入图片描述

/**
 * 处理 COMPONENT
 */
const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
  // 旧节点为 null,表示不存在旧节点,则直接挂载组件
  if (n1 == null) {
    mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
  }
  // 旧节点存在,则更新组件
  else {
    updateComponent(n1, n2, parentComponent, optimized)
  }
}

/**
 * 挂载组件
 * mountComponent 做了三件事:
 * 1、创建组件实例
 * 2、设置组件实例
 * 3、设置并运行带副作用的渲染函数
 */
const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
  // 1、创建组件实例,内部也通过对象的方式去创建了当前渲染的组件实例
  const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense))

  // 2、设置组件实例,instance 保留了很多组件相关的数据,维护了组件的上下文包括对 props、插槽,以及其他实例的属性的初始化处理
  setupComponent(instance)

  // 3、设置并运行带副作用的渲染函数
  setupRenderEffet(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized)
}
/**
 * 创建组件实例
 */
function createComponentInstance(vnode, parent, suspense) {
  // 继承父组件实例上的 appContext,如果是根组件,则直接从跟 vnode 中取
  const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;

  // 定义组件实例
  const instance = {
    uid: uid++, // 组件唯一id
    vnode, // 组件 vnode
    parent, // 父组件实例
    appContext, // app 上下文
    type: vnode.type, // vnode 节点类型
    root: null, // 根组件实例
    next: null, // 新的组件 vnode
    subTree: null, // 子节点 vnode
    update: null, // 带副作用更新函数
    render: null, // 渲染函数
    proxy: null, // 渲染上下文代理
    withProxy: null, // 带有 with 区块的渲染上下文代理
    effects: null, // 响应式相关对象
    provides: parent ? parent.provide : Object.create(appContext.provides), // 依赖注入相关
    accessCache: null, // 渲染代理的属性访问缓存
    renderCache: [], // 渲染缓存
    ctx: EMPTY_OBJ, // 渲染上下文
    data: EMPTY_OBJ, // data 数据
    props: EMPTY_OBJ, // props 数据
    attrs: EMPTY_OBJ, // 普通属性
    slots: EMPTY_OBJ, // 插槽相关
    refs: EMPTY_OBJ, // 组件或者 DOM 的 ref 引用
    setupState: EMPTY_OBJ, // setup 函数返回的响应式结果
    setupContext: null, // setup 函数上下文数据
    components: Object.create(appContext.components), // 注册的组件
    directives: Object.create(appContext.directives), // 注册的指令
    suspense, // suspense 相关
    asyncDep: null, // suspense 异步依赖
    asyncResolved: false, // suspense 异步依赖是否都已处理
    isMounted: false, // 是否挂载
    isUnmounted: false, // 是否卸载
    isDeactivated: false, // 是否激活
    bc: null, // 生命周期 before create
    c: null, // 生命周期 created
    bm: null, // 生命周期 before mount
    m: null, // 生命周期 mounted
    bu: null, // 生命周期 before update
    u: null, // 生命周期 update
    um: null, // 生命周期 unmounted
    bum: null, // 生命周期 before unmounted
    da: null, // 生命周期 deactivated
    a: null, // 生命周期 activated
    rtg: null, // 生命周期 render triggered
    rtc: null, // 生命周期 render tracked
    ec: null, // 生命周期 err captured
    emit: null // 派发事件方法
  }

  // 初始化渲染上下文
  instance.ctx = { _: instance }

  // 初始化根组件指针
  instance.root = parent ? parent.root : instance

  // 初始化派发事件方法
  instance.emit = emit.bind(null, instance)
  return instance
}
/**
 * 创建和设置组件实例
 */
function setupComponent(instance, isSSR = false) {
  const { props, children, shapeFlag } = instance.vnode

  // 判断是否是一个有状态的组件
  const isStateful = shapeFlag & 4

  // 初始化 props
  initProps(instance, props, isStateful, isSSR)

  // 初始化插槽
  initSlots(instance, children)

  // 如果是一个有状态的组件,则设置有状态的组件实例
  const setupResult = isStateful ? setupStatefulComponent(instance, isSSR) : undefined
  return setupResult
}

/**
 * 设置有状态的组件实例
 */
function setupStatefulComponent(instance, isSSR) {
  const Component = instance.type

  // 创建渲染代理的属性访问缓存
  instance.accessCache = []

  // 1、创建渲染上下文代理
  /**
   * 为什么需要代理?
   * 答:在执行组件渲染函数的时候,直接访问渲染上下文 instance.ctx 中的属性,做一层 proxy 对渲染上下文 instance.ctx 属性的访问和修改代理到对 setupState、ctx、data、props 中的数据的访问和修改(简单来说:通过 proxy 拦截对组件实例上下文的修改,代理为对 data 等的修改)
   */
  instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)

  // 2、判断处理 setup 函数
  const { setup } = Component

  // 3、组件实例的设置
  if (setup) {
    // 3.1、如果 setup 函数带参数,则创建一个 setupContext
    const setupContext = (instance.setupContext = setup.length > 1 ? createSetupContext(instance) : null)

    // 3.2、执行 setup 函数,获取结果
    const setupResult = callWithErrorHandling(setup, instance, 0/* SETUP_FUNCTION */, [instance.props, setupContext])

    // 3.3、处理 setup 返回结果
    handleSetupResult(instance, setupResult)
  } else {
    // 完成组件实例设置
    finishComponentSetup(instance)
  }
}

// 渲染上下文代理的处理器
const PublicInstanceProxyHandlers = {
  get({ _: instance }, key) {
    const { ctx, setupState, data, props, accessCache, type, appContext } = instance
    // 判断 key 不已 $ 开头的情况(!!!注意优先级) - setupState(setup 函数返回的数据) | data | props | ctx
    if (key[0] !== '$') {
      // 在缓存中访问渲染代理的属性
      const n = accessCache[key]
      // 如果缓存中存在该属性
      if (n !== undefined) {
        switch (n) {
          case 0: /* SETUP */
            return setupState[key]
          case 1: /* DATA */
            return data[key]
          case 3: /* CONTEXT */
            return ctx[key]
          case 2: /* PROPS */
            return props[key]
        }
      }
      // 从 setupState 中取数据
      else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
        accessCache[key] = 0
        return setupState[key]
      }
      // 从 data 中取数据
      else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
        accessCache[key] = 1
        return data[key]
      }
      // 从 props 中取数据
      else if (type.props && hasOwn(normalizePropsOptions(type.props)[0], key)) {
        accessCache[key] = 2
        return props[key]
      }
      // 从 ctx 中取数据
      else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
        accessCache[key] = 3
        return ctx[key]
      }
      // 都取不到
      else {
        accesssCache[key] = 4
      }
    }
    const publicGetter = publicPropertiesMap[key]
    let cssModul, globalProperties

    // 公开的 $xxx 属性或方法
    if (publicGetter) {
      return publicGetter(instance)
    }
    // css 模块,通过 vue-loader 编译的时候注入
    else if ((cssModule = type.__cssModules) && (cssModule = cssModule[key])) {
      return cssModule
    }
    // 用户的自定义的属性,也用 $ 开头
    else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
      accessCache[key] = 3
      return ctx[key]
    }
    // 全局定义的属性
    else if (((globalProperties = appContext.config.globalProperties), hasOwn(globalProperties, key))) {
      return globalProperties[key]
    } else if ((process.env.NODE_ENV !== 'production') && currentRenderingInstance && key.indexOf('__v') !== 0) {
      if (data !== EMPTY_OBJ && key[0] === '$' && hasOwn(data, key)) {
        // 如果在 data 中定义的数据以 $ 开头,会报警告,因为 $ 是保留字符,不会做代理
        warn(/* ... */)
      } else {
        // 在模板中使用的变量如果没有定义,报警告
        warn(/* ... */)
      }
    }
  },
  set({ _: instance }, key, value) {
    const { data, setupState, ctx } = instance
    // 给 setupState 赋值
    if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
      setupState[key] = value
    }
    // 给 data 赋值
    else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
      data[key] = value
    }
    // 不能直接给 props 赋值,因为不符合单向数据流的设计思想
    else if (key in instance.props) {
      (process.env.NODE_ENV !== 'production') && warn(/* ... */)
      return false
    }
    // 不能给 Vue 内部以 $ 开头的保留属性赋值
    if (key[0] === '$' && key.slice(1) in instance) {
      (process.env.NODE_ENV !== 'production') && warn(/* ... */)
      return false
    }
    // 用户自定义数据赋值
    else {
      ctx[key] = value
    }
    return true
  },
  has({ _: { data, setupState, accessCache, ctx, type, appContext } }, key) {
    // 依次判断 key 是否存在于 缓存、data、setupState、props、ctx(渲染上下文)、publicPropertiesMap(公开属性)、appContext.config.globalProperties(应用全局属性)
    return (accessCache[key] !== undefined
      || (data !== EMPTY_OBJ && hasOwn(data, key))
      || (setupState !== EMPTY_OBJ && hasOwn(setupState, key))
      || (type.props && hasOwn(normalizePropsOptions(type.props)[0], key))
      || hasOwn(ctx, key)
      || hasOwn(publicPropertiesMap, key)
      || hasOwn(appContext.config.globalProperties, key))
  }
}

/**
 * 创建 setupContext - setup 函数的第二个参数
 */
function createSetupContext(instance) {
  return {
    attrs: instance.attrs,
    slots: instance.slots,
    emit: instance.emit
  }
}

/**
 * 对 setup 进行封装,处理错误的情况
 */
function callWithErrorHandling(fn, instance, type, args) {
  let res
  try {
    res = args ? fn(...args) : fn()
  } catch (err) {
    handleError(err, instance, type)
  }
  return res
}

/**
 * 处理 setupResult - setup 返回结果
 */
function handleSetupResult(instance, setupResult) {
  // 当 setupResult 为函数,则把该函数设置为组件实例的渲染函数
  if (isFunction(setupResult)) {
    instance.render = setupResult
  }
  // 当 setupResult 为对象,则把 setupResult 变成响应式
  else if (isObject(setupResult)) {
    instance.setupState = reactive(setupResult)
  }

  // 完成组件实例设置
  finishComponentSetup(instance)
}

/**
 * 完成组件实例设置
 * finishComponentSetup 主要做了两件事:
 * 1、标准化模板或者渲染函数
 * 2、兼容 Options API
 */
function finishComponentSetup(instance) {
  const Component = instance.type
  // 1、标准化模板或者渲染函数
  /**
   * Vue 的两种写法:
   * 1、引入 Vue.js - 直接在组件对象的 template 属性中编写组件的模板(对应 runtime-compiled)
   * 2、SFC - 通过编写组件的 tempalte 模板去描述一个组件的 DOM 结构(对应 runtime-only)
   * Vue 在 Web 端有两种编译方式:
   * 1、runtime-compiled(古老写法)
   * 2、runtime-only(推荐:体积更小、运行时不用编译、耗时更少、性能更优秀)
   * 区别在于是否注册了 compile 方法
   */
  if (!instance.render) {
    // runtime-compiled 编译方式,存在 template 不存在 render,则在 JavaScript 运行时进行模板编译,生成 render 函数
    if (compile && Component.template && !Component.render) {
      Component.render = compile(Component.template, {
        isCustomElement: instance.appContext.config.isCustomElement || No
      })
      Component.render._rc = true
    }

    if ((process.env.NODE_ENV !== 'production') && !Component.render) {
      // runtime-only 编译模式,存在 tempalte 不存在 render,则警告如果想要在运行时编译应使用 runtime-compiled 模式
      if (!compile && Component.template) {
        warn(/* ... */)
      }
      // 既没有写 render,也没有 template,则警告缺少 template 或 render
      else {
        warn(/* ... */)
      }
    }

    // 把组件的 render 函数赋值给 instance.render
    instance.render = (Component.render || NOOP)
    if (instance.render._rc) {
      // 对于使用 with 块的运行时编译的渲染函数,使用 RuntimeCompiledPublicInstanceProxyHandlers 代理
      instance.withProxy = new Proxy(instance.ctx, RuntimeCompiledPublicInstanceProxyHandlers)
    }
  }

  // 2、兼容 Options API
  {
    currentInstance = instance
    applyOptions(instance, Component)
    currentInstance = null
  }
}

// 使用 with 块运行时编译的渲染函数的代理处理器,在之前渲染上下文代理 PublicInstanceProxyHandlers 的基础上进行扩展
const RuntimeCompiledPublicInstanceProxyHandlers = {
  ...PublicInstanceProxyHandlers,
  get(target, key) {
    if (key === Symbol.unscopables) return
    return PublicInstanceProxyHandlers.get(target, key, target)
  },
  has(_, key) {
    // 如果 key 以 _ 开头或者 key 在全局变量白名单内,则 has 为 false
    const has = key[0] !== '_' && !isGloballyWhitelisted(key)
    if ((process.env.NODE_ENV !== 'production') && !has && PublicInstanceProxyHandlers.has(_, key)) {
      warn(/* ... */)
    }
    return has
  }
}

/**
 * 兼容 Option API
 */
function applyOptions(instance, options, deferredData = [], deferredWatch = [], asMixin = false) {
  const {
    mixins, // 混入
    extends: extendsOptions, // 继承
    props: propsOptions, // 父组件传参
    data: dataOptions, // 状态
    computed: computedOptions, // 计算属性
    methods, // 方法
    watch: watchOptions, // 监视属性
    provide: provideOptions, // 依赖
    inject: injectOptions, // 注入
    components, // 组件
    directives, // 指令
    // 生命周期
    beforeMount,
    mounted,
    beforeUpdata,
    updated,
    activated,
    deactivated,
    beforeUnmount,
    unmounted,
    renderTracked,
    renderTriggered,
    errorCaptured
  } = options

  // instance.proxy 作为 this
  const publicThis = instance.proxy
  const ctx = instance.ctx

  // 处理全局 mixin、extend、本地 mixins、inject、methods、data、computed、watch、provide、components、directives、生命周期 option
  // ...
}
/**
 * setup 渲染副作用函数
 * 副作用:当组件数据发生变化时,effect 函数包裹的内部渲染函数 componentEffect 会重新执行一遍,从而达到重新渲染组件的目的
 */
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
  // 创建响应式的副作用渲染函数
  instance.update = effect(function componentEffect() {
    // 如果组件实例 instance 上的 isMounted 属性为 false,说明是初次渲染
    /**
     * 初始化渲染主要做两件事情:
     * 1、渲染组件生成子树 subTree
     * 2、把 subTree 挂载到 container 中
     */
    if (!instance.isMounted) {
      // 1、渲染组件生成子树 vnode
      const subTree = (instance.subTree = renderComponentRoor(instance))

      // 2、把子树 vnode 挂载到 container 中
      patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)

      // 保留渲染生成的子树根 DOM 节点
      initialVNode.el = subTree.el
      instance.isMounted = true
    }
    // 更新组件
    else {
      // ...
    }
  }, prodEffectOptions)
}
处理普通元素
/**
 * 处理 ELEMENT
 */
const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
  isSVG = isSVG || n2.type === 'svg'
  // 旧节点为 null,说明没有旧节点,为第一次渲染,则挂载元素节点
  if (n1 == null) {
    mountElement(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
  }
  // 否则更新元素节点
  else {
    patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
  }
}

/**
 * 挂载元素
 * mountElement 主要做了四件事:
 * 1、创建 DOM 元素节点
 * 2、处理 props
 * 3、处理子节点
 * 4、把创建的 DOM 元素节点挂载到 container 上
 */
const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
  let el
  const { type, props, shapeFlag } = vnode

  // 1、创建 DOM 元素节点
  el = vnode.el = hostCreateElement(vnode.type, isSVG, props && props.is)

  // 2、处理 props,比如 class、style、event 等属性
  if (props) {
    // 遍历 props,给这个 DOM 节点添加相关的 class、style、event 等属性,并作相关的处理
    for (const key in props) {
      if (!isReservedProp(key)) {
        hostPatchProp(el, key, null, props[key], isSVG)
      }
    }
  }

  // 3、处理子节点
  // 子节点是纯文本的情况
  if (shapeFlag & 8/* TEXT_CHILDREN */) {
    hostSetElementText(el, vnode.children)
  }
  // 子节点是数组的情况
  else if (shapeFlag & 16/* ARRAY_CHILDREN */) {
    mountChildren(vnode.children, el, null, parentComponent, parentSuspense, isSVG && type !== 'foreignObject', optimized || !!vnode.dynamicChildren)
  }

  // 4、把创建的 DOM 元素节点挂载到 container 上
  hostInsert(el, container, anchor)
}

/**
 * 创建元素
 */
function createElement(tag, isSVG, is) {
  // 在 Web 环境下的方式
  isSVG ? document.createElementNS(svgNS, tag) : document.createElement(tag, is ? { is } : undefined)

  // 如果是其他平台就不是操作 DOM 了,而是平台相关的 API,这些相关的方法是在创建渲染器阶段作为参数传入的
}

/**
 * 处理子节点是纯文本的情况
 */
function setElementText(el, text) {
  // 在 Web 环境下通过设置 DOM 元素的 textContent 属性设置文本
  el.textContent = text
}

/**
 * 处理子节点是数组的情况
 */
function mountChildren(children, container, anchor, parentComponent, parentSuspense, isSVG, optimized, start = 0) {
  // 遍历 chidren,获取每一个 child,递归执行 patch 方法挂载每一个 child
  for (let i = start; i < children.length; i++) {
    // 预处理 child
    const child = (children[i] = optimized ? cloneIfMounted(children[i]) : normalizeVNode(children[i]))

    // 执行 patch 挂载 child
    // 执行 patch 而非 mountElement 的原因:因为子节点可能有其他类型的 vnode,比如 组件 vnode
    patch(null, child, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
  }
}

/**
 * 把创建的 DOM 元素节点挂载到 container 下
 * 因为 insert 的执行是在处理子节点后,所以挂载的顺序是先子节点,后父节点,最终挂载到最外层的容器上
 */
function insert(child, parent, anchor) {
  // 如果有参考元素 anchor,则把 child 插入到 anchor 前
  if (anchor) {
    parent.insertBefore(child, anchor)
  }
  // 否则直接通过 appendChild 插入到父节点的末尾
  else {
    parent.appendChild(child)
  }
}

扩展:嵌套组件

组件 vnode 主要维护着组件的定义对象,组件上的各种 props,而组件本身是一个抽象节点,它自身的渲染其实是通过执行组件定义的 render 渲染函数生成的子树 vnode 来完成,然后再通过 patch 这种递归的方式,无论组件的嵌套层级多深,都可以完成整个组件树的渲染。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
组件Vue.js 中最重要的概念之一。组件可以让我们将 UI 拆分为独立、可复用的部件,使得代码更加清晰、易于维护。在 Vue.js 中,组件可以分为全局组件和局部组件,其中全局组件可在任何地方使用,而局部组件只能在其父组件中使用。 定义组件时,需要使用 Vue.component() 方法,该方法需要传入两个参数:组件名称和组件配置对象。组件名称应该采用 kebab-case(短横线分隔命名)格式,以便在 HTML 中使用。 示例代码如下: ```javascript // 定义一个名为 button-counter 的新组件 Vue.component('button-counter', { data: function () { return { count: 0 } }, template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>' }) ``` 在上述代码中,我们定义了一个名为 button-counter 的组件,该组件包含一个计数器,每次点击按钮计数器加一。 在 HTML 中使用组件时,需要使用组件名称作为自定义标签来调用组件。示例代码如下: ```html <div id="app"> <button-counter></button-counter> </div> ``` 在上述代码中,我们调用了 button-counter 组件,并将其渲染到了 id 为 app 的 div 元素中。 除了组件的 data 和 template 属性外,还可以使用 props 属性来传递组件之间的数据。使用 props 时,需要在组件的配置对象中定义 props 属性,并在 HTML 中使用 v-bind 指令来传递数据。 示例代码如下: ```javascript // 定义一个名为 todo-item 的新组件 Vue.component('todo-item', { props: ['todo'], template: '<li>{{ todo.text }}</li>' }) // 创建一个 Vue 实例 var app = new Vue({ el: '#app', data: { groceryList: [ { id: 0, text: '蔬菜' }, { id: 1, text: '水果' }, { id: 2, text: '奶酪' } ] } }) ``` 在上述代码中,我们定义了一个名为 todo-item 的组件,并使用 props 属性定义了一个名为 todo 的 prop。在 HTML 中,我们使用 v-bind 指令将 groceryList 数组中的每个对象传递给了 todo-item 组件。示例代码如下: ```html <div id="app"> <ul> <todo-item v-for="item in groceryList" v-bind:todo="item" v-bind:key="item.id"></todo-item> </ul> </div> ``` 在上述代码中,我们使用 v-for 指令遍历 groceryList 数组,并使用 v-bind 指令将数组中的每个对象传递给了 todo-item 组件。注意,我们还需要使用 v-bind:key 指令来为每个列表项指定一个唯一的 key 值。 插槽是 Vue.js 中另一个重要的概念。插槽可以让父组件在子组件中插入任意的 HTML 内容,使得组件更加灵活、可复用。 在子组件中,使用 <slot> 标签来定义插槽。在父组件中,使用子组件的自定义标签来调用组件,并在标签内部插入 HTML 内容。示例代码如下: ```javascript // 定义一个名为 alert-box 的新组件 Vue.component('alert-box', { template: ` <div class="alert-box"> <strong>Error!</strong> <slot></slot> </div> ` }) // 创建一个 Vue 实例 var app = new Vue({ el: '#app' }) ``` 在上述代码中,我们定义了一个名为 alert-box 的组件,并在组件中定义了一个插槽。在 HTML 中,我们调用了 alert-box 组件,并在标签内部插入了一些 HTML 内容。示例代码如下: ```html <div id="app"> <alert-box> <p>Something bad happened.</p> </alert-box> </div> ``` 在上述代码中,我们调用了 alert-box 组件,并在标签内部插入了一些 HTML 内容。该 HTML 内容会被插入到 alert-box 组件的插槽中。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jackson Mseven

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值