Vuejs设计与实现读书笔记-组件化
2022-7-13
| 2023-2-19
0  |  0 分钟
password
Created
Feb 19, 2023 04:50 PM
type
Post
status
Published
date
Jul 13, 2022
slug
summary
Vuejs设计与实现读书笔记-组件化
tags
Vue源码
Vue
category
Vue
icon

组件实现机制

假如我们有这样的一个组件
const MyComponent = { name: 'MyComponent', data() { return { foo: 'hello world' } }, render() { return { type: 'div', children: `foo value: ${this.foo}` } } }
为了渲染这个组件, 我们声明了一个mountComponent方法, 如下
function mountComponent(vnode, container, anchor) { const componentOptions = vnode.type const { render, data } = componentOptions const state = reactive(data()) // 保证数据变化的时候能够重新刷新UI, 所以使用effect包裹 effect(() => { const subTree = render.call(state, state) patch(null, subTree, container, anchor) }, { // 通过调度来合并state, 一起更新以提升性能 // queueJob在响应式中有提及, 不再赘述 scheduler: queueJob }) }
上面的组件mount函数解决了组件更新的问题
通过effect副作用函数来触发组件的更新
同时, 通过调度器来调度整个的数据更新流程, 达到状态合并渲染的目的

组件的生命周期

当然, 上面的代码肯定是不全面的, 它没有区分更新还是新挂载, 同时, 也没有给组件挂上对应的生命周期函数
在这里, 我们为组件加上一个isMounted布尔值, 代表组件是否被挂载
同时, 在对应的位置为组件挂载上对应的生命周期
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 }) }
需要注意的是, 实际场景下setup的执行时机在beforeCreate之前
 
对应的, 生命周期是使用的vue暴露对象声明的, 但是使用是在setup中, 那么生命周期执行的时候怎么获得当前组件的信息呢?
生命周期执行过程中, 有一个全局变量参与其中, 用于存储当前被初始化的组件实例
let currentInstance = null function setCurrentInstance(instance) { currentInstance = instance }
对应执行的过程中, 我们在setup执行前初始化instance就好了
setCurrentInstance(instance) const setupResult = setup(shallowReadonly(instance.props), context) setCurrentInstance(null)
对应在具体的生命周期中, 我们维护一个数组, 生命的生命周期做入栈处理
function onMounted(fn) { if (currentInstance) { currentInstance.mounted.push(fn) } else { console.error('onMounted函数只能在setup中调用') } }

组件的props

组件的props如果是静态值, 则传给子组件的也是一个静态值, 如果是一个bind的值, 那么传递给子组件的是一个变量
如果子组件没有声明此值, 会被放在attrs中
最后, 会将解析出的 props 数据包装为 shallowReactive 并定义到子组件实例上

setup函数

setup会在beforeCreate生命周期前被执行, 如果执行的结果是一个函数, 则直接作为render渲染函数
如果结果是一个对象, 则会赋值给setupResult
const setupContext = { attrs } // 调用 setup 函数,将只读版本的 props 作为第一个参数传递,避免用户意外地修改 props 的值, // 将 setupContext 作为第二个参数传递 const setupResult = setup(shallowReadonly(instance.props),setupContext) // setupState 用来存储由 setup 返回的数据 let setupState = null // 如果 setup 函数的返回值是函数,则将其作为渲染函数 if (typeof setupResult === 'function') { // 报告冲突 if (render) console.error('setup 函数返回渲染函数,render 选项将 被忽略') // 将 setupResult 作为渲染函数 render = setupResult } else { // 如果 setup 的返回值不是函数,则作为数据状态赋值给 setupState setupState = setupResult }
 

emit

emit主要是用来在子组件上调用父组件传入的自定义函数
emit会被挂载在setup函数的第二个入参context上
// 定义 emit 函数,它接收两个参数 // event: 事件名称 // payload: 传递给事件处理函数的参数 function emit(event, ...payload) { // 根据约定对事件名称进行处理,例如 change --> onChange const eventName = `on${event[0].toUpperCase() + event.slice(1)}` // 根据处理后的事件名称去 props 中寻找对应的事件处理函数 const handler = instance.props[eventName] if (handler) { // 调用事件处理函数并传递参数 handler(...payload) } else { console.error('事件不存在') } } // 将 emit 函数添加到 setupContext 中,用户可以通过 setupContext 取 得 emit 函数 const setupContext = { attrs, emit }
实现机制很简单, 就是父组件的绑定自定义函数会被编译成onXxx这样的格式, 子组件上将事件名也转成这个格式, 调用即可
 

Slot

模板编译的时候, 会将插槽的节点便以为渲染函数
<MyComponent> <template #body> <secion>我是内容</section> </template> </MyComponent>
会被编译为:
function render() { return { type: MyComponent, children: { body() { return { type: 'section', children: '我是内容' } } } } }
对应的, 声明slot的组件会被编译为
function render() { return [{ type: 'body', children: [this.$slots.body()] }] }
 

内建组件

内建组件与渲染器的核心逻辑连接紧密

keep-alive

当我们卸载一个被KeepAlive的组件时, 它并不会真的背卸载, 而是会被移动到一个隐藏容器中.
当重新挂载该组件的时候, 它也不会真的被挂载, 而是会被从隐藏容器中取出, 再放回原来的容器中
 
keep-alive是有最大值的, 其缓存的修剪策略基于最新一次访问来进行修剪, 也就是会修剪掉最久没访问的那一个缓存
 

Teleport

 

Transition

 
 

编译优化

 
  1. PatchFlags
界面中动态的部分内容做标记, 在更新的时候只更新和diff这部分, 减少diff的比较成本
 
  1. Block
“根”节点, 会作为block
 
  1. 静态提升
渲染过程中, 如果节点并没有动态绑定, 可以将静态节点提升到渲染函数之外, 避免每次渲染的时候重复执行
 
  1. 预字符串化
是一种基于静态提升的优化策略, 静态节点可以直接序列化成字符串, 通过innerHTML来填入, 以减少性能损耗. 对大块静态内容具有很大性能优势
 
  1. 缓存内联事件处理函数
内联函数每次render更新的时候, 就会重新声明一次, 通过缓存的方式避免其每次重新声明
 

SSR

  1. @vue/server-renderer createSSRApp 渲染产生静态html
  1. createApp 打包出client.js 用来在客户端执行一次以对完成渲染的静态html绑定事件等
  1. vuex等的脱水, 全局对象注入到静态html
  1. 界面渲染时注水, 将全局数据再注入回vue中
 
React的SSR
  1. react-dom/server产生html字符串
  1. react-dom hydrate方法产生客户端js逻辑文件, 用于挂载事件等, 同时绑定原生dom与虚拟dom的关系
  1. 通过staticContext来完成注水
Vue
  • Vue源码
  • Vue
  • Go HelloWorldVuejs设计与实现读书笔记-渲染机制
    目录