Skip to content

element-plus安装

跳过PUPPETEER安装

bash
export PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1

调试

生成sourcemap

internal/build/build.config.ts配置中设置sourcemap:true

执行

bash
pnpm build
pnpm build:theme

运行dev

在源码上设置断点

新建javaScript Debug Terminal

bash
pnpm dev

vue中的渲染函数 & JSX

基本用法

TIP

h()hyperscript 的简称——意思是“能生成 HTML (超文本标记语言) 的 JavaScript”。这个名字来源于许多虚拟 DOM 实现默认形成的约定。一个更准确的名称应该是 createVNode(),但当你需要多次使用渲染函数时,一个简短的名字会更省力

Vue 提供了一个 h() 函数用于创建 vnodes:

vue
import { ref, h } from 'vue'

export default {
  props: {
    /* ... */
  },
  setup(props) {
    const count = ref(1)

    // 返回渲染函数
    return () => h('div', props.msg + count.value)
  }
}

WARNING

Vnodes 必须唯一,组件树中的 vnodes 必须是唯一的。下面是错误示范:

typescript
function render() {
  const p = h('p', 'hi')
  return h('div', [
    // 啊哦,重复的 vnodes 是无效的
    p,
    p
  ])
}

如果你真的非常想在页面上渲染多个重复的元素或者组件,你可以使用一个工厂函数来做这件事。比如下面的这个渲染函数就可以完美渲染出 20 个相同的段落:

typescript
function render() {
  return h(
    'div',
    Array.from({ length: 20 }).map(() => {
      return h('p', 'hi')
    })
  )
}

JSX / TSX

TIP

调用h函数生成vnode写法复杂,所以就有了jsx/tsx写法,更加的直观和语义化,同理vue的template模版语法也是类似效果

JSX 是 JavaScript 的一个类似 XML 的扩展,有了它,我们可以用以下的方式来书写代码:

tsx
const vnode = <div id={dynamicId}>hello, {userName}</div>

TIP

Vue 的类型定义也提供了 TSX 语法的类型推导支持。当使用 TSX 语法时,确保在 tsconfig.json 中配置了 "jsx": "preserve",这样的 TypeScript 就能保证 Vue JSX 语法转换过程中的完整性。

渲染插槽

在渲染函数中,插槽可以通过 setup() 的上下文来访问。每个 slots 对象中的插槽都是一个返回 vnodes 数组的函数

vue
export default {
  props: ['message'],
  setup(props, { slots }) {
    return () => [
      // 默认插槽:
      // <div><slot /></div>
      h('div', slots.default()),

      // 具名插槽:
      // <div><slot name="footer" :text="message" /></div>
      h(
        'div',
        slots.footer({
          text: props.message
        })
      )
    ]
  }
}

等价 JSX 语法:

tsx
// 默认插槽
<div>{slots.default()}</div>

// 具名插槽
<div>{slots.footer({ text: props.message })}</div>
传递插槽

向组件传递子元素的方式与向元素传递子元素的方式有些许不同。我们需要传递一个插槽函数或者是一个包含插槽函数的对象而非是数组,插槽函数的返回值同一个正常的渲染函数的返回值一样——并且在子组件中被访问时总是会被转化为一个 vnodes 数组。

tsx
// 单个默认插槽
h(MyComponent, () => 'hello')

// 具名插槽
// 注意 `null` 是必需的
// 以避免 slot 对象被当成 prop 处理
h(MyComponent, null, {
    default: () => 'default slot',
    foo: () => h('div', 'foo'),
    bar: () => [h('span', 'one'), h('span', 'two')]
})

等价 JSX 语法:

tsx
// 默认插槽
<MyComponent>{() => 'hello'}</MyComponent>

// 具名插槽
<MyComponent>{{
  default: () => 'default slot',
  foo: () => <div>foo</div>,
  bar: () => [<span>one</span>, <span>two</span>]
}}</MyComponent>

插槽以函数的形式传递使得它们可以被子组件懒调用。这能确保它被注册为子组件的依赖关系,而不是父组件。这使得更新更加准确及有效。

作用域插槽

为了在父组件中渲染作用域插槽,需要给子组件传递一个插槽。注意该插槽现在拥有一个 text 参数。该插槽将在子组件中被调用,同时子组件中的数据将向上传递给父组件

tsx
// 父组件
export default {
  setup() {
    return () => h(MyComp, null, {
      default: ({ text }) => h('p', text)
    })
  }
}

记得传递 null 以避免插槽被误认为 prop:

tsx
// 子组件
export default {
  setup(props, { slots }) {
    const text = ref('hi')
    return () => h('div', null, slots.default({ text: text.value }))
  }
}

等同于 JSX:

tsx
<MyComponent>{{
  default: ({ text }) => <p>{ text }</p>  
}}</MyComponent>

tabs组件原理

使用案例

vue
<template>
  <div class="play-container">
    <el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
      <el-tab-pane label="User" name="first">User</el-tab-pane>
      <el-tab-pane label="Config" name="second">Config</el-tab-pane>
      <el-tab-pane label="Role" name="third">Role</el-tab-pane>
      <el-tab-pane label="Task" name="fourth">Task</el-tab-pane>
    </el-tabs>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import type { TabsPaneContext } from 'element-plus'
const activeName = ref('first')
const handleClick = (tab: TabsPaneContext, event: Event) => {
  console.log(tab, event)
}
</script>

el-tabs作为父组件来管理整个tabs组件

在setup钩子函数中初始化 children数组来收集el-tab-pane组件,初始化注册el-tab-pane组件的registerPane方法,初始化当前激活的modelValue,使用provide往子组件注入相关属性和方法

TIP

提供的响应式状态使后代组件可以由此和提供者建立响应式的联系。当提供注入响应式的数据时,建议尽可能将任何对响应式状态的变更都保持在供给方组件中。这样可以确保所提供状态的声明和变更操作都内聚在同一个组件内,使其更容易维护。

typescript
provide(tabsRootContextKey, {
  props,// 组件属性
  currentName,//当前激活的modelValue
  registerPane,//注册el-tab-pane组件方法
  unregisterPane,//删除el-tab-pane组件方法
  nav$,// TabNav组件实例
})
Details

深度解析useOrderedChildren

在Tab组件中,虽然每个 tab-pane 在其 setup 函数中直接调用了 tabsRoot.registerPane(pane) ,但仍然需要排序的原因如下:

组件复用和重新排序 :

在复杂的应用场景中, tab-pane 可能会被动态添加、删除或重新排序,排序机制确保了即使在这些情况下,标签页仍然能够保持正确的显示顺序。

addChild

源码如下:

typescript
    const addChild = (child: T) => {
        children.value[child.uid] = child
        triggerRef(children)

        onMounted(() => {
            const childNode = child.getVnode().el! as Node
            const parentNode = childNode.parentNode!

            if (!nodesMap.has(parentNode)) {
                nodesMap.set(parentNode, [])

                const originalFn = parentNode.insertBefore.bind(parentNode)
                parentNode.insertBefore = <T extends Node>(
                    node: T,
                    anchor: Node | null,
                ) => {
                    // Schedule a job to update `orderedChildren` if the root element of child components is moved
                    const shouldSortChildren = nodesMap
                        .get(parentNode)!
                        .some((el) => node === el || anchor === el)
                    if (shouldSortChildren) triggerRef(children)

                    return originalFn(node, anchor)
                }
            }

            nodesMap.get(parentNode)!.push(childNode)
        })
    }
  • 每添加一个tabpane组件都会设置children对象,child.uid为key,child为value,同时因为children是浅响应式,需要手动触发更新,
  • 在onMounted钩子函数中拦截tabpane组件动态排序,应为在vue diff算法中如果是组件位置变化是通过InsertBefore来操作DOM的,所以通过拦截该API来监听tabpane组件是否有移动
  • 如果有的话,则强制刷新tab-nav组件
sortChildren

源码如下:

typescript
export const flattedChildren = (
    children: FlattenVNodes | VNode | VNodeNormalizedChildren,
): FlattenVNodes => {
    const vNodes = isArray(children) ? children : [ children ]
    const result: FlattenVNodes = []

    vNodes.forEach((child) => {
        if (isArray(child)) {
            result.push(...flattedChildren(child))
        } else if (isVNode(child) && child.component?.subTree) {
            result.push(child, ...flattedChildren(child.component.subTree))
        } else if (isVNode(child) && isArray(child.children)) {
            result.push(...flattedChildren(child.children))
        } else if (isVNode(child) && child.shapeFlag === 2) {
            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
            // @ts-ignore
            result.push(...flattedChildren(child.type()))
        } else {
            result.push(child)
        }
    })
    return result
}

其核心作用是深度优先遍历,递归展开嵌套的VNode(虚拟节点)结构,生成一维的VNode数组 ,便于后续统一处理子节点。

遍历递归处理每个子节点 :

  • 情况1:子节点是数组
    • 递归调用 flattedChildren 展开,将结果合并到 result
  • 情况2:子节点是VNode且包含子组件的子树 ( child.component?.subTree )该情况表示这是个组件vnode
    • 将当前VNode本身加入结果,并递归展开其子组件的子树( child.component.subTree )
  • 情况3:子节点是VNode且其子节点是数组 (如普通元素的 children 数组)该情况表示这是个普通元素vnode
    • 递归展开子节点数组,合并到 result
  • 情况4:子节点是VNode且是函数式组件 ( child.shapeFlag === 2 ,Vue中 ShapeFlags.FUNCTIONAL_COMPONENT 的枚举值为2)
    • 调用函数式组件的类型( child.type() )获取其返回的VNode,再递归展开
  • 其他情况 (如文本节点、注释节点等基础VNode)
    • 直接将子节点加入结果
typescript
const nodes = flattedChildren(vm.subTree).filter(
        (n): n is VNode =>
            isVNode(n) &&
      (n.type as any)?.name === childComponentName &&
      !!n.component,
    )
    const uids = nodes.map((n) => n.component!.uid)
    return uids.map((uid) => children[uid]).filter((p) => !!p)
}

过滤出tabpane组件,按实际渲染出的tabpane组件顺序来排序orderedChildren

typescript
orderedChildren.value = getOrderedChildren(
            vm,
            childComponentName,
            children.value,
        )
        const getOrderedChildren = <T>(
    vm: ComponentInternalInstance,
    childComponentName: string,
    children: Record<number, T>,
): T[] => {
排序组件

源码如下

typescript
  const IsolatedRenderer = (props: { render: () => VNode[] }) => {
        return props.render()
    }

    // TODO: Refactor `el-description` before converting this to a functional component
    const ChildrenSorter = defineComponent({
        setup (_, { slots }) {
            return () => {
                sortChildren()
                return slots.default
                    ? // Create a new `ReactiveEffect` to ensure `ChildrenSorter` doesn't track any extra dependencies
                    h(IsolatedRenderer, {
                        render: slots.default,
                    })
                    : null
            }
        },
    })

TIP

单独声明函数式组件 IsolatedRenderer 并通过它来渲染内容,主要目的是 创建一个独立的响应式作用域 ,避免 ChildrenSorter 组件意外追踪到不必要的依赖。

这是因为 Vue 的组件渲染过程会自动追踪响应式依赖,而 ChildrenSorter 的核心功能是在渲染前执行 sortChildren() 来排序子组件,它本身不应该依赖于其他响应式数据。通过 IsolatedRenderer 封装插槽的渲染逻辑,可以确保 ChildrenSorter 只关注排序逻辑,不被额外的响应式变化触发重渲染,从而提高性能并避免潜在的依赖追踪问题。

:::

如何管理el-tab-pane组件 和el-tab-nav组件

在el-tabs组件中先渲染el-tab-pane组件 ,等el-tab-pane组件注册完后再根据el-tab-pane组件动态生成el-tab-nav组件,所以一开始是el-tab-pane组件在前,el-tab-nav组件在后的,通过css order:-1把el-tab-nav组件显示在el-tab-pane组件之前。然后在el-tabs组件根元素的onVnodeMounted钩子函数去交换2个组件DOM元素位置

渲染el-tab-pane组件

tsx
   const panels = (
        <div class={ns.e('content')}>{renderSlot(slots, 'default')}</div>
      )
  • 获取当前实例、获取插槽(插槽中可自定义el-tab-nav组件的label内容,然后让el-tabs组件动态渲染到el-nav组件的对应插槽内)

  • inject获取注入的父组件内容

  • 初始化active计算属性表示当前el-tab-pane是否激活(通过判断currentName 和props.name属性是否相等)

  • 通过active计算属性来初始化loaded属性表示是否加载

  • 初始化shouldBeRender计算属性来判断组件是否应该被渲染(用于懒加载)

    (通过!props.lazy || loaded.value || active.value来控制元素v-if)

  • 初始化el-tab-pane组件信息

    typescript
    const pane = reactive({
      uid: instance.uid,// 组件唯一ID
      getVnode: () => instance.vnode,// 组件的vnode
      slots,// 插槽
      props,// 组件属性
      paneName,// props.name属性
      active,//当前el-tab-pane是否激活
      index,
      isClosable,
      isFocusInsidePane,
    })
  • 调用registerPane方法注册el-tab-pane组件

    typescript
    tabsRoot.registerPane(pane)
  • 如果用户自定义label插槽的话,还要在组件更新的时候通知el-tab-nav组件更新

    typescript
    onBeforeUpdate(() => {
      if (slots.label) tabsRoot.nav$.value?.scheduleRender()
    })

渲染tab-nav组件

tsx
const tabNav = () => (
      <TabNav
        ref={nav$}
        currentName={currentName.value}
        editable={props.editable}
        type={props.type}
        panes={panes.value}
        stretch={props.stretch}
        onTabClick={handleTabClick}
        onTabRemove={handleTabRemove}
      />
    )
const header = (
    <div
      class={[
        ns.e('header'),
        isVertical.value && ns.e('header-vertical'),
        ns.is(props.tabPosition),
      ]}
    >
      {createVNode(PanesSorter, null, {
        default: tabNav,
        $stable: true,
      })}
      {newButton}
    </div>
  )

TIP

$stable的作用

表示该组件的子树在渲染过程中不会因数据变化而产生副作用(如动态子节点、事件监听等)。这允许 Vue 在虚拟 DOM 的 diff 算法中跳过该组件的深度遍历,提升渲染性能

  • inject获取注入的父组件内容
  • 通过遍历渲染的el-tab-pane组件动态渲染出tab-item元素
  • 初始化tabRefsMap响应式变量,用来收集动态渲染出ab-item根元素,通过props传给el-tab-bar组件,让其可以根据元素大小设置动画效果

渲染el-tab-bar组件

  • 通过遍历渲染el-tab-pane组件获取当前激活的tab-item元素,根据元素渲染的宽高和offsetLeft 来设置tab-bar位置和大小