Skip to content

[Vue源码学习]4-组件化 #11

Open
@PeterChen1997

Description

@PeterChen1997

由于需要提前分享的原因,我们本期先安排Vue组件化相关的内容学习,编译部分的内容我们放在下一集继续进行

由于是专题学习,所以很多与专题关联不大的内容可能不会进行详细进行说明,尽量保持专题内容的内聚性,所以如果有疑问的点,可以在群内讨论,也可以参考下历史的分享文档

下面的分享内容仅代表自己的一些理解,有错误的地方也欢迎大家及时指出讨论

今天分享的内容主要有以下三点:

  • 组件化的演变
  • vue组件实例化
  • 异步组件的实现

组件化的由来

在介绍现在的组件化思路之前,我们可以回忆下使用PHP或者JSP编写前端相关代码的时候,当时的代码编写思路

最初的思路是如何将页面从后端传回给浏览器作展示。每当用户点击link的时候,服务端渲染新的page,然后浏览器跳转的新的page即可

这些页面大都是静态页面,但是如果是构建一个需要处理用户交互的web应用,这类静态页面就有无法满足需求了

此时我们可以在返回的page上 head / body 内加一些 script 来实现逻辑处理

但随着业务体量变大,需求越来越多,这么编写代码的维护成本也会越来越高,代码就需要做一定的拆分

传统的拆分方法

传统的业务的代码拆分把 HTML / JS / CSS 单独的用特定的文件从page中拆离出来:

这样可能更加直观一些:

这类拆分方法一定程度上的降低了维护难度,将内容拆解到了相关的文件中,解耦了页面内容

但其中 JS 的模块加载管理,CSS的作用域、优先级等的逻辑统一管理起来成本依然很高,并且这类代码组织方式可能会让项目 割裂严重,编辑成本很高,可能排查个问题需要连续查找并编辑多个不同的文件,才能完成

vue中的拆分方法

那么vue为了解决这些问题,选择了把page中逻辑上相关联的 HTML / CSS / JS 抽离出来统一管理:

这样单独拆分出来的内容,在Vue内可以用*.vue的文件进行组织,这类 功能内聚 且 相互间有关联的 template / script / style 的内容可称为 Vue组件

Tips: SFC 的解析

在vue中,框架允许使用者以一种名为单文件组件 SFC(single file component) 的格式撰写 Vue 组件,但是浏览器并不支持这类文件的解析,所以在打包的时候还是得走一遍编译,在vue-loader 中调用 vue-template-compiler 走一遍简单的处理,最后被处理完为一个JS对象 SFCDescriptor

// an object format describing a single-file component.
declare type SFCDescriptor = {
  template: ?SFCBlock;
  script: ?SFCBlock;
  styles: Array<SFCBlock>;
  customBlocks: Array<SFCBlock>;
};

vue-loader 中的 vue-template-compiler 的这部分代码其实是从vue源码中抽离的,真正的代码在 src/platforms/web/entry-compiler.js 中

组件化的实现

源码版本: v2.5.17

上面介绍完了Vue组件的由来,我们从一个最简单的组件demo看起,看看一个实际的组件,是怎么在vue内跑起来的

这里我们就跳过编译部分,直接在这里写render函数了

线上地址:https://codesandbox.io/s/vuejs-playground-ntprn

import Vue from "vue";

const CustomComponent = {
  render(h) {
    return h("p", "hello world");
  }
};

new Vue({
  el: "#app",
  components: {
    CustomComponent
  },
  // 这里的 h 是 createElement 方法
  render: h => h('CustomComponent')
});

组件化的实现逻辑

实例化过程

下面我们就进入源码分析环节,难度会有一部分的提升

首先我们回顾下Vue实例化的整个流程

这里大部分的内容我们在第一次的Vue实例化分享中基本都有提到,组件化这次需要额外关注这两个地方:

  • createComponent
  • __patch__

首先我们来看看createComponent

createComponent

这里的逻辑主要三个点,我们顺着思维导图依次看一下:

  • 构建子类构造函数
  • 安装组件钩子函数
  • 实例化VNode

源码地址

patch

patch这部分我们在前面文章内也大致分析过(链接

当时点到了createElm这个函数,我们现在来详细分析下这个函数的功能

这里又回到了上面讲到的init部分的内容了,我们详细看看init这部分的逻辑

接下来的过程就和我们普通实例化流程基本一样了,走一遍render / $mount, 在patch中先创建一个父节点占位符,然后再遍历所有子 VNode 递归调用 createElm,在遍历的过程中,如果遇到子 VNode 是一个组件的 VNode,则重复本节开始的过程,这样通过一个递归的方式就可以完整地构建了整个组件树

那么完整串起来,我们的整个逻辑就是这样的了

局部注册 / 全局注册

讲完了实例化,我们再来看看组件注册的两种方式,理解起来就更顺了

异步组件

我们在开发大型应用的时候,往往会把一些首屏没有用到的组件,或者不影响应用使用的组件做异步加载,用于减小首屏代码体积,提升应用性能

异步组件一共有三种类型,我们这里直接看下最复杂的这种高级异步组件

线上代码:https://codesandbox.io/s/vuejs-playground-nixx2?file=/index.js

const AsyncComponent = () => ({
  // 需要加载的组件 (应该是一个 `Promise` 对象)
  component: import('./MyComponent.vue'),
  // 异步组件加载时使用的组件
  loading: LoadingComponent,
  // 加载失败时使用的组件
  error: ErrorComponent,
  // 展示加载时组件的延时时间。默认值是 200 (毫秒)
  delay: 200,
  // 如果提供了超时时间且组件加载也超时了,
  // 则使用加载失败时使用的组件。默认值是:`Infinity`
  timeout: 3000
})

那这个功能在Vue中是怎么实现的呢?我们顺着思维导图再看看

代码

组件化的未来

到这里,基本上我们就把Vue2.x中的组件化大概的实例化过程和一部分高级功能做了较为详细的分析
在组件化的这个方向上,还有哪些其他的思考点呢,我们再来看看

Web 开发由于其存在方式,必然需要在「避免构建」和「提高生产力」之前作出选择,Vue这类组件方案,其实最后还是构建成了一个普通的JS对象,否则是无法直接被浏览器识别加载的,但是web component解决了这个问题

web component

原生组件是一个5年前,就提出的概念了,目前的特性支持率也很高了

优点:

  • Custom elements(自定义元素):一组JavaScript API,允许您定义custom elements及其行为,然后可以在您的用户界面中按照需要使用它们。
  • Shadow DOM(影子DOM):一组JavaScriptAPI,用于将封装的“影子”DOM树附加到元素(与主文档DOM分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
  • HTML templates(HTML模板):</template/> 和 </slot/>元素使您可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。
  • HTML Imports(HTML导入):一旦定义了自定义组件,最简单的重用它的方法就是使其定义细节保存在一个单独的文件中,然后使用导入机制将其导入到想要实际使用它的页面中。HTML 导入就是这样一种机制,尽管存在争议 — Mozilla 根本不同意这种方法,并打算在将来实现更合适的

但是似乎主流框架没有并没有把它整合到现有的组件实现逻辑中,这是为啥呢?

优点:

  • Custom elements(自定义元素):一组JavaScript API,允许自定义custom elements及其行为,然后可以在您的用户界面中按照需要使用它们
  • Shadow DOM(影子DOM):一组JavaScriptAPI,用于将封装的“影子”DOM树附加到元素(与主文档DOM分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
  • HTML templates(HTML模板):</template/> 和 </slot/>元素使您可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用
  • HTML Imports(HTML导入):一旦定义了自定义组件,最简单的重用它的方法就是使其定义细节保存在一个单独的文件中,然后使用导入机制将其导入到想要实际使用它的页面中。HTML 导入就是这样一种机制,尽管存在争议 — Mozilla 根本不同意这种方法,并打算在将来实现更合适的

但是似乎主流框架没有并没有把它整合到现有的组件实现逻辑中,主要原因可能有下面几点:

  • 预处理器的支持
  • 热加载
  • 运行时开销
  • 兼容性问题
  • 性能优化方案的可控性
  • 服务端渲染的支持
  • 跨平台渲染支持
  • 实验特性的开发成本

现有的组件实现逻辑相比web component,短时间内还是有很大的优势

延伸问题讨论

分享到这里,我们本次的Vue组件化分享的主要内容就已经基本分享完毕了,接下来我们可以再一起讨论下上周在群内收集到的关于hoc和mixin的这个问题

HOC / mixin in Vue

HOC定义:

  1. 高阶组件(HOC)应该是无副作用的纯函数,且不应该修改原组件
  2. 高阶组件(HOC)不关心你传递的数据(props)是什么,并且被包装组件(WrappedComponent)不关心数据来源
  3. 高阶组件(HOC)接收到的 props 应该透传给被包装组件(WrappedComponent)

仅使用mixin带来的问题:

  1. mixins 带来了隐式依赖
  2. mixins 与 mixins 之间,mixins 与组件之间容易导致命名冲突
  3. 由于 mixins 是侵入式的,它改变了原组件,所以修改 mixins 等于修改原组件,随着需求的增长 mixins 将变得复杂,导致滚雪球的复杂性。

vue现有的mixin demo:

export default consoleMixin {
  mounted () {
    console.log('I have already mounted')
  }
}
export default {
  name: 'BaseComponent',
  props: {
    test: Number
  },
  mixins: [ consoleMixin ]
  methods: {
    handleClick () {
      this.$emit('customize-click')
    }
  }
}

用于实现hoc的demo:

function WithConsole(WrappedComponent) {
  return {
    mounted() {
      console.log("I have already mounted");
    },
    props: WrappedComponent.props,
    render(h) {
      return h(WrappedComponent, {
        props: this.$props,
        attrs: this.$attrs,
        on: this.$listeners,
        scopedSlots: this.$scopedSlots
      });
    }
  };
}

vue3 中 Composition-api 的实现,对于复杂minxin的优化问题,将会是一个不错的解决方案

线上地址:https://codesandbox.io/s/vue-composition-api-example-hj3se?file=/src/main.js

//useConsole.js
export default function useConsole() {
    onMounted(() => {
      console.log("I have already mounted");
    })

    return {}
}

参考内容

  • Evan You - Inside Vue Components - Laracon EU 2017
  • vue-loader

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions