每天⼀点⾯试题(16)--------虚拟DOM
JSX的背后
这个过程⼀般在前端会称为“转译”,但其实“汇编”将是⼀个更精确的术语。
React开发⼈员敦促你在编写组件时使⽤⼀种称为JSX的语法,混合了HTML和JavaScript。但浏览器对JSX及其语法毫⽆头绪,浏览器只能理解纯碎的JavaScript,所以JSX必须转换成JavaScript。这⾥是⼀个div的JSX代码,它有⼀个class name和⼀些内容:
<div className='cn'>
Content!
</div>
以上的代码,被转换成“正经”的JavaScript代码,其实是⼀个带有⼀些参数的函数调⽤:
'div',
{ className:'cn'},
'Content!'
);
让我们仔细看看这些参数。
1. 第⼀个是元素的type。对于HTML标签,它将是⼀个带有标签名称的字符串。
2. 第⼆个参数是⼀个包含所有元素属性(attributes)的对象。如果没有,它也可以是空的对象。
3. 剩下的参数都可以认为是元素的⼦元素(children)。元素中的⽂本也算作⼀个child,是个字符串’Content!’ 作为函数调⽤的第
三个参数放置。
你应该可以想象,当我们有更多的children时会发⽣什么:
<div className='cn'>
Content 1!
<br />
Content 2!
</div>
'div',
{ className:'cn'},
'Content 1!',// 1st child
'Content 2!'// 3rd child
)
我们的函数现在有五个参数:
⼀个元素的类型
⼀个属性对象
三个⼦元素。
因为其中⼀个child是⼀个React已知的HTML标签(
),所以它也会被描述为⼀个函数调⽤(ateElement(‘br’))。
到⽬前为⽌,我们已经涵盖了两种类型的children:
简单的String
另⼀种会调⽤ateElement。
然⽽,还有其他值可以作为参数:
基本类型 false, null, undefined, true
数组
React Components
可以使⽤数组是因为可以将children分组并作为⼀个参数传递:
'div',
{ className:'cn'},
['Content 1!', ateElement('br'),'Content 2!']
)
当然了,React的厉害之处,不仅仅因为我们可以把HTML标签直接放在JSX中使⽤,⽽是我们可以⾃定义⾃⼰的组件,例如:
function Table({ rows }){
return(
<table>
{rows.map(row =>(
<tr key={row.id}>
<td>{row.title}</td>
</tr>
))}
</table>
);
}
组件可以让我们把模板分解为多个可重⽤的块。在上⾯的“函数式”(functional)组件的例⼦⾥,我们接收⼀个包含表格⾏数据的对象数组,最后返回⼀个调⽤ateElement⽅法的
元素,rows则作为children传进table。
⽆论什么时候,我们这样去声明⼀个组件时:
<Table rows={rows}/>
从浏览器的⾓度来看,我们是这么写的:
注意,这次我们的第⼀个参数不是String描述的HTML标签,⽽是⼀个引⽤,指向我们编写组件时编写的函数。组件的attributes现在是接收的props参数了。
把组件(components)组合成页⾯(a page)
所以,我们已经将所有JSX组件转换为纯JavaScript,现在我们有⼀⼤堆函数调⽤,它的参数会被其他函数调⽤的,或者还有更多的其他函数调⽤这些参数…这些带参数的函数调⽤,是怎么转化成组成这个页⾯的实体DOM的呢?
为此,我们有⼀个ReactDOM库及其它的render⽅法:
function Table({ rows }){/* ... */}// defining a component
// rendering a component
);
当der被调⽤时,ateElement最终也会被调⽤,返回以下对象:
// There are more fields, but these are most important to us
{
type: Table,
rows函数的使用方法及实例props:{
rows: rows
},
/
/ ...
}
这些对象,在React的⾓度上,构成了虚拟DOM。
他们将在所有进⼀步的渲染中相互⽐较,并最终转化为 真正的DOM(virtual VS real, 虚拟DOM VS 真实DOM)。
下⾯是另⼀个例⼦:这次div有⼀个class属性和⼏个children:
'div',
{ className:'cn'},
'Content 1!',
'Content 2!',
);
变成:
{
type:'div',
props:{
className:'cn',
children:[
'Content 1!',
'Content 2!'
]
}
}
需要注意的是,那些除了type和attribute以外的属性,原本是单独传进来的,转换之后,会作为在props.children以⼀个数组的形式打包存在。也就是说,⽆论children是作为数组还是参数列表传递都没关系 —— 在⽣成的虚拟DOM对象的时候,它们最后都会被打包在⼀起的。
进⼀步说,我们可以直接在组件中把children作为⼀项属性传进去,结果还是⼀样的:
<div className='cn' children={['Content 1!','Content 2!']}/>
在构建虚拟DOM对象完成之后,der将会按下⾯的原则,尝试将其转换为浏览器可以识别和展⽰的DOM节点:
如果type包含⼀个带有String类型的标签名称(tag name)—— 创建⼀个标签,附带上props下所有attributes。
如果type是⼀个函数(function)或者类(class),调⽤它,并对结果递归地重复这个过程。
如果props下有children属性 —— 在⽗节点下,针对每个child重复以上过程。
最后,得到以下HTML(对于我们的表格⽰例):
<table>
<tr>
<td>Title</td>
</tr>
...
</table>
重新构建DOM(Rebuilding the DOM)
在实际应⽤场景,render通常在根节点调⽤⼀次,后续的更新会有state来控制和触发调⽤。
请注意,标题中的“重新”!当我们想更新⼀个页⾯⽽不是全部替换时,React中的魔法就开始了。我们有⼀些实现它的⽅式。我们先从最简单的开始 —— 在同⼀个node节点再次执⾏der。
// Second call
);
这⼀次,上⾯的代码的表现,跟我们已经看到的有所不同。React将启动其diff算法,⽽不是从头开始创建所有DOM节点并将其放在页⾯上,来确定节点树的哪些部分必须更新,哪些可以保持不变。
那么,它是怎样⼯作的呢?其实只有少数⼏个简单的场景,理解它们将对我们的优化帮助很⼤。请记住,现在我们在看的,是在React Virtual DOM⾥⾯⽤来代表节点的对象。
场景1:type是⼀个字符串,type在通话中保持不变,props也没有改变。
// before update
{ type:'div', props:{ className:'cn'}}
// after update
{ type:'div', props:{ className:'cn'}}
这是最简单的情况:DOM保持不变。
场景2:type仍然是相同的字符串,props是不同的。
// before update:
{ type:'div', props:{ className:'cn'}}
// after update:
{ type:'div', props:{ className:'cnn'}}
type仍然代表HTML元素,React知道如何通过标准DOM API调⽤来更改元素的属性,⽽⽆需从DOM树中删除⼀个节点。
场景3:type已更改为不同的String或从String组件。
// before update:
{ type:'div', props:{ className:'cn'}}
// after update:
{ type:'span', props:{ className:'cn'}}
React看到的type是不同的,它甚⾄不会尝试更新我们的节点:old元素将和它的所有⼦节点⼀起被删除(unmounted卸载)。因此,将元素替换为完全不同于DOM树的东西代价会⾮常昂贵。幸运的是,这在现实世界中很少发⽣。
划重点,记住React使⽤===(triple equals)来⽐较type的值,所以这两个值需要是相同类或相同函数的相同实例。
下⼀个场景更加有趣,通常我们会这么使⽤React。
场景4:type是⼀个component。
// before update:
{ type: Table, props:{ rows: rows }}
// after update:
{ type: Table, props:{ rows: rows }}
如果type是对函数或类的引⽤(即常规的React组件),并且我们启动了tree diff的过程,则React会持续
地去检查组件的内部逻辑,以确保render返回的值不会改变(类似对副作⽤的预防措施)。对树中的每个组件进⾏遍历和扫描 —— 是的,在复杂的渲染场景下,成本可能会⾮常昂贵!
值得注意的是,⼀个component的render(只有类组件在声明时有这个函数)跟der不是同⼀个函数。
关注⼦组件(children)的情况
除了上述四种常见场景之外,当⼀个元素有多个⼦元素时,我们还需要考虑React的⾏为。现在假设我们有这么⼀个元素:
// ...
props:{
children:[
{ type:'div'},
{ type:'span'},
{ type:'br'}
]
},
// ...
我们想要交换⼀下这些children的顺序:
// ...
props:{
children:[
{ type:'span'},
{ type:'div'},
{ type:'br'}
]
},
// ...
之后会发⽣什么呢?
当diffing的时候,如果React在检查props.children下的数组时,按顺序去对⽐数组内元素的话:index 0将与index 0进⾏⽐较,index 1和index 1,等等。对于每⼀次对⽐,React会使⽤之前提过的diff规则。在我们的例⼦⾥,它认为div成为⼀个span,那么就会运⽤到情景3。这样不是很有效率的:想象⼀下,我们已经从1000⾏中删除了第⼀⾏。React将不得不“更新”剩余的999个⼦项,因为按index去对⽐的话,内容从第⼀条开始就不相同了。
幸运的是,React有⼀个内置的⽅法(built-in)来解决这个问题。如果⼀个元素有⼀个key属性,那么元素将按key⽽不是index来⽐较。只要key是唯⼀的,React就会移动元素,⽽不是将它们从DOM树中移除然后再将它们放回(这个过程在React⾥叫mounting和unmounting)。
// ...
props:{
children:[// Now React will look on key, not index
{ type:'div', key:'div'},
{ type:'span', key:'span'},
{ type:'br', key:'bt'}
]
},
// ...
当state发⽣了改变
到⽬前为⽌,我们只聊了下React哲学⾥⾯的props部分,却忽视了另外很重要的⼀部分state。下⾯是⼀个简单的stateful组件:
class App extends Component {
state ={ counter:0}
increment=()=>this.setState({
counter:unter +1,
})
render=()=>(<button onClick={this.increment}>
{'Counter: '+unter}
</button>)
}
在state对象⾥,我们有⼀个keycounter。点击按钮时,这个值会增加,然后按钮的⽂本也会发⽣相应的改变。但是,当我们这样做
时,DOM中发⽣了什么?哪部分将被重新计算和更新?
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论