10.vue3中组件实现原理(上)

1.组件的概念

一句话总结:组件就是一组DOM元素的封装, 这组DOM元素就是组件要渲染的内容,如下面代码

js 复制代码
 // MyComponent 是一个组件,它的值是一个选项对象
 const MyComponent = {
   name: 'MyComponent',
   data() {
   return { foo: 1 }
   }
 }

如果从渲染器中来看,一个组件则是一个特殊类型的虚拟DOM节点,那我们对于文本,字符串都可以用string,Text等类型来描述,那么对于组件来说,我们一样可以用虚拟节点的vnode.type属性来存储组件的选项对象,例如:

js 复制代码
 // 该 vnode 用来描述组件,type 属性存储组件的选项对象
 const vnode = {
   type: MyComponent
   // ...
 }

为了让渲染器可以处理组件类型的虚拟节点,我们就可以再patch函数中对组件类型的虚拟节点进行处理,简单的示例:

js 复制代码
 function patch(n1, n2, container, anchor) {
 const { type } = n2
   if (typeof type === 'string') {
   // 作为普通元素处理
   } else if (type === Text) {
   // 作为文本节点处理
   } else if (type === Fragment) {
     // 作为片段处理
   } else if (typeof type === 'object') {
     // vnode.type 的值是选项对象,作为组件来处理
     if (!n1) {
       // 挂载组件
     mountComponent(n2, container, anchor)
     } else {
       // 更新组件2 patchComponent(n1, n2, anchor)
    }
 }

我们通过代码就可以很直观的理解到,组件就好像跟文本,元素使一样的,我们把组件当成一个对象来处理,但是对象中又包含文本和元素等节点,我们就用mountComponentpatchComponent来进行组件的更新和挂载。因此,一个组件必须包含一个渲染函数,那就是我们说的render函数,并且渲染函数返回值应该是虚拟DOM。换句话说,组件渲染函数就是用来描述组件所渲染内容的接口,如下代码:

js 复制代码
 const MyComponent = {
 // 组件名称,可选
   name: 'MyComponent',
   // 组件的渲染函数,其返回值必须为虚拟 DOM
   render() {
   // 返回虚拟 DOM
   return {
     type: 'div',
     children: `我是文本内容`
     }
   }
 }

这就是最简单的组件示例,那有了组件的结构后,渲染器就可以完成组件的渲染了,最终其实也是调用的就是mountComponent函数来进行渲染

js 复制代码
  const CompVNode = {
     type: MyComponent
  }
 // 调用渲染器来渲染组件
 renderer.render(CompVNode, document.querySelector('#app'))

function mountComponent(vnode, container, anchor) {
   // 通过 vnode 获取组件的选项对象,即 vnode.type
   const componentOptions = vnode.type
   // 获取组件的渲染函数 render
   const { render } = componentOptions
   // 执行渲染函数,获取组件要渲染的内容,即 render 函数返回的虚拟 DOM
   const subTree = render() // 最后调用 patch 函数来挂载组件所描述的内容,即 subTree
   patch(null, subTree, container, anchor)
 }

2.组件的状态和自更新

那我们有了组件,那每个组件里面的data,包括render函数中渲染的变量,都是有状态的,当我们data实现了变化的时候,那我们组件也应该对应的更新,那我们就想,怎么样,才能让组件实现更新,其实很简单,我们只要将渲染任务包装到effect中,实现响应收集就好了,思路如下:

我们以简单的组件为例子:

js 复制代码
 const MyComponent = {
   name: 'MyComponent',
   // 用 data 函数来定义组件自身的状态
   data() {
     return {
       foo: 'hello world'
     }
   },
   render() {
     return {
       type: 'div',
       children: `foo 的值是: ${this.foo}` // 在渲染函数内使用组件状态
     }
   }
 }

那我们就是要再mountComponent函数中,实现以来收集

js 复制代码
 function mountComponent(vnode, container, anchor) {
   const componentOptions = vnode.type
   const { render, data } = componentOptions
   const state = reactive(data())
   // 将组件的 render 函数调用包装到 effect 内
   effect(() => {
     const subTree = render.call(state, state)
     patch(null, subTree, container, anchor)
     })
 }

这样,一旦组件的数据进行了变化,那组件就会完成更新,那这样就没有问题了嘛?我们知道effect的执行是同步的,当我们数据多次变化的时候,难道我们要多次执行渲染更新吗,这样是不是会造成性能浪费?那我们就想到,需要设计一个机制,就是响应式数据不管变化多少次,我们的副作用函数就执行一次,这就是我们常说的Vue的异步更新机制, 其实实现思路很简单,我们需要实现我们之前说的实现一个调度器,我们将它缓冲到一个微任务队列中,等到 缓冲到一个微任务队列中,等到执行栈清空后,再将它从微任务队列中取出并执行。有了缓存机制,我们就有机会对任务进行去重,从而避免多次执行副作用函数带来的性能开销。具体实现如下:执行栈清空后,再将它从微任务队列中取出并执行。有了缓存机制,我们就有机会对任务进行去重,从而避免多次执行副作用函数带来的性能开销。具体实现如下:

js 复制代码
 // 任务缓存队列,用一个 Set 数据结构来表示,这样就可以自动对任务进行去重
 const queue = new Set()
   // 一个标志,代表是否正在刷新任务队列
   let isFlushing = false
   // 创建一个立即 resolve 的 Promise 实例
   const p = Promise.resolve()
  
   // 调度器的主要函数,用来将一个任务添加到缓冲队列中,并开始刷新队列
   function queueJob(job) {
     // 将 job 添加到任务队列 queue 中
     queue.add(job)
     // 如果还没有开始刷新队列,则刷新之
     if (!isFlushing) {
     // 将该标志设置为 true 以避免重复刷新
     isFlushing = true
     // 在微任务中刷新缓冲队列
     p.then(() => {
     try {
     // 执行任务队列中的任务
       queue.forEach(job => job())
     } finally {
     // 重置状态
       isFlushing = false
       queue.clear = 0
     }
   })
  }
 }

本质上就是利用了微任务的异步执行机制, 那我们改写一下mountComponent函数的实现:

js 复制代码
 function mountComponent(vnode, container, anchor) {
  ---------
     effect(() => {
       const subTree = render.call(state, state)
       patch(null, subTree, container, anchor)
     }, {
       // 指定该副作用函数的调度器为 queueJob 即可
       scheduler: queueJob
     })
 }

3.组件的实例和生命周期

组件的实例本质上就是一个对象,它维护着组件运行过程中的所有信息,例如组件的生命周期,组件的渲染子树组件是否被挂载组件自身的状态(data),那我们就需要一个对象来描述上述的信息,简单示例如下:

js 复制代码
 function mountComponent(vnode, container, anchor) 
 // 定义组件实例,一个组件实例本质上就是一个对象,它包含与组件有关的状态
   const instance = {
   // 组件自身的状态数据,即 data
     state,
     // 一个布尔值,用来表示组件是否已经被挂载,初始值为 false
     isMounted: false,
     // 组件所渲染的内容,即子树(subTree)
     subTree:null
   }
    vnode.component = instance
}

有了组件的实例,我们就知道组件状态,是否被挂载等信息,那我们就可以根据状态去判断组件是挂载还是更新,如下示例:

js 复制代码
 function mountComponent(vnode, container, anchor) 
   ----- ----
  effect(() => {
     const subTree = render.call(state, state)
     if (!instance.isMounted) {  //判断是否挂在,没有挂载则挂载
       patch(null, subTree, container, anchor)
       instance.isMounted = true
     } else { //要不然就更新子树
       patch(instance.subTree, subTree, container, anchor)
     }
     instance.subTree = subTree
     }, { scheduler: queueJob })
}

那我们知道了组件的挂载和更新逻辑,那对于组件的生命周期钩子实现不就是很简单了嘛,我们只需要再对应的时候,加上回调函数,就实现了生命周期的钩子,完整代码:

生命周期钩子的实现

js 复制代码
 function mountComponent(vnode, container, anchor) {
   const componentOptions = vnode.type
   const { render, data, beforeCreate, created, beforeMount,
  mounted, beforeUpdate, updated } = componentOptions
  
   // 在这里调用 beforeCreate 钩子
   beforeCreate && beforeCreate()
   const state = reactive(data())
   const instance = {
     state,
     isMounted: false,
     subTree: null
   }
   vnode.component = instance
  
   // 在这里调用 created 钩子
   created && created.call(state)
  
   effect(() => {
     const subTree = render.call(state, state)
     if (!instance.isMounted) {
       // 在这里调用 beforeMount 钩子
       beforeMount && beforeMount.call(state)
       patch(null, subTree, container, anchor)
       instance.isMounted = true
       // 在这里调用 mounted 钩子
       mounted && mounted.call(state)
     } else {
       // 在这里调用 beforeUpdate 钩子
       beforeUpdate && beforeUpdate.call(state)
       patch(instance.subTree, subTree, container, anchor)
       // 在这里调用 updated 钩子
       updated && updated.call(state)
     }
       instance.subTree = subTree
   }, 
     { scheduler: queueJob })
 }

总结:

我们首先从组件的选项对象中取得注册到组件上的生命周期函数,然后在合适的时机调用它们,这其实就是组件生命周期的实现原理。但实际上,由于可能存在多个同样的组件生命周期钩子,例如来自 mixins 中的生命周期钩子函数,因此我们通常需要将组件生命周期钩子序列化为一个数组,但核心原理不变。

相关推荐
a濯1 小时前
element plus el-table多选框跨页多选保留
javascript·vue.js
蓝婷儿2 小时前
前端面试每日三题 - Day 32
前端·面试·职场和发展
星空寻流年3 小时前
CSS3(BFC)
前端·microsoft·css3
九月TTS3 小时前
开源分享:TTS-Web-Vue系列:Vue3实现固定顶部与吸顶模式组件
前端·vue.js·开源
CodeCraft Studio3 小时前
数据透视表控件DHTMLX Pivot v2.1发布,新增HTML 模板、增强样式等多个功能
前端·javascript·ui·甘特图
一把年纪学编程3 小时前
【牛马技巧】word统计每一段的字数接近“字数统计”
前端·数据库·word
llc的足迹3 小时前
el-menu 折叠后小箭头不会消失
前端·javascript·vue.js
九月TTS4 小时前
TTS-Web-Vue系列:移动端侧边栏与响应式布局深度优化
前端·javascript·vue.js
曾经的你d4 小时前
【electron+vue】常见功能之——调用打开/关闭系统软键盘,解决打包后键盘无法关闭问题
vue.js·electron·计算机外设
Johnstons4 小时前
AnaTraf:深度解析网络性能分析(NPM)
前端·网络·安全·web安全·npm·网络流量监控·网络流量分析