当前位置: 首页 > Web前端 > HTML

React的createElement源码解读

时间:2023-03-28 13:21:36 HTML

React与Babel元素标签翻译用过React的同学都知道,当我们这样写:bar

Babel会翻译成:React.createElement("div",{id:"foo"},"bar");我们会发现createElement第一个参数是元素类型,第二个参数是元素属性,第三个参数是子元素组件翻译如果我们使用组件呢?functionFoo({id}){returnfoo
}bar
Babel然后会将其翻译为:functionFoo({id}){returnReact.createElement("div",{id:id},"foo")}React.createElement(Foo,{id:"foo"},React.createElement("div",{id:"bar"},"bar"));我们会发现createElement的第一个参数是变量Foo子元素翻译如果我们有多个子元素呢?bar
bazquxBabel会将它翻译成:React.createElement("div",{id:"foo"},React.createElement("div",{id:"bar"},"bar"),React.createElement("div",{id:"baz"},"baz"),React.createElement("div",{id:"qux"},"qux"));我们会发现子元素实际上是作为参数添加到函数createElement中的,那么React.createElement到底是做什么的呢?源码我们查看React的GitHub仓库:https://github.com/facebook/react,查看pacakges/react/index.js文件,可以看到createElement的定义在./src/React文件中://Simplifiedexport{createElement}from'./src/React';我们打开./src/React.js文件:;继续查看./ReactElement.js文件,终于找到这里的最终定义,鉴于这里的代码比较长,我们将代码大大简化:constRESERVED_PROPS={key:true,ref:true,__self:true,__source:true,};导出乐趣动作createElement(type,config,...children){letpropName;//提取保留名称constprops={};//第一段letkey=''+config.key;让ref=config.ref;让self=config.__self;让source=config.__source;//第二段for(propNameinconfig){if(config.hasOwnProperty(propName)&&!RESERVED_PROPS.hasOwnProperty(propName)){props[propName]=config[propName];}}//第三段props.children=children;//第四段if(type&&type.defaultProps){constdefaultProps=type.defaultProps;for(propNameindefaultProps){if(props[propName]===undefined){props[propName]=defaultProps[propName];}}}//第五段returnReactElement(type,key,ref,self,source,ReactCurrentOwner.current,props,);}这里可以看出createElement函数主要做了一个预处理,然后将处理后的数据传入ReactElement函数,我们先分析一下createElement做了什么我们以第一个例子作为函数输入的例子:createElement的三个形参,type表示类型,可以是标签名字符串(如div或span),也可以是React组件(如Foo)。config表示传入的属性,children表示第一个子元素段代码__self和__source下面我们开始看第一段代码://第一段letkey=''+config.key;让ref=config.ref;让self=config.__self;让源=配置。__来源;可以看到在createElement函数内部,分别获取并处理了key、ref、__self、__source这四个参数。Key和ref很容易理解。__self和__source有什么用?通过本期我们了解到__self和__source是babel-preset-react注入的调试信息,可以提供更多有用的错误信息。我们查看babel-preset-react的文档,可以看到:developmentboolean类型,默认值为false。这可用于启用特定于开发环境的某些行为,例如添加__source和__self。当与env参数配置或js配置文件一起使用时,此功能很有用。如果我们尝试打开开发参数,我们将看到__self和__source参数。还是以bar为例,会被Babel翻译成:var_jsxFileName="/Users/kevin/Desktop/react-app/src/index.js";React.createElement("div",{id:"foo",__self:this,__source:{fileName:_jsxFileName,lineNumber:5,columnNumber:13}},"bar");第二段代码propsobject现在我们来看第二段代码:道具名称]=配置[道具名称];}}这段代码实现的功能很简单,就是构建一个props对象,去掉传入的key、ref、__self、__source属性,这就是为什么在组件中,我们明明传入了key和ref,但是我们无法通过this.props.key或this.props.ref获取传入的值,因为它们在这里被删除了。去掉的原因是React给出的解释是key和ref是用于React内部处理的。例如,如果要使用键值,可以传递另一个属性并使用与键相同的值。第三段代码children下面我们来看第三段代码,这段代码经过精简,非常简单://Thethirdpieceofprops.children=children;这其实是因为我们为了简化代码使用了ES6的扩展算法,实际的源码会比较复杂,有一些区别:constchildrenLength=arguments.length-2;if(childrenLength===1){props.children=children;}elseif(childrenLength>1){constchildArray=Array(childrenLength);for(leti=0;ifoo}Foo.defaultProps={id:'foo'}//类组件classHeaderextendsComponent{staticdefaultProps={id:'foo'}render(){const{id}=this.propsreturnfoo}}第五段代码owner下面我们来看第五段代码://第五段returnReactElement(type,key,ref,self,source,ReactCurrentOwner.current,props,);这一段是将前面处理过的type,key等值传入ReactElement函数,那么ReactCurrentOwner.current是个什么鬼?我们根据引用地址查看ReactCurrentOwner定义的文件:/***跟踪当前所有者。**当前所有者是应该拥有任何co的组件*当前正在构建的组件。*/constReactCurrentOwner={/***@internal*@type{ReactComponent}*/current:null,};exportdefaultReactCurrentOwner;它的初始定义很简单,根据注释,我们可以理解为ReactCurrentOwner.current指的是组件在构建过程中的owner。具体功能将在以后的文章中介绍。现在可以简单理解为用来记录临时变量ReactElement的源码。现在我们开始看ReactElement函数。其实这个函数的内容更简单,代码简化如下:constReactElement=function(type,key,ref,self,source,owner,props){constelement={//这个标签允许我们唯一标识这是一个ReactElement$$typeof:REACT_ELEMENT_TYPE,//属于元素的内置属性type:type,key:key,ref:ref,props:props,//记录负责创建这个元素的组件._owner:所有者,};返回元素;};可以看到,它返回一个对象,这个对象包括$$typeof、type、key等属性,这个对象被称为“React元素”。它描述了我们在屏幕上看到的内容。React会读取这些对象,使用它们来构建和更新DOMREACT_ELEMENT_TYPE和REACT_ELEMENT_TYPE查看引用的packages/shared/ReactSymbols文件,可以发现它是一个唯一的常量值,用于标记React元素节点exportconstREACT_ELEMENT_TYPE=Symbol。对于('react.element');还有其他类型的节点吗?查看定义REACT_ELEMENT_TYPE的文件,发现有:REACT_STRICT_MODE_TYPE:symbol=Symbol.for('react.strict_mode');导出常量REACT_PROFILER_TYPE:symbol=Symbol.for('react.profiler');导出常量constREACT_CONTEXT_TYPE:symbol=Symbol.for('react.context');//...你可以很自然地理解为$$typeof,也可以设置为REACT_FRAGMENT_TYPE等值。我们可以编写代码进行实验,比如使用Portal,打印返回的对象:importReactDOMfrom'react-dom/client';从'react-dom'导入{createPortal}constroot=ReactDOM.createRoot(document.getElementById('root'));functionModal(){constportalObject=createPortal(foo,文档.getElementById("root2"));console.log(portalObject)返回portalObject}root。渲染(<模态/>);打印的对象是:它的$$typeof确实是REACT_PORTAL_TYPE如果我们使用Fragment:importReactDOMfrom'react-dom/client';从“反应”导入反应;常量根=ReactDOM。createRoot(document.getElementById('root'));functionModal(){constfragmentObject=(foo);console.log(fragmentObject)返回fragmentObject}root.render();打印出来的对象是:我们会发现在使用fragment的时候,返回对象的$$typeof还是REACT_ELEMENT_TYPE。为什么?其实,想想我们在使用传送门的时候,我们用的是React.createPortal,但是Fragments仍然使用正常的React.createElement方法。我们也看到了createElement的代码。$$typeof没有特殊处理,自然是REACT_ELEMENT_TYPE。那么为什么$$typeof存在呢?其实主要是处理web安全问题,想象一下这样一段代码:letmessage={text:expectedTextButGotJSON};//React0.13有风险

{message.text}

如果expectedTextButGotJSON是来自服务器的值,如://服务器允许用户存储JSONletexpectedTextButGotJSON={type:'div',props:{dangerouslySetInnerHTML:{__html:'/*somethingbad*/'},},//...};letmessage={text:expectedTextButGotJSON};这很容易受到XSS攻击,虽然这种攻击是来自服务器端的漏洞,但是使用React如果我们用Symbol标记每个React元素我们可以更好地处理它,因为服务器端数据不会有Symbol.for('react.element'),React可以检测element.$$typeof,如果元素缺失或者无效,可以拒绝处理该元素,从而保证安全。回过头来,我们已经完整的阅读了React.createElement的源码,现在我们再看React官方文档的这一段:下面两段示例代码完全等价:constelement=(Hello,world!);constelement=React.createElement('h1',{className:'greeting'},'Hello,world!');React.createElement()预先执行一些检查以帮助你写出无错误的代码,但实际上它创建了一个这样的对象://注意:这是一个简化的结构constelement={type:'h1',props:{className:'greeting',children:'Hello,世界!'}};这些对象称为“React元素”。它们描述了您想在屏幕上看到的内容。React读取这些对象并使用它们来构建DOM并使其保持最新。现在你对这段话有更深的理解了吗?React系列讲解React的源码、ReactAPI背后的实现机制、React的最佳实践、React的发展与历史等,估计有50篇左右。如果喜欢或者有什么灵感,欢迎star。也是对作者的鼓励。