第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 的返回值有两种情况

  1. 返回一个函数 作为组件的 render 函数
  2. 返回一个对象,该对象中包含的数据将暴露给模板使用

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章、组件的实现原理的更多相关文章

  1. 【vue.js权威指南】读书笔记(第一章)

    最近在读新书<vue.js权威指南>,一边读,一边把笔记整理下来,方便自己以后温故知新,也希望能把自己的读书心得分享给大家. [第1章:遇见vue.js] vue.js是什么? vue.j ...

  2. 【vue.js权威指南】读书笔记(第二章)

    [第2章:数据绑定] 何为数据绑定?答曰:数据绑定就是将数据和视图相关联,当数据发生变化的时候,可以自动的来更新视图. 数据绑定的语法主要分为以下几个部分: 文本插值:文本插值可以说是最基本的形式了. ...

  3. INSPIRED启示录 读书笔记 - 第12章 产品探索

    软件项目可以划分为两个阶段 探索产品阶段:弄清楚要开发什么产品(定义正确的产品) 在探索产品的阶段,产品经理负责分析各种创意,广泛收集用户需求,了解如何运用新技术,拿出产品原型并加以测试 从全局视角思 ...

  4. 《C++ Primer 4th》读书笔记 第12章-类

    原创文章,转载请注明出处:http://www.cnblogs.com/DayByDay/p/3936473.html

  5. C++ primer plus读书笔记——第12章 类和动态内存分配

    第12章 类和动态内存分配 1. 静态数据成员在类声明中声明,在包含类方法的文件中初始化.初始化时使用作用域运算符来指出静态成员所属的类.但如果静态成员是整形或枚举型const,则可以在类声明中初始化 ...

  6. Linux内核设计与实现 读书笔记 转

    Linux内核设计与实现  读书笔记: http://www.cnblogs.com/wang_yb/tag/linux-kernel/ <深入理解LINUX内存管理> http://bl ...

  7. 【2018.08.13 C与C++基础】C++语言的设计与演化读书笔记

    先占坑 老实说看这本书的时候,有很多地方都很迷糊,但却说不清楚问题到底在哪里,只能和Effective C++联系起来,更深层次的东西就想不到了. 链接: https://blog.csdn.net/ ...

  8. 《Linux内核设计与实现》第八周读书笔记——第四章 进程调度

    <Linux内核设计与实现>第八周读书笔记——第四章 进程调度 第4章 进程调度35 调度程序负责决定将哪个进程投入运行,何时运行以及运行多长时间,进程调度程序可看做在可运行态进程之间分配 ...

  9. 《Linux内核设计与分析》第六周读书笔记——第三章

    <Linux内核设计与实现>第六周读书笔记——第三章 20135301张忻估算学习时间:共2.5小时读书:2.0代码:0作业:0博客:0.5实际学习时间:共3.0小时读书:2.0代码:0作 ...

  10. 《LINUX内核设计与实现》第三周读书笔记——第一二章

    <Linux内核设计与实现>读书笔记--第一二章 20135301张忻 估算学习时间:共2小时 读书:1.5 代码:0 作业:0 博客:0.5 实际学习时间:共2.5小时 读书:2.0 代 ...

随机推荐

  1. SSH Exporter:基于Prometheus的远程系统性能监控神器

    SSH Exporter English | 中文 介绍 SSH Exporter 是一个基于 Prometheus 规范的监控工具,通过 SSH 协议远程收集目标服务器的系统性能数据,如 CPU 使 ...

  2. 8、SpringBoot2之打包及运行

    为了演示高级启动时动态配置参数的使用,本文在SpringBoot2之配置文件的基础上进行 8.1.概述 普通的 web 项目,会被打成一个 war 包,然后再将 war 包放到 tomcat 的 we ...

  3. 【Vue】Re14 Router 第一部分(入门案例)

    一.箭头函数(Lambda匿名函数) <!DOCTYPE html> <html lang="en"> <head> <meta char ...

  4. python数据分析与可视化基础

    一.数据分析介绍:1.数据分析含义:数据分析是指用适当的统计分析方法对收集来的大量数据进行分析,将它们加以汇总和理解并消化,以求最大化地开发数据的功能,发挥数据的作用.数据分析是为了提取有用信息和形成 ...

  5. 对国内的人行机器人(humanoid)的一些技术类的提问?

    贵公司产品在机器人仿真时具体采用的仿真软件(NVIDIA家的.开源的webot,等等)是哪款,如果没有使用NVIDIA全家桶那么其原因是什么(如:技术依赖没有独立技术.技术栈太过于复杂暂时没有精力和能 ...

  6. 使用CPU运行大语言模型(LLM),以清华开源大模型ChatGLM3为例:无需显卡!用CPU搞定大模型运行部署!【详细手把手演示】

    教程视频地址: 无需显卡!用CPU搞定大模型运行部署![详细手把手演示] 按照上面视频进行安装配置之前需要注意,python编程环境需要大于等于python3.10,否则会运行报错.下载好GitHub ...

  7. Jax框架支持的python和numpy版本

    官方: https://jax.readthedocs.io/en/latest/deprecation.html 每个推出的JAX版本都会支持45个月内推出的python,对此具体解释一下: 比如J ...

  8. 一个简单的例子测试numpy和Jax的性能对比 (续)

    相关: 一个简单的例子测试numpy和Jax的性能对比 numpy代码: import numpy as np import time x = np.random.random([10000, 100 ...

  9. vue开发者工具dev-tool的安装

    1.下载网址:https://github.com/Redxym/dev-tools 2.谷歌浏览器->扩展程序,拖动刚刚下载好的压缩包,将chrome文件夹添加至'加载已解压的扩展程序' // ...

  10. 粉丝提问|c语言:如何定义一个和库函数名一样的函数,并在函数中调用该库函数

    问题描述: 某个函数fun_1()是在lib内,没法修改的,在程序中大量的使用了该函数,现在想把原本fun_1失效(现在失效的方法是#define fun_1(..)),用另外一个函数fun_2(), ...