为什么react每次改变一个节点的值都要重新生成一个完整的虚拟dom树?
如上图,我改变c的值,为什么要重新生成一颗完整的虚拟dom树呢?不能只针对改变的地方生成呢?
vue好像是只针对改变的地方生成,求解惑
回答:
我来正式答一下吧
React
更新
之前的内容有点问题,React
源码还是很早之前看的,更新fiber
后只是简单的看了下源码没深入,又重新看了一遍React
源码来修复之前的问题
从触发setState
组件开始,往下遍历调用子元素render
,中间可以跳过shouldComponentUpdate
等方法跳过
正确的说法是:从触发setState
节点开始,先往上找到root最顶层根元素,然后往下根据已存在属性拷贝一份新的fiber
,直到触发setState
节点,再往下遍历调用子元素render
,中间可以根据shouldComponentUpdate
等方法跳过
多了一个往上找直到root开始遍历(因为是fiber链表结构没有层级概念,依然用树的那一套就会导致渲染重复),并且父、祖父元素都拷贝一份新fiber
的步骤
React
自从16.8开始使用了 fiber
架构
fiber
使用了链表结构串联来虚拟dom树,主要的三个参数:child(子)、sibling(下一个兄弟)、return(父)fiber
遍历过程就是找第一个元素一直找到底,然后找兄弟,没兄弟了往上
这个阶段分为两块,往下的过程是一些调用render
或者克隆一个fiber
节点的操作,往上的过程是生成effect
和updateQueue
更新内容的操作
假如在Text2
内触发setState
:
给Text2
标记更新字段lanes: 1
,并一直往上找,找到root
,并给沿途所以父节点设置childLanes: 0
,然后从root根节点开始遍历
当遍历到
lanes: 0
的时候:childLanes: 0
表示子孙元素没有变动直接跳过,也等于跳过了diff
childLanes: 1
就只是从之前的fiber
克隆一个新的fiber
节点
当遍历到
lanes: 1
的时候- 如果
shouldComponentUpdate
或类似的操作不更新,则走到上面lanes: 0
的流程 - 调用
render
生成新的fiber
- 如果
当到达最底部没有子元素的时候,开始compile
生成updateQueue
节点然后重复上面步骤(叶子节点为没有子元素的节点)叶子节点->兄弟->父->子->叶子
,最终回到root
结束
然后commit
阶段,这时候有个Effect
链表(Effect
链表只有变动的节点),遍历Effect
拿到节点的updateQueue
更新了哪些内容,将updateQueue
渲染到dom上
Vue
Vue
众所周知是依靠响应式那么怎么依靠呢
<div> <div>{{name}}</div>
<div>{{age}}</div>
</div>
当我们在修改name
或者age
的时候就会触发render
Vue
并不会管哪个触发的,反正只要有一个或多个触发一合并最终调用render
方法就行了,这里有个误区,vue可以精细到具体dom
上,响应式可以知道在哪个函数里调用了依赖,但是不知道你把变量赋值给谁了啊
function fn(){ const a = vue.name
}
可以知道在函数fn
里依赖了响应式的name
属性,因为fn
被包装处理了,但是没法知道你赋值给a了,相同道理也没办法知道赋值给<div>{{name}}</div>
,就算利用编译时处理也没法处理各种动态情况<div>{{obj[name][c]}}</div>
。
回到正题,响应式的子组件是否渲染是怎么做的?
分为两种情况,props
的传递,一种是传递值,一种是传递引用
值
<template> <div @click="handleClick">A组件</div>
<Text :value="obj.title"></Text>
</template>
<script setup>
const obj = reactive({
title: '你好'
})
function handleClick() {
obj.title = 'hello'
}
</script>
我们把title
传给了Text
组件,但是 obj.title
这个语法就触发依赖追踪了,这个触发是在A组件里的,传给Text
的是个文本【A组件render
用了obj
的title
字段】
所以obj.title = 'hello'
触发的是A组件的render
,Text
是否要更新呢?,Vue
浅层对比props
发现title
字段由你好
变成hello
,就触发了Text
的更新,Text
的render
被调用了
如果更新的是其他字段,Text
自动浅层数据props
对比发现没变化就会自动跳过
引用
现在我们改一下,改成引用传递
<Text :value="obj"></Text>// Text组件内部:
<div>{{value.title}}</div>
这时候就不一样了,因为把obj
传给了value
,所以value.title
就相当于obj.title
,这就触发了响应式追踪【在Text
组件render
内使用了obj
的title
属性】
那么当obj.title = 'hello'
就会触发Text
的render
,不会触发父级的A组件render
了,也不会经过Text
组件的浅层数据比对了,精准触发了Text
的render
总结
当传递值的时候更新的是父元素,因为传递的是值只需要浅层对比
当传递引用的时候,更新的是使用的那个组件的render
单纯是否决定渲染来说:Vue
相比于React
,不用手动处理数据的比对,哪个组件的数据发生变化就调用哪个的render
二者的核心流程都差不多,render
方法调用,是否更新组件,diff
(不更新组件就直接跳过这个组件的diff)
细节上差别也挺大,React
有fiber
和Concurrent rendering
,Vue
有响应式和模板标记优化
回答:
最近也在看 react fiber 相关的代码,我从一开始也有这个疑问。哈哈,专门注册了一个账号来回答。
我说说我的看法,其实如 @liuye1296 在回答中说到,react 是单向数据流,理论上我们可以只处理以 setState 节点及其指数。但是实际上 react 的做法如 @李十三 所说,大概是:
- 从 setState 往上回溯直到 root,目的是把当前的优先级标准到父节点上
从 root 往下遍历,判断:
- properties 发生改变,需要更新,继续遍历
- properties 未发生改变,优先级满足,说明 state 发生改变,需要更新(还要考虑 shouldUpdate 方法,如果返回 false 也不需要遍历),继续遍历
- properties 未发生改变,当前节点优先级不满足,但是子节点优先级满足,需要更新子节点,继续遍历
- 不符合以上几种情况,不需要往下遍历
为什么不直接从 setState 节点开始更新,是因为 react 存放任务优先级的机制。每次 set 之后会生成一个新任务。react 会根据当前任务情况(未完成的任务,上次被打断的任务等)计算一个更新优先级,如果当然任务的优先级等于这个更新优先级,就不会新启动一个任务,而是复用之前的任务。也就是说一个任务中,存的更新可能不只是当前组件的 setState 引起的,还可能包括其他组件。因此不能直接从某个 setState 的组件开始更新,而是要从 root 开始遍历。
回答:
个人认为:
一个节点发生变化可能会影响附件的节点,那如果去比较发生变化的节点的子节点需要一层一层的比较,时间复杂度比较高。
react是自顶向下,一次可以遍历整个dom结构生成新的虚拟树,而且允许用shouldComponentUpdate
来决定是否需要进行diff。
vue是在发生变化时,把变化的信息存入更新队列,在合适的时机调用render,然后对新旧2个虚拟树进行diff和patch。
ps: 一般都是研究diff算法的,对于变化时如何重新生成虚拟树的具体过程的文章比较少,我也不是非常清楚。
回答:
@李十三 其实说的很对的,你的问题和你如何得到这个结论有很大关系。
其实这是一个很简单的问题,是更新容易还是删除后重建容易?明显是后者,例如“您的爱好”这种复选框,每次保存的时候后端开发者通常都是删掉老数据然后保存新数据的,否则就要先对比哪些没变,哪些需要删掉,然后还要写入什么。
所以,当组件的props变化时,更新虚拟dom树的最佳办法就是产生该组件的虚拟dom树,然后对页面的虚拟dom树上的节点进行替换。如果同一个props用于多个组件,那么这个组件的公共父将被替换。state也是如此,但是被局限在当前组件内而已。
...反复修改,折腾1个多小时了,感觉确实很难解释清楚,丢一小段代码给你,也许有帮助。
import React, {useState, useEffect} from 'react';import ReactDOM from 'react-dom';
const Element = (props) => {
const [num, setNum] = useState(0);
useEffect(() => {
setNum(props.num * 2);
}, [props]);
return <>
<h1>Great! ({props.num})</h1>
<h1>Hello! ({num})</h1>
<h1>Merge! ({props.num}) ({num})</h1>
<div>
<h1>Great! ({props.num})</h1>
<h1>Hello! ({num})</h1>
</div>
</>
}
let i = 0;
setInterval(() => {
i++;
ReactDOM.render(<Element num={i} />, document.getElementById('container'));
}, 5000);
回答:
setState告诉react需要更新,但它不知道哪里改变了,于是生成一棵新的树,和原来的对比,才能知道如何修改dom。
vue能知道那里变更了是因为它有依赖收集。触发getter的时候就能将vnode和data绑定上,发生变更时直接对有依赖的vnode比较就可以了。
回答:
如果你的【完整的虚拟dom树】指的是render/functon component return的那个东西的话,那确实如此
不过,在vue2里也是这么做的,vue3据说才有一定的优化
精准dom更新只有solidjs、svelte这种框架能做到
回答:
React 并不是生成一颗完整的DOM 树 而是以调用setState的组件为 ROOT节点 开始重新生成一棵DOM树
为什么出现这种情况呢?
Vue 是对一个对象 监听 这个对象会收集所有依赖过他的组件,每次这个调用对象的set方法的时候 就会去通知它收集的依赖 去更新。
React 则是用户触发setState 通知React 更新。而基于React 数据单向流设计 组件更新是不会影响到父组件的 只会影响他的子组件。所以React 会从自己开始往下生成一颗完整的DOM树 再对比 (React 需要开发者手动调用API 去对比Props 子组件是否更新。而Vue 本身已经做了这一步了)
结论就是: Vue 基于他的依赖收集 可以做到颗粒度更细的更新 而React 是没有依赖收集的 则是自顶向下重新渲染
回答:
最近在看 react 18.1 的源码 也有类似的问题,我认为可能是这样的
虽然触发的是fiber tree 的内部的某个 component 的 state hook,但是 react 会回溯到顶层root,但是会向上冒泡 childLines ,同时设置当前触发了 state hook 的fiber lane = 1,
在下一次更新任务的时候,就是从root 开始的。这里有一个比较重要的比较点,react 会比较 props 和memoProps 是否相等如果是相同的 会执行 bailout 操作,只会单纯的clone current fiber 的数据,这里的操作其实并不多,对于这种bailout的fiber 他的 所有child 都会做bailout操作,如果不是在 触发了 state hook 的那个分支的 tree上面。这应该是基于 react 双缓存的先决条件来做的,保证work的fiber 和 current 的fiber 数据的独立性。
但是,还是有一个问题,如果到达了触发 state 的 fiber那里,这个fiber 的子集child 就不能到bailout这个逻辑了,因为这个时候 clone children的时候会从 element.props 设置fiber 的props,导致 在和 memoProps 比较的时候 永远不会相等,这里感觉react 执行了很多的无用代码,导致整个子 tree 都需要走一遍 component (组件)的逻辑。而上面的遍历因为有bailout的操作其实用不了多久时间,但是走 component的逻辑就很花时间。
以上是 为什么react每次改变一个节点的值都要重新生成一个完整的虚拟dom树? 的全部内容, 来源链接: utcz.com/p/937213.html