一个为 Vue JS 2.0 打造的 Material 风格的组件库

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一个为 Vue JS 2.0 打造的 Material 风格的组件库相关的知识,希望对你有一定的参考价值。

参考技术A

让Vue和Material Design的强大力量在开发上助您一臂之力

Vuetify是Vue的语义组件框架。它旨在提供干净,语义和可重用的组件,使您的应用程序构建变得轻而易举。

利用Vue,Material Design和大量精心制作的组件和功能库,构建 出色的 应用程序。Vuetify组件根据Google的Material Design Spec构建,具有易于记忆的语义设计,可将记忆复杂的类和标记转换为具有简单明了名称的类型即可。

Vuetify支持所有现代浏览器,包括IE11和Safari 9+(使用polyfill)。来自手机 到笔记本电脑 到桌面 ,您可以放心,您的应用程序将按预期工作。

您知道哪些好用的Vue 组件库,欢迎评论分享,共同探讨学习

Vue.js 3.0 组件是如何渲染为 DOM 的?

题图 来自 Vue.js 官网


本文主要是讲述 Vue.js 3.0 中一个组件是如何转变为页面中真实 DOM 节点的。对于任何一个基于 Vue.js 的应用来说,一切的故事都要从应用初始化(通常会命名为 APP 的根组件挂载到 HTML 页面 DOM 节点上)说起。所以,我们可以从应用的根组件为切入点。


主线思路:聚焦于一个组件是如何转变为 DOM 的。

辅助思路:

  1. 涉及到源代码的地方,需要明确标记源码所在文件,同时将 TS 简化为 JS 以便于直观理解

  2. 思路每前进一步要能够得出结论

  3. 尽量总结归纳出流程图

应用初始化

在 Vue.js 3.0 中,初始化一个应用的方式和 Vue.js 2.x 有差别但是差别不大(本质上都是把 App 组件挂载到 id 为 app 的 DOM 节点上),在 Vue.js 3.0 中用法如下:

import { createApp } from 'vue'import App from './app'
const app = createApp(App)app.mount('#app')

createApp 简化版源码

// packages/runtime-dom/src/index.ts// 创建应用const createApp = ((...args) => { // 1. 创建 app 对象 const app = ensureRenderer().createApp(...args)  const { mount } = app // 2. 重写 mount 方法 app.mount = (containerOrSelector) => { // ... }  return app})
createApp 方法中主要做了两件事:
  1. 创建 app 对象

  2. 重写 app.mount 方法


接下来会分别看一下这两个过程都做了什么事情。


创建 app 对象

从 ensureRenderer() 着手。在 Vue.js 3.0 中有一个「渲染器」的概念,我们先对渲染器有一个初步的印象:渲染器可以用于跨平台渲染,是一个包含了平台渲染核心逻辑的 JavaScript 对象。接下来,我们通过简化版源码来验证这个结论:

// packages/runtime-dom/src/index.ts// 定义渲染器变量let renderer// 创建一个渲染器对象// 惰性创建渲染器(当用户只依赖响应式包的时候可以通过 tree-shaking 的方式移除核心渲染逻辑相关的代码)function ensureRenderer() { return renderer || (renderer = createRenderer(rendererOptions))}// packages/runtime-core/src/renderer.tsexport function createRenderer(options) { return baseCreateRenderer(options)}

可以看出渲染器最终由 baseCreateRenderer 函数生成,是一个包含 render 和createApp 函数的 JS 对象。其中 createApp 函数是由 createAppAPI 函数返回的。那 createApp 接收的参数有哪些呢?为了寻求答案,我们需要看一下 createAppAPI  做了什么事情。

// packages/runtime-core/src/apiCreateApp.ts// 接收一个渲染器 render 作为参数,接收一个可选参数 hydrate,返回一个用于创建 app 的函数export function createAppAPI(render, hydrate) { // createApp 接收两个参数:根组件对象和根组件的prop return function createApp(rootComponent, rootProps = null) { const context = createAppContext()    const app = (context.app = { _uid: uid++, _component: rootComponent, _props: rootProps, _container: null, _context: context, version, get config() {}, set config(v) {}, use(plugin: Plugin, ...options: any[]) {}, mixin(mixin: ComponentOptions) {}, component(name: string, component?: Component): any {}, directive(name: string, directive?: Directive) {}, mount(rootContainer: HostElement, isHydrate?: boolean): any { // 创建根组件的 vnode const vnode = createVNode(rootComponent, rootProps) // 利用函数参数传入的渲染器渲染 vnode render(vnode, rootContainer) app._container = rootContainer return vnode.component.proxy }, unmount() {}, provide(key, value) {} }    return app }}

渲染器对象的 createApp 方法接收两个参数:根组件对象和根组件的prop。这和应用初始化 demo 中 createApp(App) 的使用方式是吻合的。还可以看到的是:createApp 返回的 app 对象在最初定义时包含了 _uid 、 use 、 mixin 、 component 、mount 等属性。


此时,我们可以得出结论:在应用层调用的 createApp 方法内部,首先会生成一个渲染器,然后调用渲染器的 createApp 方法创建 app 对象。app 对象中具有一系列我们在日常开发应用时已经很熟悉的属性。


在应用层调用的 createApp 方法内部创建好 app 对象后,接下来便是对 app.mount 方法重写。


重写 app.mount 方法

先看一下简化版的 app.mount  源码:

// packages/runtime-dom/src/index.tsconst { mount } = appapp.mount = (containerOrSelector) => { // 1. 标准化容器(将传入的 DOM 对象或者节点选择器统一为 DOM 对象) const container = normalizeContainer(containerOrSelector) if (!container) return  const component = app._component // 2. 标准化组件(如果根组件不是函数,并且没有 render 函数和 template 模板,则把根组件 innerHTML 作为 template) if (!isFunction(component) && !component.render && !component.template) { component.template = container.innerHTML } // 3. 挂载前清空容器的内容 container.innerHTML = ''  // 4. 执行渲染器创建 app 对象时定义的 mount 方法(在后文中称之为「标准 mount 函数」)来渲染根组件 const proxy = mount(container)  return proxy}
浏览器平台 app.mount 方法重写主要做了 4 件事情:
  1. 标准化容器

  2. 标准化组件

  3. 挂载前清空容器的内容

  4. 执行标准 mount 函数渲染组件


此时可能会有人思考一个问题:为什么要重写app.mount 呢?答案是因为 Vue.js 需要支持跨平台渲染。


支持跨平台渲染的思路:不同的平台具有不同的渲染器,不同的渲染器中会调用标准的 baseCreateRenderer 来保证核心(标准)的渲染流程是一致的。


以浏览器端和服务端渲染的代码实现为例:


Vue.js 3.0 组件是如何渲染为 DOM 的?


createApp 流程图

在分别了解了 创建 app 对象和重写 app.mount 过程后,我们来以整体的视角看一下 createApp 函数的实现:



目前为止,只是对应用的初始化有了一个初步的印象,但是还没有涉及到具体的组件渲染过程。可以看到根组件的渲染是在标准 mount 函数中进行的。所以接下来需要去深入了解标准 mount 函数。


标准 mount 函数

简化版源码

// packages/runtime-core/src/apiCreateApp.ts// createAppAPI 函数内部返回的 createApp 函数中定义了 app 对象,mount 函数是 app 对象的方法之一mount(rootContainer, isHydrate) { // 1. 创建根组件的 vnode const vnode = createVNode(rootComponent, rootProps) // 2. 利用函数参数传入的渲染器渲染 vnode render(vnode, rootContainer)  app._container = rootContainer  return vnode.component.proxy}

createVNode 方法做了两件事:

  1. 基于根组件「创建 vnode」

  2. 在根组件容器中「渲染 vnode」


vnode 大致可以理解为 Virtual DOM(虚拟 DOM)概念的一个具体实现,是用普通的 JS 对象来描述 DOM 对象。因为不是真实的 DOM 对象,所以叫做 Virtual DOM。


我们来一起看一下创建 vnode 和渲染 vnode 的具体过程。

创建 vnode

简化版源码(已经把分支逻辑拿掉)

// packages/runtime-core/src/vnode.tsfunction _createVNode(type, props, children, patchFlag, dynamicProps, isBlockNode = false{ // 1. 对 VNodeTypes 或 ClassComponent 类型的 type 进行各种标准化处理:规范化 vnode、规范化 component、规范化 CSS 类和样式  // 2. 将 vnode 类型信息编码为位图 const shapeFlag = isString(type) ? ShapeFlags.ELEMENT : __FEATURE_SUSPENSE__ && isSuspense(type) ? ShapeFlags.SUSPENSE : isTeleport(type) ? ShapeFlags.TELEPORT : isObject(type) ? ShapeFlags.STATEFUL_COMPONENT : isFunction(type) ? ShapeFlags.FUNCTIONAL_COMPONENT : 0 // 3. 创建 vnode 对象 const vnode = { __v_isVNode: true, [ReactiveFlags.SKIP]: true, type, // 把函数入参 type 赋值给 vnode  props, children: null, component: null, staticCount: 0, shapeFlag, // 把 vnode 类型信息赋值给 vnode // 还有很多属性 } // 4. 标准化子节点 children normalizeChildren(vnode, children) return vnode}
createVNode 做了 4 件事:
  1. 对 VNodeTypes 或 ClassComponent 类型的 type 进行各种标准化处理

  2. 将 vnode 类型信息编码为位图

  3. 创建 vnode 对象

  4. 标准化子节点 children


细心的同学会发现:在标准 mount 函数中执行 createVNode(rootComponent, rootProps) 时,参数是根组件 rootComponent 和根组件属性 rootProps,但是在 _createVNode 在定义时函数签名的前两个参数确实 type 和 props。rootComponent 与 type 的关系是什么呢?函数名为什么差了一个 _ 呢?


首先函数名的差异,是由于在定义函数时,基于代码运行环境做了一个判断:

export const createVNode = __DEV__ ? createVNodeWithArgsTransform  : _createVNode
其次,rootComponent 与 type 的关系我们可以从 type 的类型定义中得到答案:
function _createVNode( type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT, props: (Data & VNodeProps) | null = null): VNode { }

当 createVNode把这 4 件事情做好后,会返回已经创建好 vnode,接下来做的事情是渲染 vnode。

渲染 vnode

即使不看具体源码实现,我们其实大致可以用一句话总结出渲染 vnode 过程做了什么事情:把 vnode 转化为真实 DOM。


前文我们提过,渲染器是一个包含了平台渲染核心逻辑的 JavaScript 对象。渲染 vnode 正是通过调用渲染器的 render 方法做的。

// 返回渲染器对象return { render, hydrate, createApp: createAppAPI(render, hydrate)}
我们来看一下 render 函数的定义(简化版源码):
// packages/runtime-core/src/renderer.tsconst render = (vnode, container) => { if (vnode == null) { // 如果 vnode 为 null,但是容器中有 vnode,则销毁组件 if (container._vnode) { unmount(container._vnode, null, null, true) } } else { // 创建或更新组件 patch(container._vnode || null, vnode, container) } // packages/runtime-core/src/scheduler.ts flushPostFlushCbs()  // 缓存 vnode 节点(标识该 vnode 已经完成渲染) container._vnode = vnode}

抽象来看,render 做的事情是:如果传入的 vnode 为空,则销毁组件,否则就创建或者更新组件。其中有两个关键函数:patchunmountpatchunmountrender是在baseCreateRenderer函数内部的方法)。


可以从 patch 着手,看一下是如何将 vnode 转化为 DOM 的。

patch

// packages/runtime-core/src/renderer.tsconst patch = ( n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false) => { // 1. 如果是更新 vnode 并且新旧 vnode 类型不一致,则销毁旧的 vnode if (n1 && !isSameVNodeType(n1, n2)) { anchor = getNextHostNode(n1) unmount(n1, parentComponent, parentSuspense, true) n1 = null } // 2. 处理不同类型节点的渲染 const { type, ref, shapeFlag } = n2 switch (type) { case Text: // 处理文本节点 processText(n1, n2, container, anchor) break case Comment: // 处理注释节点 break case Static: // 处理静态节点 break case Fragment: // 处理 Fragment 元素(https://v3.vuejs.org/guide/migration/fragments.html#fragments) break default: if (shapeFlag & ShapeFlags.ELEMENT) { // 处理普通 DOM 元素 } else if (shapeFlag & ShapeFlags.COMPONENT) { // 处理组件 } else if (shapeFlag & ShapeFlags.TELEPORT) { // 处理 TELEPORT } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { // 处理 SUSPENSE } else if (__DEV__) { warn('Invalid VNode type:', type, `(${typeof type})`) } }}
patch 函数做了 2 件事情:
  1.  如果是更新 vnode 并且新旧 vnode 类型不一致,则销毁旧的 vnode

  2. 处理不同类型节点的渲染


在 patch 函数的多个参数中,我们优先关注前 3 个参数:

  1. n1 表示旧的 vnode,当 n1 为 null 的时候,表示是一次新建(挂载)的过程

  2. n2 表示新的 vnode 节点,后续会根据这个 vnode 类型执行不同的处理逻辑

  3. container 表示 DOM 容器,也就是 vnode 渲染生成 DOM 后,会挂载到 container 下面


以新建文本 DOM 节点为例,此时 n1 为 null,n2 类型为 Text,所以会走分支逻辑:processText(n1, n2, container, anchor)processText 内部会去调用 hostCreateTexthostSetText


hostCreateTexthostSetText 是从 baseCreateRenderer 函数入参 options 中解析出来的方法:

// packages/runtime-core/src/renderer.tsconst { insert: hostInsert, remove: hostRemove, patchProp: hostPatchProp, forcePatchProp: hostForcePatchProp, createElement: hostCreateElement, createText: hostCreateText, createComment: hostCreateComment, setText: hostSetText, setElementText: hostSetElementText, parentNode: hostParentNode, nextSibling: hostNextSibling, setScopeId: hostSetScopeId = NOOP, cloneNode: hostCloneNode, insertStaticContent: hostInsertStaticContent} = options
来看看 options 是怎么来的:
// packages/runtime-core/src/renderer.ts// 在调用 baseCreateRenderer 时,传入了渲染参数function baseCreateRenderer(options{ }

还记得前文提到的我们在哪里调用了 baseCreateRenderer 吗?

// packages/runtime-dom/src/index.ts// 创建应用const createApp = ((...args) => { // 1. 创建 app 对象 const app = ensureRenderer().createApp(...args)  return app})
// packages/runtime-dom/src/index.tsconst rendererOptions = extend({ patchProp, forcePatchProp }, nodeOps)
function ensureRenderer() { return renderer || (renderer = createRenderer(rendererOptions))}// packages/runtime-core/src/renderer.tsexport function createRenderer(options) { return baseCreateRenderer(options)}

可以看到在创建渲染器时,我们调用了 baseCreateRenderer 并传入了 options。options 的值为 extend({ patchProp, forcePatchProp }, nodeOps)


我们如果知道了 nodeOps  中的 createTextsetText 等方法做了什么事情,就清楚了某一个确定类型的 vnode 是如何转变为 DOM 的。先看一下 nodeOps 的定义:

// packages/runtime-dom/src/nodeOps.tsexport const nodeOps = { createText: text => doc.createTextNode(text), setText: (node, text) => {}, // 其他方法}

此时已经非常接近问题的答案了,关键是看一下 doc 变量是什么:

const doc = (typeof document !== 'undefined' ? document : nullas Document


至此,我们知道了答案:先把组件转化为 vnode,针对特定类型的 vnode 执行不同的渲染逻辑,最终调用 document 上的方法将 vnode 渲染成 DOM。


抽象一下,从组件到渲染生成 DOM 需要经历 3 个过程:创建 vnode - 渲染 vnode - 生成 DOM。


在渲染 vnode 部分,我们以一个简单的 Text 类型的 vnode 为例来找到了答案。其实在 baseCreateRenderer 中有 30+ 个函数来处理不同类型的 vnode 的渲染。比如:用来处理组件类型的 processComponent 函数、用来处理普通 DOM 元素类型的processElement 函数等。由于 vnode 是一个树形数据结构,在处理过程中还应用到了递归思想。建议感兴趣的同学自行查看。

总结

最后,我们来做个总结:

  • 在 Vue.js 中, vnode 是对抽象事物的描述。

  • 从组件到渲染生成 DOM 需要经历 3 个过程:创建 vnode - 渲染 vnode - 生成 DOM。

  • 组件是如何转变为 DOM 的:先把组件转化为 vnode,针对特定类型的 vnode 执行不同的渲染逻辑,最终调用 document 上的方法将 vnode 渲染成 DOM。

  • 渲染器是一个包含了平台渲染核心逻辑的 JavaScript 对象,可以用于跨平台渲染。

  • 渲染器对象中的 createApp 方法,创建了一个具有 mount 方法的 app 实例。app.mount 方法中先是用根组件创建了 vnode,然后调用渲染器对象中的 render 方法去渲染 vnode,最终通过 DOM API 将 vnode 转化为 DOM。


附录

Vue.js 中使用了哪些 DOM 的方法:

  1. createElement

  2. createElementNS

  3. createTextNode

  4. createComment

  5. querySelector

  6. insertBefore

  7. insert

  8. removeChild

  9. setAttribute

  10. cloneNode

以上是关于一个为 Vue JS 2.0 打造的 Material 风格的组件库的主要内容,如果未能解决你的问题,请参考以下文章

翻译Vue.js 2.0 教程 起步

Vue.js 2.0 轻松入门

vue.js 2.0 在 laravel 5.3 中动态加载组件

Vue.js 2.0 在一个项目上呈现相同的组件,但在另一个项目上不呈现

Vue.js 2.0 Render 函数

预渲染 vue.js 2.0 组件(类似于 vue 1 中的 this.$compile)