前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
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 官方说明 制定迁移与安全策略。

假设我们有如下模板:

<MyComponent>
  <div></div>
</MyComponent>
@前端进阶之旅: 代码已经复制到剪贴板

由这段模板可知,我们为 MyComponent 组件提供了一个空的 div 标签作为默认插槽内容,从DOM结构上看 <MyComponent> 标签有一个 div 标签作为子节点,通常我们可以将其编译为如下 VNode:

const compVNode = {
  flags: VNodeFlags.COMPONENT_STATEFUL_NORMAL,
  tag: MyComponent,
  children: {
    flags: VNodeFlags.ELEMENT,
    tag: 'div'
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

这其实没什么问题,但是我们更倾向于新建一个 slots 属性来存储这些子节点,这在语义上更加贴切,所以我们希望将模板编译为如下 VNode:

const compVNode = {
  flags: VNodeFlags.COMPONENT,
  tag: MyComponent,
  children: null,
  slots: {
    // 默认插槽
    default: {
      flags: VNodeFlags.ELEMENT,
      tag: 'div'
    }
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

可以看到,如上 VNode 的 children 属性值为 null。当我们使用 mountComponent 函数挂载如上 VNode 时,我们可以在组件实例化之后并且在组件的渲染函数执行之前将 compVNode.slots 添加到组件实例对象上,这样当组件的渲染函数执行的时候,就可以访问插槽数据:

function mountComponent(vnode, container) {
  // 创建组件实例
  const instance = new vnode.tag()

  // 设置 slots
  instance.$slots = vnode.slots

  // 渲染
  instance.$vnode = instance.render()
  // 挂载
  mountElement(instance.$vnode, container)

  vnode.ref && vnode.ref(instance)
}
@前端进阶之旅: 代码已经复制到剪贴板

在 MyComponent 组件的 render 函数内,我们就可以通过组件实例访问 slots 数据:

class MyComponent {
  render() {
    return {
      flags: VNodeFlags.ELEMENT,
      tag: 'h1'
      children: this.$slots.default
    }
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

实际上,这就是普通插槽的实现原理,至于作用域插槽(scopedSlots),与普通插槽并没有什么本质的区别,我们知道作用域插槽可以访问子组件的数据,在实现上来看其实就是函数传参:

class MyComponent {
  render() {
    return {
      flags: VNodeFlags.ELEMENT,
      tag: 'h1'
      // 插槽变成了函数,可以传递参数
      children: this.$slots.default(1)
    }
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

如上代码所示,只要 this.$slots.default 是函数即可实现,所以在模板编译时,我们最终需要得到如下 VNode:

const compVNode = {
  flags: VNodeFlags.COMPONENT,
  tag: MyComponent,
  children: null,
  slots: {
    // 作用域插槽,可以接受组件传递过来的数据
    default: (arg) => {
      const tag = arg === 1 ? 'div' : 'h1'
      return {
        flags: VNodeFlags.ELEMENT,
        tag
      }
    }
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

现在你应该明白为什么普通插槽和作用域插槽本质上并没有区别了,因为普通插槽也可以是函数,只是不接收参数罢了。这么看的话其实普通插槽是作用域插槽的子集,那为什么不将它们合并呢?没错从 Vue2.6 起已经将之合并,所有插槽在 VNode 中都是函数,一个返回 VNode 的函数。

TIP

用过 React 的朋友,这让你想起 Render Prop 了吗!

# key 和 ref

key 就像 VNode 的唯一标识,用于 diff 算法的优化,它可以是数字也可以是字符串:

{
  flags: VNodeFlags.ELEMENT_HTML,
  tag: 'li',
  key: 'li_0'
}
@前端进阶之旅: 代码已经复制到剪贴板

ref 的设计是为了提供一种能够拿到真实DOM的方式,当然如果将 ref 应用到组件上,那么拿到的就是组件实例,我们通常会把 ref 设计成一个函数,假设我们有如下模板:

<div :ref="el => elRef = el"></div>
@前端进阶之旅: 代码已经复制到剪贴板

我们可以把这段模板编译为如下 VNode:

const elementVNode = {
  flags: VNodeFlags.ELEMENT_HTML,
  tag: 'div',
  ref: el => elRef = el
}
@前端进阶之旅: 代码已经复制到剪贴板

在使用 mountElement 函数挂载如上 VNode 时,可以轻松的实现 ref 功能:

function mountElement(vnode, container) {
  const el = document.createElement(vnode.tag)
  container.appendChild(el)
  vnode.ref && vnode.ref(el)
}
@前端进阶之旅: 代码已经复制到剪贴板

如果挂载的是组件而非普通标签,那么只需要将组件实例传递给 vnode.ref 函数即可:

function mountComponent(vnode, container) {
  // 创建组件实例
  const instance = new vnode.tag()
  // 渲染
  instance.$vnode = instance.render()
  // 挂载
  mountElement(instance.$vnode, container)

  vnode.ref && vnode.ref(instance)
}
@前端进阶之旅: 代码已经复制到剪贴板

# parentVNode 以及它的作用

与 VNode 的 slots 属性相同,parentVNode 属性也是给组件的 VNode 准备的,组件的 VNode 为什么需要这两个属性呢?它俩的作用又是什么呢?想弄清楚这些,我们至少要先弄明白:一个组件所涉及的 VNode 都有哪些。什么意思呢?看如下模板思考一个问题:

<template>
  <div>
    <MyComponent />
  </div>
</template>
@前端进阶之旅: 代码已经复制到剪贴板

从这个模板来看 MyComponent 组件至少涉及到两个 VNode,第一个 VNode 是标签 <MyComponent /> 的描述,其次 MyComponent 组件本身也有要渲染的内容,这就是第二个 VNode。

  • 第一个 VNode 用来描述 <MyComponent /> 标签:
{
  // 省略...
  tag: MyComponent
}
@前端进阶之旅: 代码已经复制到剪贴板
  • 第二个 VNode 是组件渲染内容的描述,即组件的 render 函数产出的 VNode:
class MyComponent {
  render () {
    return {/* .. */} // 产出的 VNode
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

组件实例的 $vnode 属性值就是组件 render 函数产出的 VNode,这通过如下代码可以一目了然:

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

    • 从源码解读Vue生命周期
    • 组件的本质
    • 有状态组件的设计
      • key 和 ref
      • parentVNode 以及它的作用
      • contextVNode
      • el
    • 设计 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 官方说明 制定迁移与安全策略。

假设我们有如下模板:

<MyComponent>
  <div></div>
</MyComponent>
@前端进阶之旅: 代码已经复制到剪贴板

由这段模板可知,我们为 MyComponent 组件提供了一个空的 div 标签作为默认插槽内容,从DOM结构上看 <MyComponent> 标签有一个 div 标签作为子节点,通常我们可以将其编译为如下 VNode:

const compVNode = {
  flags: VNodeFlags.COMPONENT_STATEFUL_NORMAL,
  tag: MyComponent,
  children: {
    flags: VNodeFlags.ELEMENT,
    tag: 'div'
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

这其实没什么问题,但是我们更倾向于新建一个 slots 属性来存储这些子节点,这在语义上更加贴切,所以我们希望将模板编译为如下 VNode:

const compVNode = {
  flags: VNodeFlags.COMPONENT,
  tag: MyComponent,
  children: null,
  slots: {
    // 默认插槽
    default: {
      flags: VNodeFlags.ELEMENT,
      tag: 'div'
    }
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

可以看到,如上 VNode 的 children 属性值为 null。当我们使用 mountComponent 函数挂载如上 VNode 时,我们可以在组件实例化之后并且在组件的渲染函数执行之前将 compVNode.slots 添加到组件实例对象上,这样当组件的渲染函数执行的时候,就可以访问插槽数据:

function mountComponent(vnode, container) {
  // 创建组件实例
  const instance = new vnode.tag()

  // 设置 slots
  instance.$slots = vnode.slots

  // 渲染
  instance.$vnode = instance.render()
  // 挂载
  mountElement(instance.$vnode, container)

  vnode.ref && vnode.ref(instance)
}
@前端进阶之旅: 代码已经复制到剪贴板

在 MyComponent 组件的 render 函数内,我们就可以通过组件实例访问 slots 数据:

class MyComponent {
  render() {
    return {
      flags: VNodeFlags.ELEMENT,
      tag: 'h1'
      children: this.$slots.default
    }
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

实际上,这就是普通插槽的实现原理,至于作用域插槽(scopedSlots),与普通插槽并没有什么本质的区别,我们知道作用域插槽可以访问子组件的数据,在实现上来看其实就是函数传参:

class MyComponent {
  render() {
    return {
      flags: VNodeFlags.ELEMENT,
      tag: 'h1'
      // 插槽变成了函数,可以传递参数
      children: this.$slots.default(1)
    }
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

如上代码所示,只要 this.$slots.default 是函数即可实现,所以在模板编译时,我们最终需要得到如下 VNode:

const compVNode = {
  flags: VNodeFlags.COMPONENT,
  tag: MyComponent,
  children: null,
  slots: {
    // 作用域插槽,可以接受组件传递过来的数据
    default: (arg) => {
      const tag = arg === 1 ? 'div' : 'h1'
      return {
        flags: VNodeFlags.ELEMENT,
        tag
      }
    }
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

现在你应该明白为什么普通插槽和作用域插槽本质上并没有区别了,因为普通插槽也可以是函数,只是不接收参数罢了。这么看的话其实普通插槽是作用域插槽的子集,那为什么不将它们合并呢?没错从 Vue2.6 起已经将之合并,所有插槽在 VNode 中都是函数,一个返回 VNode 的函数。

TIP

用过 React 的朋友,这让你想起 Render Prop 了吗!

# key 和 ref

key 就像 VNode 的唯一标识,用于 diff 算法的优化,它可以是数字也可以是字符串:

{
  flags: VNodeFlags.ELEMENT_HTML,
  tag: 'li',
  key: 'li_0'
}
@前端进阶之旅: 代码已经复制到剪贴板

ref 的设计是为了提供一种能够拿到真实DOM的方式,当然如果将 ref 应用到组件上,那么拿到的就是组件实例,我们通常会把 ref 设计成一个函数,假设我们有如下模板:

<div :ref="el => elRef = el"></div>
@前端进阶之旅: 代码已经复制到剪贴板

我们可以把这段模板编译为如下 VNode:

const elementVNode = {
  flags: VNodeFlags.ELEMENT_HTML,
  tag: 'div',
  ref: el => elRef = el
}
@前端进阶之旅: 代码已经复制到剪贴板

在使用 mountElement 函数挂载如上 VNode 时,可以轻松的实现 ref 功能:

function mountElement(vnode, container) {
  const el = document.createElement(vnode.tag)
  container.appendChild(el)
  vnode.ref && vnode.ref(el)
}
@前端进阶之旅: 代码已经复制到剪贴板

如果挂载的是组件而非普通标签,那么只需要将组件实例传递给 vnode.ref 函数即可:

function mountComponent(vnode, container) {
  // 创建组件实例
  const instance = new vnode.tag()
  // 渲染
  instance.$vnode = instance.render()
  // 挂载
  mountElement(instance.$vnode, container)

  vnode.ref && vnode.ref(instance)
}
@前端进阶之旅: 代码已经复制到剪贴板

# parentVNode 以及它的作用

与 VNode 的 slots 属性相同,parentVNode 属性也是给组件的 VNode 准备的,组件的 VNode 为什么需要这两个属性呢?它俩的作用又是什么呢?想弄清楚这些,我们至少要先弄明白:一个组件所涉及的 VNode 都有哪些。什么意思呢?看如下模板思考一个问题:

<template>
  <div>
    <MyComponent />
  </div>
</template>
@前端进阶之旅: 代码已经复制到剪贴板

从这个模板来看 MyComponent 组件至少涉及到两个 VNode,第一个 VNode 是标签 <MyComponent /> 的描述,其次 MyComponent 组件本身也有要渲染的内容,这就是第二个 VNode。

  • 第一个 VNode 用来描述 <MyComponent /> 标签:
{
  // 省略...
  tag: MyComponent
}
@前端进阶之旅: 代码已经复制到剪贴板
  • 第二个 VNode 是组件渲染内容的描述,即组件的 render 函数产出的 VNode:
class MyComponent {
  render () {
    return {/* .. */} // 产出的 VNode
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

组件实例的 $vnode 属性值就是组件 render 函数产出的 VNode,这通过如下代码可以一目了然:

function mountComponent(vnode, container) {
  // 创建组件实例
  const instance = new vnode.tag()
  // 渲染,$vnode 的值就是组件 render 函数产出的 VNode
  instance.$vnode = instance.render()
  // 挂载
  mountElement(instance.$vnode, container)

  vnode.ref && vnode.ref(instance)
}
@前端进阶之旅: 代码已经复制到剪贴板

而 instance.$vnode.parentVNode 的值就是用来描述组件(如:<MyComponent />)标签的 VNode,我们只需在如上代码中添加一行代码即可实现:

function mountComponent(vnode, container) {
  // 创建组件实例
  const instance = new vnode.tag()
  // 渲染,$vnode 的值就是组件 render 函数产出的 VNode
  instance.$vnode = instance.render()
  // vnode 就是用来描述组件标签的 VNode
  instance.$vnode.parentVNode = vnode
  // 挂载
  mountElement(instance.$vnode, container)

  vnode.ref && vnode.ref(instance)
}
@前端进阶之旅: 代码已经复制到剪贴板

同时我们也可以在组件实例上添加 $parentVNode 属性,让其同样引用组件的标签描述:

function mountComponent(vnode, container) {
  // 创建组件实例
  const instance = new vnode.tag()
  // 渲染,$vnode 的值就是组件 render 函数产出的 VNode
  instance.$vnode = instance.render()
  // vnode 就是用来描述组件标签的 VNode
  instance.$parentVNode = instance.$vnode.parentVNode = vnode
  // 挂载
  mountElement(instance.$vnode, container)

  vnode.ref && vnode.ref(instance)
}
@前端进阶之旅: 代码已经复制到剪贴板

组件的实例为什么需要引用 parentVNode 呢?这是因为组件的事件监听器都在 parentVNode 上,如下模板所示:

← 组件的本质设计 VNode →

fe