《Vue.js 设计与实现》读书笔记 - 第12章、组件的实现原理
第12章、组件的实现原理
12.1 渲染组件
在渲染器内部的实现看,一个组件是一个特殊类型的虚拟 DOM 节点。之前在 patch
我们判断了 VNode 的 type
值来处理,现在来处理类型为对象的情况。
// n1 旧node n2 新node container 容器
function patch(n1, n2, container, anchor) {
if (n1 && n1.type !== n2.type) {
unmount(n1)
n1 = null
}
const { type } = n2
if (typeof type === 'string') {
// ...
} else if (type === Text) {
// ...
} else if (type === Fragment) {
// ...
} else if (typeof type === 'object') {
// 组件
if (!n1) {
// 挂载
mountComponent(n2, container, anchor)
} else {
patchComponent(n1, n2, anchor)
}
}
}
其中 mountComponent
就是先通过组件的 render
函数获取对应的 vnode
然后再挂载。
function mountComponent(vnode, container, anchor) {
const componentOptinos = vnode.type
const { render } = componentOptinos
const subTree = render()
patch(null, subTree, container, anchor)
}
在渲染时使用组件类型:
const MyComponent = {
name: 'MyComponent',
render() {
return {
type: 'div',
children: 'Text',
}
},
}
const CompVNode = {
type: MyComponent,
}
renderer.render(CompVNode, document.querySelector('#app'))
12.2 组件状态与自更新
完成了组件的初始渲染,现在开始设计组件自身的状态。
我们在渲染时把组件的状态设置为响应式,并把渲染函数放在 effect
中执行,这样就实现了组件状态改变时重新渲染。同时指定 scheduler
来让渲染队列在一个微任务中执行并进行去重。
function mountComponent(vnode, container, anchor) {
const componentOptinos = vnode.type
const { render, data } = componentOptinos
const state = reactive(data()) // 让组件的数据变成响应式
// 为了让组件状态发生变化时能自动渲染
effect(
() => {
const subTree = render.call(state, state)
patch(null, subTree, container, anchor)
},
{
scheduler: queueJob,
}
)
}
const MyComponent = {
name: 'MyComponent',
data() {
return {
foo: 'hello world',
}
},
render() {
return {
type: 'div',
children: `foo = ${this.foo}`,
}
},
}
12.3 组件实例与组件的生命周期
当状态修改导致组件再次渲染时,patch
不应该还是挂载,所以我们需要维护一个实例,记录组件的状态,是否被挂载和上一次的虚拟 DOM 节点。
同时我们的组件有很多生命周期函数,我们需要在相应的时机调用对应的生命周期函数。
function mountComponent(vnode, container, anchor) {
const componentOptinos = vnode.type
const {
render,
data,
beforeCreate,
created,
beforeMount,
mounted,
beforeUpdate,
updated,
} = componentOptinos
beforeCreate && beforeCreate()
const state = reactive(data()) // 让组件的数据变成响应式
const instance = {
state,
isMounted: false,
subTree: null,
}
vnode.component = instance
created && created.call(state)
// 为了让组件状态发生变化时能自动渲染
effect(
() => {
const subTree = render.call(state, state)
if (!instance.isMounted) {
// 检测组件是否已经被挂载
beforeMount && beforeMount.call(state)
patch(null, subTree, container, anchor)
instance.isMounted = true
mounted && mounted.call(state)
} else {
beforeUpdate && beforeUpdate.call(state)
patch(instance.subTree, subTree, container, anchor)
updated && updated.call(state)
}
instance.subTree = subTree
},
{
scheduler: queueJob,
}
)
}
12.4 props 与组件的被动更新
在 Vue3 中要显示指定需要的属性,如果没有指定将会被存储到 attrs
对象中。
function mountComponent(vnode, container, anchor) {
const componentOptinos = vnode.type
const {
render,
data,
props: propsOption,
// ...
} = componentOptinos
beforeCreate && beforeCreate()
const state = reactive(data ? data() : {}) // 让组件的数据变成响应式
const [props, attrs] = resolveProps(propsOption, vnode.props)
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null,
}
// ...
}
function resolveProps(options, propsData) {
const props = {}
const attrs = {}
for (const key in propsData) {
if (key in options) {
props[key] = propsData[key]
} else {
attrs[key] = propsData[key]
}
}
return [props, attrs]
}
当父元素的数据发生变化时,父元素更新,导致子元素更新。在 patch
更新子元素时,由于存在旧节点,会调用 patchComponent
进行更新。在 patchComponent
中我们只需要更新组件属性。
function patchComponent(n1, n2, anchor) {
const instance = (n2.component = n1.component)
const { props } = instance
if (hasPropsChanged(n1.props, n2.props)) {
const [nextProps] = resolveProps(n2.type.props, n2.props)
for (const k in nextProps) {
props[k] = nextProps[k]
}
for (const k in props) {
if (!(k in nextProps)) delete props[k]
}
}
}
function hasPropsChanged(prevProps, nextProps) {
const nextKeys = Object.keys(nextProps)
if (nextKeys.length !== Object.keys(prevProps).length) {
return true
}
for (let i = 0; i < nextKeys.length; i++) {
const key = nextKeys[i]
if (nextProps[key] !== prevProps[key]) return true
}
return false
}
但是这样仅仅在示例保存了 props
并不能在渲染函数中访问他们,所以需要封装一个渲染上下文对象,生命周期函数和渲染函数都绑定该对象。
function mountComponent(vnode, container, anchor) {
// ...
vnode.component = instance
const renderContext = new Proxy(instance, {
get(t, k, r) {
const { state, props } = t
if (state && k in state) {
return state[k]
} else if (k in props) {
return props[k]
} else {
console.error('不存在')
}
},
set(t, k, r) {
const { state, props } = t
if (state && k in state) {
state[k] = v
} else if (k in props) {
console.warn('不可以设置props的值')
} else {
console.error('不存在')
}
return true
},
})
created && created.call(renderContext)
// 为了让组件状态发生变化时能自动渲染
effect(() => {
const subTree = render.call(renderContext, renderContext)
// ...
})
}
12.5 setup 函数的作用与实现
setup
的返回值有两种情况
- 返回一个函数 作为组件的
render
函数 - 返回一个对象,该对象中包含的数据将暴露给模板使用
setup
函数接受两个参数,第一个参数是 props
数据对象,第二个参数是 setupContext
对象。
const Comp = {
props: {
foo: String,
},
setup(props, setupContext) {
props.foo // 访问 props 属性值
// expose 用于显式地对外暴露组件数据
const { slots, emit, attrs, expose } = setupContext
},
}
接下来在 mountComponent
中实现 setup
。
function mountComponent(vnode, container, anchor) {
const componentOptions = vnode.type
let {
render,
data,
setup,
// ...
} = componentOptions
// ...
// 暂时只有 attrs
const setupContext = { attrs }
const setupResult = setup(shallowReactive(instance.props), setupContext)
let setupState = null
if (typeof setupResult === 'function') {
if (render) {
console.warn('setup 返回渲染函数,render选项将被忽略')
}
render = setupResult
} else {
setupState = setupResult
}
vnode.component = instance
const renderContext = new Proxy(instance, {
get(t, k, r) {
const { state, props } = t
if (state && k in state) {
return state[k]
} else if (k in props) {
return props[k]
} else if (setupState && k in setupState) {
return setupState[k]
} else {
console.error('不存在')
}
},
set(t, k, r) {
const { state, props } = t
if (state && k in state) {
state[k] = v
} else if (k in props) {
console.warn('不可以设置props的值')
} else if (setupState && k in setupState) {
setupState[k] = v
} else {
console.error('不存在')
}
return true
},
})
// ...
}
可以看到我们执行了 setup 并把结果放入了渲染上下文。
12.6 组件事件与 emit 的实现
emit
用来发射组件的自定义事件,本质上就是根据时间名称去 props
数据对象中寻找对用的事件处理函数并执行。
function mountComponent(vnode, container, anchor) {
// ...
function emit(event, ...payload) {
const eventName = `on${event[0].toUpperCase() + event.slice(1)}`
const handler = instance.props[eventName]
if (handler) {
handler(...payload)
} else {
console.warn(`${event} 事件不存在`)
}
}
const setupContext = { attrs, emit }
// ...
}
function resolveProps(options, propsData) {
const props = {}
const attrs = {}
for (const key in propsData) {
if (key in options || key.startsWith('on')) { // 事件不需要显示声明
props[key] = propsData[key]
} else {
attrs[key] = propsData[key]
}
}
return [props, attrs]
}
12.7 插槽的工作原理
在 vnode 中,插槽会被编译为渲染函数,如下:
// 子组件
const MyComponent = {
name: 'MyComponent',
render() {
return {
type: Fragment,
children: [
{
type: 'header',
children: [this.$slots.header()],
},
]
}
},
}
// 父组件
const vnode = {
type: MyComponent,
children: {
header() {
return {
type: 'h1',
children: '我是标题',
}
},
},
}
具体实现就是在子元素的渲染函数中,当他获取 $slots
的值,就把父元素传入的 children
返回。
function mountComponent(vnode, container, anchor) {
// ...
const slots = vnode.children || {}
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null,
slots,
}
// ...
const renderContext = new Proxy(instance, {
get(t, k, r) {
const { state, props, slots } = t
if (k === '$slots') return slots
// ...
},
// ...
})
// ...
}
12.8 注册生命周期
在 setup
中通过 onMounted
等钩子函数可以注册该组件的生命周期函数。但是 onMounted
是如何知道是哪个组件的生命周期?
原理也很简单,和开始的收集依赖有点像,就是在全局保存当前正在执行 setup
的组件实例。
// 全局变量 保存当前在执行 setup 的实例
let currentInstance = null
function setCurrentInstance(instance) {
currentInstance = instance
}
// 以 onMounted 举例 会把函数添加到组件的 mounted 属性内
function onMounted(fn) {
if (currentInstance) {
currentInstance.mounted.push(fn)
} else {
console.error('onMounted 函数只能在 setup 中调用')
}
}
function mountComponent(vnode, container, anchor) {
// ...
const instance = {
// ...
// 在组件实例中添加 mounted 数组,用来存储通过 onMounted 函数注册的生命周期函数
mounted: [],
}
// ...
let setupState = null
if (setup) {
const setupContext = { attrs, emit, slots }
setCurrentInstance(instance)
const setupResult = setup(shallowReactive(instance.props), setupContext)
setCurrentInstance(null)
// ...
}
// ...
effect(
() => {
const subTree = render.call(renderContext, renderContext)
if (!instance.isMounted) {
// ...
// 挂载时执行 instance.mounted 中添加的钩子函数
instance.mounted &&
instance.mounted.forEach((hook) => hook.call(renderContext))
} else {
// ...
}
instance.subTree = subTree
},
{
scheduler: queueJob,
}
)
}
《Vue.js 设计与实现》读书笔记 - 第12章、组件的实现原理的更多相关文章
- 【vue.js权威指南】读书笔记(第一章)
最近在读新书<vue.js权威指南>,一边读,一边把笔记整理下来,方便自己以后温故知新,也希望能把自己的读书心得分享给大家. [第1章:遇见vue.js] vue.js是什么? vue.j ...
- 【vue.js权威指南】读书笔记(第二章)
[第2章:数据绑定] 何为数据绑定?答曰:数据绑定就是将数据和视图相关联,当数据发生变化的时候,可以自动的来更新视图. 数据绑定的语法主要分为以下几个部分: 文本插值:文本插值可以说是最基本的形式了. ...
- INSPIRED启示录 读书笔记 - 第12章 产品探索
软件项目可以划分为两个阶段 探索产品阶段:弄清楚要开发什么产品(定义正确的产品) 在探索产品的阶段,产品经理负责分析各种创意,广泛收集用户需求,了解如何运用新技术,拿出产品原型并加以测试 从全局视角思 ...
- 《C++ Primer 4th》读书笔记 第12章-类
原创文章,转载请注明出处:http://www.cnblogs.com/DayByDay/p/3936473.html
- C++ primer plus读书笔记——第12章 类和动态内存分配
第12章 类和动态内存分配 1. 静态数据成员在类声明中声明,在包含类方法的文件中初始化.初始化时使用作用域运算符来指出静态成员所属的类.但如果静态成员是整形或枚举型const,则可以在类声明中初始化 ...
- Linux内核设计与实现 读书笔记 转
Linux内核设计与实现 读书笔记: http://www.cnblogs.com/wang_yb/tag/linux-kernel/ <深入理解LINUX内存管理> http://bl ...
- 【2018.08.13 C与C++基础】C++语言的设计与演化读书笔记
先占坑 老实说看这本书的时候,有很多地方都很迷糊,但却说不清楚问题到底在哪里,只能和Effective C++联系起来,更深层次的东西就想不到了. 链接: https://blog.csdn.net/ ...
- 《Linux内核设计与实现》第八周读书笔记——第四章 进程调度
<Linux内核设计与实现>第八周读书笔记——第四章 进程调度 第4章 进程调度35 调度程序负责决定将哪个进程投入运行,何时运行以及运行多长时间,进程调度程序可看做在可运行态进程之间分配 ...
- 《Linux内核设计与分析》第六周读书笔记——第三章
<Linux内核设计与实现>第六周读书笔记——第三章 20135301张忻估算学习时间:共2.5小时读书:2.0代码:0作业:0博客:0.5实际学习时间:共3.0小时读书:2.0代码:0作 ...
- 《LINUX内核设计与实现》第三周读书笔记——第一二章
<Linux内核设计与实现>读书笔记--第一二章 20135301张忻 估算学习时间:共2小时 读书:1.5 代码:0 作业:0 博客:0.5 实际学习时间:共2.5小时 读书:2.0 代 ...
随机推荐
- C# 网络编程:.NET 开发者的核心技能
前言 数字化时代,网络编程已成为软件开发中不可或缺的一环,尤其对于 .NET 开发者而言,掌握 C# 中的网络编程技巧是迈向更高层次的必经之路.无论是构建高性能的 Web 应用,还是实现复杂的分布式系 ...
- c++ 关于返回值、将亡值的调用研究
c++11引入右值引用,而出现右值引用的有这几种:返回值(将亡值),常量. class Obj { public: Obj() { cout << "构造函数" < ...
- 【导出Excel】 JS的Excel导出库 Export2Excel
Export2Excel库默认放在ElementUI-Admin项目的src/vendor包中 不是通过package.json安装的依赖 这里直接贴库的源码: /* eslint-disable * ...
- 【DataBase】SQL优化案例:其一
原始SQL: 这里想做的事情就是查询一周的一个计算值 可以理解为报表的那种 主表 t_wechat_clue 生产库上200万数据量 然后需要联表一些限制条件 SELECT IFNULL(SUM((C ...
- 【IDEA】DEBUG调试问题
不要将断点打在方法的声明上: 会有一个菱形标志,在标记之后运行DEBUG模式会跑不起来 查看所有的断点标记: 在这里直接找到所有标记位置,弄掉就会跑起来了
- pytorch-a2c-ppo-acktr-gail 算法代码
地址: https://github.com/ikostrikov/pytorch-a2c-ppo-acktr-gail
- 如何查看华为的大模型(AI模型),华为官方的mindspore下的大模型???
由于华为官方的mindspore网站的设计比较反人性话,操作起来十分的复杂,因此如果想要在华为的官方网站上查找这个华为的官方大模型还是比较困难的,为此直接给出链接地址. PS. 要注意,华为的AI官方 ...
- baselines中环境包装器EpisodicLifeEnv的分析
如题: class EpisodicLifeEnv(gym.Wrapper): def __init__(self, env): """Make end-of-life ...
- 解密prompt系列35. 标准化Prompt进行时! DSPy论文串烧和代码示例
一晃24年已经过了一半,我们来重新看下大模型应用中最脆弱的一环Prompt Engineering有了哪些新的解决方案.这一章我们先看看大火的DSPy框架,会先梳理DSPy相关的几篇核心论文了解下框架 ...
- Apache SeaTunnel 及 Web 功能部署指南(小白版)
在大数据处理领域,Apache SeaTunnel 已成为一款备受青睐的开源数据集成平台,它不仅可以基于Apache Spark和Flink,而且还有社区单独开发专属数据集成的Zeta引擎,提供了强大 ...