开局一张图,不充钱也能精通Reactjs内部实现原理(1)

勿忘初心2018-10-26 11:53

此文已由作者杨介正授权网易云社区发布。

欢迎访问网易云社区,了解更多网易技术产品运营经验。


开局这张图看得懂的大V玩家,吃西瓜看个热闹,因为本文内容全是我编的。看得不是很懂但接触过Reactjs的平民玩家,可以刷下这篇文章掉点装备来增强战斗力,因为这篇文章能刷到5件好东西:

  第1章 (可略过)

  第2章 组件渲染原理

  第3章 事件机制原理

  第4Batch updatediff算法原理

  第5章 组件的生命周期及性能优化

  第6章 服务端渲染原理


1章 新手指引

  一点也看不懂开局的图,也没接触过Reactjs的传说中玩游戏1块钱也不充的屌丝玩家,可以到https://facebook.github.io/react/docs/hello-world.html打野升下级,或者吃下本章新手指引这颗老君仙丹直接升级(其他玩家请直接跳到第2章去推塔):


1.1 什么是Reactjs

  完全组件化编程,从最顶层的Root组件开始,所有的交互逻辑都被封装成一个个小组件里并进行组合;对DOM的操作方式由传统的直接操作变成了通过操作数据,驱动Virtual DOM的状态更新并最终反馈到Actual DOM中。下面通过一个例子来比较下Reactjs框架与传统js操作DOM的区别:在页面中创建元素<div id=”test”>a</div>,然后更改这个DOMtextContent b


1.1.1 传统的实现方式(直接操作DOM

var DOM = document.createElement('div');
DOM.textContent = 'a';
document.body.appendChild(DOM);
DOM.textContent = 'b';


1.1.2 Reactjs的实现方式(组件化、数据驱动)

class Root extends React.Component {
  constructor (props) {
    super(props);
    // 初始化状态
    this.state = {text: 'a'}
  }
  componentDidMount () {
    // 更新数据,驱动Reactjs Virtual DOM的状态更新并最终反馈到Actual DOM
    this.setState({
      text: 'b'
    })
  },
  render () {
    // 组件对应的Virtual DOM树结构,最终会被渲染成Actual DOM树并安装到
    // container里
    return <div>{this.state.text}</div>
  }
}
ReactDOM.render(<Root />, document.body);


1.2 什么是Reactjs组件

  Reactjs组件(自定义)就是一个函数(类),返回对应的Virtual DOM树结构。如果此函数继承至React.Component,则称之为有状态组件,拥有改变组件状态的方法-setState和用于返回Virtual DOM树结构的render方法。1.1例子中的组件Root继承至React.Component,所以是个有状态组件。反之,则称之为无状态组件。无状态组件就是一个普通函数,返回Virtual DOM树结构,无法更改状态信息,相当于一个纯静态展示组件,例如下面这个组件Test就是个无状态组件。

function Test () {
  return (
    <div>I have no state</div>
  )
}


1.3 htmlVirtual DOM树

  上面的组件结构穿插着htmlclassextendsES6的语法,不能直接运行,需要通过babeljsbabel react插件编译成合法的js语法,编译的基本原理是将字符串先转成AST,然后用React.createElement方法进行组合,可以参考我之前的一篇关于解析html的文章http://ks.netease.com/blog?id=5384。具体开发的时候可以通过webpackbabel-load配合react参数进行编译,webpack的用法请参考https://webpack.js.org/configuration/。编译之后,class会编译成继承至React.Component的普通函数;render返回的html结构会编译成合法的Virtual DOM:React.createElement(‘div’, null, this.state.text)。当然,如果不想用babeljs等编译器,也可以直接通过ES5的语法进行编写,就是稍微麻烦一些。


1.4 Reactjs的优势

  Reactjs能带来什么好处?目前能看到的就是数据和DOM结构分离了,可以随意更改DOM结构和样式而不会影响数据展示;组件的复用和随意组合使代码变得清晰高效。之后的几章大家还能看到:batch update可以减少对DOM的操作使运行更快速;巧妙的事件代理机制使大数据下事件响应依然快速、内存占有率依然保持较低水平;Reactjs还支持服务端渲染。


  好了,简单的新手指引就到这,虽然讲得少,但都是精华。如果老君仙丹吃了没用,还是去打野吧。下面开始原理分析。


2章 组件渲染原理

  请用鼠标滚动到开局的那张图,这是一张Reactjs渲染Root组件的原理图。何为组件渲染?就是将组件render方法返回的Virtual DOM树映射成Actual DOM树安装到指定的containerDOM)里。下面我们根据这张原理图从上到下分析一下整个渲染工序(为了完整性,整个工序包括前期babeljs编译)。


2.1 首先看一下组件Root和组件A的结构

class Root extends React.Component {
  render () {
    return (
      <div>
        <p>I am P</p>
        <A></A>
      </div>
    )
  }
}
class A extends React.Component {
  render () {
    return (
     <div>
       <p>Hello A</p>
     </div>
   )
  }
}

  两个组件都继承至React.Component函数,所以都是有状态组件。render方法返回的是各自html形式的Virtual DOM树结构。


2.2 编译组件

  通过webpackbabel-load对组件进行编译,得到如图组件Root和组件AVirtual DOM树结构。

    Virtual DOM是个非常轻量的Object。只有typepropschildren 3个键值。typeVirtual DOM的类型,可以是字符串或者函数。如果是字符串,其值可以是divphtml标签名或者mv等自定义标签名。propsVirtual DOM的属性集合,对应Actual DOM的属性,类型为ObjectchildrenVirtual DOM的子级,类型为Array,成员可以是Virtual DOM或者是简单的String

2.3 渲染组件

    调用ReactDOM.render(React.createElement(Root), document.body)开始渲染Root组件。上文说过,渲染过程就是将组件render方法返回的Virtual DOM映射成Actual DOM树并添加到container(比如document.body)中。


    Reactjs会根据Virtual 树结构,生成一颗 host component树,这是一颗最重要的树,Virtual DOM树到Actual DOM树的映射就是由它来完成的。为了方便起见,我用一个简化版的图来展示一下这个映射过程:


    1.通过React.createElement(X)将组件封装成Virtual DOM

    2.初始化Virtual DOMhost componentReact根据Virtual DOM不同的type,生成3种类型的host component。函数对应composition component,字符串对应 dom component,简单文本对应text componenthost component内部用_hostParent这个属性来组织父子关系。

    3.安装dom componenttext componentdom component_tag属性被赋值为Virtual DOM的类型值。dom component根据_tag生成对应的Actual DOM,然后根据组织好的父子关系安插到对应Actual DOM上。

 

    这里对composition component做一下详细说明:组件自身会被封装成一个类型为FunctionVirtual DOM,渲染过程中对应composition component。它的作用是将组件render方法返回的Virtual DOM树对应的子host component树安连接到主host component树上。下面是整个安插过程:

1. 组件自身生成composition component

2. 组件render方法返回的Virtual DOM树会生成一颗子host component tree

3. composition component将自身的_hostParent赋值给子host component tree的顶点节点,因此composition component 和子component tree的顶点节点对应同一个parent host component,就像上图每个composition component都会包裹着一个dom component那样。

 

    总得来说,composition component dom component text component的树形结构组织好,然后映射出对应的Actual DOM树。Actual DOM树里各个节点创建和属性的更新由各自对应的dom component和text component 完成。当然,composition component还有其他重要的作用,比如监听事件(第3章)、更新子host component tree(第4章)。

第3章事件机制原理

    上面这张图就是Reactjs事件机制的框架图(秒懂的各位司机师傅请向右转走小道去第4)下面分块说明一下整个事件框架是怎么运作的。


3.1添加事件


通过格式为onXxx(Capture)的属性名可以为Virtual DOM添加事件。


<div onClick={this.onDivClick}></div>
或者
react.createElement(‘div’, {onClick: this.onDivClick}, null)

如果想监听捕获阶段的事件,只需为属性名加上Capture后缀即可。


<div onClickCapture={this.onDivCaptureClick}></div>。
或者
react.createElement(‘div’, {onClickCapture: this.onDivCaptureClick}, null)

    添加事件的原理是:当host component检测到Virtual DOM props格式为onXxx(Capture)的属性名时,首先检查Xxx的事件是否为Reactjs支持,如果支持,则将对应的event listenerhost component ID(由Reactjs产生唯一标识每个host component)作为key注册到event plugin hublistener bank(一个map)里。由于是用host componentID作为key,所以这里可以理解为event listener是被绑定在了Virtual DOM对应的host component上。这里需要注意,只有type为字符串的Virtual DOM能添加事件,其他类型的Virtual DOM添加事件是无效的,只能被当作普通属性值,如<Root onClick={this.onRootClick} />

3.2 Event Listener


    Event listener的功能主要是监听Actual DOMnative event。监听的原理是:为documentwindow(主要是scroll wheel等事件)绑定event listener。当native event触发并propagatedocumentwindow时,将其丢进Event Emitter里。


3.3 Event Emitter


    Event Emitternative event emitevent plugin hub里并构建出Reactjssynthetic event,然后运行synthetic event里的event listeners。这里的event listeners就是之前注册在DOM component上的event listeners


3.4 Event Plugin Hub



    Event plugin hub是整个Reactjs事件框架的核心,它主要的功能就是将native event组装成synthetic event,并搜集event listeners,然后运行它们。Reactjssynthetic event分为simple eventchange eventtap event7大类型并以plugin的形式注册到event plugin hub里。下面简单说一下组装过程:

    1.native event达到后,event plugin hub根据event type用注册好的plugins创建出synthetic event。比如click event,由于click事件属于simple type,那么synthetic event等于 new SimpleSyntheticEvent()

    2.根据native event的属性target找到对应的host component和它的顶级root host component

    3.event propagator 根据root host component current host component分别从上到下和从下到上遍历,模拟出事件的捕获和冒泡阶段,并在这个过程中从listener bank里搜集event listener

    4.运行搜集到的event listener

3.5 总结

    Reactjs把事件从原生的浏览器事件机制中抽了出来,并模拟出了一套相仿的事件机制使事件可以在Reactjs host component内传播。其优势是:1.使事件的处理更加的灵活,例如可以在事件的开始和结束统一做一些逻辑处理。2.事件的处理更加的快速:由于事件的listener注册在了Reactjshost component中,省去了DOMECMAScript间的上下文切换的性能损耗。3.事件的绑定更加节省内存:原生的事件绑定机制通常是为每一个DOM的绑定一个listener,在DOM的数量比较多的时候,会消耗掉很多的内存。而Reactjs通过事件代理的方式只在documentwindow绑定了listener

网易云免费体验馆,0成本体验20+款云产品! 

更多网易技术、产品、运营经验分享请点击