【React深切】深切分析假造DOM的衬着道理和特征

《【React深切】深切分析假造DOM的衬着道理和特征》

导读

React的假造DOMDiff算法是React的异常重要的中心特征,这部份源码也异常庞杂,明白这部份学问的道理对更深切的控制React是异常必要的。

原本想将假造DOMDiff算法放到一篇文章,写完假造DOM发明文章已很长了,所以本篇只剖析假造DOM

本篇文章从源码动身,剖析假造DOM的中心衬着道理(初次衬着),以及React对它做的机能优化点。

说实话React源码真的很难读😅,假如本篇文章协助到了你,那末请给个赞👍支撑一下吧。

开辟中的常见题目

  • 为什么必需援用React
  • 自定义的React组件为什么必需大写
  • React怎样防备XSS
  • ReactDiff算法和其他的Diff算法有何区分
  • keyReact中的作用
  • 怎样写出高机能的React组件

假如你对上面几个题目还存在疑问,申明你对React的假造DOM以及Diff算法完成道理另有所短缺,那末请好好浏览本篇文章吧。

起首我们来看看究竟什么是假造DOM:

假造DOM

《【React深切】深切分析假造DOM的衬着道理和特征》

在原生的JavaScript顺序中,我们直接对DOM举行建立和变动,而DOM元素经由历程我们监听的事宜和我们的应用顺序举行通信。

React会先将你的代码转换成一个JavaScript对象,然后这个JavaScript对象再转换成实在DOM。这个JavaScript对象就是所谓的假造DOM

比方下面一段html代码:

<div class="title">
      <span>Hello ConardLi</span>
      <ul>
        <li>苹果</li>
        <li>橘子</li>
      </ul>
</div>

React能够存储为如许的JS代码:


const VitrualDom = {
  type: 'div',
  props: { class: 'title' },
  children: [
    {
      type: 'span',
      children: 'Hello ConardLi'
    },
    {
      type: 'ul',
      children: [
        { type: 'li', children: '苹果' },
        { type: 'li', children: '橘子' }
      ]
    }
  ]
}

当我们须要建立或更新元素时,React起首会让这个VitrualDom对象举行建立和变动,然后再将VitrualDom对象衬着成实在DOM

当我们须要对DOM举行事宜监听时,起首对VitrualDom举行事宜监听,VitrualDom会代办原生的DOM事宜从而做出响应。

为什么运用假造DOM

React为什么采纳VitrualDom这类计划呢?

进步开辟效力

运用JavaScript,我们在编写应用顺序时的关注点在于怎样更新DOM

运用React,你只须要通知React你想让视图处于什么状况,React则经由历程VitrualDom确保DOM与该状况相匹配。你没必要本身去完成属性操纵、事宜处置惩罚、DOM更新,React会替你完成这一切。

这让我们更关注我们的营业逻辑而非DOM操纵,这一点即可大大提拔我们的开辟效力。

关于提拔机能

许多文章说VitrualDom能够提拔机能,这一说法现实上是很单方面的。

直接操纵DOM是异常斲丧机能的,这一点毋庸置疑。然则React运用VitrualDom也是没法防止操纵DOM的。

假如是初次衬着,VitrualDom不具有任何上风,以至它要举行更多的盘算,斲丧更多的内存。

VitrualDom的上风在于ReactDiff算法和批处置惩罚战略,React在页面更新之前,提早盘算好了怎样举行更新和衬着DOM。现实上,这个盘算历程我们在直接操纵DOM时,也是能够本身推断和完成的,然则一定会斲丧异常多的精神和时候,而且每每我们本身做的是不如React好的。所以,在这个历程当中React协助我们”提拔了机能”。

所以,我更倾向于说,VitrualDom协助我们进步了开辟效力,在反复衬着时它协助我们盘算怎样更高效的更新,而不是它比DOM操纵更快。

假如您对本部份的剖析有什么差别看法,迎接在批评区拍砖。

跨浏览器兼容

《【React深切】深切分析假造DOM的衬着道理和特征》

React基于VitrualDom本身完成了一套本身的事宜机制,本身模仿了事宜冒泡和捕捉的历程,采纳了事宜代办,批量更新等要领,抹平了各个浏览器的事宜兼容性题目。

跨平台兼容

《【React深切】深切分析假造DOM的衬着道理和特征》

VitrualDomReact带来了跨平台衬着的才能。以React Native为例子。React依据VitrualDom画出响应平台的ui层,只不过差别平台画的姿态差别罢了。

假造DOM完成道理

假如你不想看冗杂的源码,或许如今没有充足时候,能够跳过这一章,直接👇假造DOM道理总结

《【React深切】深切分析假造DOM的衬着道理和特征》

在上面的图上我们继续举行扩大,根据图中的流程,我们顺次来剖析假造DOM的完成道理。

JSX和createElement

我们在完成一个React组件时能够挑选两种编码体式格局,第一种是运用JSX编写:

class Hello extends Component {
  render() {
    return <div>Hello ConardLi</div>;
  }
}

第二种是直接运用React.createElement编写:

class Hello extends Component {
  render() {
    return React.createElement('div', null, `Hello ConardLi`);
  }
}

现实上,上面两种写法是等价的,JSX只是为 React.createElement(component, props, ...children) 要领供应的语法糖。也就是说一切的JSX 代码末了都邑转换成React.createElement(...) Babel协助我们完成了这个转换的历程。

以下面的JSX

<div>
  <img src="avatar.png" className="profile" />
  <Hello />
</div>;

将会被Babel转换为

React.createElement("div", null, React.createElement("img", {
  src: "avatar.png",
  className: "profile"
}), React.createElement(Hello, null));

注重,babel在编译时会推断JSX中组件的首字母,当首字母为小写时,其被认定为原生DOM标签,createElement的第一个变量被编译为字符串;当首字母为大写时,其被认定为自定义组件,createElement的第一个变量被编译为对象;

别的,由于JSX提早要被Babel编译,所以JSX是不能在运行时动态挑选范例的,比方下面的代码:

function Story(props) {
  // Wrong! JSX type can't be an expression.
  return <components[props.storyType] story={props.story} />;
}

须要变成下面的写法:

function Story(props) {
  // Correct! JSX type can be a capitalized variable.
  const SpecificStory = components[props.storyType];
  return <SpecificStory story={props.story} />;
}

所以,运用JSX你须要装置Babel插件babel-plugin-transform-react-jsx

{
    "plugins": [
        ["transform-react-jsx", {
            "pragma": "React.createElement"
        }]
    ]
}

建立假造DOM

下面我们来看看假造DOM的实在样子容貌,将下面的JSX代码在控制台打印出来:

<div className="title">
      <span>Hello ConardLi</span>
      <ul>
        <li>苹果</li>
        <li>橘子</li>
      </ul>
</div>

《【React深切】深切分析假造DOM的衬着道理和特征》

这个组织和我们上面本身描写的组织很像,那末React是怎样将我们的代码转换成这个组织的呢,下面我们来看看createElement函数的细致完成(文中的源码经由精简)。

《【React深切】深切分析假造DOM的衬着道理和特征》

createElement函数内部做的操纵很简单,将props和子元素举行处置惩罚后返回一个ReactElement对象,下面我们来一一剖析:

(1).处置惩罚props:

《【React深切】深切分析假造DOM的衬着道理和特征》

  • 1.将特别属性refkeyconfig中掏出并赋值
  • 2.将特别属性selfsourceconfig中掏出并赋值
  • 3.将除特别属性的其他属性掏出并赋值给props

背面的文章会细致引见这些特别属性的作用。

(2).猎取子元素

《【React深切】深切分析假造DOM的衬着道理和特征》

  • 1.猎取子元素的个数 —— 第二个参数背面的一切参数
  • 2.若只要一个子元素,赋值给props.children
  • 3.若有多个子元素,将子元素添补为一个数组赋值给props.children

(3).处置惩罚默许props

《【React深切】深切分析假造DOM的衬着道理和特征》

  • 将组件的静态属性defaultProps定义的默许props举行赋值

ReactElement

ReactElement将传入的几个属性举行组合,并返回。

  • type:元素的范例,能够是原生html范例(字符串),或许自定义组件(函数或class
  • key:组件的唯一标识,用于Diff算法,下面会细致引见
  • ref:用于接见原生dom节点
  • props:传入组件的props
  • owner:当前正在构建的Component所属的Component

$$typeof:一个我们不常见到的属性,它被赋值为REACT_ELEMENT_TYPE

var REACT_ELEMENT_TYPE =
  (typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) ||
  0xeac7;

可见,$$typeof是一个Symbol范例的变量,这个变量能够防备XSS

假如你的服务器有一个破绽,许可用户存储恣意JSON对象, 而客户端代码须要一个字符串,这能够会成为一个题目:

// JSON
let expectedTextButGotJSON = {
  type: 'div',
  props: {
    dangerouslySetInnerHTML: {
      __html: '/* put your exploit here */'
    },
  },
};
let message = { text: expectedTextButGotJSON };
<p>
  {message.text}
</p>

JSON中不能存储Symbol范例的变量。

ReactElement.isValidElement函数用来推断一个React组件是不是是有用的,下面是它的细致完成。

ReactElement.isValidElement = function (object) {
  return typeof object === 'object' && object !== null && object.$$typeof === REACT_ELEMENT_TYPE;
};

可见React衬着时会把没有$$typeof标识,以及划定规矩校验不经由历程的组件过滤掉。

当你的环境不支撑Symbol时,$$typeof被赋值为0xeac7,至于为什么,React开辟者给出了答案:

0xeac7看起来有点像
React

selfsource只要在非生产环境才会被到场对象中。

  • self指定当前位于哪一个组件实例。
  • _source指定调试代码来自的文件(fileName)和代码行数(lineNumber)。

假造DOM转换为实在DOM

上面我们剖析了代码转换成了假造DOM的历程,下面来看一下React怎样将假造DOM转换成实在DOM

本部份逻辑较庞杂,我们先用流程图梳理一下全部历程,全部历程也许可分为四步:

《【React深切】深切分析假造DOM的衬着道理和特征》

历程1:初始参数处置惩罚

在编写好我们的React组件后,我们须要挪用ReactDOM.render(element, container[, callback])将组件举行衬着。

render函数内部现实挪用了_renderSubtreeIntoContainer,我们来看看它的细致完成:

  render: function (nextElement, container, callback) {
    return ReactMount._renderSubtreeIntoContainer(null, nextElement, container, callback);
  },

《【React深切】深切分析假造DOM的衬着道理和特征》

  • 1.将当前组件运用TopLevelWrapper举行包裹

TopLevelWrapper只一个空壳,它为你须要挂载的组件供应了一个rootID属性,并在render函数中返回该组件。

TopLevelWrapper.prototype.render = function () {
  return this.props.child;
};

ReactDOM.render函数的第一个参数能够是原生DOM也能够是React组件,包裹一层TopLevelWrapper能够在背面的衬着中将它们举行一致处置惩罚,而不必体贴是不是原生。

  • 2.推断根结点下是不是已衬着过元素,假如已衬着过,推断实行更新或许卸载操纵
  • 3.处置惩罚shouldReuseMarkup变量,该变量示意是不是须要从新标记元素
  • 4.挪用将上面处置惩罚好的参数传入_renderNewRootComponent,衬着完成后挪用callback

_renderNewRootComponent中挪用instantiateReactComponent对我们传入的组件举行分类包装:

《【React深切】深切分析假造DOM的衬着道理和特征》

依据组件的范例,React依据原组件建立了下面四大类组件,对组件举行分类衬着:

  • ReactDOMEmptyComponent:空组件
  • ReactDOMTextComponent:文本
  • ReactDOMComponent:原生DOM
  • ReactCompositeComponent:自定义React组件

他们都具有以下三个要领:

  • construct:用来吸收ReactElement举行初始化。
  • mountComponent:用来天生ReactElement对应的实在DOMDOMLazyTree
  • unmountComponent:卸载DOM节点,解绑事宜。

细致是怎样衬着我们在历程3中举行剖析。

历程2:批处置惩罚、事件挪用

_renderNewRootComponent中运用ReactUpdates.batchedUpdates挪用batchedMountComponentIntoNode举行批处置惩罚。

ReactUpdates.batchedUpdates(batchedMountComponentIntoNode, componentInstance, container, shouldReuseMarkup, context);

batchedMountComponentIntoNode中,运用transaction.perform挪用mountComponentIntoNode让其基于事件机制举行挪用。

 transaction.perform(mountComponentIntoNode, null, componentInstance, container, transaction, shouldReuseMarkup, context);

关于批处置惩罚事件,在我前面的剖析setState实行机制中有更多引见。

历程3:天生html

mountComponentIntoNode函数中挪用ReactReconciler.mountComponent天生原生DOM节点。

mountComponent内部现实上是挪用了历程1天生的四种对象的mountComponent要领。起首来看一下ReactDOMComponent

《【React深切】深切分析假造DOM的衬着道理和特征》

  • 1.对特别DOM标签、props举行处置惩罚。
  • 2.依据标签范例建立DOM节点。
  • 3.挪用_updateDOMPropertiesprops插进去到DOM节点,_updateDOMProperties也可用于props Diff,第一个参数为上次衬着的props,第二个参数为当前props,若第一个参数为空,则为初次建立。
  • 4.天生一个DOMLazyTree对象并挪用_createInitialChildren将孩子节点衬着到上面。

那末为什么不直接天生一个DOM节点而是要建立一个DOMLazyTree呢?我们先来看看_createInitialChildren做了什么:

《【React深切】深切分析假造DOM的衬着道理和特征》

推断当前节点的dangerouslySetInnerHTML属性、孩子节点是不是为文本和其他节点离别挪用DOMLazyTreequeueHTMLqueueTextqueueChild

《【React深切】深切分析假造DOM的衬着道理和特征》

能够发明:DOMLazyTree现实上是一个包裹对象,node属性中存储了实在的DOM节点,childrenhtmltext离别存储孩子、html节点和文本节点。

它供应了几个要领用于插进去孩子、html以及文本节点,这些插进去都是有条件限定的,当enableLazy属性为true时,这些孩子、html以及文本节点会被插进去到DOMLazyTree对象中,当其为false时会插进去到实在DOM节点中。

var enableLazy = typeof document !== 'undefined' &&
  typeof document.documentMode === 'number' ||
  typeof navigator !== 'undefined' &&
  typeof navigator.userAgent === 'string' &&
  /\bEdge\/\d/.test(navigator.userAgent);

可见:enableLazy是一个变量,当前浏览器是IEEdge时为true

IE(8-11)Edge浏览器中,一个一个插进去无子孙的节点,效力要远高于插进去一全部序列化完全的节点树。

所以lazyTree重要处理的是在IE(8-11)Edge浏览器中插进去节点的效力题目,在背面的历程4我们会剖析到:若当前是IEEdge,则须要递归插进去DOMLazyTree中缓存的子节点,其他浏览器只须要插进去一次当前节点,由于他们的孩子已被衬着好了,而不必忧郁效力题目。

下面来看一下ReactCompositeComponent,由于代码异常多这里就不再贴这个模块的代码,其内部重要做了以下几步:

  • 处置惩罚propscontex等变量,挪用组织函数建立组件实例
  • 推断是不是为无状况组件,处置惩罚state
  • 挪用performInitialMount生命周期,处置惩罚子节点,猎取markup
  • 挪用componentDidMount生命周期

performInitialMount函数中,起首挪用了componentWillMount生命周期,由于自定义的React组件并非一个实在的DOM,所以在函数中又挪用了孩子节点的mountComponent。这也是一个递归的历程,当一切孩子节点衬着完成后,返回markup并挪用componentDidMount

历程4:衬着html

mountComponentIntoNode函数中挪用将上一步天生的markup插进去container容器。

在初次衬着时,_mountImageIntoNode会清空container的子节点后挪用DOMLazyTree.insertTreeBefore

《【React深切】深切分析假造DOM的衬着道理和特征》

推断是不是为fragment节点或许<object>插件:

  • 假如是以上两种,起首挪用insertTreeChildren将此节点的孩子节点衬着到当前节点上,再将衬着完的节点插进去到html
  • 假如是其他节点,先将节点插进去到插进去到html,再挪用insertTreeChildren将孩子节点插进去到html
  • 若当前不是IEEdge,则不须要再递归插进去子节点,只须要插进去一次当前节点。

《【React深切】深切分析假造DOM的衬着道理和特征》

  • 推断不是IEbEdgereturn
  • children不为空,递归insertTreeBefore举行插进去
  • 衬着html节点
  • 衬着文本节点

原生DOM事宜代办

有关假造DOM的事宜机制,我曾特地写过一篇文章,有兴致能够👇【React深切】React事宜机制

假造DOM道理、特征总结

React组件的衬着流程

  • 运用React.createElementJSX编写React组件,现实上一切的JSX 代码末了都邑转换成React.createElement(...) Babel协助我们完成了这个转换的历程。
  • createElement函数对keyref等特别的props举行处置惩罚,并猎取defaultProps对默许props举行赋值,而且对传入的孩子节点举行处置惩罚,终究组织成一个ReactElement对象(所谓的假造DOM)。
  • ReactDOM.render将天生好的假造DOM衬着到指定容器上,个中采纳了批处置惩罚、事件等机制而且对特定浏览器举行了机能优化,终究转换为实在DOM

假造DOM的构成

ReactElementelement对象,我们的组件终究会被衬着成下面的组织:

  • type:元素的范例,能够是原生html范例(字符串),或许自定义组件(函数或class
  • key:组件的唯一标识,用于Diff算法,下面会细致引见
  • ref:用于接见原生dom节点
  • props:传入组件的propschidrenprops中的一个属性,它存储了当前组件的孩子节点,能够是数组(多个孩子节点)或对象(只要一个孩子节点)
  • owner:当前正在构建的Component所属的Component
  • self:(非生产环境)指定当前位于哪一个组件实例
  • _source:(非生产环境)指定调试代码来自的文件(fileName)和代码行数(lineNumber)

防备XSS

ReactElement对象另有一个$$typeof`属性,它是一个`Symbol`范例的变量`Symbol.for('react.element')`,当环境不支撑`Symbol`时,`$$typeof被赋值为0xeac7

这个变量能够防备XSS。假如你的服务器有一个破绽,许可用户存储恣意JSON对象, 而客户端代码须要一个字符串,这能够为你的应用顺序带来风险。JSON中不能存储Symbol范例的变量,而React衬着时会把没有$$typeof标识的组件过滤掉。

批处置惩罚和事件

React在衬着假造DOM时应用了批处置惩罚以及事件机制,以进步衬着机能。

关于批处置惩罚以及事件机制,在我之前的文章【React深切】setState的实行机制中有细致引见。

针对性的机能优化

IE(8-11)Edge浏览器中,一个一个插进去无子孙的节点,效力要远高于插进去一全部序列化完全的节点树。

React经由历程lazyTree,在IE(8-11)Edge中举行单个节点顺次衬着节点,而在其他浏览器中则起首将全部大的DOM组织构建好,然后再团体插进去容器。

而且,在零丁衬着节点时,React还斟酌了fragment等特别节点,这些节点则不会一个一个插进去衬着。

假造DOM事宜机制

React本身完成了一套事宜机制,其将一切绑定在假造DOM上的事宜映射到真正的DOM事宜,并将一切的事宜都代办到document上,本身模仿了事宜冒泡和捕捉的历程,而且举行一致的事宜分发。

React本身组织了合成事宜对象SyntheticEvent,这是一个跨浏览器原生事宜包装器。 它具有与浏览器原生事宜雷同的接口,包含stopPropagation() preventDefault() 等等,在一切浏览器中他们工作体式格局都雷同。这抹平了各个浏览器的事宜兼容性题目。

上面只剖析假造DOM初次衬着的道理和历程,固然这并不包含假造 DOM举行 Diff的历程,下一篇文章我们再来细致讨论。

关于开篇提的几个题目,我们鄙人篇文章中举行一致回复。

引荐浏览

末端

本文源码中的版本为React15版本,相对16版本会有一些相差,关于16版本的修改,背面的文章会零丁剖析。

文中若有毛病,迎接在批评区斧正,或许您对文章的排版,浏览体验有什么好的发起,迎接在批评区指出,感谢浏览。

想浏览更多优良文章、下载文章中头脑导图源文件、浏览文中demo源码、可关注我的github博客,你的star✨、点赞和关注是我延续创作的动力!

引荐关注我的微信民众号【code隐秘花圃】,天天推送高质量文章,我们一同交换生长。

《【React深切】深切分析假造DOM的衬着道理和特征》

    原文作者:ConardLi
    原文地址: https://segmentfault.com/a/1190000018891454
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞