前言
因为最近开始写Vue了,对于我一个react骨灰级玩家来说其实是一个挑战。其实我现在更偏向于写原生JS,因为市场上绝大部分做得好的框架库几乎都脱离不了Vitrual DOM
体系。
而我们知道的是,通过Vitrual DOM
来更新真实DOM,性能肯定是比不过直接对原生DOM进行操作的性能。如果我能明确知道哪个DOM要发生变化,那直接 document.getElementById(id).xx
多好?
Vitrual DOM
的价值从来都不在性能方面。emmm... 今天主题是对Vue的源码进行一个解毒,目的是能够清晰知道Vue到底做了哪些事情,优劣势又分别在哪。
1. 先从Vue 的 diff 算法开始解剖
走Vitrual DOM
路线的都逃不过diff
算法。 diff
算法家家有,那 Vue3
的diff
算法又是长什么样的。
先来看个栗子。
<ul key="ul1">
<li>渣男<li>
<li>胖子<li>
<li>就知道吃<li>
<ul>
需要转化成:
<ol key="ul1">
<li>渣男<li>
<li>胖子<li>
<li>就知道吃吗?<div>你个渣男!</div><li>
<ol>
Q: 就把
ul变成
ol ,key都没变,甚至其子结点都不变。请问 Vue 重新如何渲染?
答: 全部重新渲染一遍。
所以,合理吗? 如果存在即合理,那为什么要这样设计呢? 这里有人要diss我了,这种场景实际开发中太少见了。(被怼得很难过,这个后续再说吧。真的是可以解决这种问题的……?)
diff的执行策略
同一个虚拟节点,才进行精细化diff比较。
// 先看源码中的一个方法
function isSameVNodeType(n1, n2) {
// ...
return n1.type === n2.type && n1.key === n2.key
}
看方法名你其实就明白了,这是个判断两个VNode 是否是同一个。 看函数返回值你就更加明白,两个VNode要一致就得结点类型一样、key也得一样。
只进行同层比较,不会进行跨层比较
那回到上面的问题,继续看个栗子:
<ul key="ul1">
<li>渣男<li>
<li>胖子<li>
<li>就知道吃吗?<div>你个渣男!</div><li>
<ul>
Q: 如果 ul 不再变,只是其中一个 li 元素的内容发生了变化。那请问又是咋渲染的?
答:如果li
发送变动,只会进行li
同层的diff比较,不会进行li
子元素div
diff 。 我相信使用过Vue的人都知道答案。
patchChildren - 更新子结点
上源码。
const patchChildren = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized = false) => {
const c1 = n1 && n1.children;
const prevShapeFlag = n1 ? n1.shapeFlag : 0;
const c2 = n2.children;
const { patchFlag, shapeFlag } = n2;
// fast path
if (patchFlag > 0) {
if (patchFlag & 128 /* KEYED_FRAGMENT */) {
// this could be either fully-keyed or mixed (some keyed some not)
// presence of patchFlag means children are guaranteed to be arrays
/*
*1 - patchKeyedChildren
*/
patchKeyedChildren(c1, c2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
return;
}
else if (patchFlag & 256 /* UNKEYED_FRAGMENT */) {
// unkeyed
/*
* 2 - patchUnkeyedChildren
*/
patchUnkeyedChildren(c1, c2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
return;
}
}
// children has 3 possibilities: text, array or no children.
if (shapeFlag & 8 /* TEXT_CHILDREN */) {
// text children fast path
if (prevShapeFlag & 16 /* ARRAY_CHILDREN */) {
unmountChildren(c1, parentComponent, parentSuspense);
}
if (c2 !== c1) {
hostSetElementText(container, c2);
}
}
else {
if (prevShapeFlag & 16 /* ARRAY_CHILDREN */) {
// prev children was array
if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
// two arrays, cannot assume anything, do full diff
patchKeyedChildren(c1, c2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
}
else {
// no new children, just unmount old
unmountChildren(c1, parentComponent, parentSuspense, true);
}
}
else {
// prev children was text OR null
// new children is array OR null
if (prevShapeFlag & 8 /* TEXT_CHILDREN */) {
hostSetElementText(container, '');
}
// mount new if array
if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
mountChildren(c2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
}
}
}
};
看这段源码你就知道:
- 结点有
patchFlag
,shapeFlag
两个属性。 patchChildren
入参中 n1 为旧结点,并且prevShapeFlag = n1.shapeFlag
。- n2 为新结点(旧结点更新后)
patchFlag
为快速通道标志,一旦结点上有这个标志且值 > 0 则直接进行 有key的diff处理。- 非快速通道 则要进行三种判断:文本结点、子结点、没有子结点。 其中遇见array结点则进行递归处理。
我在其中标注了两个地方(源码太多,只展示关键部分)
- 1 - patchKeyedChildren: 处理有key的节点
const patchKeyedChildren = (c1/*旧的vnode*/, c2/*新的vnode*/, container, parentAnchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
let i = 0;/* 记录索引 */
const l2 = c2.length; /* 新vnode的数量 */
let e1 = c1.length - 1; // prev ending index : 老vnode 最后一个节点的索引
let e2 = l2 - 1; // next ending index : 新节点最后一个节点的索引
// 1. sync from start
while (i <= e1 && i <= e2) { // ### 1. 头头比较,发现不同就跳出
const n1 = c1[i];
const n2 = (c2[i] = optimized
? cloneIfMounted(c2[i])
: normalizeVNode(c2[i]));
if (isSameVNodeType(n1, n2)) {
patch(n1, n2, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
}
else {
break;
}
i++;
}
// 2. sync from end
while (i <= e1 && i <= e2) { // ### 2. 尾尾比较,发现不同就跳出
const n1 = c1[e1];
const n2 = (c2[e2] = optimized
? cloneIfMounted(c2[e2])
: normalizeVNode(c2[e2]));
if (isSameVNodeType(n1, n2)) {
patch(n1, n2, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
}
else {
break;
}
e1--;
e2--;
}
// 3. common sequence + mount
// 老节点全部patch,还有新节点
if (i > e1) { // / 新节点大于老节点
if (i <= e2) { // // 并且新节点e2指针还没有走完,表示需要新增节点
const nextPos = e2 + 1;
const anchor = nextPos < l2 ? c2[nextPos].el : parentAnchor;
while (i <= e2) {
patch(null, (c2[i] = optimized
? cloneIfMounted(c2[i])
: normalizeVNode(c2[i])), container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
i++;
}
}
}
// 4. common sequence + unmount
// 新节点全部patch,还有老节点
else if (i > e2) { // 新节点e2指针全部patch完
while (i <= e1) { // 新节点数小于老节点数,需要卸载节点
unmount(c1[i], parentComponent, parentSuspense, true);
i++;
}
}
// 5. unknown sequence : 剩余不确定元素
// [i ... e1 + 1]: a b [c d e] f g
// [i ... e2 + 1]: a b [e d c h] f g
// i = 2, e1 = 4, e2 = 5
else {
const s1 = i; // prev starting index
const s2 = i; // next starting index
// 5.1 build key:index map for newChildren
const keyToNewIndexMap = new Map();
for (i = s2; i <= e2; i++) {
const nextChild = (c2[i] = optimized
? cloneIfMounted(c2[i])
: normalizeVNode(c2[i]));
if (nextChild.key != null) {
if (keyToNewIndexMap.has(nextChild.key)) {
warn(`Duplicate keys found during update:`, JSON.stringify(nextChild.key), `Make sure keys are unique.`);
}
keyToNewIndexMap.set(nextChild.key, i);
}
}
// 5.2 loop through old children left to be patched and try to patch
// matching nodes & remove nodes that are no longer present
// code ....
// 5.3 move and mount
// generate longest stable subsequence only when nodes have moved
// code ...
}
};
亲,先看看源码当中那些带数字标号的引文注释,都是源码自带的。 看不懂就再看看中文注释,那是我加的。
好吧,如果看到源码就头疼,那我来总结一下这个方法中的数字 5
。
5.1 build key,记录新的节点
先看看代码中声明的变量:
const s1 = i // 第一步遍历到的index
const s2 = i
const keyToNewIndexMap = new Map() // 把没有比较过的新的vnode节点,通过map保存
for (i = s2; i <= e2; i++) {
if (nextChild.key != null) {
keyToNewIndexMap.set(nextChild.key, i)
}
}
let j // 新指针j
let patched = 0
const toBePatched = e2 - s2 + 1 // 没有经过 path 的 新的节点的数量
let moved = false // 是否需要移动
let maxNewIndexSoFar = 0
const newIndexToOldIndexMap = new Array(toBePatched)
// 建立一个数组,每个子元素都是0 [ 0, 0, 0, 0, 0, 0 ]
for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0;
在 keyToNewIndexMap
变量中,我们得到的结果是:(假设节点 e 的key是 e)。
keyToNewIndexMap = {"e" => 2, "d" => 3, "c" => 4, "h" => 5}
用新指针 j
来记录剩下的新的节点的索引。
newIndexToOldIndexMap
用来存放新节点索引,和旧节点索引。
5.2 匹配节点,删除不存在的节点
for (i = s1; i <= e1; i++) { /* 开始遍历老节点 */
const prevChild = c1[i] // c1是老节点
if (patched >= toBePatched) {
/* 已经patch数量大于等于剩余节点数量,卸载老的节点 */
unmount(prevChild, parentComponent, parentSuspense, true)
continue
}
let newIndex // 目标新节点的索引
/* 如果,老节点的key存在 ,通过key找到对应的新节点的index */
if (prevChild.key != null) {
newIndex = keyToNewIndexMap.get(prevChild.key)
} else {
/*
如果,老节点的key不存在,遍历剩下的所有新节点
按我们上面的节点来讲,就是遍历 [e d c h],代码中s2=2 e2=5,
*/
for (j = s2; j <= e2; j++) {
if (
newIndexToOldIndexMap[j - s2] === 0 &&
isSameVNodeType(prevChild, c2[j])
) {
/* 如果找到与当前老节点对应的新节点那么 ,将新节点的索引,赋值给newIndex */
newIndex = j
break
}
}
}
if (newIndex === undefined) {
/* 没有找到与老节点对应的新节点,删除当前节点 */
unmount(prevChild, parentComponent, parentSuspense, true)
} else {
/* 把老节点的索引,记录在存放新节点的数组中, */
newIndexToOldIndexMap[newIndex - s2] = i + 1
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex
} else {
/* 证明有节点已经移动了 */
moved = true
}
/* 找到新的节点进行patch */
patch(
prevChild,
c2[newIndex],
container,
null,
parentComponent,
parentSuspense,
isSVG,
optimized
)
patched++ // 记录已经在新节点中找到了了多少个老节点了
}
}
所以你可以理解为主要执行了2步操作:
Step 1:
通过老节点的key,找到新节点的 index,这里有两种情况:
- 老节点没有key,遍历剩下的所有新节点,尝试找到索引
- 老节点有key,在
keyToNewIndexMap
中找到索引
Step 2:
- 如果第一步依旧没有找到 Index,则表示没有和新节点对应的老节点,删除当前旧节点。
- 如果找到了Index,则表示老节点中有对应的节点,赋值新节点索引到
newIndex
。再把老节点索引,记录到新节点的数组newIndexToOldIndexMap
中,这里索引+1,是因为初始值就0,如果直接存放索引,从第一个开始就发生变化那么存入的索引会是0,则会直接被当作没有老节点匹配。
解释判断: newIndex >= maxNewIndexSoFar
因为遍历老数组是从前往后遍历,那么假如说在遍历的时候,就记录该节点在新节点数组中的位置,假如发生倒转,那么就是 maxNewIndexSoFar > newIndex
, 就代表说新老节点的某节点已经发生了调换,在 diff
过程中肯定会涉及元素的移动。
// 举个栗子
if 旧节点 = [a, b, c, f];
if 新节点 = [a, f, b, c];
so
循环遍历旧结点:
when Pointer -> b ,newIndex = 2 and maxNewIndexSoFar = 0
when Pointer -> c ,newIndex = 3 and maxNewIndexSoFar = 2
when Pointer -> f ,newIndex = 1 and maxNewIndexSoFar = 3
result -> moved = true
// 把流程串起来
旧节点: a b [c d e] f g , c key 存在,d、e 的 key === undefined
新节点: a b [e d c h] f g
得到待处理的节点: [e d c h]
按以上逻辑,先遍历 [c d e]。
when when Pointer -> c, newIndex = 4 s2 = 2 newIndexToOldIndexMap = [0,0,3,0].执行 patch
when when Pointer -> d, newIndex = undefined ,删除 d
when when Pointer -> e, newIndex = undefined ,删除 e
多么可怕的事实,如果key不存在,直接删除旧结点。 所以得出结论:写Vue代码,一定要注意要有key ?我自己都差点信了?
提出一个很重要的概念: 最长递增子序列
我会给大家写上中文注释的。?
// 5.3 move and mount
// generate longest stable subsequence only when nodes have moved
// 移动老节点、创建新节点
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: EMPTY_ARR;
// // 用于节点移动判断
j = increasingNewIndexSequence.length - 1;
// looping backwards so that we can use last patched node as anchor
// 向后循环,也就是倒序遍历。 因为插入节点时使用 insertBefore, 即向前插以便我们可以使用最后一个更新的节点作为锚点
for (i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i;
const nextChild = c2[nextIndex];
const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : parentAnchor;
if (newIndexToOldIndexMap[i] === 0) { // 如果仍然是默认值 0, 证明是一个全新的节点
// mount new
patch(null, nextChild, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
}
else if (moved) {
// move if:
// There is no stable subsequence (e.g. a reverse)
// OR current node is not among the stable sequence: 当前索引不是最长递增子序列里的值,需要移动
if (j < 0 || i !== increasingNewIndexSequence[j]) {
move(nextChild, container, anchor, 2 /* REORDER */);
}
else {
// 是最长递增子序列里的值,则指向下一个
j--;
}
}
}
- 2 - patchUnkeyedChildren: 处理有没有key的节点
至于没有key的结点咋处理……
辣鸡,非常粗暴,简直无法直视。 自己去看源码。(就是对比新旧结点的length,新的长就直接mount new。 旧的长就先umount old)
小结一下:
- 没有key的结点发生变化,直接火葬场吧。
- 有key的结点发生变化
- 头和头比较一下
- 尾和尾比较一下
- 头和尾比较一下
- 找出最长递增子序列,随时移动,随时创建新结点。
2. 时间切片(Time Slicing)
Vue3 抛弃了时间切片,这简直令我……。emmmm, 我还能说什么呢,你不卡谁卡。
关于为什么Vue3不使用时间切片(Time Slicing), 尤雨溪在 Vuejs issue
里面有很详细的回答。 尤雨溪回答的原文地址
好吧。我来翻译一下(我就在想,我不翻译让老铁们直接去看原文会被打吗?)。
在web应用程序中,更新内容丢帧(janky)
通常是由大量CPU时间
+原始DOM更新
的同步操作引起的。时间切片
是在CPU工作期间保持应用程序响应的一种尝试,但它只影响CPU工作。但DOM更新的刷新必须仍然是同步的,目的是确保最终DOM状态的一致性。
所以,想象两种丢帧更新的场景:
1.CPU工作时间在16ms以内,但原生DOM的更新操作量很大(例如,mount 大量新的 DOM内容)。无论有没有使用时间切片,该应用程序仍会感觉“僵硬(丢帧)”。
- CPU任务非常繁重,需要超过16ms的时间。从理论上讲,时间切片开始发挥作用了。然而,HCI的研究表明,除非它在进行动画,否则对于正常的用户交互,大多数人不会感觉到差异,除非更新时间超过100毫秒。
也就是说,只有当频繁的更新需要超过100毫秒的纯CPU时间时,时间切片才变得实际有用。
也就是说,只有在频繁进行超过100ms的纯CPU任务更新时,时间切片才实际有用。
有趣的地方在于,这样的场景更经常地发生在React中,因为:
-
i. React的虚拟DOM操作(
reconciliation 调度算法
)天生就比较慢,因为它使用了大量的Fiber架构
; -
ii. React使用JSX来渲染函数相对较于用模板来渲染更加难以优化,模板更易于静态分析。
-
iii. React Hooks将大部分组件树级优化(即防止不必要的子组件的重新渲染)留给了开发人员,开发人员在大多数情况下需要显式地使用
useMemo
。而且,不管什么时候React接收到了children
属性,它几乎总要重新渲染,因为每次的子组件都是一棵新的vdom树。这意味着,一个使用Hook的React应用在默认配置下会过度渲染。更糟糕的是,像useMomo
这类优化不能轻易地自动应用,因为:- 它需要正确的deps数组;
- 盲目地任意使用它可能会阻塞本该进行的更新,类似与
PureComponent
。
不幸的是,大多数开发人员都很懒,不会积极地优化他们的应用。所以大多数使用Hook的React应用会做很多不必要的CPU工作。
相比之下,Vue就上面的问题做一下比较:
-
本质上更简单,因此虚拟DOM操作更快(
no时间切片-> no
fiber-> 更低开销
); -
通过分析模板进行了大量的AOT优化,减少了虚拟DOM操作的基本开销。Benchmark显示,对于一个典型的DOM代码块来说,动态与静态内容的比例大约是1:4,Vue3的原生执行速度甚至比Svelte更快,在CPU上花费的时间不到React的1/10。
-
智能组件树级优化通过响应式跟踪,将插槽编译成函数(避免子元素重复渲染)和自动缓存内联句柄(避免内联函数重复渲染)。除非必要,否则子组件永远不需要重新渲染。这一切不需要开发人员进行任何手动优化。
这意味着对于同一个更新,React应用可能造成多个组件重新渲染,但在Vue中大部分情况下只会导致一个组件重新渲染。
默认情况下
, Vue3应用比React应用花费更少的CPU工作时间, 并且CPU工作时间超过100ms的机会大幅度减少了,除非在一些极端的情况下,DOM可能成为更主要的瓶颈。
现在,时间切片或并发模式带来了另一个问题:因为框架现在安排和协调了所有更新,它在优先级、失效、重新实例化等方面产生了大量额外的复杂性。所有这些逻辑处理都不可能被tree-shaken
,这将导致运行时所占CPU内存的大小膨胀。即使包含了Suspense
和所有的tree-shaken
,Vue 3的运行时仍然只有当前React + React DOM的1/4大小。
注意,这并不是说并发模式作为一个整体是一个坏主意。它确实提供了处理某类问题的有趣的新方法(特别是与协调异步状态转换相关的),但时间切片(作为并发的一个子功能)专门解决了React中比其他框架中更突出的问题,同时也产生了自己的成本。对于Vue 3来说,这种权衡似乎并不值得。
如果你也是个老react玩家,想必你会不服气。 尤雨溪的回复当中看上去好像指出了 react 的一些弊端和短板。恰有一种踩低别人抬高自己的节奏。
尤雨溪指出:
- React + React DOM 在运行中所占CPU内存要高于Vue运行时所占内存,比例已经高达
4:1
- React Hooks 不好用,即使用好了
useMemo 、 memo
也还得保证 deps 的正确性。 - React的操作虚拟DOM,其实就是指
React
的调度算法比较慢。而Vue
通过分析模板进行了大量的AOT优化
,减少了虚拟DOM操作的基本开销。所以Vue的操作虚拟 DOM 要比 React 快。
- 并发模式不是坏死,但时间切片就不一定了,至少React 的时间切片作法就不咋地。
作为一个过来人,深知React的一些缺点。 我们换个角度来看待1-4点。
-
老实讲,谁跑得快得分时间。 如果React 需要4个小时,Vue需要1个小时,请问你觉得谁快? 但React 跑400ms,Vue跑100ms,请问你觉得谁快?换句话说,针对此问题,真的很有必要吗?前端性能瓶颈如何优化?React好做还是Vue好做?
-
React Hooks 用起来很好用,但能用好确实不容易。但如果我用好了,这个问题还存在吗?
-
React 调度算法慢,Vue就相比较下快,那就得分两个方面来
- React 可以通过 实操写代码来控制快慢,例如每次操作尽可能少的VDOM。 Vue的AOT优化可以让开发人员去做吗?很明显,Vue 不可以。
- React 真的慢吗? 或者说在操作大量DOM的场景下,Vue 真的优于 React 吗?
-
稍微解释一下所谓的
React 时间切片做法
。 React 会将Fiber 字任务交给浏览器的空闲时间去完成,这个过程可以随时被中断,中断以后下次还能接着上一次的位置继续执行任务。- “时间切片” 在react中的应用远不是为快不快的问题而存在的,而是为了可恢复性。例如用户在做负责的交互行为,或者页面要做复杂动画的时候,如果React加强了自身消耗却保证了交互、动画的流畅性,你觉得值吗?
小结一下
其实,现在市场上关于React 和 Vue 有很多激烈的讨论,都是由于自身的优缺点而产生的。
例如网络上很多人在互相攻击:
“ Vue 只适合小项目,大项目扛不起来”
”React 无数个回调,无数个选择表达式,this绑定…乱!“
“Vue好上手,岗位多”
“大厂基本都用 React,不用 Vue ”
那如果从使用层面上来考虑的话,emmm,列个框吧。
问题 | Vue | React | this混乱 | 源码实现已经处理好了this,不需要你额外处理 | React Hooks 已经不存在this这个东西了。 | 上手 | easy | normal | 用好 | normal | hard | 新手友好 | 极度友好 | 不友好 | 可扩展性 | 一般 | 强 | 底层实现 | 硬核,能做的都做得挺好 | 硬核,但内容更多 | hook | 细讲 | 细讲 |
---|
3. Vue3 & React17 比较
Vue 3.0 Beta 版本刚发布的时候,大家吵得很凶。印象深刻的有两点吐槽。
- 吐槽意大利面代码结构
- 杂七杂八一堆丢在
setup
里,我还不如直接用 react - 代码结构不清晰,语义不明确,这操作无异于把 vue 自身优点都扔了
- 结构不清晰,担心代码量一上去不好维护
- 杂七杂八一堆丢在
- 抄袭 React
Vue-Composition-Api
的主要灵感来源是React Hooks
的创造力(这也是吐槽最狠的地方)
其实真的用过并且懂 React hooks 的人看到这个都会意识到 Vue Composition API (VCA)
跟 hooks
本质上的区别。VCA
在实现上也其实只是把 Vue 本身就有的响应式系统更显式地暴露出来而已。真要说像的话,VCA
跟 MobX
还更像一点。
(这里我为Vue洗冤屈了,这说明我还是很可观的。毕竟是研究过Vue源码后的发言)
举一个 Vue CLI UI file explorer 官方吐槽的例子,这个组件是 Vue-CLI 的 gui 中(也就是平常我们命令行里输入 vue ui
出来的那个图形化控制台)的一个复杂的文件浏览器组件,这是 Vue 官方团队的大佬写的,相信是比较有说服力的一个案例了。
自看去github上看,我这就不贴代码了,深夜凌晨1点了都。
然后,看官方给的图你也明白了。
图左边是原始风格,右边是 hook
风格。
其中一个 hook 风格的方法:
function useCreateFolder(openFolder) {
// originally data properties
const showNewFolder = ref(false);
const newFolderName = ref("");
// originally computed property
const newFolderValid = computed(() => isValidMultiName(newFolderName.value));
// originally a method
async function createFolder() {
if (!newFolderValid.value) return;
const result = await mutate({
mutation: FOLDER_CREATE,
variables: {
name: newFolderName.value,
},
});
openFolder(result.data.folderCreate.path);
newFolderName.value = "";
showNewFolder.value = false;
}
return {
showNewFolder,
newFolderName,
newFolderValid,
createFolder,
};
}
我们来看一下Vue Hook风格下的一段代码:
export default {
setup() {
// ...
},
};
function useCreateFolder(openFolder){
// ...
}
function useCurrentFolderData(networkState) {
// ...
}
function useFolderNavigation({ networkState, currentFolderData }) {
// ...
}
function useFavoriteFolder(currentFolderData) {
// ...
}
function useHiddenFolders() {
// ...
}
function useCreateFolder(openFolder) {
// ...
}
再来看看现在的 setup
函数。
export default {
setup() {
// Network
const { networkState } = useNetworkState();
// Folder
const { folders, currentFolderData } = useCurrentFolderData(networkState);
const folderNavigation = useFolderNavigation({ networkState, currentFolderData });
const { favoriteFolders, toggleFavorite } = useFavoriteFolders(currentFolderData);
const { showHiddenFolders } = useHiddenFolders();
const createFolder = useCreateFolder(folderNavigation.openFolder);
// Current working directory
resetCwdOnLeave();
const { updateOnCwdChanged } = useCwdUtils();
// Utils
const { slicePath } = usePathUtils();
return {
networkState,
folders,
currentFolderData,
folderNavigation,
favoriteFolders,
toggleFavorite,
showHiddenFolders,
createFolder,
updateOnCwdChanged,
slicePath,
};
},
};
??了,干净不?
对比一下hook原理吧。
还是举个栗子。
<template>
<div>
<span>{{count}}</span>
<button @click="add"> Add By 1 </button>
</div>
</template>
export default {
setup() {
const count = ref(0)
const add = () => count.value++
effect(function active(){
console.log('count changed!', count.value)
})
return { count, add }
}
}
非常简单的一个栗子。
- setup只执行一次,
- 如果需要在
count
发生变化的时候做某件事,我们只需要引入effect
函数。 - 这个
active
函数只会产生一次,这个函数在读取count.value
的时候会收集它作为依赖,那么下次count.value
更新后,自然而然的就能触发active
函数重新执行了。
总结一下: hook 初始化一次,后用无穷。
再来看个栗子。
export default function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('渣男');
const add = () => setCount((prev) => prev + 1);
useEffect(()=>{
setName(`渣男渣了${count}次`)
},[count])
return (
<div>
<span>{count}</span>
<span>{name}</span>
<button onClick={add}> +1 </button>
</div>
);
}
看得出,功能一样,但这是个React 组件。通过引用 <Counter />
这种方式引入的,我们知道JSX就是js,Babel 实际上会把它编译成 React.createElement(Counter)
这样的函数执行。
也就是说每次渲染,这个函数都会被完整的执行一次。
useState
返回的 count
和 setCount
则会被保存在组件对应的 Fiber
节点上,并且每个 React 函数每次执行 Hook 的顺序必须是相同的。
React Hooks里的钩子函数都是可以被多次调用的,这也是目前我觉得React 对开发者最为友好的一个个创意。我可以充分利用这些钩子函数去最大程度颗粒化我的逻辑,达到高度复用且互不影响。
上述有说到 deps 依赖的弊端。 React Hooks 很多钩子都是需要依赖于状态变量的。 简单点说就是所依赖的状态变量发生了改变,那就可以执行相应的操作。听起来很美好对伐? 但一个搞不好就是闭包陷进…… 你用的好,就牛。用不好你就是辣鸡。
所以如果你是函数式编程风格的死忠粉,React Hooks绝对是你的最爱。
另外,忽然想到网络上一句话: Vue 给你持久,React给你自由。
所以,技术调研的时候,考虑清楚你的场景。其它真没啥,代码总是人写的,Vue再好用也能写成si,React 再难用,写好了也能上天。
凌晨1:26分了,技术文章是写起来就没边了,因为能讲的真的很多很多…… 关于React源码解毒,可以看看过往文章。关于Vue 剩下源码,其实真的不多,相比之下Vue的源码真的少太多了,注释还丰富(比较国人写英文更容易看懂些)。所以,有机会再补上吧。
end
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!