活出自己 發表於 2022-6-9 18:40:00

[react] 什么是虚拟dom?虚拟dom比操作原生dom要快吗?虚拟dom是如何转变成真实dom并渲染到页面的?

<div align="center"><img src="https://img2022.cnblogs.com/blog/1213309/202206/1213309-20220609163742187-416297054.png"></div>
<h1 id="壹--引">壹 ❀ 引</h1>
<p>虚拟<code>DOM</code>(Virtual DOM)在前端领域也算是老生常谈的话题了,若你了解过<code>vue</code>或者<code>react</code>一定避不开这个话题,因此虚拟<code>DOM</code>也算是面试中常问的一个点,那么通过本文,你将了解到如下几点:</p>
<ul>
<li>虚拟<code>DOM</code>究竟是什么?</li>
<li>虚拟<code>DOM</code>的优势是什么?解决了什么问题?</li>
<li>虚拟<code>DOM</code>的性能比操作原生<code>DOM</code>要快吗?</li>
<li><code>react</code>中的虚拟<code>DOM</code>是如何生成的?</li>
<li><code>react</code>是如何将虚拟<code>DOM</code>转变成真实<code>dom</code>的?</li>
</ul>
<p>阅读前建议与提醒:</p>
<ul>
<li>本篇文章可能比较长,建议挑一个空闲的时间段阅读,还请保持耐心,我将以通俗易懂的口吻带你了解这些问题。</li>
<li>本文源码分析部分<code>react</code>版本为<code>17.0.2</code>,无须担心低版本源码分析对你之后面试帮助不大的问题。</li>
<li>如果可以,泡上一杯性温的茶或者咖啡,保持一个舒服的姿势会让你阅读更加愉快。</li>
</ul>
<p>那么本文开始。</p>
<h1 id="贰--在虚拟dom之前">贰 ❀ 在虚拟dom之前</h1>
<p>在聊虚拟<code>DOM</code>之前,我还是想先聊聊在没有虚拟<code>DOM</code>概念的时候,我们是如何更新页面的,所以在这里我将先引出前端框架(库)的发展史,通过这个变迁过程也便于大家理解虚拟dom的出现到底解决了什么问题。</p>
<h3 id="贰--壹-石器时代jquery">贰 ❀ 壹 石器时代jQuery</h3>
<p>其实在15年以及更早之前,前端面试涉及到性能优化问题,往往都会提到<strong>尽可能少的操作<code>DOM</code></strong>这一点。为什么呢?因为在原生JS的年代,前端项目文件都明确分为<code>html、js</code>与<code>css</code>三种,我们在<code>js</code>中获取<code>DOM</code>,并为其绑定事件,通过<strong>事件监听</strong>感知用户在UI层的操作,并随之更新<code>DOM</code>,从而达到页面交互的目的:</p>
<div align="center"><img src="https://img2022.cnblogs.com/blog/1213309/202206/1213309-20220609163746500-534884916.png"></div>
<p>而在后面,<code>jQuery</code>的出现极大简化了开发者操作<code>DOM</code>的成本,抹平了当时不同浏览器操作<code>DOM</code>的<code>API</code>差异,为当时苦于<code>ie</code>以及不同浏览器自研<code>API</code>的开发者解决了不少兼容性问题,当然<code>JQ</code>也并未改变开发者在<code>JS</code>层直接操作<code>DOM</code>这一现状。</p>
<p>那么我们为什么说要尽可能少的操作<code>DOM</code>呢,这里就涉及到重绘与回流两个概念,比如单纯修改颜色就会引发重绘,删除或新增一个<code>DOM</code>节点就会引发回流和重绘,用户虽然无法感知这个过程,但对于浏览器而言也存在消耗性能。所以针对于回流,在此之后又提出了<code>DocumentFragment</code>文档对象以优化多次操作<code>DOM</code>的方案。简单理解就是,假如我要依次替换五个<code>li</code>节点,那么我们可以创建一个<code>DocumentFragment</code>对象保存这五个节点,然后一次性替换。</p>
<p>关于节流与重绘,若有兴趣可读读博主页面优化,谈谈重绘(repaint)和回流(reflow)一文。</p>
<p>关于<code>DocumentFragment</code>可读读博主页面优化,DocumentFragment对象详解一文。</p>
<p>这些都是时代的眼泪,现在应该很少会有人提及,这里就不再赘述了。</p>
<h3 id="贰--贰-青铜时代angularjs">贰 ❀ 贰 青铜时代angularjs</h3>
<p>在<code>JQ</code>之后,<code>angularjs</code>(这里指angularjs1而非angular)横空出世,一招双向绑定在当时更是惊为天人,除此之外,<code>angularjs</code>的模板语法也格外惊艳,我们将所有与数据挂钩的节点通过<code>{{}}</code>包裹(vue在早期设计上大量借鉴了angularjs),比如:</p>
<pre><code class="language-html">&lt;span&gt;{{vm.name}}&lt;/span&gt;
</code></pre>
<p>之后 <code>view</code> 视图层就自动与 <code>Model</code> 数据层进行挂钩(MVC那一套),只要 <code>Model</code> 层数据发生变化,<code>view</code> 层便自动更新。<code>angularjs</code> 的这种做法,彻底将开发者从操作 <code>DOM</code> 上解放了出来(为jq没落埋下伏笔),自此之后开发者只用专注 <code>Model</code> 层的数据加工以及业务处理,至于页面如何渲染全权交给 <code>angularjs</code> 底层处理即好了。</p>
<p>但需要注意的是,<code>angularjs</code> 在当时并没有虚拟<code>dom</code>的概念,那它是怎么做感知数据层变化以及更新视图层的呢?<code>angularjs</code>有一套脏检测机制<code>$digest</code>,<code>html</code>中凡是使用了模板语法<code>{{}}</code>或者<code>ng-bind</code>指令的部分,都会被加入到脏检测的<code>warchers</code>列表中,它是一个数组,之后只要用户通过<code>ng-click(与传统click不同,内置绑定了触发脏检测的机制)</code>等方法改变了<code>Model</code>的数据,<code>angularjs</code>就会从顶层<code>rootScope</code>向下递归,依次访问每个子<code>scope</code>中的<code>warchers</code>列表,并对其中监听的部分做新旧对比,如果不同则进行数据替换,以及<code>DOM</code>层的更新。</p>
<p>但是你要想想,一个应用那么大的结构,只要某一个数据变化了就得从顶层向下对比N个子 <code>scope</code> 中 <code>warchers</code> 下的所有监听对象,全量对比的性能有多差可想而知,<code>angularjs</code> 自身也意识到了这点,所以之后直接放弃了 <code>angularjs</code> 的维护转而新开了 <code>angular</code> 项目。</p>
<p>对于 <code>angularjs</code> 脏检测感兴趣可以读读博主深入了解angularjs中的𝑑𝑖𝑔𝑒𝑠𝑡与apply方法,从区别聊到使用优化一文,同样是时代的眼泪了。</p>
<h3 id="贰--叁-铁器时代react与vue">贰 ❀ 叁 铁器时代react与vue</h3>
<p>如果从 <code>angularjs</code> 转到 <code>vue</code> ,你会发现早期<code>vue</code>的模板语法、指令,双向绑定等很多灵感其实都借鉴了<code>angularjs</code>,但在更新机制上,<code>vue </code>并不是一个改动牵动全身,而是组件均独立更新。<code>react</code> 与 <code>vue</code> 一样相对 <code>angularjs</code> 也是局部更新,只是 <code>react</code> 中的局部是以当前组件为根以及之下的所有子组件。</p>
<p>打个比方,如果组件 <code>A</code> 状态发生变化,那么 <code>A</code> 的所有子组件默认都会触发更新,即使子组件的<code>props</code>未发生改变,所以对于<code>react</code>我们需要使用 <code>PureComponent</code>、<code>shouldComponentUpdate</code> 以及 <code>memo</code> 来避免这种场景下的多余渲染。而在更新体系中,<code>react</code> 与 <code>vue</code> 都引入了虚拟 <code>DOM</code> 的概念,当然这也是本文需要探讨的重点。</p>
<p>我们先总结下上述的观点:</p>
<p><code>js</code> 和 <code>jq</code>:研发在专注业务的同时,还要亲自操作 <code>dom</code>。</p>
<p><code>angularjs版本1</code>:将研发从操作 <code>dom</code> 中解脱了出来,更新 <code>dom</code> 交由 <code>angularjs</code> 底层实现,这一套机制由脏检测机制所支撑。</p>
<p><code>react/vue</code>:同样由底层更新 <code>dom</code>,只是在此之前多了虚拟<code>dom</code>的对比,先对比再更新,以此达到最小更新目的。</p>
<p>所以相对传统更新 <code>dom</code> 的策略,虚拟<code>dom</code>的更新如下:</p>
<div align="center"><img src="https://img2022.cnblogs.com/blog/1213309/202206/1213309-20220609163749516-249351160.png"></div>
<p>到这里,我们站在宏观的角度解释了前端框架的变迁,以及有虚拟<code>dom</code>前后我们如何更新<code>dom</code>,也许到这里你的脑中隐约对于虚拟<code>dom</code>有了一丝感悟,但又不是很清晰,虚拟<code>dom</code>到底解决了什么问题,别着急,接下来才是虚拟<code>dom</code>的正餐,我们接着聊。</p>
<h1 id="叁--什么是虚拟dom">叁 ❀ 什么是虚拟DOM?</h1>
<p>本文将默认你有 <code>react</code> 或者 <code>vue</code> 的开发经历,当然本文出发点还是以<code>react</code>为主。</p>
<p>熟悉 <code>react</code> 的同学对于 <code>React.createElement</code> 方法一定不会陌生,它用于创建<code>reactNode</code>,语法如下:</p>
<pre><code class="language-javascript">/*
* component 组件名,一个标签也可以理解成一个最基础的组件
* props 当前组件的属性,比如class,或者其它属性
* children 组件的子组件,就像标签套标签
*/
React.createElement(component, props, ...children)
</code></pre>
<p>比如我们定一个最简单的<code>html</code>片段:</p>
<pre><code class="language-html">&lt;span className='span'&gt;hello echo&lt;/span&gt;
</code></pre>
<p>用<code>React.createElement</code>表示如下:</p>
<pre><code class="language-javascript">React.createElement('div', {className:'span'}, 'hello echo');
</code></pre>
<p>这样看好像也没什么大问题,但是假定我们<code>dom</code>存在嵌套关系:</p>
<pre><code class="language-html">&lt;div className='span'&gt;
&lt;span&gt;
    hello echo
&lt;/span&gt;
&lt;/div&gt;
</code></pre>
<p>用<code>React.createElement</code>表示就相对比较麻烦了,你需要在<code>createElement</code>中不断嵌套:</p>
<pre><code class="language-javascript">React.createElement('span', {className:'span'}, React.createElement("span", null, "hello echo"));
</code></pre>
<p>这还仅仅是两层嵌套,实际开发中<code>dom</code>结构往往要复杂的多,因此<code>react</code>中我们常常推荐直接使用<code>jsx</code>文件定义业务逻辑以及<code>html</code>片段。</p>
<p>我们可以将<code>jsx</code>中定义的<code>html</code>模板理解成<code>React.createElement</code>的语法糖,它方便了开发者以<code>html</code>的习惯去定义<code>reactNode</code>片段,而在编译之后,这些<code>reactNode</code>本质上还是会被转变成<code>React.createElement</code>所创建的对象,这个过程可以理解为:</p>
<div align="center"><img src="https://img2022.cnblogs.com/blog/1213309/202206/1213309-20220609163751367-120912735.png"></div>
<p>为方便理解,我们可以将<code>React.createElement</code>创建对象结构抽象为:</p>
<pre><code class="language-javascript">const VitrualDom = {
type: 'span',
props: {
    className: 'span'
},
children: [{
    type: 'span',
    props: {},
    children: 'hello echo'
}]
}
</code></pre>
<p>说到底,这个就是传递给<code>React.createElement</code>的结构,而<code>React.createElement</code>接收后生成的数据,其实才是真正意义上的虚拟<code>dom</code>。我们可以简单定一个<code>react</code>组件,来查看虚拟<code>dom</code>真正的结构:</p>
<pre><code class="language-javascript">class C extends React.PureComponent {
render() {
    console.log(this.props.children);
    return &lt;div&gt;{this.props.children}&lt;/div&gt;;
}
}

class P extends Component {
render() {
    return (
      &lt;C&gt;
      &lt;span className="span"&gt;
          &lt;span&gt;hello echo&lt;/span&gt;
      &lt;/span&gt;
      &lt;/C&gt;
    );
}
}
</code></pre>
<div align="center"><img src="https://img2022.cnblogs.com/blog/1213309/202206/1213309-20220609163753123-1673233804.png"></div>
<p>那么到这里,我们搞清楚了虚拟<code>DOM</code>究竟是什么,所谓虚拟<code>DOM</code>其实只是一个包含了标签类型<code>type</code>,属性<code>props</code>以及它包含子元素<code>children</code>的对象。</p>
<h1 id="肆--虚拟dom的优势是什么">肆 ❀ 虚拟DOM的优势是什么?</h1>
<h3 id="肆--壹-销毁重建与局部更新">肆 ❀ 壹 销毁重建与局部更新</h3>
<p>在提及虚拟<code>DOM</code>的优势之前,我们可以先抛开什么虚拟<code>DOM</code>以及什么<code>MVC</code>思想,回想下在纯 <code>js</code> 或者 <code>jq</code> 开发角度,我们是如何连接<code>UI</code>和数据层的。其实在16年之前,博主所经历的项目开发中,<code>UI</code>和数据处理都是强耦合,比如我们页面渲染完成,使用<code>onload</code>进行监听,然后发起<code>ajax</code>请求,并在回调中加工数据,以及在此生成<code>DOM</code>片段,并将其替换到需要更新的地方。</p>
<p>打个比方,后端返回了一个用户列表<code>userList</code>:</p>
<pre><code class="language-javascript">const userList = [
'echo',
'听风是风',
'时间跳跃'
]
</code></pre>
<p>前端在请求完成,于是在<code>ajax</code>回调中进行<code>dom</code>片段生成以及替换工作,比如:</p>
<pre><code class="language-html">&lt;ul id='userList'&gt;&lt;/ul&gt;
</code></pre>
<pre><code class="language-javascript">const ulDom = document.querySelector('#userList');
// 生成代码片段
const fragment = document.createDocumentFragment();

for (let i = 0; i &lt; userList.length; i++) {
const liDom = document.createElement("li");
liDom.innerHTML = userList;
// 依次生成li,并加入到代码片段
fragment.appendChild(liDom);
}

// 最终将代码片段塞入到ul
ulDom.appendChild(fragment);
</code></pre>
<p>所以不管是页面初始化,还是之后用户通过事件发起请求更新了用户数据,到头来还是都是调用上面生成<code>li</code>的这段逻辑。在当时能想着把这段逻辑复用成一个方法,再考虑用上<code>createDocumentFragment</code>减少操作<code>dom</code>的次数,能做到这些,这在当时都是能小吹一波的了....</p>
<div align="center"><img src="https://img2022.cnblogs.com/blog/1213309/202206/1213309-20220609163755826-280069838.png"></div>
<p>所以你会发现,在原生<code>js</code>的角度,根本没有所谓的<code>dom</code>对比,都是重新创建,因为在写代码之前,我们已经<strong>明确知道了哪部分是静态页面,哪部分需要结合数据进行动态展示</strong>。那么只需要将需要动态生成的<code>dom</code>的逻辑提前封装成方法,然后在不同时期去调用,这在当年已经是非常不错的复用了(组件的前生)。</p>
<p>那么问题来了,假定现在我们有一个类似<code>form</code>表单的展示功能,点击不同用户,表单就会展示用户名,年龄等一系列信息:</p>
<div align="center"><img src="https://img2022.cnblogs.com/blog/1213309/202206/1213309-20220609163757491-1310501744.png"></div>
<p>用<code>js</code>写怎么做?还是一样的,点击不同用户,肯定会得到一个用户信息对象,我们根据这个对象动态生成多个信息展示的<code>input</code>等相关<code>dom</code>,然后塞入到<code>form</code>表单中,所以每次点击,这个<code>form</code>其实都等同于<strong>完全重建</strong>了。</p>
<p>假定现在我们不希望完整重建这个结构,而是希望做前后<code>dom</code>节点对比,比如<code>input</code>的<code>value</code>前后不一样,某个<code>style</code>颜色不同,我们单点更新这个属性,比较笨拙的想法肯定还是得生成一份新<code>dom</code>片段,然后递归对比两个结构,且属性一一对比,只有不同的部分我们才需要更新。但仅仅通过下面这段代码,你就能预想到这个做法的性能有多糟糕了:</p>
<pre><code class="language-javascript">// 一个li节点自带的属性就有307个
const liDom = document.createElement("li");
let num = 0;
for (let key in liDom) {
num += 1;
}
console.log(num); // 307
</code></pre>
<p>我们生成了一个最基本的<code>li</code>节点,并通过遍历依次访问节点的属性,经过统计发现<code>li</code>单属性就<code>307</code>个,而这仅仅是一个节点。</p>
<p>在前面我们也提到过,不管是<code>jq</code>封装,还是<code>react vue</code>的模板语法,它的前提一定是研发自己提前知道了哪部分内容未来是可变的,所以我们才要动态封装,才需要使用<code>{}</code>进行包裹,那既然如此,我们就对比<strong>未来可能会变的部分</strong>不是更好吗?</p>
<p>而回到上文我们对于虚拟结构的抽象,对于<code>react</code>而言,<code>props</code>是可变的,<code>child</code>是可变的,<code>state</code>也是可变的,而这些属性恰好都在虚拟<code>dom</code>中均有呈现。</p>
<p>所以到这里,我们解释了虚拟<code>dom</code>的第一个优势,站在对比更新的角度,虚拟<code>dom</code>能聚焦于需要对比什么,相对原生<code>dom</code>它提供更高效的对比可行性。</p>
<h3 id="肆--贰-更佳的兼容性">肆 ❀ 贰 更佳的兼容性</h3>
<p>我们在上文提到,<code>react与babel</code>将<code>jsx</code>转成了<code>js</code>对象(虚拟dom),之后又通过<code>render</code>生成<code>dom</code>,那为啥还要转成<code>js</code>而不是直接生成<code>dom</code>呢,因为在这个中间<code>react</code>还需要做<code>diff</code>对比,兼容处理,以及跨平台的考虑,我们先说兼容处理。</p>
<p>准确来说,虚拟<code>dom</code>只是<code>react</code>中的一部分,要真正体现虚拟<code>dom</code>的价值,肯定得结合<code>react</code>中的其它设计来一起讲,其中一点就是结合合成事件所体现的强大的兼容性。</p>
<p>我们在介绍<code>jq</code>时强调了它在操作<code>dom</code>的便捷,以及各类<code>api</code>兼容性上的贡献,而<code>react</code>中使用了虚拟<code>dom</code>也做了大量的兼容。</p>
<p>打个比方,原生的<code>input</code>有<code>change</code>事件,普通的<code>div</code>总没有<code>onchange</code>事件吧?不管你有没有留意,其实<code>dom</code>和事件在底层已经做了强关联,不同的<code>dom</code>能触发的事件,浏览器在一开始就已经定义好了,而且你根本改不了。</p>
<p>但是虚拟<code>dom</code>就不同了,虚拟<code>dom</code>一方面模仿了原生<code>dom</code>的行为,其次在事件方面也做了合成事件与原生事件的映射关系,比如:</p>
<pre><code class="language-javascript">{
onClick: ['click'],
onChange: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange']
}
</code></pre>
<p><code>react</code>暴露给我们的合成事件,其实在底层会关联到多个原生事件,通过这种做法抹平了不同浏览器之间的<code>api</code>差异,也带来了更强大的事件系统。</p>
<p>若对于合成事件若感兴趣,可以阅读博主八千字长文深入了解react合成事件底层原理,原生事件中阻止冒泡是否会阻塞合成事件?一文。</p>
<h3 id="肆--叁-渲染优化">肆 ❀ 叁 渲染优化</h3>
<p>我们知道<code>react</code>遵循<code>UI = Render(state)</code>,只要<code>state</code>发生了改变,那么<code>render</code>就会重新触发,以达到更新<code>ui</code>层的效果。而更改<code>state</code>依赖了<code>setState</code>,大家都知道<code>setState</code>对于<code>state</code>更新的行为其实是异步的,假设我们在一次事件中更改了多次<code>state</code>,你会发现页面也仅会渲染一次。</p>
<p>而假定我们是直接操作<code>dom</code>,那还有哪门子的异步和渲染等待,当你<code>append</code>完一个子节点,页面早渲染完了。所以虚拟<code>dom</code>的对比提前,以及<code>setState</code>的异步处理,本质上也是在像尽可能少的操作<code>dom</code>靠近。</p>
<p>若对于<code>setState</code>想有更深入的了解,可以阅读博主这两篇文章:</p>
<p>react中的setState是同步还是异步?react为什么要将其设计成异步?</p>
<p>react 聊聊setState异步背后的原理,react如何感知setState下的同步与异步?</p>
<h3 id="肆--肆-跨平台能力">肆 ❀ 肆 跨平台能力</h3>
<p>同理,之所以加入虚拟<code>dom</code>这个中间层,除了解决部分性能问题,加强兼容性之外,还有个目的是将<code>dom</code>的更新抽离成一个公共层,别忘了<code>react</code>除了做页面引用外,<code>react</code>还支持使用<code>React Native</code>做原生<code>app</code>。所以针对同一套虚拟<code>dom</code>体系,<code>react</code>只是在最终将体现在了不同的平台上而已。</p>
<h1 id="伍--虚拟dom比原生快吗">伍 ❀ 虚拟DOM比原生快吗?</h1>
<p>那么问题来了,聊了这么久的虚拟<code>dom</code>,虚拟<code>dom</code>性能真的比操作原生<code>dom</code>要更快吗?很遗憾的说,并不是,或者说不应该这样粗暴的去对比。</p>
<p>我们在前面虽然对比了虚拟<code>dom</code>属性以及原生<code>dom</code>的属性量级,但事实上我们并不会对原生<code>dom</code>属性进行递归对比,而是直接操作<code>dom</code>。而且站在<code>react</code>角度,即便经历了<code>diff</code>算法以及一系列的优化,<code>react</code>到头来还是要操作原生<code>dom</code>,只是对于研发来讲不用关注这一步罢了。</p>
<p>所以我们可以想象一下,现在要替换<code>p</code>标签的内容,用原生就是直接修改<code>innerHTML</code>属性,对于<code>react</code>而言它需要先生成虚拟<code>dom</code>,然后新旧<code>diff</code>找出变化的部分,最后才修改原生<code>dom</code>,单论这个例子,一定是原生快。</p>
<p>但我们既然说虚拟<code>dom</code>,就一定得结合<code>react</code>的使命来解释,虚拟<code>dom</code>的核心目的是<strong>模拟了原生<code>dom</code>大部分特性,让研发高效无痛写<code>html</code>的同时,还达到了单点刷新而不是整个替换(前面表单替换的例子),最重要的,它也将研发从繁琐的<code>dom</code>操作中解放了出来</strong>。</p>
<p>总结来说,单论修改一个<code>dom</code>节点的性能,不管<code>react</code>还是<code>vue</code>亦或是<code>angular</code>,一定是原生最快,但虚拟<code>dom</code>有原生<code>dom</code>比不了的价值,起码<code>react</code>这些框架能让研发更专注业务以及数据处理,而不是陷入繁琐的<code>dom</code>增删改查中。</p>
<h1 id="陆--虚拟dom的实现原理">陆 ❀ 虚拟DOM的实现原理</h1>
<p>文章开头的五个问题到这里已经解释了三个,还剩两个问题均与源码有一定关系,虽然略显枯燥但我会精简给大家阐述这个过程,另外,为了让知识量不会显得格外庞大,本文将不会阐述<code>diff</code>算法与<code>fiber</code>部分,这两个知识点我会另起文章单独介绍,敬请期待。</p>
<p>除此之外,接下来两个问题的源码,我将均以<code>react17.0.2</code>源码为准,所以大家也不用担心版本差异,会不会有理解了用不上的问题,而且目前用<code>react 18</code>的公司也不会很多。</p>
<p>我们先解释虚拟<code>dom</code>的创建过程,要聊这个那必然逃不开<code>React.createElement</code>方法,github源码,具体代码如下(我删除了<code>dev</code>环境特有的逻辑):</p>
<pre><code class="language-javascript">/**
* 创建并返回给定类型的新ReactElement。
* See https://reactjs.org/docs/react-api.html#createelement
*/
function createElement(type, config, children) {
let propName;

// 创建一个全新的props对象
const props = {};

let key = null;
let ref = null;
let self = null;
let source = null;

// 有传递自定义属性进来吗?有的话就尝试获取ref与key
if (config != null) {
    if (hasValidRef(config)) {
      ref = config.ref;
    }
    if (hasValidKey(config)) {
      key = '' + config.key;
    }

    // 保存self和source
    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;

    // 剩下的属性都添加到一个新的props属性中。注意是config自身的属性
    for (propName in config) {
      if (
      hasOwnProperty.call(config, propName) &amp;&amp;
      !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
      props = config;
      }
    }
}

// 处理子元素,默认参数第二个之后都是子元素
const childrenLength = arguments.length - 2;
// 如果子元素只有一个,直接赋值
if (childrenLength === 1) {
    props.children = children;
} else if (childrenLength &gt; 1) {
    // 如果是多个,转成数组再赋予给props
    const childArray = Array(childrenLength);
    for (let i = 0; i &lt; childrenLength; i++) {
      childArray = arguments;
    }
    props.children = childArray;
}

// 处理默认props,不一定有,有才会遍历赋值
if (type &amp;&amp; type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      // 默认值只处理值不是undefined的属性
      if (props === undefined) {
      props = defaultProps;
      }
    }
}

// 调用真正的React元素创建方法
return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
}
</code></pre>
<p>代码看着好像有点多,但其实一共就只做了两件事:</p>
<ul>
<li>根据<code>createElement</code>所接收参数<code>config</code>做数据加工与赋值。</li>
<li>加工完数据后调用真正的虚拟<code>dom</code>创建<code>API ReactElement</code>。</li>
</ul>
<p>而数据加工部分可分为三步,大家可以对应上面代码理解,其实注释写的也很清晰了:</p>
<ul>
<li>第一步,判断<code>config</code>有没有传,不为<code>null</code>就做处理,步骤分为
<ul>
<li>判断<code>ref</code>、<code>key</code>,<code>__self</code>、<code>__source</code>这些是否存在或者有效,满足条件就分别赋值给前面新建的变量。</li>
<li>遍历<code>config</code>,并将<code>config</code>自身的属性依次赋值给前面新建<code>props</code>。</li>
</ul>
</li>
<li>第二步,处理子元素。默认从第三个参数开始都是子元素。
<ul>
<li>如果子元素只有一个,直接赋值给<code>props.children</code>。</li>
<li>如果子元素有多个,转成数组后再赋值给<code>props.children</code>。</li>
</ul>
</li>
<li>第三步,处理默认属性<code>defaultProps</code>,一个纯粹的标签也可以理解成一个最最最基础的组件,而组件支持 <code>defaultProps</code>,所以这一步判断有没有<code>defaultProps</code>,如果有同样遍历,并将值不为<code>undefined</code>的部分都拷贝到<code>props</code>对象上。</li>
</ul>
<p>至此,第一大步全部做完,紧接着调用<code>ReactElement</code>,我们接着看这一块的源码,同样我删掉<code>dev</code>部分的逻辑,然后你会发现就这么一点代码,github源码:</p>
<pre><code class="language-javascript">const ReactElement = function (type, key, ref, self, source, owner, props) {
const element = {
    // 这个标签允许我们将其标识为唯一的React Element
    $$typeof: REACT_ELEMENT_TYPE,
    // 元素的内置属性
    type: type,
    key: key,
    ref: ref,
    props: props,
    // 记录负责创建此元素的组件。
    _owner: owner,
};
return element;
};
</code></pre>
<p>这个方法啥也没干,单纯接受我们在上个方法加工后的数据,并将其组装成了一个<code>element</code>对象,也就是我们前文所说的虚拟<code>dom</code>。</p>
<p>不过针对这个虚拟<code>dom</code>,我们可以把<code>$$typeof: REACT_ELEMENT_TYPE</code>拧出来单独讲讲。我们可以看看它的具体实现:</p>
<pre><code class="language-javascript">// The Symbol used to tag the ReactElement-like types.
export const REACT_ELEMENT_TYPE = Symbol.for('react.element');
</code></pre>
<p>大家在查看虚拟<code>dom</code>时应该都有发现它的<code>$$typeof</code>定义为<code>Symbol(react.element)</code>,而<code>Symbol</code>一大特性就是标识唯一性,即便两个看着一模一样的<code>Symbol</code>,它们也不会相等。而<code>react</code>之所以这样做,本质也是为了防止<code>xss</code>攻击,防止外部伪造虚拟<code>dom</code>结构。</p>
<p>其次,如果大家有在开发中留意,虚拟<code>dom</code>的不允许修改,哪怕你为这个对象新增属性也不可以,这是因为在<code>ReactElement</code>方法省略的<code>dev</code>代码中,<code>react</code>使用<code>Object.freeze</code>冻结了虚拟<code>dom</code>使其无法修改。但实际上我们确实有为虚拟<code>dom</code>添加属性的场景,解决这个问题时我们可以借用顶层<code>React.cloneElement()</code>方法,它会以你传递的虚拟<code>dom</code>为模板克隆并返回一个新的虚拟<code>dom</code>对象,同时这个过程中你可以为其添加新的<code>config</code>,具体用法可见 React.cloneElement。</p>
<p>其次,如果当前环境不支持<code>Symbol</code>时,<code>REACT_ELEMENT_TYPE</code>的值为<code>0xeac7</code>。</p>
<pre><code class="language-javascript">var REACT_ELEMENT_TYPE = 0xeac7;
</code></pre>
<p>为什么是<code>0xeac7</code>呢?官方答复是,因为它看起来像<code>React</code>....好了,那么到这里,关于如何生成虚拟<code>dom</code>的源码分析结束。</p>
<h1 id="柒--react中虚拟dom是如何转变成真实dom的">柒 ❀ react中虚拟dom是如何转变成真实dom的</h1>
<p>终于,我们来到了本文的最后一个问题,要想搞清这个问题,我们的关注点自然是<code>ReactDOM.render</code>方法了,这个部分比较麻烦,大家跟着我的思路走就行。(有兴趣可以直接把<code>react</code>脚手架项目跑起来,写一个最基本的组件,然后去<code>react-dom.development.js</code>文件断点也可以)。</p>
<pre><code class="language-javascript">// 我为了方便断点,定义了一个class组件P
class P extends Component {
state = {
    name: 1,
};
handleClick = () =&gt; {};
render() {
    return &lt;span onClick={this.handleClick}&gt;111&lt;/span&gt;;
}
}
ReactDOM.render(&lt;P /&gt;, document.getElementById("root"));
</code></pre>
<p>首先我们来到<code>render</code>方法,代码如下:</p>
<pre><code class="language-javascript">function render(element, container, callback) {
        // 我删除了对于container是否合法的效验逻辑
return legacyRenderSubtreeIntoContainer(null, element, container, false, callback);
}
</code></pre>
<p><code>render</code>做的事情其实很简单,验证<code>container</code>是否合法,如果不是一个有效的<code>dom</code>就会抛错,核心逻辑看样子都在<code>legacyRenderSubtreeIntoContainer</code>中,根据命名可以推测是将组件子树都渲染到容器元素中。</p>
<pre><code class="language-javascript">// 同样,我删除了部分对主逻辑理解没啥影响的代码
function legacyRenderSubtreeIntoContainer(parentComponent, children, container, forceHydrate, callback) {
var root = container._reactRootContainer;
var fiberRoot;
        // 有fiber的root节点吗?没有就新建
if (!root) {
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);
    fiberRoot = root._internalRoot;
    unbatchedUpdates(function () {
      // 核心关注这里
      updateContainer(children, fiberRoot, parentComponent, callback);
    });
} else {
    fiberRoot = root._internalRoot;

    updateContainer(children, fiberRoot, parentComponent, callback);
}
return getPublicRootInstance(fiberRoot);
}
</code></pre>
<p>因为<code>react 16</code>引入了<code>fiber</code>的概念,所以后续其实很多代码就是在创建<code>fiber</code>节点,<code>legacyRenderSubtreeIntoContainer</code>一样,它一开始判断有没有<code>root</code>节点(一个fiber对象),很显然我们初次渲染走了新建逻辑,但不管是不是新建,最终都会调用<code>updateContainer</code>方法。但此方法没有太多我们需要关注的逻辑,一直往下走,我们会遇到一个很重要的<code>beginWork</code>(开始干正事)方法,代码如下:</p>
<pre><code class="language-javascript">function beginWork(current, workInProgress, renderLanes) {
        // 删除部分无影响的代码
workInProgress.lanes = NoLanes;

switch (workInProgress.tag) {
    // 模糊定义的组件
    case IndeterminateComponent:
      {
      return mountIndeterminateComponent(current, workInProgress, workInProgress.type, renderLanes);
      }
                // 函数组件
    case FunctionComponent:
      {
      var _Component = workInProgress.type;
      var unresolvedProps = workInProgress.pendingProps;
      var resolvedProps = workInProgress.elementType === _Component ? unresolvedProps : resolveDefaultProps(_Component, unresolvedProps);
      return updateFunctionComponent(current, workInProgress, _Component, resolvedProps, renderLanes);
      }
                // class组件
    case ClassComponent:
      {
      var _Component2 = workInProgress.type;
      var _unresolvedProps = workInProgress.pendingProps;

      var _resolvedProps = workInProgress.elementType === _Component2 ? _unresolvedProps : resolveDefaultProps(_Component2, _unresolvedProps);

      return updateClassComponent(current, workInProgress, _Component2, _resolvedProps, renderLanes);
      }
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderLanes);
}
}
</code></pre>
<p><code>beginWork</code>方法做了很重要的一件事,那就是根据你<code>render</code>接收的组件类型,来执行不同的组件更新的方法,毕竟我们可能给<code>render</code>传递一个普通标签,也可能是函数组件或者<code>Class</code>组件,亦或是<code>hooks</code>的<code>memo</code>组件等等。</p>
<p>比如我此时定义的<code>P</code>是<code>class</code>组件,于是走了<code>ClassComponent</code>路线,紧接着调用<code>updateClassComponent</code>更新组件。</p>
<pre><code class="language-javascript">function updateClassComponent(current, workInProgress, Component, nextProps, renderLanes) {
// 删除了添加context部分的逻辑
        // 获取组件实例
var instance = workInProgress.stateNode;
var shouldUpdate;
        // 如果没有实例,那就得创建实例
if (instance === null) {
    if (current !== null) {
      current.alternate = null;
      workInProgress.alternate = null;

      workInProgress.flags |= Placement;
    }
    // 全体目光向我看齐,看我看我,这里new Class创建组件实例
    constructClassInstance(workInProgress, Component, nextProps);
    // 挂载组件实例
    mountClassInstance(workInProgress, Component, nextProps, renderLanes);
    shouldUpdate = true;
} else if (current === null) {
    shouldUpdate = resumeMountClassInstance(workInProgress, Component, nextProps, renderLanes);
} else {
    shouldUpdate = updateClassInstance(current, workInProgress, Component, nextProps, renderLanes);
}
// Class组件的收尾工作
var nextUnitOfWork = finishClassComponent(current, workInProgress, Component, shouldUpdate, hasContext, renderLanes);
}
</code></pre>
<p>在看这段代码前,我们自己也可以提前想象下这个过程,比如<code>Class</code>组件你一定是得<code>new</code>才能得到一个实例,只有拿到实例后才能调用其<code>render</code>方法,拿到其虚拟<code>dom</code>结构,之后再根据结构创建真实<code>dom</code>,添加属性,最后加入到页面。</p>
<p>所以在<code>updateClassComponent</code>中,首先会对组件做<code>context</code>相关的处理,这部分代码我删掉了,其余,判断当前组件是否有实例,如果有就去更新实例,如果没有那就创建实例,所以我们聚焦到<code>constructClassInstance</code>与<code>mountClassInstance、finishClassComponent</code>三个方法,看命名就能猜到,前者一定是创造实例,后者是应该是挂载实例前的一些处理,先看第一个方法:</p>
<pre><code class="language-javascript">function constructClassInstance(workInProgress, ctor, props) {
        // 删除了对组件context进一步加工的逻辑
        // ....

// 看我看我,我宣布个事,这里创建了组件实例
// 验证了前面的推测,这里new了我们的组件,并且传递了当前组件的props以及前面代码加工的context
var instance = new ctor(props, context);
var state = workInProgress.memoizedState = instance.state !== null &amp;&amp; instance.state !== undefined ? instance.state : null;
adoptClassInstance(workInProgress, instance);

// 删除了对于组件生命周期钩子函数的处理,比如很多即将被废弃的钩子,在这里都会被添加 UNSAFE_ 前缀
//.....

return instance;
}
</code></pre>
<div align="center"><img src="https://img2022.cnblogs.com/blog/1213309/202206/1213309-20220609163759405-1233473738.png"></div>
<p><code>constructClassInstance</code>正如我们推测的一样,这里通过<code>new ctor(props, context)</code>创建了组件实例,除此之外,<code>react</code>后续版本已将部分声明周期钩子标记为不安全,对于钩子命名的加工也在此方法中。</p>
<p>紧接着,我们得到了一个组件实例,接着看<code>mountClassInstance</code>方法:</p>
<pre><code class="language-javascript">function mountClassInstance(workInProgress, ctor, newProps, renderLanes) {
        // 此方法主要是对constructClassInstance创建的实例进行数据组装,为其赋予props,state等一系列属性
var instance = workInProgress.stateNode;
instance.props = newProps;
instance.state = workInProgress.memoizedState;
instance.refs = emptyRefsObject;
initializeUpdateQueue(workInProgress);

// 删除了部分特殊情况下,对于instance的特殊处理逻辑
}
</code></pre>
<p>虽然命名是挂载,但其实离真正的挂载还远得很,本方法其实是为<code>constructClassInstance</code>创建的组件实例做数据加工,为其赋予<code>props state</code>等一系列属性。</p>
<p>在上文代码中,其实还有个<code>finishClassComponent</code>方法,此方法在组件自身都准备完善后调用,我们期待已久的<code>render</code>方法处理就在里面:</p>
<pre><code class="language-javascript">function finishClassComponent(current, workInProgress, Component, shouldUpdate, hasContext, renderLanes) {
var instance = workInProgress.stateNode;
ReactCurrentOwner$1.current = workInProgress;
var nextChildren;
if (didCaptureError &amp;&amp; typeof Component.getDerivedStateFromError !== 'function') {
                        // ...
} else {
    {
      setIsRendering(true);
      // 关注点在这,通过调用组件实例的render方法,得到内部的元素
      nextChildren = instance.render();
      setIsRendering(false);
    }
}
workInProgress.memoizedState = instance.state;
return workInProgress.child;
}
</code></pre>
<p>在此方法内部,我们通过获取之前创建的组件实例,然后调用了它的<code>render</code>方法,于是成功执行了我们组件<code>P</code>的<code>render</code>方法:</p>
<pre><code class="language-javascript">render() {
return &lt;span onClick={this.handleClick}&gt;111&lt;/span&gt;;
}
</code></pre>
<p>需要注意的是,<code>render</code>返回的其实是一个<code>jsx</code>的模板语法,在真正<code>return</code>之前,<code>react</code>还会再次调用生成虚拟<code>dom</code>的逻辑也就是<code>ReactElement</code>方法,将<code>span</code>这一段转变成虚拟<code>dom</code>。</p>
<p>而对于<code>react</code>而言,很明显虚拟<code>dom</code>的<code>span</code>也可能理解成一个最最最基础的组件,所以它会重走<code>beginWork</code>这条路线,只是到了组件分类时,这一次会走<code>HostComponent</code>路线,然后触发<code>updateHostComponent</code>方法,我们直接跳过相同的流程,之后就会走到<code>completeWork</code>方法。</p>
<p>到这里,我们可以理解例子<code>P</code>组件虚拟<code>dom</code>都准备完毕,现在要做的是对于<code>虚拟</code>dom这种最基础的组件做转成真实<code>dom</code>的操作,见如下代码:</p>
<pre><code class="language-javascript">function completeWork(current, workInProgress, renderLanes) {
var newProps = workInProgress.pendingProps;
        // 根据tag类型做不同的处理
switch (workInProgress.tag) {
    // 标签类的基础组件走这条路
    case HostComponent:
      {
      popHostContext(workInProgress);
      var rootContainerInstance = getRootHostContainer();
      var type = workInProgress.type;

      if (current !== null &amp;&amp; workInProgress.stateNode != null) {
          // ...
      } else {
          // ...
          } else {
            // 关注点1:创建虚拟dom的实例
            var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
            appendAllChildren(instance, workInProgress, false, false);
            workInProgress.stateNode = instance; // Certain renderers require commit-time effects for initial mount.
            // 关注点2:初始化实例的子元素
            if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) {
            markUpdate(workInProgress);
            }
          }
      }
      }
}
}
</code></pre>
<p>可以猜到,虽然同样还是调用<code>createInstance</code>生成实例,但目前咱们的组件是个虚拟<code>dom</code>对象啊,一个普通的<code>span</code>标签,所以接下来一定会创建最基本的<code>span</code>节点,代码如下:</p>
<pre><code class="language-javascript">function createInstance(type, props, rootContainerInstance, hostContext, internalInstanceHandle) {
        // 根据span创建节点,调用createElement方法
var domElement = createElement(type, props, rootContainerInstance, parentNamespace);
precacheFiberNode(internalInstanceHandle, domElement);
// 将虚拟dom span的属性添加到span节点上
updateFiberProps(domElement, props);
return domElement;
}

// createElement具体实现
function createElement(type, props, rootContainerElement, parentNamespace) {
var isCustomComponentTag;
var ownerDocument = getOwnerDocumentFromRootContainer(rootContainerElement);
var domElement;
var namespaceURI = parentNamespace;

if (namespaceURI === HTML_NAMESPACE$1) {
    if (type === 'script') {
      var div = ownerDocument.createElement('div');
      div.innerHTML = '&lt;script&gt;&lt;' + '/script&gt;';
      var firstChild = div.firstChild;
      domElement = div.removeChild(firstChild);
    } else if (typeof props.is === 'string') {
      domElement = ownerDocument.createElement(type, {
      is: props.is
      });
    } else {
      // 在这里,真实dom span节点创建完毕
      domElement = ownerDocument.createElement(type);
      if (type === 'select') {
      var node = domElement;

      if (props.multiple) {
          node.multiple = true;
      } else if (props.size) {
          node.size = props.size;
      }
      }
    }
} else {
    domElement = ownerDocument.createElementNS(namespaceURI, type);
}
return domElement;
}
</code></pre>
<p>在<code>createElement</code>方法中,<code>react</code>会根据你的标签类型来决定怎么创建<code>dom</code>,比如如果你是<code>script</code>,那就创建一个<code>div</code>用于包裹一个<code>script</code>标签。而我们的<code>span</code>很显然就是通过<code>ownerDocument.createElement(type)</code>创建,如下图:</p>
<div align="center"><img src="https://img2022.cnblogs.com/blog/1213309/202206/1213309-20220609163800478-710099574.png"></div>
<p>创建完成后,此时的<code>span</code>节点还是一个啥都没有的空<code>span</code>,所以通过<code>updateFiberProps</code>将还未加工的<code>span</code>的子节点以及其它属性强行赋予给<code>span</code>,在之后会进一步加工,之后返回我们的<code>span</code>:</p>
<div align="center"><img src="https://img2022.cnblogs.com/blog/1213309/202206/1213309-20220609163801822-1266371902.png"></div>
<p>然后来到<code>finalizeInitialChildren</code>方法,这里开始对创建的<code>span</code>节点的子元素进一步加工,其实就是文本<code>111</code>,</p>
<pre><code class="language-javascript">function finalizeInitialChildren(domElement, type, props, rootContainerInstance, hostContext) {
// 实际触发的其实是这个
setInitialProperties(domElement, type, props, rootContainerInstance);
return shouldAutoFocusHostComponent(type, props);
}

// 跳过对于部分,接着看 setInitialDOMProperties
function setInitialProperties(domElement, tag, rawProps, rootContainerElement) {
var props;

switch (tag) {
                // ...
    default:
      props = rawProps;
}
        // 验证props合法性
assertValidProps(tag, props);
// 正式设置props
setInitialDOMProperties(tag, domElement, rootContainerElement, props, isCustomComponentTag);
}
}
</code></pre>
<p>又是一系列的跳转,为<code>dom</code>设置属性的逻辑现在又聚焦在了<code>setInitialDOMProperties</code>中,我们直接看代码:</p>
<pre><code class="language-javascript">function setInitialDOMProperties(tag, domElement, rootContainerElement, nextProps, isCustomComponentTag) {
for (var propKey in nextProps) {
    // 遍历所有属性,只要这个属性不是原型属性,那就开始正式处理
    if (!nextProps.hasOwnProperty(propKey)) {
      continue;
    }

    var nextProp = nextProps;
                // 如果属性是样式,那就通过setValueForStyles为dom设置样式
    if (propKey === STYLE) {
      {
      if (nextProp) {
          Object.freeze(nextProp);
      }
      }
      setValueForStyles(domElement, nextProp);
    } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {

    } else if (propKey === CHILDREN) {
      if (typeof nextProp === 'string') {
      var canSetTextContent = tag !== 'textarea' || nextProp !== '';
      if (canSetTextContent) {
          // 设置文本属性
          setTextContent(domElement, nextProp);
      }
      } else if (typeof nextProp === 'number') {
      setTextContent(domElement, '' + nextProp);
      }
    } else if (propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING) ; else if (propKey === AUTOFOCUS) ; else if (registrationNameDependencies.hasOwnProperty(propKey)) {
      if (nextProp != null) {
      if ( typeof nextProp !== 'function') {
          warnForInvalidEventListener(propKey, nextProp);
      }

      if (propKey === 'onScroll') {
          listenToNonDelegatedEvent('scroll', domElement);
      }
      }
    } else if (nextProp != null) {
      setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
    }
}
}
</code></pre>
<p>这段代码看着有点长,其实做的事情非常的清晰,遍历<code>span</code>目前的<code>props</code>,如果<code>props</code>的<code>key</code>是<code>style</code>,那就通过<code>setValueForStyles</code>为当前真实<code>dom</code>一一设置样式,如果<code>key</code>是<code>children</code>,很明显我们虚拟<code>dom</code>的<code>111</code>是放在<code>children</code>属性中的,外加上如果这个<code>children</code>类型还是<code>string</code>,那就通过<code>setTextContent</code>为<code>dom</code>添加文本信息。</p>
<p>这里给大家展示为真实<code>dom</code>设置<code>style</code>以及设置<code>innerHTML</code>的源码:</p>
<pre><code class="language-javascript">// 为真实dom添加样式的逻辑
function setValueForStyles(node, styles) {
// 获取真是dom的style对象,后面就遍历styles对象,依次覆盖
var style = node.style;
for (var styleName in styles) {
    if (!styles.hasOwnProperty(styleName)) {
      continue;
    }
    var isCustomProperty = styleName.indexOf('--') === 0;
    {
      if (!isCustomProperty) {
      warnValidStyle$1(styleName, styles);
      }
    }
    // 获取样式的值
    var styleValue = dangerousStyleValue(styleName, styles, isCustomProperty);
    if (styleName === 'float') {
      styleName = 'cssFloat';
    }
                // 最终覆盖node节点原本的值
    if (isCustomProperty) {
      style.setProperty(styleName, styleValue);
    } else {
      style = styleValue;
    }
}
}

// 为真实dom添加innerHTML的逻辑
var setTextContent = function (node, text) {
if (text) {
    var firstChild = node.firstChild;

    if (firstChild &amp;&amp; firstChild === node.lastChild &amp;&amp; firstChild.nodeType === TEXT_NODE) {
      firstChild.nodeValue = text;
      return;
    }
}
// 为真实dom设置文本信息
node.textContent = text;
};
</code></pre>
<p>那么到这里,其实我们的组件<code>P</code>已经准备完毕,包括真实<code>dom</code>也都创建好了,就等插入到页面了,那这些<code>dom</code>什么时候插入到页面的呢?后面我又跟了下调用栈,根据我页面啥时候绘制的<code>111</code>一步步断点缩小范围,最终定位到了<code>insertOrAppendPlacementNodeIntoContainer</code>方法,直译过来就是将节点插入或者追加到容器节点中:</p>
<pre><code class="language-javascript">function insertOrAppendPlacementNodeIntoContainer(node, before, parent) {
var tag = node.tag;
var isHost = tag === HostComponent || tag === HostText;
if (isHost || enableFundamentalAPI ) {
    var stateNode = isHost ? node.stateNode : node.stateNode.instance;
    if (before) {
      // 在容器节点前插入
      insertInContainerBefore(parent, stateNode, before);
    } else {
      // 在容器节点后追加
      appendChildToContainer(parent, stateNode);
    }
} else if (tag === HostPortal) ; else {
    var child = node.child;
                // 只要子节点不为null,继续递归调用
    if (child !== null) {
      insertOrAppendPlacementNodeIntoContainer(child, before, parent);
      var sibling = child.sibling;
                        // 只要兄弟节点不为null,继续递归调用
      while (sibling !== null) {
      insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
      sibling = sibling.sibling;
      }
    }
}
}
</code></pre>
<p>在<code>insertOrAppendPlacementNodeIntoContainer</code>中,<code>react</code>会根据当前节点是否有子节点,或者兄弟节点进行递归调用,然后分别根据<code>insertInContainerBefore</code>与<code>appendChildToContainer</code>做最终的节点插入页面操作,这里我们看看<code>appendChildToContainer</code>的实现:</p>
<pre><code class="language-javascript">function appendChildToContainer(container, child) {
var parentNode;

if (container.nodeType === COMMENT_NODE) {
    parentNode = container.parentNode;
    parentNode.insertBefore(child, container);
} else {
    parentNode = container;
    // 将子节点插入到父节点中
    parentNode.appendChild(child);
var reactRootContainer = container._reactRootContainer;

if ((reactRootContainer === null || reactRootContainer === undefined) &amp;&amp; parentNode.onclick === null) {
    // TODO: This cast may not be sound for SVG, MathML or custom elements.
    trapClickOnNonInteractiveElement(parentNode);
}
}
</code></pre>
<p>由于我们定义的组件非常简单,<code>P</code>组件只有一个<code>span</code>标签,所以这里的<code>parentNode</code>其实就是容器根节点,当执行完<code>parentNode.appendChild(child)</code>,可以看到页面就出现了<code>111</code>了。</p>
<div align="center"><img src="https://img2022.cnblogs.com/blog/1213309/202206/1213309-20220609163803240-717628951.png"></div>
<p>至此,组件的虚拟<code>dom</code>生成,真实<code>dom</code>的创建,加工以及渲染全部执行完毕。</p>
<p>可能大家对于这个过程还是比较迷糊,我大致画个图描述下这个过程:</p>
<div align="center"><img src="https://img2022.cnblogs.com/blog/1213309/202206/1213309-20220609163805896-567787527.png"></div>
<p>而<code>react</code>是怎么知道谁是谁的子节点,谁是谁的父节点,这个就需要了解<code>fiber</code>对象了,其实我们在创建完真实<code>dom</code>后,它还是会被加工成一个<code>fiber</code>节点,而此节点中通过<code>child</code>可以访问到自己的子节点,通过<code>sibling</code>获取自己的兄弟节点,最后通过<code>return</code>属性获取自己的父节点,通过这些属性为构建<code>dom</code>树提供了支撑,当然<code>fiber</code>我会另开一篇文章来解释,这里不急。</p>
<p>前文,我们验证了<code>Class</code>组件是通过<code>new</code>得到组件实例,然后开展后续操作,那对于函数组件,是不是直接调用拿到子组件呢?这里我简单跟了下源码,发现了如下代码:</p>
<div align="center"><img src="https://img2022.cnblogs.com/blog/1213309/202206/1213309-20220609163807783-2045803986.png"></div>
<pre><code class="language-javascript">function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes) {
// ....
var children = Component(props, secondArg);
}
</code></pre>
<p>可以发现确实如此,拿到子节点,然后后续还是跟之前一样,将虚拟<code>dom</code>转变成真实<code>dom</code>,以及后续的一系列操作。</p>
<p>不过有点意外的是,我以为我定义的函数组件在判断组件类型时,会走<code>case FunctionComponent</code>分支路线,结果它走的<code>case IndeterminateComponent</code>,也就是模糊定义的组件,不过影响不大,还是符合我们的推测。</p>
<p>好了,到这里,我已经写了一万字,关于虚拟<code>dom</code>如何转变成真实<code>dom</code>也介绍完毕了。</p>
<h1 id="捌--我是如何阅读源码的">捌 ❀ 我是如何阅读源码的</h1>
<p>在文章结束前,我顺带分享下我是如何阅读<code>react</code>源码的,本来在写这篇文章前,我也想着要不查查资料,看看大家都是怎么写的,结果部分高赞的文章基本发布时间都在<code>19</code>年,那时候的<code>react</code>版本基本都是<code>15</code>,连<code>fiber</code>的概念都没有,无奈之下我只能自己来尝试读源码并解决我自己提出的问题。如果将源码阅读理解成一次探险,我是这样做的。</p>
<h3 id="捌--壹-确定阅读前的目标">捌 ❀ 壹 确定阅读前的目标</h3>
<p><code>react</code>的源码比较多,一个<code>react</code>一个<code>react-dom</code>加起来代码量都几万行了,所以在读之前,一定要搞清楚自己的目标,这样你也能少受不重要逻辑的干扰,比如我在阅读之前初步定下的目标是:</p>
<ul>
<li>虚拟<code>dom</code>是怎么生成的?</li>
<li>函数组件和<code>class</code>组件渲染有什么不同?</li>
<li>为啥我之前尝试直接修改虚拟<code>dom</code>,添加属性没成功(对应后面typeof Symbol的解释)</li>
<li>虚拟<code>dom</code>是怎么转变成真实<code>dom</code>的?</li>
<li>啥时候才把真实<code>dom</code>插入到页面?</li>
<li>...</li>
</ul>
<p>清晰了目标,那就可以找到起点开始看了,我要看渲染,那自然看<code>render</code>,但接下来就麻烦了,如果你跟着<code>render</code>一步步往下走,那估计你看不了五分钟,应该就没耐心看了,因为这里面存在大量你根本看不懂,或者对你帮助不大的代码,那么我是怎么做的呢?</p>
<h3 id="捌--贰-以点成线">捌 ❀ 贰 以点成线</h3>
<p>我要看虚拟<code>dom</code>转变真实<code>dom</code>,<code>react</code>到头来还是要操作真实<code>dom</code>,那它就一定得通过原生的<code>createElement</code>来创建<code>dom</code>节点,所以我直接在源码中搜<code>createElement</code>,然后看看这些命名出现的上下文,根据语境大致推断是否是自己想要的,不确定也可以打个断点。</p>
<p>哎,然后我就发现我成功找到<code>function createElement</code>方法,而且它还真是我想要的方法,但是呢,此时逻辑距离<code>render</code>可谓是十万八千里,这中间究竟发生了什么?这时候就可以根据执行栈进行梳理:</p>
<div align="center"><img src="https://img2022.cnblogs.com/blog/1213309/202206/1213309-20220609163809448-1216463734.jpg"></div>
<p>比如上图就是我定位到给真实<code>dom</code>添加属性的方法,然后我根据调用栈命名,大致知道它在干嘛,同时排除那些没意义的函数的干扰,从终点反向走回起点,看看这一路<code>react</code>是怎么处理的。</p>
<p>同理,我在找最后<code>react</code>将真实<code>dom</code>插入到页面的逻辑时,我发现我跟不下去了,因为断点乱跳,于是我就看页面渲染<code>111</code>的时机,然后初略断点,如果这个断点还没走到<code>111</code>已经渲染了,说明这个操作在之前,通过这种方式不断缩小范围范围,最终定位到了<code>insertOrAppendPlacementNodeIntoContainer</code>方法,也解开了我前面的疑惑。</p>
<h3 id="捌--叁-以线成面">捌 ❀ 叁 以线成面</h3>
<p>通过以点连线的方式,你能非常快的理清一小段一小段的逻辑,而这些逻辑的交叉,阅读前的目标就逐渐清晰了。比如我在梳理了<code>Class</code>组件后,我就在想,函数组件又是怎么渲染的?于是非常快的定位到了函数组件渲染子节点的逻辑。</p>
<p>我们可以把源码理解成夜晚的星空,小时候总是喜欢选几个点练成线,再用线连成图案,什么北极星织女星,不就是这样画出来的吗,而现在只是将这种做法投射到了源码阅读中罢了。</p>
<h1 id="玖--总">玖 ❀ 总</h1>
<p>写到这已经一万一千字,差不多一篇论文的长度了。而这篇文章,从查资料,读源码到写作结束,也差不多用了我一周的零碎时间。一开始只是想写写概念,写着写着对自己要求越来越高,于是一篇文章写得停不下来了,不过好在终于写到了尾声,我也松了口气了(下一篇<code>fiber</code>感觉也很难受的样子)。</p>
<p>通过本文,我们介绍了虚拟<code>dom</code>的概念,了解了究竟什么是虚拟<code>dom</code>。结合文章开头框架发展史,我们也解释了虚拟<code>dom</code>存在的价值以及它所具备的优势,而且框架之间也不应该盲目的去对比。在文章后半段,我们介绍了<code>React.createElement</code>与<code>ReactDOM.render</code>的源码,理解了虚拟<code>dom</code>的创建过程,以及<code>react</code>是如何将虚拟<code>dom</code>转变成真实<code>dom</code>的,如果有时间,我也推荐大家自行断点,根据我的提示来加深理解这个过程,它并不难,只是需要足够的耐心。</p>
<p>希望本文能为有缘的你提供一些帮助,那么本文到这里正式结束。</p><br><br>
来源:https://www.cnblogs.com/echolun/p/16359890.html
頁: [1]
查看完整版本: [react] 什么是虚拟dom?虚拟dom比操作原生dom要快吗?虚拟dom是如何转变成真实dom并渲染到页面的?