《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 代 ...
随机推荐
- SSH Exporter:基于Prometheus的远程系统性能监控神器
SSH Exporter English | 中文 介绍 SSH Exporter 是一个基于 Prometheus 规范的监控工具,通过 SSH 协议远程收集目标服务器的系统性能数据,如 CPU 使 ...
- 8、SpringBoot2之打包及运行
为了演示高级启动时动态配置参数的使用,本文在SpringBoot2之配置文件的基础上进行 8.1.概述 普通的 web 项目,会被打成一个 war 包,然后再将 war 包放到 tomcat 的 we ...
- 【Vue】Re14 Router 第一部分(入门案例)
一.箭头函数(Lambda匿名函数) <!DOCTYPE html> <html lang="en"> <head> <meta char ...
- python数据分析与可视化基础
一.数据分析介绍:1.数据分析含义:数据分析是指用适当的统计分析方法对收集来的大量数据进行分析,将它们加以汇总和理解并消化,以求最大化地开发数据的功能,发挥数据的作用.数据分析是为了提取有用信息和形成 ...
- 对国内的人行机器人(humanoid)的一些技术类的提问?
贵公司产品在机器人仿真时具体采用的仿真软件(NVIDIA家的.开源的webot,等等)是哪款,如果没有使用NVIDIA全家桶那么其原因是什么(如:技术依赖没有独立技术.技术栈太过于复杂暂时没有精力和能 ...
- 使用CPU运行大语言模型(LLM),以清华开源大模型ChatGLM3为例:无需显卡!用CPU搞定大模型运行部署!【详细手把手演示】
教程视频地址: 无需显卡!用CPU搞定大模型运行部署![详细手把手演示] 按照上面视频进行安装配置之前需要注意,python编程环境需要大于等于python3.10,否则会运行报错.下载好GitHub ...
- Jax框架支持的python和numpy版本
官方: https://jax.readthedocs.io/en/latest/deprecation.html 每个推出的JAX版本都会支持45个月内推出的python,对此具体解释一下: 比如J ...
- 一个简单的例子测试numpy和Jax的性能对比 (续)
相关: 一个简单的例子测试numpy和Jax的性能对比 numpy代码: import numpy as np import time x = np.random.random([10000, 100 ...
- vue开发者工具dev-tool的安装
1.下载网址:https://github.com/Redxym/dev-tools 2.谷歌浏览器->扩展程序,拖动刚刚下载好的压缩包,将chrome文件夹添加至'加载已解压的扩展程序' // ...
- 粉丝提问|c语言:如何定义一个和库函数名一样的函数,并在函数中调用该库函数
问题描述: 某个函数fun_1()是在lib内,没法修改的,在程序中大量的使用了该函数,现在想把原本fun_1失效(现在失效的方法是#define fun_1(..)),用另外一个函数fun_2(), ...