【赠书】Preact(React)核心原理详解
豆皮粉儿们,又见面了,今天这一期,由字节跳动数据平台的“winge(宝丁)”,带大家见识见识前端“轮子”之一Preact框架。
提到Preact,你肯定会先想到React吧。React的出现给我们带来了全新的Web开发体验,其中也带来了许多新的概念:JSX、virtual-dom、组件化、合成事件等。当我们想从源码层面去研究它的原理时,庞大又晦涩难懂的源码就大大地提高了困难度。然而Preact与React有一样的API的同时,又相对代码简练,更容易学习和研究原理,我们可以通过学习Preact和研究它的原理,而增长新的姿势!
本文将分为以下几个重点内容介绍Preact,宝宝们,准备好了没:
Preact
是什么?Preact
和React
的区别有哪些?Preact是怎么工作的
JSX
VirtualDom
Preact
的VirtualDOM
diff算法PreactHooks
的实现一个组件的生命周期
一、Preact 是什么
简单而言, Preact
是 React
的3KB轻量级替代方案,它拥有着和 React
一样的API。有同学或许会问, Preact
中的 P
的含义是什么,根据 Preact
的作者表述的是 performance
的含义,这也是 Preact
框架的目标之一。
我们先来看用 Preact
编写的几个例子:
图1
图2
大家第一眼看上去,和 React
的写法基本上一致的,如果仔细的看,大家可能会几个疑问:
h
进行了变量的声明,但是没有使用,这个有什么意义?可以去掉么?表单里面使用的是
onInput
方法,而不是在React
中写的onChange
方法,这是为什么?
在这里我先不直接告诉大家答案,这些疑问会在下面的内容中一一为大家解答。
二、Preact 和 React 的区别有哪些?
Preact
号称打包后的体积只有3KB,自然相比 React
而言,在某些方面进行了精简,并且它本身的定位也不是准备重新实现一个 React
,所以两者之间肯定是存在一些区别。
我们在这里仅介绍两者最主要的区别:
事件系统
更符合
Dom
规范的描述
2.1 事件系统
通过一个例子,大家就能知道两者的区别
图3
在 React
内部,其自身实现了一套事件合成系统,所以我们一般在 React
的表单组件中使用的都是 onChange
方法来进行组件值的更新,而在 Preact
内部,没有事件合成系统,它直接使用的是由浏览器原生提供的事件系统,这也是为什么 Preact
在表单里面使用的是 onInput
方法,而不是在 React
中写的 onChange
方法。这也是它体积更小的直接原因之一。
2.2 更符合Dom规范的描述
在 React
中我们想描述一个 DOM
的类名,必须要使用 className
, 而在 Preact
中,不仅可以使用 className
来描述,也可以直接使用 class
来描述 DOM
的类名,这也使得 Preact
更接近原生 DOM
规范的描述。
当然除了这些, Preact
和 React
之间还有一些差别,由于它不是本文的重点,在这里我们就不一一展开介绍,大家可以直接通过Preact官网来进一步了解。
三、Preact是怎么工作的
在本节,我们将开始介绍 Preact
的内部工作流程,希望阅读本节过后,大家对 Preact
会有进一步的认识。
3.1 JSX
在介绍 JSX
之前,我们先想一下如何在 JS
中来描述 DOM
结构,很多同学可能会想,可以通过浏览器的操作 DOM
的API来完成,或者封装成一个工厂函数来进行接收一定的输入,输出就是相应的 DOM
图4
但是如果每次都需要通过这么复杂的方式来进行 DOM
结构的描述,想必 Preact
的性能再优秀,也不能进一步的进行推广。
这个时候,如果换一种图5这样的的方式,是不是大家就很熟悉
图5
没错,左侧其实就是我们平时写的 JSX
语法,经过 babel
或者其他的插件转换之后变成我们上面所说的函数式的描述,然后再经过一系列的处理,变成我们所熟悉的原生 DOM
的结构,这也是 JSX
产生的本质原因。
综合来看,其实 JSX
的本质就是 JS
的扩展,它允许你用类似 HTML/XML
的结构,进而编译成类似图6的一个函数调用。
图6
这个时候,我们就不得不提 babel
的强大之处了,原来从 JSX
转化到函数调用这个阶段是由 React
团队提供的, 后面因为 babel
做的更好,更强大,就逐渐演变成了 @babel/plugin-transform-react-jsx
这个核心插件了, 那么这个时候我们也可以揭开上文中提到的 h
函数的神秘面纱,正是因为在 Preact
中, JSX
的语法会通过 babel
这个插件转换成一个名称为 h
的工厂函数,类似于在 React
中的 React.createElement
的作用, 所以我们才需要去声明 h
函数,虽然我们在实际开发环境上用不到,但是它的作用是体现在 babel
转换后的代码中的, 大家也可以通过这个链接来体验 babel
的强大所在。
3.2 Virtual Dom
在本节当中,我们将会介绍 Preact
中的 VirtualDom
是什么?那么它和我们前面说的 JSX
之间有什么关联呢?
我们前面提到了 h
函数是一个工厂函数,输入我们知道了,是一些描述 DOM
结构的基本信息,那么它的输出是什么呢?我们可以通过下图来揭晓谜底。
图7
从图7我们可以看出,其实 h
函数的输出是一个特殊类型的数据结构,而 VirtualDOM
本质上就是一种用来描述 DOM
结构的数据结构,所以 h
函数的输出其实就是我们常说的 VirtualDOM
。
不管在 React
中还是在 Preact
中,最核心的都是 VirtualDom
的 diff
算法,怎么把最新的数据所驱动的 DOM
结构表现在页面当中,这个也是大家最关心的环节。
3.3 Preact 的 Virtual DOM 的 diff 算法
在 Preact
中, VirtualDOM
的 diff
算法可以拆解为三大块。
Diff children
Diff
这里的 type
指的是组件的类型,主要分成component
、Fragment
和dom node
三种。Diff props
接下来我们会分别仔细的介绍这三块
3.3.1 Diff children
图8
对比 children
差异主要有两个流程,首先我们先看左侧的流程图,在这个 diff
阶段,我们会先对新的 children
进行遍历,如果发现新的 child
可以在老的 children
中找到相同的 key
,那么会执行 diff<type>
这个阶段,如果没找到相同的 key
,会去看是不是相同的类型,比如是不是相同的 dom node
的类型,或者是相同的构造函数等,找到了的话 也会执行 diff<type>
这个阶段,如果没有找到,会把这个老的 child
放到一个数组当中。
新的 children
遍历完毕之后,我们会执行下一个流程,也就是右侧的流程图,会进行遍历没有使用的 old child
数组,将它们一一 unmount
掉,这个时候也会执行相应的生命周期。当这个 child
是一个父组件的话,会对它的 children
重复这个流程,直到全部 unmount
。
在这个阶段,我们也可以得到“写 key
是一个非常小但是却非常有用的性能优化手段”的结论,因为在一定的程度上它会有效的减少 diff
过程中所带来的性能损耗。
3.3.2 Diff
图9
Diff<type>
环节可以说是在整个 diff
算法中最重要的一个环节,也是最复杂的一个环节。首先我们会进行新的 vnode
判断它所属于的类型,目前来看,主要包括: Fragment
、 component
和 dom node
,其中当判断 vnode
的组件是一个空函数的时候表示的就是 Fragment
,而为非空函数的就是 component
类型。然后根据当前的 vnode
所属的类型进行下一步的处理。
当 type
为 Fragment
的时候,就直接会将 Fragment
内部的 children
进入到上文中提到的 diff children
阶段。
当 type
为 component
时,我们会先判断当前的 vnode
所代表的组件是否已经存在过,如果没有存在则执行 create
操作,同时也会执行相对应的生命周期,如果已经存在对应的组件,那么则会执行 update
操作,并且执行相对应的生命周期函数,在这里我们可以强调一下 shouldComponentUpdate
生命周期函数,当它返回 false
的时候,那么我们就不会再去执行下一步要执行的 render
函数,只有当该生命周期函数不存在或者返回非 false
的时候,我们会继续执行 render
函数,然后继续走该 Diff<type>
阶段。
当 type
为 dom node
时,我们首先会判断新老 vnode
是否为同一 node type
,如果不同,则会创建新的 dom
并且代替,如果相同,则会进行更新操作。
回过头来看 Diff<type>
环节,并且结合我们平时写组件的习惯,可以发现,最后我们写的组件都是原生的 dom
结构,所以最后都会进入到 diff dom node
这一流程中,也是在这一流程中,真正的去创建和更新 dom
。
3.3.3 Diff props
图10
我相信,大家可能会有点奇怪这一个阶段是做什么的?在上文中我们提到了当两个 dom node
节点类型相同的时候,会执行更新操作,那么该环节主要是为这个更新操作而服务。
它的原理很简单:先循环老的 dom
的 props
,如果它不在新的 dom
上,那么就会将它设为空,然后循环新的 props
,然后和老的 props
中相同的 prop
去做比较,然后设置最新的 prop
的值。
到这里,一个完整的 virtualdom
diff过程也就完成了,今天要介绍的 Preact
内部的工作原理部分也结束了。但是大家可能还比较难和一个真实的组件来相关联,接下来我们通过一个真实的组件,来将上面的过程进行串联,加深大家对它的理解。
四、结合实际组件了解整体渲染流程
首先,我们先编写一个如下图的 Clock
组件:
图11
接下来我们会通过两个阶段来介绍:
初次渲染
执行
setState
为了方便介绍,我在画了一个流程图,大家可以搭配图12的流程图和文字来看,方便大家更容易理解。
图12
4.1 初次渲染
入口函数为
render(<Clock/>,document.body)
,将
JSX
语法转化成h
函数的形式之后,也就是createElement
函数来创建一个用来描述子组件为Clock
组件的vitrual node
(下文简称为vnode
),类似于这种结构 { type: Fragment, children: [Clock], props: null }将该
vnode
,用数组包裹起来,然后送入到diff children
阶段当
diff children
阶段结束之后,会执行commitRoot
方法来执行挂载组件的componentDidMount
方法,内部主要是通过promise
或者setTimeout
来做有异步的处理。接下来我们主要来进行描述
diff children
的流程因为是第一次渲染,所以我们都没有老的
vnode
也就没有所谓的是否具有相同key
或者相同type
的新老vnode
直接进入到
diff(newChild,oldChild)
这一阶段判断我们的
vnode
的type
是一个component
,并且是一个新的组件,这个时候我们创建新组件,并且执行对应的生命周期,然后调用我们的render
函数因为
render
函数的返回值其实依然是一个vnode
,所以会继续流转到diff(newChild,oldChild)
这一个阶段,直到判断type
是dom node
时,会执行dom
的操作变化。
4.2 执行setState
我们可以从流程图中看到,其实
setState
本质上的操作,会将它所在的vnode
送入到diff(newChild,oldChild)
中,而newChild
和oldChild
的主要区别其实就是state
的变化因为
Clock
组件是一个component
类型的vnode
,所以我们会继续判断它是不是新组件,很显然已经不是了,于是会执行对应的生命周期,如果没有shouldComponentUpdate
生命周期函数或者返回了true
,那么我们会继续执行render
函数,不然我们会停止组件的渲染。这个时候
render
函数中,已经有了我们最新的state
了,那么对应的接下来会继续走diff(newChild,oldChild)
流程,直到将更改的state
值在真实的dom
结构中的props
中体现出来。
到这里,整个 Clock
组件的渲染过程就介绍完了,也希望大家通过这个例子,能够对 Preact
的底层工作原理有了更深的认识。
五、Preact hooks
hooks
是 Reactv16.8
版本中引入的新 API
, Preact
作为 React
的可代替方案,自然也会跟上这个变化,在 Preact
中, hooks
是作为一个单独的包引入的,包括注释总代码仅300行。
在 Preact
中, hooks
可以分为三类:
MemoHook
ReducerHook
EffectHook
接下来我们将通过这三类来介绍
5.1 MemoHook
MemoHook
的主要作用是用来做一些性能优化的 hook
集合。并且在 MemoHook
内部,有一个通用的数据结构,用来表示该 Hook
内部的数据结构。
5.1.1 useMemo
useMemo
的作用主要是:我们可以记住计算的结果,并且仅在其中一个依赖项发生更改时才重新计算它。
当我们每次进行渲染的时候,都会去执行 expensive
这个非常耗费性能的计算,这样下来,会造成一定的性能的损耗,那我们可以使用 useMemo
来进行优化。这样如果 expensive
依赖的值没有变化,就不需要执行这个函数,而是取它的缓存值。
其实它的内部原理很简单,我们可以通过下图通过它的源码进行分析
本质上就是进行前后比较它的依赖的数据是否发生了改变,如果发生了变化,则调用传入的 callback
函数,否则就直接返回原来的内部的 state
的值。
5.1.2 useCallback
作用:它可用于确保只要没有依赖项发生更改,返回的函数将始终保持引用相等。
用上图的例子来说明它的作用就是,当它的依赖项 a b
未发生变化的时候, onClick
这个函数始终是相同的。
实际上 useCallback(fn,deps)
和 useMemo(()=>fn,deps)
是等价的,因为 useCallback
就是用 useMemo
来实现的,只是它返回的是一个没有进行调用的 callback
,所以上图的代码可以等价于
即当 a b
不发生变化的时候, ()=>console.log(a,b)
也就不会发生变化。
5.1.3 useRef
作用:获得对功能组件内部的DOM节点的引用。它的工作原理类似于 createRef
。
它的原理也是十分的简单
本质上就是初始化的时候创建一个内部状态为 {current:initialValue}
的组件,且不依赖任何数据,需要则通过手动赋值修改。
5.2 ReducerHook
ReducerHook
的主要作用是用来做一些性能优化的 hook
集合。并且在 ReducerHook
内部,有一个通用的数据结构,用来表示该 Hook
内部的数据结构。
5.2.1 useReducer
useReducer
的使用方式和 redux
非常像。
对于使用过 redux
的同学来说,这样的用法应该会很熟悉。
我们可以通过源码来进行分析它的实现原理
更新 state
就是调用 dispatch
,也就是通过 reducer(preState,action)
计算出下次的state赋值给_value。然后调用组件的 setState
方法进行组件的diff和相应更新操作。
5.2.2 useState
useState
大概是平时在开发过程中最常使用的 hook
, 它类似于 class 组件中的 state
状态值。
它的原理很简单,就是利用 useReducer
来进行实现的, 也就是 useState
其实只是传特定 reducer
的 useReducer
一种实现。
5.3 EffectHook
“副作用”一词在很多参与过 React
相关的项目开发的同学来说,肯定不会陌生,无论是要从API获取某些数据还是要对文档触发效果,基本上可以发现 EffectHook
几乎可以满足所有需求。这也是 hooks API
的主要优点之一,它使得你能够更聚焦于对效果的思考,而不再是对组件生命周期的思考。
在整个 EffectHook
中,都贯穿了下面这样的通用数据结构.
5.3.1 useEffect 和 useLayoutEffect
这两个 hook
的用法完全一致,都是在 render
过程中执行一些副作用的操作,可来实现以往 class
组件中一些生命周期的操作。区别在于, useEffect
的 callback
执行是在本次渲染结束之后,下次渲染之前执行。useLayoutEffect
则是在本次会在浏览器 layout
之后, painting
之前执行,是同步的。
使用的方式和前面的 hook
的使用方式基本上一致,传递一个回调函数和一个依赖数组,数组的依赖参数变化时,重新执行回调。
他们的实现机制,稍微有些复杂,我们先看源码
从代码上来看,它们的实现几乎一样,唯一的区别是进入的回调分别是 _renderCallbacks
、 _pendingEffects
,从而达到了不同时机下进行渲染,这一块的具体逻辑,大家可以参考这篇文章了解更多的细节。
整体来看, preact
的 hook
模块的代码实现虽然不多,但是却体现出了它的精炼以及 preact
优秀的架构。
结束语
豆皮粉儿们,你们通过本文,对 Preact
的整体工作机制有没有更深入的理解啦?有时间的同学也可以结合本文尝试自己阅读 Preact
的源码,我相信大家阅读之后也会对 React
的理解更上一层楼哟!
The End
如果你觉得这篇文章对你有帮助,有启发,我想请你帮我两个小忙:
1、点个「在看」,让更多的人也能看到这篇文章内容;
奖励时刻
这么好的文章,不送点礼物给大家怎么行呢?这次为大家送出的是《深入浅出React和Redux》5本
《深入浅出React和Redux》
本书由浅入深地介绍如何用React和Redux构建现代化的、高效的前端项目,产出高质量的前端代码。主要内容包括:React的基础知识、如何设计易于维护的React组件、如何使用Redux控制数据流、React和Redux的相结合的方式、同构的React和Redux架构、React和Redux的性能优化、组件的测试等。
如何获取:
3. 群内回复你的截图
4. 抽奖,发奖
5. 如果你不想参与活动,可以点击上面的链接购买,京东价 53.6元