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
编译优化
- PatchFlags
界面中动态的部分内容做标记, 在更新的时候只更新和diff这部分, 减少diff的比较成本
- Block
“根”节点, 会作为block
- 静态提升
渲染过程中, 如果节点并没有动态绑定, 可以将静态节点提升到渲染函数之外, 避免每次渲染的时候重复执行
- 预字符串化
是一种基于静态提升的优化策略, 静态节点可以直接序列化成字符串, 通过innerHTML来填入, 以减少性能损耗. 对大块静态内容具有很大性能优势
- 缓存内联事件处理函数
内联函数每次render更新的时候, 就会重新声明一次, 通过缓存的方式避免其每次重新声明
SSR
- @vue/server-renderer createSSRApp 渲染产生静态html
- createApp 打包出client.js 用来在客户端执行一次以对完成渲染的静态html绑定事件等
- vuex等的脱水, 全局对象注入到静态html
- 界面渲染时注水, 将全局数据再注入回vue中
React的SSR
- react-dom/server产生html字符串
- react-dom hydrate方法产生客户端js逻辑文件, 用于挂载事件等, 同时绑定原生dom与虚拟dom的关系
- 通过staticContext来完成注水