入门React的内部机制(render->JSX->createElement->virtualDom))

13 篇文章 0 订阅

JSX的原理

我们常常吧这个叫做 “转译”,其实用“汇编”来描述更为正确。
React官方推荐使用一种混合HTML和javascript的语法,即JSX来编写自己的组件。但浏览器对JSX及其语法并不理解。浏览器只理解Javascript ,所以JSX必须转化为javascript。下面是JSX的代码,一个与class和一些内容的div

<div className="cn">
	Content!
</div>

//转化为javascript只需要调用以下函数
React.createElement(
'div',
{className:'cn},
'Contont!'
);

调用的 这个函数接受三个参数,

  • 第一个是元素类型,对应html标签名称
  • 是带有所有元素属性的对象。如果他们没有属性也可以有空对象;
  • 余下的参数(参数可以有很多)是元素的子节点。元素中的文本也算一个child,所以放置了‘Contont!’
<div className="cn">
	Content 1!
	<br/>
	Content 2!
</div>

//转化后
React.createElement(
'div',
{className:'cn'},
'Content 1!',
React.createElement('br')
'Content 2!',
)

当然底下这个多参数可以这样写,但是他也支持其他的值当做参数
1.基本数据类型 false,null,underfined,true
2.数组Arrays
3.React组件

React.craeteElement(
'div',
{className:'cn'];
['Content 1!',React.createElement('br'),'Content 2!'],
)

当然react最厉害的不是这些,平时开发也不会是这些,而是来自用户创建的组件,例如:

function Table({rows}){
return(
<table>
	{rows.map(row=>{
		<tr key={row.id}>
			<td>{row.title}</td>
		</tr>
	})}
</table>
)
};

在这种“函数式“组件中我们接受一个包含表格行数据的对象数组 ,并返回调用 React.createElement的table元素,rows则作为他的子节点传入。

//这样写无论怎么样我们调用的时候都可以这样引入组件
<Table rows={ rows }>

//对于浏览器
React.craeteElement(Table,{rows:rows});
//注意这次第一个参数不再是html元素的字符串,而是一个在编写组件时定义的函数的引用。函数的属性就是props(即第二个参数就是props)

使用组件拼装页面

所以,我们已经将所有JSX组件转化为纯JavaScript,现在我们得到了一堆函数调用,他的参数会被其他函数调用,或者还有更多的其他函数调用这些参数…(说白了就是函数套函数),那么他到底是是怎么转化组成网页的DOM元素的呢?
为此,有一个ReactDOM库及其他的render方法:

function Table({rows}){/* ... */}//申明一个组价

//渲染一个组件
ReactDOM.render(
	React.createElement(Table,{rows:rows}),
	document.getElementById('root')
)

当ReactDOM.render被调用,React.createElement最终也被调用,它返回下列对象

{
type:Table,
props:{
	rows:rows
},
//...
}

这些对象在React看来便构成了虚拟DOM。

他们将在所有进一步的渲染中相互比较,并最终转化为真正的DOM(而不是虚拟)。
下面是一个例子

React.createElement(
  'div',
  { className: 'cn' },
  'Content 1!',
  'Content 2!',
);

//变成
{
	type:‘div’,
	props:{
	className:'cn',
	children:[
		'Content 1!',
		'Content 2!'
	]
	}
}

在JSX 原理中说过转化成Javascript的过程中,课传递很多参数,第一个元素类型(html标签的字符串,自定义的函数组件的函数引用)第二个参数为属性对象,其余的参数我们可以独立传递给React.createElement也可以打包图数组的形式传递,最后他们都会在props中一children为key属性中找到他们。所以无论 ildren 是作为数组还是参数列表传递都无关紧要,在生成虚拟DOM对象中,他们总会被打包在一起。
重要的是我们可以直接在JSX代码中将children添加到props中,效果是一样的。比如下面:

<div className="cn" children={['Content 1!', 'Content 2!']} />

虚拟DOM对象构建完成后,ReactDOM.render会尝试将其转移为浏览器根据一下规则可以展示的DOM节点:

  • 如果type 包含一个带有String 类型的标签名称–>创建一个标签并附带props下的所有属性。
  • 如果type是一个函数或者类,调用他,并对结果递归的重复这个过程。
  • 如果props下有children属性–>在父节点下,针对每个child重复以上过程。
    最后的结果,我们就得到了如下的HTML(刚才的 Table示例):
<table>
	<tr>
	<td>Title</td>
	</tr>
</table>

重新构建DOM(Rebuilding the DOM)

实际的应用场景,render通常在根节点调用一次,后续的更新会有state来控制和触发调用。
React神奇的是,当我们想更新一个页面而不是全部替换
先来一种简单是替换

ReactDOM.render(
React.createElement(Table,{rows: rows}),
document.getElementById('root')	
);

这个上面的代码将会之前看到的有所不同,它不是从头开始创建所有DOM节点并将它们放在页面上,而是React会启动 reconciliation(或diffing)算法,以确定节点树的哪些布冯必须更新,哪些可以保持不变
那么具体他是怎么工作呢?具体只有少数几个简单场景,理解它们将对我们的优化有很大帮助。在我们看来是在React Virtual DOM(虚拟DOM) 里面用来代表节点的对象。

  • 场景1:type是一个字符串,type在调用过程中保持不变,props也没有改变。
// 更新之前

{ type: 'div', props: { className: 'cn' } }


// 更新之后

{ type: 'div', props: { className: 'cn' } }

这个示例比较简单,DOM 保持不变。

  • type仍然是那个字符串,props不同。
// 更新之前

{ type: 'div', props: { className: 'cn' } }


// 更新之后

{ type: 'div', props: { className: 'cnn' } }

由于我们的类型仍然代表 HTML 元素,因此 React 知道如何通过标准的DOM API调用来更改其属性,而不用从dom树中删除节点。

  • 场景3:type改变成一个不同的字符串,或者将字符串改成一个组件.
// 更新之前

{ type: 'div', props: { className: 'cn' } }


// 更新之后

{ type: 'span', props: { className: 'cn' } }

由于React现在认为类型不同,它甚至不会尝试更新我们的节点:old元素将与其所有子节点一起被删除,因此,将元素奇幻为完全不用于DOM树的东西代价可能会非常昂贵。但是这种情况在显示中很少发生

记住React 使用 === 来比较类型值是很重要的,所以它们必须是相同类或相同函数的想用实列。
  • 场景4:type是一个组件
// 更新之前

{ type: Table, props: { rows: rows } }


// 更新之后

{ type: Table, props: { rows: rows } }

但是没有任何改变啊,如果你这么说,就错了。
因为组件内部会进行改变只是组件名不变


注意:component的render(只有类组件在声明时有这个函数)和ReactDOM.render不是同一个函数

如果type是一个函数或类的引用(常规的React组件),并且我们启动了 tree diff的过程,则React会尝试一致检查组件的内部逻辑,以确保render返回的值不会改变(防止副作用的措施)。对树中的每个组件进行遍历和扫描,但是在复杂的场景这个渲染过程成本会很高!

关注子节点

除了上述四种常见情况之外,当元素有多个子元素时,我们还需要考虑React的行为。假设我们有这样的元素:

// ...

props: {
  children: [
      { type: 'div' },
      { type: 'span' },
      { type: 'br' }
  ]
},

// ...

接下来我们想要交换这些元素的顺序:

// ...

props: {
  children: [
    { type: 'span' },
    { type: 'div' },
    { type: 'br' }
  ]
},

// ...

接下来会发生什么?
当进行diffing的时候,React检查props,children里面的数组时,它开始将数组中的元素与之前看到的元素按照数组下标顺序进行比较:0与0,1与1,以此类推,每次比较,React都会运用上述规则进行。在我们的例子中,它会认为div变成了span,应用之前的场景三,并不是很高效,想象一下我们从1000行的表格里删除了第一行。React将会不得不更新后面的999行,因为按照索引来对比,它们的索引都发生了变化。

幸运的是,React一个内置的方法 built-inway来解决这个问题。如果一个元素有一个key属性,元素可以通过key的值来编辑哦,而不是从DOM树种删除它们,然后把它们再加回来(在React中叫挂载/卸载)

// ...

props: {
  children: [ // 现在 React 将关注 Key,不再关注下标。
    { type: 'div', key: 'div' },
    { type: 'span', key: 'span' },
    { type: 'br', key: 'bt' }
  ]
},

// ...

在实际开发中,如果循环渲染同一个被复用的组件,使用相同key 的数据渲染同一个组件,只会被渲染一次。

当state发生变化

到目前为止,我们只涉及到React哲学的props部分,却忽略了state。这是一个简单的有状态的组件

class App extends Component {
  state = { counter: 0 }


  increment = () => this.setState({
    counter: this.state.counter + 1,
  })


  render = () => (<button onClick={this.increment}>
    {'Counter: ' + this.state.counter}
  </button>)

}

所以,在我们的state对象中,有一个key为counter,点击按钮时它的值就会增加,并且按钮的文本也会改变。但是当我们这么做是时候,到底在DOM 中发生了什么?那部分将被重新计算和更新?

调用this.setState 也会导致重新渲染,但不会影响整个页面,只会影响整个页面,只会影响组件本身及其子节点。父节点和兄弟节点都不会受到影响。当我们有一个很庞大的树形结构时,只重绘它的一部分就很方便。

我们添加一行时,React都在计算和比较整个虚拟DOM树。现在尝试点击一行中的counter按钮。你讲看到在 state 变化之后虚拟DOM是如何更新的,只有引用了state key的元素及其chidren受到了影响。

优化:挂载、卸载

当一个元素或组件在内部有很多个子节点表示为数组,你可以获得非常显著的速度提升。

<div>
	<Message />
	<Table />
	<Footer />
</div>

在我们的虚拟DOM中,上述代码将表现为:

props:{
	children:[
		{type:Message},
		{type:Table},
		{type:Footer}
	]
}

这里有个简单的Message例子,一个div有一些文本,和一个超过1000行的庞大Table。它们都包括在封闭的div中,所以它们被放置在父节点的props。children下,并且它们都没有key。React甚至不会通过控制台警告我们要给他们分配key,因为children正在被React.createElement作为参数列表传递给父元素,而不是直接遍历一个数组。

现在我们的用户已读了一个通知,Message组件从DOM树上移除后,剩下Table和Footer组件。

props:{
	children:[
		{type:Table},
		{type:Footer}
	]
}

站在React的角度看,上述过程子节点是不断变化的:第0个节点从Message组件现在变成了Table组件。这里没有keys来与之比较,所以它比较types 时,又发现它们俩不是同一个function 的引用,于是会把整个Table卸载,然后在挂载回去,渲染它的1000多行子节点数据

因此,你可以添加唯一的key(这种情况下,使用keys并不是最佳选择),或者采用更智能的技巧:使用短路计算:比如

<div>
	{isShown && <Message />}
	<Table />
	<Footer />
</div>

虽然 Message组件不会再画面显示,父元素 div 的props.children 任然有三个元素。children有一个值 false 。然后 true、false,null,undefined 是虚拟DOM对象 type 属性允许的值,我们最终得到下面的结果:

props:{
	children:[
		false,// isShown && <Message /> 结果为false
		{type:Table},
		{type:Footer}
	]
}

所以,Message 有或没有,我们的索引都不会变,当然,Table任然与 Table 进行比较(当type是一个引用类型时,对比一定会进行),但是仅仅对比虚拟DOM也比删掉DOM节点再从头创建它们来的快的多。
现在我们来看看更多高级的东西。HOC。HOC(高阶组件)是一个将组件作为参数,执行某些操作并返回不同功能的函数:

function withName(SomeComponent){

return function(props){
	return <SomeComponent {...props} name={name} />;
}
}

这是一种非常常见的模式,但你需要小心。

class App extends React.Component(){
render(){
	const ComponentWillName = withName(SomeComponent);
	return <ComponentWillName />;
}
}

我们在父组件的渲染方式中创建一个 HOC。当我们重新渲染组件树的时候,我们的虚拟DOM 将如下所示:

// 第一次渲染:
{
  type: ComponentWithName,
  props: {},
}


// 第二次渲染:
{
  type: ComponentWithName, // 相同的名字,但是不同的实例
  props: {},
}

现在 React 在ComponentWithName组件运行diffing算法,但此时同名引用了不同的实例,
built-inway(内置方法元素可以通过key的值来比较,而不是使用索引,只要这个key是唯一的,React便可以移动元素而不是DOM树中挂载/卸载)等于失败,一个完整的re-mount 会发生(整个节点换掉)注意它也会导致状态丢失,如此所说,只要在render外面创建一个HOC:

const ComponentWithName = withName(Component);

class App extends React.Component(){
	render(){
		return <ComponentWithName />;
	}
}
优化:更新

所以,除非必要否则外面不建议 re-mount 。但是,对位于 DOM树根部附近的组件所做的任何更改都会导致其所有子节点的diffing和reconciliation。对于结构复杂的应用这资源开销也很大并且通常是可以避免的。


有方法告诉React不要检查某个分支,因为我们确定它没有变化。


这就是shouldComponentUpdate 他是组件生命周期的一部分。这个方法会在组件的render和组件接收到state或props的值更新之前调用。然后我们可以自由地将它们与我们当前的值进行比较,并决定是否更新我们的组件(返回true或false)。如果我们返回false,React 将不会重新渲染组件,也不会检查它的所有子组件。
通常,比较两个集合props和state一个简单的浅比较就足够了:如果顶层的值不同,我们不必接着比较,浅比较不是Javascript的特性,但有很多这方面的工具

classTableRow extends React.component{
	// 将要返回true如果新的 props/state与旧的不同
	shouldComponentUpdate(nextProps,nextState){
		const {props,state} = this;
		return !shallowequal(props,nextProps)&& !shallowequal(state,nextState);
	}
	render(){/* ... */}
}

你甚至不需要自己编写代码,因为React将这个特性内置在一个名为React.PureComponent 的类中。它类似于React.Component,只是在shouldComponentUpdate已经帮你实现了一个浅的props/state 比较。
这听起来像是意见容易的事,只需在类定义的继承部分将 Component 改为 PureComponent,即可享受高效率,虽然不是很快!考虑这些例子:

<Table
	//map返回一个新的数组实例,所以浅比较将失败
	rows = {rows.map(/*...*/)}
	//对象的字面量总是与前一个不一样
	style={{color:'red'}}
	//箭头函数式一个新 的未命名的东西在作用域内,所以总会有一个完整的 diffing
	onUpdate={() => {/*...*/}}
/>

上面的代码片段演示了三种最常见的反例,尽量避免它们!

如果你能注意到,在render定义之外创建所有对象,数组和函数,并确保它们在调用期间不会变化。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值