前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • Vue

    • 从源码解读Vue生命周期
    • 组件的本质
    • 有状态组件的设计
    • 设计 VNode
    • 辅助创建 VNode 的 h 函数
    • 自定义渲染器和异步渲染
    • 渲染器之挂载
    • 渲染器的核心 Diff 算法
    • 渲染器之patch
    • 图解 Vue 响应式原理
    • 图解 Vue 异步更新
    • 剖析 Vue 内部运行机制
    • vue响应式原理模拟
    • vue状态管理之vuex
    • 理解Vue的设计思想及实现Vue
    • diff算法深入
    • vue router vuex原理分析
    • Vue3初探响应式原理.
    • vue2源码分析
    • vue组件化实践
  • React

  • Webpack

  • Node

  • Javascript

  • 综合

文章学习导航
面试总览

30 秒速记

  • 组件接收输入,组织状态并产出界面描述
  • 渲染器把描述落实到宿主环境
  • 组件描述与内部渲染子树不是同一层
  • 有状态逻辑需要跨更新保存上下文
  • 函数式外观不等于所有框架都没有内部实例

组件是一块有明确输入和职责的界面单元,渲染结果交给框架落到实际页面。 外层组件节点描述要创建哪个组件、传入什么,组件内部再根据状态产出自己的子树。这样业务可以按组件组合,不必自己维护每一步节点操作。有状态组件还需要保存状态和生命周期上下文;不同框架如何表示实例属于实现选择,不能仅看函数写法就断言没有状态。

版本校准: 本文出现 Object.defineProperty、Dep、Watcher 和双端 Diff 时,主要描述 Vue 2 实现;Vue 3 使用 Proxy、effect 与新的渲染器路径。Vue 2 已于 2023-12-31 结束维护,新项目应以 Vue 3 为基线,旧项目参考 Vue 2 EOL 官方说明 制定迁移与安全策略。

当我们使用 Vue 或 React 时,往往会将页面拆分为各种组件,通过拼装组件来形成页面和应用,就像搭积木一样。

那么,大家有没有思考过:“组件的产出是什么?”

# 组件的产出是什么

在 JQuery 盛行的年代,比起组件,“模板引擎”的概念要更加流行。

我们可以使用 lodash.template 函数来回忆一下当年是如何用模板开发一个页面的:

import { template } from 'lodash'

const compiler = template('<h1><%= title %></h1>')
const html = compiler({ title: 'My Component' })

document.getElementById('app').innerHTML = html
@前端进阶之旅: 代码已经复制到剪贴板

模板引擎的概念是:字符串 + 数据 => html。

lodash.template 函数虽然称不上是“引擎”,但足以说明问题。

我们将模板字符串传递给 template 函数,该函数返回一个编译器 compiler,只要把数据传入 compiler 函数,便能得到最终想要渲染的内容。

当数据发生变化时,我们需要使用新的数据重新编译模板:

const newHtml = compiler({ title: 'New Component' })
@前端进阶之旅: 代码已经复制到剪贴板

如果把上面的逻辑封装成一个函数,那么一个组件就诞生了:

const MyComponent = props => {
  const compiler = MyComponent.cache || (MyComponent.cache = template('<h1><%= title %></h1>'))
  return compiler(props)
}
MyComponent.cache = null
@前端进阶之旅: 代码已经复制到剪贴板

我们可以这样使用它:

document.getElementById('app').innerHTML = MyComponent({ title: 'MyComponent' })
@前端进阶之旅: 代码已经复制到剪贴板

TIP

完整代码&在线体验地址: https://codesandbox.io/s/m7nqlj8m9j

MyComponent 组件也许会带给你这样的感觉:一个组件就是一个函数,给我什么样的数据,我就渲染对应的 html 内容。

这个概念,与我们如今谈论的 Vue 或 React 并没有什么不同。所以,这 就是 组件的本质。

组件的本质虽然没变,但组件的产出却改变了。在模板引擎的年代,组件的产出是 html 字符串:

而如今的 Vue 或 React,它们的组件所产出的内容并不是 html 字符串,而是大家所熟知的 Virtual DOM。

拿 Vue 来说,一个组件最核心的东西是 render 函数,剩余的其他内容,如 data、compouted、props 等都是为 render 函数提供数据来源服务的。render 函数本可以直接产出 html 字符串,但却产出了 Virtual DOM,借助 snabbdom 的 API 我们可以很容易地用代码描述这个公式:

import { h } from 'snabbdom'

// h 函数用来创建 VNode,组件的产出是 VNode
const MyComponent = props => {
  return h('h1', props.title)
}
@前端进阶之旅: 代码已经复制到剪贴板

Virtual DOM 终究要渲染真实 DOM,这个过程就可以理解为模板引擎年代的完全替换 html,只不过它采用的不是完全替换,我们通常把这个过程叫做 patch,同样可以借助 snabbdom 的 API 轻松地实现:

import { h, init } from 'snabbdom'
// init 方法用来创建 patch 函数
const patch = init([])

const MyComponent = props => {
  return h('h1', props.title)
}

// 组件的产出是 VNode
const prevVnode = MyComponent({ title: 'prev' })
// 将 VNode 渲染成真实 DOM
patch(document.getElementById('app'), prevVnode)
@前端进阶之旅: 代码已经复制到剪贴板

当数据变更时,组件会产出新的 VNode,我们只需再次调用 patch 函数即可:

// 数据变更,产出新的 VNode
const nextVnode = MyComponent({ title: 'next' })
// 通过对比新旧 VNode,高效地渲染真实 DOM
patch(prevVnode, nextVnode)
@前端进阶之旅: 代码已经复制到剪贴板

TIP

完整代码&在线体验地址: https://codesandbox.io/s/mq0q8r50w8

以上就是我们要达成的共识:组件的产出就是 Virtual DOM。

为何组件要从直接产出 html 变成产出 Virtual DOM 呢?其原因是 Virtual DOM 带来了 分层设计,它对渲染过程的抽象,使得框架可以渲染到 web(浏览器) 以外的平台,以及能够实现 SSR 等。

至于 Virtual DOM 相比原生 DOM 操作的性能,这并非 Virtual DOM 的目标,确切地说,如果要比较二者的性能是要“控制变量”的,例如:页面的大小、数据变化量等。

# 组件的 VNode 如何表示

TIP

在后续行文时,将统一使用 VNode 来简称 Virtual DOM 。

VNode 是真实 DOM 的描述,比如我们可以用如下对象描述一个 div 标签:

const elementVnode = {
  tag: 'div'
}
@前端进阶之旅: 代码已经复制到剪贴板

想要把 elementVnode 渲染成真实 DOM,我们还需要一个渲染器(Renderer):

function render(vnode, container) {}
@前端进阶之旅: 代码已经复制到剪贴板

渲染器接收两个参数,分别是将要渲染的 vnode 和 元素挂载点(真实 DOM 被渲染的位置)。

为了渲染如上的 div 标签,我们可以这样调用 render 函数:

// 把 elementVnode 渲染到 id 为 app 的元素下
render(elementVnode, document.getElementById('app'))
@前端进阶之旅: 代码已经复制到剪贴板

render 函数的实现也很简单:

function render(vnode, container) {
  mountElement(vnode, container)
}

function mountElement(vnode, container) {
  // 创建元素
  const el = document.createElement(vnode.tag)
  // 将元素添加到容器
  container.appendChild(el)
}
@前端进阶之旅: 代码已经复制到剪贴板

如上,在 render 函数内调用了 mountElement,它的作用是根据 VNode 创建真实 DOM 并将其添加到容器中。

这段代码对于标准 html 标签是可以正常工作的(不考虑 SVG),但并不适用于组件。为了能够渲染组件,我们需要思考:组件的 VNode 应该如何表示?

对于 html 标签的 VNode 来说,其 tag 属性的值就是标签的名字,但如果是组件的话,其 VNode 中 tag 属性的值应该是什么呢?

很简单,我们可以将其指向组件自身。假设我们有如下组件:

class MyComponent {
  render() {
    // render 函数产出 VNode
    return {
      tag: 'div'
    }
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

如上,我们使用 class 定义了一个类,它是一个组件(有状态组件),我们可以使用如下 VNode 来描述它:

const componentVnode = {
  tag: MyComponent
}
@前端进阶之旅: 代码已经复制到剪贴板

如上,直接将 tag

基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • Vue

    • 从源码解读Vue生命周期
    • 组件的本质
      • 组件的产出是什么
      • 组件的 VNode 如何表示
      • 组件的种类
    • 有状态组件的设计
    • 设计 VNode
    • 辅助创建 VNode 的 h 函数
    • 自定义渲染器和异步渲染
    • 渲染器之挂载
    • 渲染器的核心 Diff 算法
    • 渲染器之patch
    • 图解 Vue 响应式原理
    • 图解 Vue 异步更新
    • 剖析 Vue 内部运行机制
    • vue响应式原理模拟
    • vue状态管理之vuex
    • 理解Vue的设计思想及实现Vue
    • diff算法深入
    • vue router vuex原理分析
    • Vue3初探响应式原理.
    • vue2源码分析
    • vue组件化实践
  • React

  • Webpack

  • Node

  • Javascript

  • 综合

完整面试题地址:
作者:程序员poetry
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

Vue组件的本质|原理篇

文章学习导航
面试总览

30 秒速记

  • 组件接收输入,组织状态并产出界面描述
  • 渲染器把描述落实到宿主环境
  • 组件描述与内部渲染子树不是同一层
  • 有状态逻辑需要跨更新保存上下文
  • 函数式外观不等于所有框架都没有内部实例

组件是一块有明确输入和职责的界面单元,渲染结果交给框架落到实际页面。 外层组件节点描述要创建哪个组件、传入什么,组件内部再根据状态产出自己的子树。这样业务可以按组件组合,不必自己维护每一步节点操作。有状态组件还需要保存状态和生命周期上下文;不同框架如何表示实例属于实现选择,不能仅看函数写法就断言没有状态。

版本校准: 本文出现 Object.defineProperty、Dep、Watcher 和双端 Diff 时,主要描述 Vue 2 实现;Vue 3 使用 Proxy、effect 与新的渲染器路径。Vue 2 已于 2023-12-31 结束维护,新项目应以 Vue 3 为基线,旧项目参考 Vue 2 EOL 官方说明 制定迁移与安全策略。

当我们使用 Vue 或 React 时,往往会将页面拆分为各种组件,通过拼装组件来形成页面和应用,就像搭积木一样。

那么,大家有没有思考过:“组件的产出是什么?”

# 组件的产出是什么

在 JQuery 盛行的年代,比起组件,“模板引擎”的概念要更加流行。

我们可以使用 lodash.template 函数来回忆一下当年是如何用模板开发一个页面的:

import { template } from 'lodash'

const compiler = template('<h1><%= title %></h1>')
const html = compiler({ title: 'My Component' })

document.getElementById('app').innerHTML = html
@前端进阶之旅: 代码已经复制到剪贴板

模板引擎的概念是:字符串 + 数据 => html。

lodash.template 函数虽然称不上是“引擎”,但足以说明问题。

我们将模板字符串传递给 template 函数,该函数返回一个编译器 compiler,只要把数据传入 compiler 函数,便能得到最终想要渲染的内容。

当数据发生变化时,我们需要使用新的数据重新编译模板:

const newHtml = compiler({ title: 'New Component' })
@前端进阶之旅: 代码已经复制到剪贴板

如果把上面的逻辑封装成一个函数,那么一个组件就诞生了:

const MyComponent = props => {
  const compiler = MyComponent.cache || (MyComponent.cache = template('<h1><%= title %></h1>'))
  return compiler(props)
}
MyComponent.cache = null
@前端进阶之旅: 代码已经复制到剪贴板

我们可以这样使用它:

document.getElementById('app').innerHTML = MyComponent({ title: 'MyComponent' })
@前端进阶之旅: 代码已经复制到剪贴板

TIP

完整代码&在线体验地址: https://codesandbox.io/s/m7nqlj8m9j

MyComponent 组件也许会带给你这样的感觉:一个组件就是一个函数,给我什么样的数据,我就渲染对应的 html 内容。

这个概念,与我们如今谈论的 Vue 或 React 并没有什么不同。所以,这 就是 组件的本质。

组件的本质虽然没变,但组件的产出却改变了。在模板引擎的年代,组件的产出是 html 字符串:

而如今的 Vue 或 React,它们的组件所产出的内容并不是 html 字符串,而是大家所熟知的 Virtual DOM。

拿 Vue 来说,一个组件最核心的东西是 render 函数,剩余的其他内容,如 data、compouted、props 等都是为 render 函数提供数据来源服务的。render 函数本可以直接产出 html 字符串,但却产出了 Virtual DOM,借助 snabbdom 的 API 我们可以很容易地用代码描述这个公式:

import { h } from 'snabbdom'

// h 函数用来创建 VNode,组件的产出是 VNode
const MyComponent = props => {
  return h('h1', props.title)
}
@前端进阶之旅: 代码已经复制到剪贴板

Virtual DOM 终究要渲染真实 DOM,这个过程就可以理解为模板引擎年代的完全替换 html,只不过它采用的不是完全替换,我们通常把这个过程叫做 patch,同样可以借助 snabbdom 的 API 轻松地实现:

import { h, init } from 'snabbdom'
// init 方法用来创建 patch 函数
const patch = init([])

const MyComponent = props => {
  return h('h1', props.title)
}

// 组件的产出是 VNode
const prevVnode = MyComponent({ title: 'prev' })
// 将 VNode 渲染成真实 DOM
patch(document.getElementById('app'), prevVnode)
@前端进阶之旅: 代码已经复制到剪贴板

当数据变更时,组件会产出新的 VNode,我们只需再次调用 patch 函数即可:

// 数据变更,产出新的 VNode
const nextVnode = MyComponent({ title: 'next' })
// 通过对比新旧 VNode,高效地渲染真实 DOM
patch(prevVnode, nextVnode)
@前端进阶之旅: 代码已经复制到剪贴板

TIP

完整代码&在线体验地址: https://codesandbox.io/s/mq0q8r50w8

以上就是我们要达成的共识:组件的产出就是 Virtual DOM。

为何组件要从直接产出 html 变成产出 Virtual DOM 呢?其原因是 Virtual DOM 带来了 分层设计,它对渲染过程的抽象,使得框架可以渲染到 web(浏览器) 以外的平台,以及能够实现 SSR 等。

至于 Virtual DOM 相比原生 DOM 操作的性能,这并非 Virtual DOM 的目标,确切地说,如果要比较二者的性能是要“控制变量”的,例如:页面的大小、数据变化量等。

# 组件的 VNode 如何表示

TIP

在后续行文时,将统一使用 VNode 来简称 Virtual DOM 。

VNode 是真实 DOM 的描述,比如我们可以用如下对象描述一个 div 标签:

const elementVnode = {
  tag: 'div'
}
@前端进阶之旅: 代码已经复制到剪贴板

想要把 elementVnode 渲染成真实 DOM,我们还需要一个渲染器(Renderer):

function render(vnode, container) {}
@前端进阶之旅: 代码已经复制到剪贴板

渲染器接收两个参数,分别是将要渲染的 vnode 和 元素挂载点(真实 DOM 被渲染的位置)。

为了渲染如上的 div 标签,我们可以这样调用 render 函数:

// 把 elementVnode 渲染到 id 为 app 的元素下
render(elementVnode, document.getElementById('app'))
@前端进阶之旅: 代码已经复制到剪贴板

render 函数的实现也很简单:

function render(vnode, container) {
  mountElement(vnode, container)
}

function mountElement(vnode, container) {
  // 创建元素
  const el = document.createElement(vnode.tag)
  // 将元素添加到容器
  container.appendChild(el)
}
@前端进阶之旅: 代码已经复制到剪贴板

如上,在 render 函数内调用了 mountElement,它的作用是根据 VNode 创建真实 DOM 并将其添加到容器中。

这段代码对于标准 html 标签是可以正常工作的(不考虑 SVG),但并不适用于组件。为了能够渲染组件,我们需要思考:组件的 VNode 应该如何表示?

对于 html 标签的 VNode 来说,其 tag 属性的值就是标签的名字,但如果是组件的话,其 VNode 中 tag 属性的值应该是什么呢?

很简单,我们可以将其指向组件自身。假设我们有如下组件:

class MyComponent {
  render() {
    // render 函数产出 VNode
    return {
      tag: 'div'
    }
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

如上,我们使用 class 定义了一个类,它是一个组件(有状态组件),我们可以使用如下 VNode 来描述它:

const componentVnode = {
  tag: MyComponent
}
@前端进阶之旅: 代码已经复制到剪贴板

如上,直接将 tag 属性的值指向组件自身。但想要正确地渲染该组件,我们还需要修改 render:

function render(vnode, container) {
  if (typeof vnode.tag === 'string') {
    // html 标签
    mountElement(vnode, container)
  } else {
    // 组件
    mountComponent(vnode, container)
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

如上,通过 判断 vnode.tag 是否是字符串 来区分:一个 VNode 到底是 html 标签还是组件。如果是组件的话,调用 mountComponent 函数挂载组件,而非 mountElement,如下是 mountComponent 函数的实现:

function mountComponent(vnode, container) {
  // 创建组件实例
  const instance = new vnode.tag()
  // 渲染
  instance.$vnode = instance.render()
  // 挂载
  mountElement(instance.$vnode, container)
}
@前端进阶之旅: 代码已经复制到剪贴板

道理很简单,由于 vnode.tag 指向组件类,所以我们创建一个组件实例,接着调用其 render 函数产出 VNode 并将其添加到实例属性 instance.$vnode,最后借用 mountElement 函数完成标签的挂载即可。

实际上为了便于理解,本节代码的简化程度很大,更加详细严谨的代码会在后续的具体章节中体现。不过这足以说明问题:可以让 VNode 的 tag 属性指向组件本身,从而使用 VNode 来描述组件。

TIP

完整代码&在线体验地址: https://codesandbox.io/s/8817m3zz5j

# 组件的种类

大家可能会注意到,在上文中,采用了两种不同的方式来描述组件。

第一种方式是使用一个普通的函数:

function MyComponent(props) {}
@前端进阶之旅: 代码已经复制到剪贴板

第二种方式是使用一个类:

class MyComponent {}
@前端进阶之旅: 代码已经复制到剪贴板

← 从源码解读Vue生命周期有状态组件的设计 →

fe