• React 事件和 Dom 事件

    2019-06-11 12:05:50
    注意:Chrome 中打印的对象展开的时候显示的是当前对象的,可能已经不是打印的时候的了,所以需要通过在打印的地方打断点的形式来查看准确的。或者直接通过打断点查看。 React 事件和 dom 事件 两者很像,只是...

    注意:Chrome 中打印的对象展开的时候显示的是当前对象的值,可能已经不是打印的时候的值了,所以需要通过在打印的地方打断点的形式来查看准确的值。或者直接通过打断点查看。

    React 事件和 dom 事件

    两者很像,只是有一些语法上的不一样。

    1. 事件名 jsx 中采用驼峰命名。
    2. 参数是一个事件处理函数

    原生的 dom 事件

    click 事件和 href 的优先级:

    click >>> href

    第一种:Dom 元素中直接绑定

    1. 直接在 html 中写为字符串

    如果在后面写 return false 可以阻止默认事件。注意只能是字符串形式的 js 代码,方法调用不行。

    <span onclick="console.log('我被点击了!!!')">Click me</span>
    复制代码
    1. 将 js 代码放到 js 函数中
        <span onclick="handle()">Click me</span>
        function handle() {
            console.log('我被点击了!!!);
        }
    复制代码

    第二种:在 JavaScript 代码中绑定

    1. DOM0 的写法:直接在 dom 对象上注册事件名称

    此时返回 false 可以阻止默认事件。

        elementObject.onclick = function(){
            // 事件处理代码
        }
    复制代码
    1. Dom 2 的写法:

      1. DOM2 支持同一dom元素注册多个同种事件。
      2. DOM2 新增了捕获和冒泡的概念。
      3. DOM2 事件通过 addEventListener 和 removeEventListener 管理
        ele.addEventListener(‘click’, handle, false);
    复制代码

    事件对象

    无论在DOM0还是DOM2还是DOM3中都会在事件函数中传入事件对象;

    常用属性:

        currentTarget : 当前时间程序正在处理的元素, 和this一样的;
        target || srcElement: 事件的目标
        view : 与元素关联的window, 我们可能跨iframe;
        eventPhase: 如果值为1表示处于捕获阶段, 值为2表示处于目标阶段,值为三表示在冒泡阶段
        preventDefault() 取消默认事件;
        stopPropagation() 取消冒泡或者捕获;
        stopImmediatePropagation 阻止绑定在事件触发元素其他同类事件的callback的运行
        
        trusted: 为ture是浏览器生成的,为false是开发人员创建的(DOM3)
    复制代码

    注意误区:

    1. 在同一个对象上注册事件,并不一定按照注册顺序执行

    事件目的地节点既绑定了冒泡事件也绑定了捕获事件,如果是同一类型则按照代码执行顺序执行

    1. event.stopPropagation(); 就是阻止事件的冒泡?

    除了阻止事件的冒泡,还阻止事件的继续捕获,简而言之就是阻止事件的进一步传播。

    React 事件

    接口和原来的 dom 事件一致。绑定处理函数分两种情况:

    包装了浏览器的原生的事件,并且是跨浏览器的。

    1. 不传参数
        <div className="box1" onClick={this.handleClickOne}>
    复制代码
    1. 传递参数有两种写法:
        1. 箭头函数:<div className="box2" onClick={e => this.handleClickTwo(e)}
        2. bind 方法:<div className="box2" onClick={this.handleClickTwo.bind(this, e, others)}
    复制代码

    事件重用

    Event Pooling:事件池

    react 中的事件会被重用,每一次事件对应的回调函数执行后事件上的所有属性都会失效。

    比如通过 setTimeout 异步方式访问事件会报错。

    此时的解决方法是调用 persist() 方法移出事件池从而保留事件对象。

    事件处理函数的执行时机

    事件处理函数都是在冒泡阶段执行,如果要让事件处理函数在捕获阶段执行,事件名后面加 Capture 就行。

        onClick  ==> onClickCapture 
    复制代码

    React 中阻止事件冒泡的三种方式

    合成事件中的 currentTarget 指向当前 dom 元素,但是 nativeEventcurrentTarget 指向 document

    1. 阻止合成事件间的冒泡,用e.stopPropagation();
    2. 阻止合成事件与最外层 document 上的事件间的冒泡
        nativeEvent 的 currentTarget 指向 document
        e.nativeEvent.stopImmediatePropagation();
    复制代码
    1. 阻止合成事件与 除最外层document上的原生事件 上的冒泡,通过判断 e.target 来避免
        document.body.addEventListener('click',e=>{
            // 通过e.target判断阻止冒泡
            if(e.target&&e.target.matches('a')){
                return;
            }
            console.log('body');
        })
    复制代码

    自定义 dom 事件

    使用到的事件对象有 EventCustomEvent

    自定义事件和触发事件:

        // 通过 Event 创建新的事件
    	const event = new Event('newEvent');
    
    	box1.addEventListener('newEvent', function() {
    		console.log('newEvent 事件被触发了!!!!')
    	});
    
    	setTimeout(function() {
    	    // 通过调用元素的 dispatchEvent 方法在该元素上面触发该事件
     		box1.dispatchEvent(event);
    	}, 2000);
    复制代码

    自定义事件的时候,为事件对象添加数据:

    	const customEvent = new CustomEvent('customEvent', { 'data': '2121212' });
    	box1.addEventListener('customEvent', function(e) {
    		console.log('customEvent 事件被触发了!!!!', e, customEvent)
    	});
    	setTimeout(function() {
    		box1.dispatchEvent(customEvent);
    	}, 2000);
    复制代码

    代码控制触发内置事件

    	box1.addEventListener('click', function(e) {
    		console.log('代码控制触发内置事件', e)
    	});
    
    	setTimeout(function() {
    		const click = new MouseEvent('click')
    		box1.dispatchEvent(click)
    	}, 2000)
    复制代码

    其中 click 事件还可以通过 HTMLElement 对象上面的 的 click() 方法模拟。

    其中 querySelector 和 querySelectorAllElement 上面的方法。

    转载于:https://juejin.im/post/5c39a0c86fb9a049c84faee6

    展开全文
  • React在前端界一直很流行,而且学起来也不是很难,只...在复杂的前端项目中一个页面可能包含状态,对React框架理解得更精细一些对前端优化很重要。曾经这页面点击条记录展示详情会卡顿数秒,而这仅仅是前...

    React在前端界一直很流行,而且学起来也不是很难,只需要学会JSX、理解StateProps,然后就可以愉快的玩耍了,但想要成为React的专家你还需要对React有一些更深入的理解,希望本文对你有用。

    这是Choerodon的一个前端页面

    在复杂的前端项目中一个页面可能包含上百个状态,对React框架理解得更精细一些对前端优化很重要。曾经这个页面点击一条记录展示详情会卡顿数秒,而这仅仅是前端渲染造成的。

    为了能够解决这些问题,开发者需要了解React组件从定义到在页面上呈现(然后更新)的整个过程。

    React在编写组件时使用混合HTMLJavaScript的一种语法(称为JSX)。 但是,浏览器对JSX及其语法一无所知,浏览器只能理解纯JavaScript,因此必须将JSX转换为HTML。 这是一个div的JSX代码,它有一个类和一些内容:

    <div className='cn'>
      文本
    </div>
    

    在React中将这段jsx变成普通的js之后它就是一个带有许多参数的函数调用:

    React.createElement(
      'div',
      { className: 'cn' },
      '文本'
    );
    

    它的第一个参数是一个字符串,对应html中的标签名,第二个参数是它的所有属性所构成的对象,当然,它也有可能是个空对象,剩下的参数都是这个元素下的子元素,这里的文本也会被当作一个子元素,所以第三个参数是 “文本”

    到这里你应该就能想象这个元素下有更多children的时候会发生什么。

    <div className='cn'>
      文本1
      <br />
      文本2
    </div>
    
    React.createElement(
      'div',
      { className: 'cn' },
      '文本1',              // 1st child
      React.createElement('br'), // 2nd child
      '文本1'               // 3rd child
    )
    

    目前的函数有五个参数:元素的类型,全部属性的对象和三个子元素。 由于一个child也是React已知的HTML标签,因此它也将被解释成函数调用。

    到目前为止,本文已经介绍了两种类型的child参数,一种是string纯文本,一种是调用其他的React.createElement函数。其实,其他值也可以作为参数,比如:

    • 基本类型 false,null,undefined和 true
    • 数组
    • React组件

    使用数组是因为可以将子组件分组并作为一个参数传递:

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

    当然,React的强大功能不是来自HTML规范中描述的标签,而是来自用户创建的组件,例如:

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

    组件允许开发者将模板分解为可重用的块。在上面的“纯函数”组件的示例中,组件接受一个包含表行数据的对象数组,并返回React.createElement

    元素及其行作为子元素的单个调用 。

    每当开发者将组件放入JSX布局中时它看上去是这样的:

    <Table rows={rows} />
    

    但从浏览器角度,它看到的是这样的:

    React.createElement(Table, { rows: rows });
    

    请注意,这次的第一个参数不是以string描述的HTML元素,而是组件的引用(即函数名)。第二个参数是传入该组件的props对象。

    将组件放在页面上

    现在,浏览器已经将所有JSX组件转换为纯JavaScript,现在浏览器获得了一堆函数调用,其参数是其他函数调用,还有其他函数调用…如何将它们转换为构成网页的DOM元素?

    为此,开发者需要使用ReactDOM库及其render方法:

    function Table({ rows }) { /* ... */ } // 组件定义
    
    // 渲染一个组件
    ReactDOM.render(
      React.createElement(Table, { rows: rows }), // "创建" 一个 component
      document.getElementById('#root') // 将它放入DOM中
    );
    

    ReactDOM.render被调用时,React.createElement最终也会被调用,它返回以下对象:

    // 这个对象里还有很多其他的字段,但现在对开发者来说重要的是这些。
    {
      type: Table,
      props: {
        rows: rows
      },
      // ...
    }
    

    这些对象构成了React意义上的Virtual DOM

    它们将在所有进一步渲染中相互比较,并最终转换为真正的DOM(与Virtual DOM对比)。

    这是另一个例子:这次有一个div具有class属性和几个子节点:

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

    变成:

    {
      type: 'div',
      props: {
        className: 'cn',
        children: [
          'Content 1!',
          'Content 2!'
        ]
      }
    }
    

    所有的传入的展开函数,也就是React.createElement除了第一第二个参数剩下的参数都会在props对象中的children属性中,不管传入的是什么函数,他们最终都会作为children传入props中。

    而且,开发者可以直接在JSX代码中添加children属性,将子项直接放在children中,结果仍然是相同的:

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

    在Virtual DOM对象被建立出来之后ReactDOM.render会尝试按以下规则把它翻译成浏览器能够看得懂的DOM节点:

    • 如果Virtual DOM对象中的type属性是一个string类型的tag名称,创建一个tag,包含props里的全部属性。
    • 如果Virtual DOM对象中的type属性是一个函数或者class,调用它,它返回的可能还是一个Virtual DOM然后将结果继续递归调用此过程。
    • 如果props中有children属性,对children中的每个元素进行以上过程,并将返回的结果放到父DOM节点中。

    最后,浏览器获得了以下HTML(对于上述table的例子):

    <table>
      <tr>
        <td>Title</td>
      </tr>
      ...
    </table>
    

    重建DOM

    接下浏览器要“重建”一个DOM节点,如果浏览器要更新一个页面,显然,开发者并不希望替换页面中的全部元素,这就是React真正的魔法了。如何才能实现它?先从最简单的方法开始,重新调用这个节点的ReactDOM.render方法。

    // 第二次调用
    ReactDOM.render(
      React.createElement(Table, { rows: rows }),
      document.getElementById('#root')
    );
    

    这一次,上面的代码执行逻辑将与看到的代码不同。React不是从头开始创建所有DOM节点并将它们放在页面上,React将使用“diff”算法,以确定节点树的哪些部分必须更新,哪些部分可以保持不变。

    那么它是怎样工作的?只有少数几个简单的情况,理解它们将对React程序的优化有很大帮助。请记住,接下来看到的对象是用作表示React Virtual DOM中节点的对象。

    ▌Case 1:type是一个字符串,type在调用之间保持不变,props也没有改变。

    // before update
    { type: 'div', props: { className: 'cn' } }
    
    // after update
    { type: 'div', props: { className: 'cn' } }
    

    这是最简单的情况:DOM保持不变。

    ▌Case 2:type仍然是相同的字符串,props是不同的。

    // before update:
    { type: 'div', props: { className: 'cn' } }
    
    // after update:
    { type: 'div', props: { className: 'cnn' } }
    

    由于type仍然代表一个HTML元素,React知道如何通过标准的DOM API调用更改其属性,而无需从DOM树中删除节点。

    ▌Case 3:type已更改为不同的组件String或从String组件更改为组件。

    // before update:
    { type: 'div', props: { className: 'cn' } }
    
    // after update:
    { type: 'span', props: { className: 'cn' } }
    

    由于React现在看到类型不同,它甚至不会尝试更新DOM节点:旧元素将与其所有子节点一起被删除(unmount)。因此,在DOM树上替换完全不同的元素的代价会非常之高。幸运的是,这在实际情况中很少发生。

    重要的是要记住React使用===(三等)来比较type值,因此它们必须是同一个类或相同函数的相同实例。

    下一个场景更有趣,因为这是开发者最常使用React的方式。

    ▌Case 4:type是一个组件。

    // before update:
    { type: Table, props: { rows: rows } }
    
    // after update:
    { type: Table, props: { rows: rows } }
    

    你可能会说,“这好像没有任何变化”,但这是不对的。

    如果type是对函数或类的引用(即常规React组件),并且启动了树diff比较过程,那么React将始终尝试查看组件内部的所有child以确保render的返回值没有更改。即在树下比较每个组件 - 是的,复杂的渲染也可能变得昂贵!

    组件中的children

    除了上面描述的四种常见场景之外,当元素有多个子元素时,开发者还需要考虑React的行为。假设有这样一个元素:

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

    开发者开发者想将它重新渲染成这样(spandiv交换了位置):

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

    那么会发生什么?

    当React看到里面的任何数组类型的props.children,它会开始将它中的元素与之前看到的数组中的元素按顺序进行比较:index 0将与index 0,index 1与index 1进行比较,对于每对子元素,React将应用上述规则集进行比较更新。在以上的例子中,它看到div变成一个span这是一个情景3中的情况。但这有一个问题:假设开发者想要从1000行表中删除第一行。React必须“更新”剩余的999个孩子,因为如果与先前的逐个索引表示相比,他们的内容现在将不相等。

    幸运的是,React有一种内置的方法来解决这个问题。如果元素具有key属性,则元素将通过key而不是索引进行比较。只要key是唯一的,React就会移动元素而不将它们从DOM树中移除,然后将它们放回(React中称为挂载/卸载的过程)。

    // ...
    props: {
      children: [ // 现在react就是根据key,而不是索引来比较了
        { type: 'div', key: 'div' },
        { type: 'span', key: 'span' },
        { type: 'br', key: 'bt' }
      ]
    },
    // ...
    

    当状态改变时

    到目前为止,本文只触及了props,React哲学的一部分,但忽略了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对象有一个counter属性。单击按钮会增加其值并更改按钮文本。但是当用户点击时,DOM会发生什么?它的哪一部分将被重新计算和更新?

    调用this.setState也会导致重新渲染,但不会导致整个页面重渲染,而只会导致组件本身及其子项。父母和兄弟姐妹都可以幸免于难。

    修复问题

    本文准备了一个DEMO,这是修复问题前的样子。你可以在这里查看其源代码。不过在此之前,你还需要安装React Developer Tools

    打开demo要看的第一件事是哪些元素以及何时导致Virtual DOM更新。导航到浏览器的Dev Tools中的React面板,点击设置然后选择“Highlight Updates”复选框:

    现在尝试在表中添加一行。如你所见,页面上的每个元素周围都会出现边框。这意味着每次添加行时,React都会计算并比较整个Virtual DOM树。现在尝试按一行内的计数器按钮。你将看到Virtual DOM如何更新 (state仅相关元素及其子元素更新)。

    React DevTools暗示了问题可能出现的地方,但没有告诉开发者任何细节:特别是有问题的更新是指元素“diff”之后有不同,还是组件被unmount/mount了。要了解更多信息,开发者需要使用React的内置分析器(请注意,它不能在生产模式下工作)。

    转到Chrome DevTools中的“Performance”标签。点击record按钮,然后点击表格。添加一些行,更改一些计数器,然后点击“Stop”按钮。稍等一会儿之后开发者会看到:

    在结果输出中,开发者需要关注“Timing”。缩放时间轴,直到看到“React Tree Reconciliation”组及其子项。这些都是组件的名称,旁边有[update]或[mount]。可以看到有一个TableRow被mount了,其他所有的TableRow都在update,这并不是开发者想要的。

    大多数性能问题都由[update]或[mount]引起

    一个组件(以及组件下的所有东西)由于某种原因在每次更新时重新挂载,开发者不想让它发生(重新挂载很慢),或者在大型分支上执行代价过大的重绘,即使组件似乎没有发生任何改变。

    修复mount/unmount

    现在,当开发者了解React如何决定更新Virtual DOM并知道幕后发生的事情时,终于准备好解决问题了!修复性能问题首先要解决 mount/unmount。

    如果开发者将任何元素/组件的多个子元素在内部表示为数组,那么程序可以获得非常明显的速度提升。

    考虑一下:

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

    在虚拟DOM中,将表示为:

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

    一个简单的Message组件(是一个div带有一些文本,像是猪齿鱼的顶部通知)和一个很长的Table,比方说1000多行。它们都是div元素的child,因此它们被放置在父节点的props.children之下,并且它们没有key。React甚至不会通过控制台警告来提醒开发者分配key,因为子节点React.createElement作为参数列表而不是数组传递给父节点。

    现在,用户已经关闭了顶部通知,所以Message从树中删除。TableFooter是剩下的child。

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

    React如何看待它?它将它视为一系列改变了type的child:children[0]的type本来是Message,但现在他是Table。因为它们都是对函数(和不同函数)的引用,它会卸载整个Table并再次安装它,渲染它的所有子代:1000多行!

    因此,你可以添加唯一键(但在这种特殊情况下使用key不是最佳选择)或者采用更智能的trick:使用 && 的布尔短路运算,这是JavaScript和许多其他现代语言的一个特性。像这样:

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

    即使Message被关闭了(不再显示),props.children父母div仍将拥有三个元素,children[0]具有一个值false(布尔类型)。还记得true/false, null甚至undefined都是Virtual DOM对象type属性的允许值吗?浏览器最终得到类似这样的东西:

    // ...
    props: {
      children: [
        false, //  isShowMessage && <Message /> 短路成了false
        { type: Table },
        { type: Footer }
      ]
    }
    // ...
    

    所以,不管Message是否被显示,索引都不会改变,Table仍然会和Table比较,但仅仅比较Virtual DOM通常比删除DOM节点并从中创建它们要快得多。

    现在来看看更高级的东西。开发者喜欢HOC。高阶组件是一个函数,它将一个组件作为一个参数,添加一些行为,并返回一个不同的组件(函数):

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

    开发者在父render方法中创建了一个HOC 。当React需要重新渲染树时,React 的Virtual DOM将如下所示:

    // On first render:
    {
      type: ComponentWithName,
      props: {},
    }
    
    // On second render:
    {
      type: ComponentWithName, // Same name, but different instance
      props: {},
    }
    

    现在,React只会在ComponentWithName上运行一个diff算法,但是这次同名引用了一个不同的实例,三等于比较失败,必须进行完全重新挂载。注意它也会导致状态丢失,幸运的是,它很容易修复:只要返回的实例都是同一个就好了:

    // 单例
    const ComponentWithName = withName(Component);
    
    class App extends React.Component() {
      render() {
        return <ComponentWithName />;
      }
    }
    

    修复update

    现在浏览器已经确保不会重新装载东西了,除非必要。但是,对位于DOM树根目录附近的组件所做的任何更改都将导致其所有子项的进行对比重绘。结构复杂,价格昂贵且经常可以避免。

    如果有办法告诉React不要查看某个分支,那将是很好的,因为它没有任何变化。

    这种方式存在,它涉及一个叫shouldComponentUpdate的组件生命周期函数。React会在每次调用组件之前调用此方法,并接收propsstate的新值。然后开发者可以自由地比较新值和旧值之间的区别,并决定是否应该更新组件(返回truefalse)。如果函数返回false,React将不会重新渲染有问题的组件,也不会查看其子组件。

    通常比较两组propsstate一个简单的浅层比较就足够了:如果顶层属性的值相同,浏览器就不必更新了。浅比较不是JavaScript的一个特性,但开发者很多方法来自己实现它,为了不重复造轮子,也可以使用别人写好的方法

    在引入浅层比较的npm包后,开发者可以编写如下代码:

    class TableRow extends React.Component {
      shouldComponentUpdate(nextProps, nextState) {
        const { props, state } = this;
        return !shallowequal(props, nextProps)
               && !shallowequal(state, nextState);
      }
      render() { /* ... */ }
    }
    

    但是你甚至不必自己编写代码,因为React在一个名为React.PureComponent的类中内置了这个功能,它类似于React.Component,只是shouldComponentUpdate已经为你实现了浅层props/state比较。

    或许你会有这样的想法,能替换ComponentPureComponent就去替换。但开发者如果错误地使用PureComponent同样会有重新渲染的问题存在,需要考虑下面三种情况:

    <Table
        // map每次都会返回一个新的数组实例,所以每次比较都是不同的
        rows={rows.map(/* ... */)}
        // 每一次传入的对象都是新的对象,引用是不同的。
        style={ { color: 'red' } }
        // 箭头函数也一样,每次都是不同的引用。
        onUpdate={() => { /* ... */ }}
    />
    

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

    正确地使用PureComponent,你可以在这里看到所有的TableRow都被“纯化”后渲染的效果。

    但是,如果你迫不及待想要全部使用纯函数组件,这样是不对的。比较两组propsstate不是免费的,对于大多数基本组件来说甚至都不值得:运行shallowCompare比diff算法需要更多时间。

    可以使用此经验法则:纯组件适用于复杂的表单和表格,但它们通常会使按钮或图标等简单元素变慢。

    现在,你已经熟悉了React的渲染模式,接下来就开始前端优化之旅吧。

    关于Choerodon猪齿鱼

    Choerodon猪齿鱼是一个开源企业服务平台,基于Kubernetes的容器编排和管理能力,整合DevOps工具链、微服务和移动应用框架,来帮助企业实现敏捷化的应用交付和自动化的运营管理的开源平台,同时提供IoT、支付、数据、智能洞察、企业应用市场等业务组件,致力帮助企业聚焦于业务,加速数字化转型。

    大家也可以通过以下社区途径了解猪齿鱼的最新动态、产品特性,以及参与社区贡献:

    展开全文
  • 一个虚拟DOM(元素)是一个一般的js对象,准确的说是一个对象树(倒立的);虚拟DOM保存了真实DOM的层次关系和一些基本属性,与真实DOM一一对应;如果只是更新虚拟DOM,页面是不会重绘的。 Virtual DOM 算法的基本步骤:...

    1.虚拟DOM是什么?

    一个虚拟DOM(元素)是一个一般的js对象,准确的说是一个对象树(倒立的);虚拟DOM保存了真实DOM的层次关系和一些基本属性,与真实DOM一一对应;如果只是更新虚拟DOM,页面是不会重绘的。
    Virtual DOM 算法的基本步骤:
    用JS对象树表示DOM树的结构;然后用这个树构建一个真正的DOM树插到文档当中;当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异;把差异应用到真实DOM树上,视图就更新了。
    进一步理解:
    Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)。

    2.JSX

    (1)JSX(JavaScript XML)

    react定义的一种类似于XML的JS扩展语法:XML+JS
    作用:用来创建react虚拟DOM(元素)对象。
    js中直接可以套标签,但标签要套js需要放在{}中,在解析显示js数组时,会自动遍历显示。
    把数据的数组转换为标签的数组:

    var liArr = dataArr.map(function(item, index){
            return <li key={index}>{item}</li>
          })
    

    注意:标签必须有结束;标签的class属性必须改为className属性;标签的style属性值必须为: {{color:‘red’, width:12}}

    例如:

    var ele = <h1>Hello JSX<h1>
    

    注意:它不是字符串,也不是HTML/XML标签;它最终产生的就是一个JS对象。

    (2)标签名任意:HTML标签或其他标签;标签属性任意:HTML标签属性或其他。

    (3)基本语法规则

    遇到 <开头的代码,以标签的语法解析:html同名标签转换为html同名元素,其它标签需要特别解析;遇到以 { 开头的代码,以JS的语法解析: 标签中的js代码必须用{}包含。

    (4)babel.js的作用

    浏览器的js引擎是不能直接解析JSX语法代码的,需要babel转译为纯JS的代码才能运行;只要用了JSX,都要加上type=“text/babel”,声明需要babel来处理。

    3.渲染虚拟DOM(元素)

    语法: ReactDOM.render(virtualDOM, containerDOM)
    作用: 将虚拟DOM元素渲染到真实容器DOM中显示
    参数说明:
    参数一: 纯js或jsx创建的虚拟dom对象
    参数二: 用来包含虚拟DOM元素的真实dom元素对象(一般是一个div)

    4.创建虚拟DOM的两种方式

    (1)纯js(一般不用)

    React.createElement('h1', {id:'myTitle'}, title)
    

    (2)JSX

    页面结构:

    <h1 id='myTitle'>{title}</h1>
    <div id='example1'></div>
    <div id='example2'></div>
    

    JS:
    在这里插入图片描述

    页面效果:
    在这里插入图片描述

    关注微博、微信公众号搜索前端小木,让我们一起学习成长。

    展开全文
  • React虚拟DOM的好处

    2019-06-11 10:56:22
    关于React提供的虚拟DOM的好处有一些困惑和误解需要阐明。 我们总是或多或少的听说过直接操作DOM是低效和缓慢的。然而,我们几乎没有可用的数据来支持这观点。关于React虚拟DOM的令人愉悦的地方在于web开发过程中...

    关于React提供的虚拟DOM的好处有一些困惑和误解需要阐明。

    我们总是或多或少的听说过直接操作DOM是低效和缓慢的。然而,我们几乎没有可用的数据来支持这个观点。关于React虚拟DOM的令人愉悦的地方在于web开发过程中,它采用了更加高效的方式来更新view层。

    我们把使用React的其他好处姑且放到一边,例如单项绑定和组件化。本节我讲详细的讨论一下虚拟DOM,随后公正的抉择为什么使用React而不是其他UI库(或者任何UI库都不使用)。

    为什么需要UI库

    在反应式的系统中两个最重要的idea应该是事件驱动和对数据变化的响应。

    DOM用户接口组件有内部状态,当无论何时有情况发生变化的时候,更新浏览器并不是像简单的重新生成DOM那么简单。举个例子,如果Gmail曾经这么做的话,因为浏览器窗口为了显示新的信息,需要不断的刷新整个页面,如果你正在写邮件的话,所有的都会被清除,你一定会经常恼怒不已。

    DOM的这种状态方式决定了为什么我们需要一个用户界面库,因为这些库提供了一些解决方案,例如键/值 观察(在Ember库中使用了此方法),或者脏值检测(Angular使用了此方法)。UI库可以观察数据模型的改变,然后在改变发生的时候只改变对应的部分DOM,或者观察DOM改变,然后更新数据模型。

    这种观察和更新的模式叫做双向绑定,这种方式常常让与用户界面的工作更加复杂和让人困惑。

    React的不同之处

    React的虚拟DOM与其他库的不同之处在于从程序员的角度看,这种模式更加的简单。你只需要写纯javascript,然后更新React组件,React将为你更新DOM。数据绑定并没有和应用程序搅和在一起。

    React使用单向数据绑定使得事情变得更加简单,每一次在ReactUI的input域中输入内容,React并不直接改变状态。相反,React更新数据模型,随后引起UI更新,你输入的文字将在域中出现。

    DOM真的慢吗?

    关于虚拟DOM的很多演讲和文章都指出,尽管现在jiavascript的引擎已经很快了,但是读取和写入浏览器的DOM还是很慢,

    这不是特别的准确。DOM是很快的。增加和去除DOM节点就是设置一些javascript对象的属性,这才是简单的操作,这并不是需要做很多的工作。

    真正慢的地方自在于,每次DOM改变的时候,都需要在浏览器中进行渲染。每一次DOM改变的时候,浏览器都需要重新计算CSS,进行布局处理,然后重新渲染页面。这都需要时间。

    浏览器的开发者持续不断的工作来缩短渲染页面的时间。最关键的需要完成的事情是最小化DOM改变,然后批处理DOM变化,在必要的时候才重新渲染页面。(目前原生浏览器还是无法做到)

    这种批处理DOM改变的策略,把我们提升到了一个更加抽象的层次,这也是React虚拟DOM背后的idea。

    虚拟DOM是怎么工作的?

    与真实DOM相似,虚拟DOM也是节点树,以对象的形式列出内容,属性。React的render方法从React组建中创建节点树,因为动作改变等引起数据模型变化的时候,React更新节点树。

    每次React app内部的数据改变,用户界面的虚拟DOM都会构建。

    这里就是最有趣的地方,在React中更新浏览器DOM需要三步: 1. 每次数据模型变化的时候,虚拟DOM节点树都会重新构建。 2. React依赖某个算法(称之为diff算法)来与上一个虚拟DOM节点数进行比较,只有在不同的情况下才重新进行计算。 3.所有的变化要经过批处理,完成之后,真实DOM才进行更新。

    虚拟DOM慢吗?

    有人认为只要有一点改变就重新构造整个虚拟DOM节点树有点浪费,但是他没有提及一个事实,React在内存中存储有两个虚拟DOM树。

    但是,真实情况是渲染虚拟DOM总是比直接渲染浏览器DOM快。这种论断与你使用的浏览器也没多大的关系。

    我们看一些数据

    我不会做一些基准测试,因为已经有很多开发者创建了很多测试来搞清楚React虚拟DOM方法是否比原生的DOM渲染更加快速。但是频繁的结论显示似乎不是这样,但是因为很多测试都是不实际的,所以这些测试结果也无关紧要。

    简单来说,虚拟DOM就是把所有浏览器为了最小化DOM操作,这些对于开发者来说透明的操作进行了抽象。这层额外的抽象的缺点就是增加了CPU的开销。

    这里有一个"hello,world"的例子,使用了原生的DOM操作:

    随后我们在React中做相同的事情。在这里请注意我们需要包含React,React DOM 和babel,其中babel负责把在render方法中的类XML的代码进行转换成原生的javascript。

    结果显示,本地方法更加的快速。这并不是开玩笑,让我们看一下证据。

    这里是加载和渲染真实DOM的性能分析图。(来源于Chrome)

    这里是在同样的浏览器中加载和展示React的"hello,world"应用时间线。

    请注意本质上来说,事情是相同的。React比直接操作DOM慢,而且慢了很多。那么,与jQuery比又如何呢?

    从分析图中,我们可以看到,jQuery进行展示最简单的hello,world的应用比原生的纯javascript满了50毫秒,但是jQuery版本和原声版本都是React的3倍。

    所以,很清晰,如果要说哪一个更快,原生的javascript和jQuery无疑略胜一筹。

    但是,使用库总是要比不使用库慢一些,这显然是再正常不过了,在内存里进行构建DOM,然后再进行真实的DOM操作更慢,也是很符合逻辑的情况。

    废话说多了,现在让我们准确思考一下,怎么使得虚拟DOM加载更快。

    怎么使用虚拟DOM

    我的"hello world"项目对于React来说是不公平的,原因在于他们仅仅处理的是初次渲染页面的性能比较。React设计的目的是用来更新网页。

    因为虚拟DOM的存在,每一次数据模型的改变都会触发虚拟用户接口的刷新。这与其他库进行直接的DOM更新是不一样的。虚拟DOM实际上使用了更少的内训,因为它不需要在内存中设置观察者。

    然而,每一次动作发生的时候,在内存中比较虚拟DOM也是低效的。这需要大量的CPU运算。

    因为这个愿意,React开发者在决定需要渲染什么的时候并不是完全被动的。如果你知道特定的动作不会影响特定的组件,你可以告诉React不要去分析组件差别,这无疑可以大大节省资源,加快应用程序的性能。示范操作中的React程序是可以进行性能提升的。

    真实情况是,没办法准确的知道虚拟DOM是否比直接进行DOM操作更快。因为这依赖于很多变量,尤其是和你怎么优化程序密切相关。

    这并不是革命性或者令人惊讶的事情,每个人使用的工具都是挺好的。React和虚拟DOM给予我们的,是用一种简单的方式进行UI渲染,它提供给我们一种更加简便的方式来更新浏览器。这种简化可以大大释放我们的脑力负担,使得优化用户界面更加的容易。这才是React真正好处所在的地方,如果处理得到,使用React,兼具性能和生产力,何乐而不为呢?

    原文地址:www.accelebrate.com/blog/the-re…

    作者: Chris Minnick

    说明:原文有一处举了一个100000个墨西哥玉米卷的例子,只是想说明不用每一次拿一个玉米卷,而是等待想拿的玉米卷确定之后然后在进行运输到自己国家进行消费。本质上还是映射React的工作方式,原文闲的稍加啰嗦,有删节。

    总结: 本文核心有两点:1.稍微介绍一下React的虚拟DOM和原生DOM的区别和联系。2.文末点出,虚拟DOM如果处理得到,是可以处理好性能问题的,但是React的真正好处在于,它的模块化思想,释放了生产力。

    颠覆:文中有几个观点需要重新审视,我们也经常听说直接操作DOM是低效的,但是这种低效在什么地方,似乎也没有国内的开发者进行准确的数据论证比较,作者给出了它的解释,直接操作DOM并不低效,低效的地方在于渲染页面的过程繁杂浪费时间,相比于React批处理之后只渲染一遍无疑是高效了很多。

    转载于:https://juejin.im/post/5c13b169e51d4556a446276f

    展开全文
  • React 初次渲染的时候无非会经历如下四步骤:ReactElement 元素的创建,以及它如何表达嵌套在上章节中也已阐述。React 的难点就在于...在遇到每层的时候根据元素类型创建不同的 DOM 元素,最后将这一个庞大...

    React 初次渲染的时候无非会经历如下四个步骤:


    ReactElement 元素的创建,以及它如何表达嵌套在上个章节中也已阐述。React 的难点就在于如何进行数据处理,并且保持高效,我们暂且忽略它。秉持由浅入深的原则进行学习。

    想要从 ReactElement 元素的层层嵌套结构中解析出 DOM,能直接想到的办法就是使用递归。在遇到每一层的时候根据元素类型创建不同的 DOM 元素,最后将这一个庞大的 DOM 元素树插入到真实的 DOM 中。

    这些功能,都集中在:

    packages/react-dom/src/client/ReactDOMComponent.js

    createElement

    /**
     * 创建元素
     * @param type
     * @param props
     * @param rootContainerElement
     * @param parentNamespace
     * @returns {Element}
     */
    export function createElement(
      type: string,
      props: Object,
      rootContainerElement: Element | Document,
      parentNamespace: string,
    ): Element {
      let isCustomComponentTag;
    
      // 我们在它们的父容器的命名空间中创建标记,除了 HTML 标签没有命名空间。
      // getOwnerDocumentFromRootContainer 方法用来获取 document
      const ownerDocument: Document = getOwnerDocumentFromRootContainer(
        rootContainerElement,
      );
      let domElement: Element;
      let namespaceURI = parentNamespace;
      if (namespaceURI === HTML_NAMESPACE) {
        namespaceURI = getIntrinsicNamespace(type);
      }
      if (namespaceURI === HTML_NAMESPACE) {
    
        if (type === 'script') {
          // 通过 .innerHTML 创建脚本,这样它的 “parser-inserted” 标志就被设置为true,而且不会执行
          const div = ownerDocument.createElement('div');
          div.innerHTML = '<script><' + '/script>'; // eslint-disable-line
          // 这将保证生成一个脚本元素。
          const firstChild = ((div.firstChild: any): HTMLScriptElement);
          domElement = div.removeChild(firstChild);
        } else if (typeof props.is === 'string') {
          // $FlowIssue `createElement` 应该为 Web Components 更新
          domElement = ownerDocument.createElement(type, {is: props.is});
        } else {
          // 因为 Firefox bug,分离 else 分支,而不是使用 `props.is || undefined`
          // 参见 https://github.com/facebook/react/pull/6896
          // 和 https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
          domElement = ownerDocument.createElement(type);
          // 通常在 `setInitialDOMProperties` 中分配属性,
          // 但是 `select` 上的 `multiple` 和 `size` 属性需要在 `option` 被插入之前添加。
          // 这可以防止:
          // 一个错误,其中 `select` 不能滚动到正确的选项,因为单一的 `select` 元素自动选择第一项 #13222
          // 一个 bug,其中 `select` 将第一个项目设置为 selected,而忽略 `size` 属性 #14239
          // 参见 https://github.com/facebook/react/issues/13222
          // 和 https://github.com/facebook/react/issues/14239
          if (type === 'select') {
            const node = ((domElement: any): HTMLSelectElement);
            if (props.multiple) {
              node.multiple = true;
            } else if (props.size) {
              // 设置大于 1 的 size 会使 select 的行为类似于 `multiple=true`,其中可能没有选择任何选项。
              // select 只有在“单一选择模式”下才需要这样做。
              node.size = props.size;
            }
          }
        }
      } else {
        domElement = ownerDocument.createElementNS(namespaceURI, type);
      }
    
      return domElement;
    }复制代码

    createElement 方法会根据不同的类型创建不同的 DOM 元素。值得指出的是,它不仅支持原生的 html 标签,还支持 WebComponent。

    createTextNode

    export function createTextNode(
      text: string,
      rootContainerElement: Element | Document,
    ): Text {
      return getOwnerDocumentFromRootContainer(rootContainerElement).createTextNode(
        text,
      );
    }复制代码

    createTextNode 方法创建了一个文本节点

    setInitialProperties

    export function setInitialProperties(
      domElement: Element,
      tag: string,
      rawProps: Object,
      rootContainerElement: Element | Document,
    ): void {
      const isCustomComponentTag = isCustomComponent(tag, rawProps);
    
      // TODO: Make sure that we check isMounted before firing any of these events.
      let props: Object;
      switch (tag) {
        case 'iframe':
        case 'object':
          trapBubbledEvent(TOP_LOAD, domElement);
          props = rawProps;
          break;
        case 'video':
        case 'audio':
          // Create listener for each media event
          for (let i = 0; i < mediaEventTypes.length; i++) {
            trapBubbledEvent(mediaEventTypes[i], domElement);
          }
          props = rawProps;
          break;
        case 'source':
          trapBubbledEvent(TOP_ERROR, domElement);
          props = rawProps;
          break;
        case 'img':
        case 'image':
        case 'link':
          trapBubbledEvent(TOP_ERROR, domElement);
          trapBubbledEvent(TOP_LOAD, domElement);
          props = rawProps;
          break;
        case 'form':
          trapBubbledEvent(TOP_RESET, domElement);
          trapBubbledEvent(TOP_SUBMIT, domElement);
          props = rawProps;
          break;
        case 'details':
          trapBubbledEvent(TOP_TOGGLE, domElement);
          props = rawProps;
          break;
        case 'input':
          ReactDOMInputInitWrapperState(domElement, rawProps);
          props = ReactDOMInputGetHostProps(domElement, rawProps);
          trapBubbledEvent(TOP_INVALID, domElement);
          // For controlled components we always need to ensure we're listening
          // to onChange. Even if there is no listener.
          ensureListeningTo(rootContainerElement, 'onChange');
          break;
        case 'option':
          ReactDOMOptionValidateProps(domElement, rawProps);
          props = ReactDOMOptionGetHostProps(domElement, rawProps);
          break;
        case 'select':
          ReactDOMSelectInitWrapperState(domElement, rawProps);
          props = ReactDOMSelectGetHostProps(domElement, rawProps);
          trapBubbledEvent(TOP_INVALID, domElement);
          // For controlled components we always need to ensure we're listening
          // to onChange. Even if there is no listener.
          ensureListeningTo(rootContainerElement, 'onChange');
          break;
        case 'textarea':
          ReactDOMTextareaInitWrapperState(domElement, rawProps);
          props = ReactDOMTextareaGetHostProps(domElement, rawProps);
          trapBubbledEvent(TOP_INVALID, domElement);
          // For controlled components we always need to ensure we're listening
          // to onChange. Even if there is no listener.
          ensureListeningTo(rootContainerElement, 'onChange');
          break;
        default:
          props = rawProps;
      }
    
      assertValidProps(tag, props);
    
      setInitialDOMProperties(
        tag,
        domElement,
        rootContainerElement,
        props,
        isCustomComponentTag,
      );
    
      switch (tag) {
        case 'input':
          // TODO: Make sure we check if this is still unmounted or do any clean
          // up necessary since we never stop tracking anymore.
          track((domElement: any));
          ReactDOMInputPostMountWrapper(domElement, rawProps, false);
          break;
        case 'textarea':
          // TODO: Make sure we check if this is still unmounted or do any clean
          // up necessary since we never stop tracking anymore.
          track((domElement: any));
          ReactDOMTextareaPostMountWrapper(domElement, rawProps);
          break;
        case 'option':
          ReactDOMOptionPostMountWrapper(domElement, rawProps);
          break;
        case 'select':
          ReactDOMSelectPostMountWrapper(domElement, rawProps);
          break;
        default:
          if (typeof props.onClick === 'function') {
            // TODO: This cast may not be sound for SVG, MathML or custom elements.
            trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
          }
          break;
      }
    }复制代码

    trapBubbledEvent 为元素添加事件。

    setInitialDOMProperties

    /**
     * 设置初始 DOM 属性
     * @param tag
     * @param domElement
     * @param rootContainerElement
     * @param nextProps
     * @param isCustomComponentTag
     */
    function setInitialDOMProperties(
      tag: string,
      domElement: Element,
      rootContainerElement: Element | Document,
      nextProps: Object,
      isCustomComponentTag: boolean,
    ): void {
      for (const propKey in nextProps) {
        if (!nextProps.hasOwnProperty(propKey)) {
          continue;
        }
        // 当前遍历的属性值
        const nextProp = nextProps[propKey];
        // 设置默认 style 标签的属性
        if (propKey === STYLE) {
          if (__DEV__) {
            if (nextProp) {
              // Freeze the next style object so that we can assume it won't be
              // mutated. We have already warned for this in the past.
              Object.freeze(nextProp);
            }
          }
          // 使用 node.style['cssFloat'] 这样的对象语法来为节点设置样式
          // 依赖于 `updateStylesByID` 而不是 `styleUpdates`.
          setValueForStyles(domElement, nextProp);
        } else if (propKey === DANGEROUSLY_SET_INNER_HTML) { // 单独处理 html 字符串
          const nextHtml = nextProp ? nextProp[HTML] : undefined;
          if (nextHtml != null) {
            setInnerHTML(domElement, nextHtml); // 设置 innerHTML
          }
        } else if (propKey === CHILDREN) { // 处理 children 属性
          if (typeof nextProp === 'string') {
            // 避免在文本为空时设置初始文本内容。
            // 在IE11中,在 <textarea> 上设置 textContent
            // 将导致占位符(placeholder)不显示在 <textarea> 中,直到它再次被 focus 和 blur。
            // https://github.com/facebook/react/issues/6731#issuecomment-254874553
            const canSetTextContent = tag !== 'textarea' || nextProp !== '';
            if (canSetTextContent) {
              setTextContent(domElement, nextProp); // 设置文本
            }
          } else if (typeof nextProp === 'number') {
            setTextContent(domElement, '' + nextProp); // 设置文本
          }
        } else if (
          propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
          propKey === SUPPRESS_HYDRATION_WARNING
        ) {
          // Noop
        } else if (propKey === AUTOFOCUS) {
          // We polyfill it separately on the client during commit.
          // We could have excluded it in the property list instead of
          // adding a special case here, but then it wouldn't be emitted
          // on server rendering (but we *do* want to emit it in SSR).
        } else if (registrationNameModules.hasOwnProperty(propKey)) {
          if (nextProp != null) {
            if (__DEV__ && typeof nextProp !== 'function') {
              warnForInvalidEventListener(propKey, nextProp);
            }
            ensureListeningTo(rootContainerElement, propKey); // 事件监听
          }
        } else if (nextProp != null) {
          // 设置属性值
          setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
        }
      }
    }复制代码

    setInitialDOMProperties 用于设置 DOM 元素初始的属性。

    • 首先,它会遍历所有的属性,过滤掉非自身的属性
    • 处理 dangerouslySetInnerHTML 属性,该属性接收一个 { __html: '' } 格式的对象,因此这里取出其 __html,使用   setInnerHTML  方法将其设置为 innerHTML
    • 处理字符串和数字文本,使用文本节点的 .nodeValue 属性设置其文本内容
    • 处理事件处理函数属性
    • 使用  setValueForProperty  方法设置普通属性的值

    其中有意思的是 React 处理 html 字符串的方式,将其放置在了 svg 标签中。为何如此处理?

    const setInnerHTML = createMicrosoftUnsafeLocalFunction(function(
      node: Element,
      html: string,
    ): void {
    
      // IE 没有针对 SVG 节点的 innerHTML,
      // 因此我们将新标记注入临时节点,然后将子节点移动到目标节点
      // 也就是说不能通过 SVG 的 innerHTML 属性直接赋值
    
      if (node.namespaceURI === Namespaces.svg && !('innerHTML' in node)) {
        reusableSVGContainer =
          reusableSVGContainer || document.createElement('div');
        reusableSVGContainer.innerHTML = '<svg>' + html + '</svg>';
        const svgNode = reusableSVGContainer.firstChild;
        while (node.firstChild) {
          node.removeChild(node.firstChild);
        }
        while (svgNode.firstChild) {
          node.appendChild(svgNode.firstChild);
        }
      } else {
        node.innerHTML = html;
      }
    });复制代码

    updateProperties

    /**
     * 应用 diff,更新属性
     * @param domElement
     * @param updatePayload
     * @param tag
     * @param lastRawProps
     * @param nextRawProps
     */
    export function updateProperties(
      domElement: Element,
      updatePayload: Array<any>,
      tag: string,
      lastRawProps: Object,
      nextRawProps: Object,
    ): void {
      // 更新名称前的选中状态
      // 在更新过程中,可能会有多个被选中项
      // 当一个选中的单选框改变名称时,浏览器会使其他的单选框的选中状态为 false
      if (
        tag === 'input' &&
        nextRawProps.type === 'radio' &&
        nextRawProps.name != null
      ) {
        ReactDOMInputUpdateChecked(domElement, nextRawProps); // 更新 radio 的 Checked 状态
      }
    
      const wasCustomComponentTag = isCustomComponent(tag, lastRawProps);
      const isCustomComponentTag = isCustomComponent(tag, nextRawProps);
      // 应用 diff.
      updateDOMProperties(
        domElement,
        updatePayload,
        wasCustomComponentTag,
        isCustomComponentTag,
      );
    
      // TODO: Ensure that an update gets scheduled if any of the special props
      // changed.
      switch (tag) {
        case 'input':
          // 在更新 props 之后更新 input 的包装器。
          // 这必须发生在 `updateDOMProperties` 之后。
          // 否则,HTML5 输入验证将发出警告并阻止新值的分配。
          ReactDOMInputUpdateWrapper(domElement, nextRawProps);
          break;
        case 'textarea':
          ReactDOMTextareaUpdateWrapper(domElement, nextRawProps);
          break;
        case 'select':
          // <select> 的值更新需要在 <option> 子元素 reconciliation 之后发生
          ReactDOMSelectPostUpdateWrapper(domElement, nextRawProps);
          break;
      }
    }复制代码


    updateDOMProperties

    function updateDOMProperties(
      domElement: Element,
      updatePayload: Array<any>,
      wasCustomComponentTag: boolean,
      isCustomComponentTag: boolean,
    ): void {
      // TODO: Handle wasCustomComponentTag
      for (let i = 0; i < updatePayload.length; i += 2) {
        const propKey = updatePayload[i];
        const propValue = updatePayload[i + 1];
        if (propKey === STYLE) {
          setValueForStyles(domElement, propValue);
        } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
          setInnerHTML(domElement, propValue);
        } else if (propKey === CHILDREN) {
          setTextContent(domElement, propValue);
        } else {
          setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
        }
      }
    }复制代码

    由于当前元素已经存在于 dom 中了,因此只需要根据传入的数据进行更新就行了。

    diffProperties

    /**
     * 计算两个对象之间的差异。
     * 为什么需要更新,无非三种情况:
     * 1. 之前有,之后没有,需要删除
     * 2. 之前没有,之后有,需要新增
     * 3. 前后值不一样,需要更新
     *
     * 但在 react 的实现中,新增和更新的逻辑都是一样,因此可以合并:
     * 1. 之前有,之后没有,需要删除
     * 2. 前后值不一样,需要更新
     *
     * @param domElement
     * @param tag
     * @param lastRawProps
     * @param nextRawProps
     * @param rootContainerElement
     * @returns {Array<*>}
     */
    export function diffProperties(
      domElement: Element,
      tag: string,
      lastRawProps: Object,
      nextRawProps: Object,
      rootContainerElement: Element | Document,
    ): null | Array<mixed> {
      if (__DEV__) {
        validatePropertiesInDevelopment(tag, nextRawProps);
      }
    
      let updatePayload: null | Array<any> = null;
    
      let lastProps: Object;
      let nextProps: Object;
      switch (tag) {
        case 'input':
          lastProps = ReactDOMInputGetHostProps(domElement, lastRawProps);
          nextProps = ReactDOMInputGetHostProps(domElement, nextRawProps);
          updatePayload = [];
          break;
        case 'option':
          lastProps = ReactDOMOptionGetHostProps(domElement, lastRawProps);
          nextProps = ReactDOMOptionGetHostProps(domElement, nextRawProps);
          updatePayload = [];
          break;
        case 'select':
          lastProps = ReactDOMSelectGetHostProps(domElement, lastRawProps);
          nextProps = ReactDOMSelectGetHostProps(domElement, nextRawProps);
          updatePayload = [];
          break;
        case 'textarea':
          lastProps = ReactDOMTextareaGetHostProps(domElement, lastRawProps);
          nextProps = ReactDOMTextareaGetHostProps(domElement, nextRawProps);
          updatePayload = [];
          break;
        default:
          lastProps = lastRawProps;
          nextProps = nextRawProps;
          if (
            typeof lastProps.onClick !== 'function' &&
            typeof nextProps.onClick === 'function'
          ) {
            // TODO: This cast may not be sound for SVG, MathML or custom elements.
            trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
          }
          break;
      }
    
      assertValidProps(tag, nextProps);
    
      let propKey;
      let styleName;
      let styleUpdates = null; // 用以记录需要更新的样式,1. 需要清除的样式 2. 新增的或者值改变的样式
      for (propKey in lastProps) {
        // nextProps 有此属性不处理
        // lastProps 没有此属性不处理
        // lastProps 此属性值为 null 或 undefined 不处理
        // 也就是说,只处理 lastProps 有,但 nextProps 没有的,且 lastProps 中值不为 null 或 undefined 的
        // 更直白点说就是重置需要删除的属性
        if (
          nextProps.hasOwnProperty(propKey) ||
          !lastProps.hasOwnProperty(propKey) ||
          lastProps[propKey] == null
        ) {
          continue;
        }
        if (propKey === STYLE) { // 处理 style 属性
          const lastStyle = lastProps[propKey];
          for (styleName in lastStyle) {
            if (lastStyle.hasOwnProperty(styleName)) {
              if (!styleUpdates) {
                styleUpdates = {};
              }
              styleUpdates[styleName] = ''; // 清除之前的样式
            }
          }
        } else if (propKey === DANGEROUSLY_SET_INNER_HTML || propKey === CHILDREN) {
          // Noop. This is handled by the clear text mechanism.
        } else if (
          propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
          propKey === SUPPRESS_HYDRATION_WARNING
        ) {
          // Noop
        } else if (propKey === AUTOFOCUS) {
          // 无操作。无论如何,它都不能在更新上工作。
        } else if (registrationNameModules.hasOwnProperty(propKey)) {
          // 这是一个特例。如果任何侦听器更新,
          // 我们需要确保 "current" props 指针得到更新,
          // 因此我们需要一个提交来更新此元素。
          if (!updatePayload) {
            updatePayload = [];
          }
        } else {
          // 对于所有其他已删除的属性,我们将其添加到队列中。相反,我们在提交阶段使用白名单。
          (updatePayload = updatePayload || []).push(propKey, null);
        }
      }
      for (propKey in nextProps) {
        const nextProp = nextProps[propKey];
        const lastProp = lastProps != null ? lastProps[propKey] : undefined;
        // nextProps 没有的属性不添加到更新
        // nextProp 和 lastProp 相等不处理
        // nextProp 和 lastProp 都为 null 或 undefined 不处理
        // 也就是说值处理 nextProps 有的,值不为 null 或 undefined,且 nextProp 和 lastProp 的情况
        // 更直白点说就是更新值发生改变的属性
        if (
          !nextProps.hasOwnProperty(propKey) ||
          nextProp === lastProp ||
          (nextProp == null && lastProp == null)
        ) {
          continue;
        }
        if (propKey === STYLE) {
          if (__DEV__) {
            if (nextProp) {
              // Freeze the next style object so that we can assume it won't be
              // mutated. We have already warned for this in the past.
              Object.freeze(nextProp);
            }
          }
          if (lastProp) {
            // Unset styles on `lastProp` but not on `nextProp`.
            for (styleName in lastProp) {
              if ( // lastProp 有这个属性,并且 nextProp 不存在或者没有此属性。
                lastProp.hasOwnProperty(styleName) &&
                (!nextProp || !nextProp.hasOwnProperty(styleName))
              ) {
                if (!styleUpdates) {
                  styleUpdates = {};
                }
                styleUpdates[styleName] = ''; // 置空样式属性值
              }
            }
            // 更新自 `lastProp` 以来更改的样式。
            for (styleName in nextProp) {
              // nextProp 存在的样式属性,且前后值不一样
              if (
                nextProp.hasOwnProperty(styleName) &&
                lastProp[styleName] !== nextProp[styleName]
              ) {
                if (!styleUpdates) {
                  styleUpdates = {};
                }
                styleUpdates[styleName] = nextProp[styleName];
              }
            }
          } else { // 以前不存在 style 属性
            // 依赖于 `updateStylesByID` 而不是 `styleUpdates`.
            if (!styleUpdates) {
              if (!updatePayload) {
                updatePayload = [];
              }
              updatePayload.push(propKey, styleUpdates); // 等同于 updatePayload.push(propKey, null);
            }
            styleUpdates = nextProp;
          }
        } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
          const nextHtml = nextProp ? nextProp[HTML] : undefined; // 取出 .__html 中存储的 html 字符串
          const lastHtml = lastProp ? lastProp[HTML] : undefined; // 取出 .__html 中存储的 html 字符串
          if (nextHtml != null) {
            if (lastHtml !== nextHtml) {
              (updatePayload = updatePayload || []).push(propKey, '' + nextHtml); // 需要更新的 html
            }
          } else {
            // TODO: It might be too late to clear this if we have children
            // inserted already.
          }
        } else if (propKey === CHILDREN) {
          // 处理纯文本
          if (
            lastProp !== nextProp &&
            (typeof nextProp === 'string' || typeof nextProp === 'number')
          ) {
            (updatePayload = updatePayload || []).push(propKey, '' + nextProp);
          }
        } else if (
          propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
          propKey === SUPPRESS_HYDRATION_WARNING
        ) {
          // Noop
        } else if (registrationNameModules.hasOwnProperty(propKey)) {
          if (nextProp != null) {
            // We eagerly listen to this even though we haven't committed yet.
            if (__DEV__ && typeof nextProp !== 'function') {
              warnForInvalidEventListener(propKey, nextProp);
            }
            ensureListeningTo(rootContainerElement, propKey);
          }
          if (!updatePayload && lastProp !== nextProp) {
            // 这是一个特例。如果任何侦听器更新,
            // 我们需要确保 "current" props 指针得到更新,
            // 因此我们需要一个提交来更新此元素。
            updatePayload = [];
          }
        } else {
          // 对于任何其他属性,我们总是将其添加到队列中,然后在提交期间使用白名单过滤掉。
          (updatePayload = updatePayload || []).push(propKey, nextProp);
        }
      }
      if (styleUpdates) {
        if (__DEV__) {
          validateShorthandPropertyCollisionInDev(styleUpdates, nextProps[STYLE]);
        }
        (updatePayload = updatePayload || []).push(STYLE, styleUpdates); // 添加样式更新
      }
      return updatePayload;
    }复制代码

    diffHydratedProperties

    diffHydratedText

    计算文本节点之间的差异

    /**
     * 计算文本节点之间的差异。
     * @param textNode
     * @param text
     * @returns {boolean}
     */
    function diffHydratedText(textNode: Text, text: string): boolean {
      const isDifferent = textNode.nodeValue !== text;
      return isDifferent;
    }复制代码

    restoreControlledState

    /**
     * 恢复受控状态
     * @param domElement
     * @param tag
     * @param props
     */
    export function restoreControlledState(
      domElement: Element,
      tag: string,
      props: Object,
    ): void {
      switch (tag) {
        case 'input':
          ReactDOMInputRestoreControlledState(domElement, props);
          return;
        case 'textarea':
          ReactDOMTextareaRestoreControlledState(domElement, props);
          return;
        case 'select':
          ReactDOMSelectRestoreControlledState(domElement, props);
          return;
      }
    }复制代码

    遗留问题

    上面创建的元素只是创建了一个,如何创建一个 DOM 树?


    转载于:https://juejin.im/post/5cf7113251882524156c9b5c

    展开全文
  • React.js中基本不需要对DOM进行操作,可以直接通过 setState 的方式重新渲染组件,渲染的时候可以把新的 props 传递给子组件,从而达到页面数据更新的效果。 但是有的时候需要组件渲染完后对DOM进行一些操作,比如...
  • React在这部分已经做到足够透明,在实际开发中我们基本无需关心虚拟DOM是如何运作的。然而,作为有态度的程序员,我们总是对技术背后的原理充满着好奇。理解其运行机制不仅有助于更好的理解React组件的
  • React-router-dom的入门使用 1.项目引入react-router-dom 安装: npm install react-router-dom --save 引入: import { BrowserRouter as Router, Route, Link, Switch, Redirect } from 'react-...
  • this.state:开发中,组件肯定要与用户进行交互,React大创新就是将组件看成了一个状态机,开始有一个初始状态,然后用户交互,导致状态变化,从而触发重新渲染页面 1、当用户点击组件,导致状态变化,this....
  • 传统的 DOM 操作是直接在 DOM 操作的,当需要修改系列元素中的时,就会直接对 DOM 进行操作。而采用 Virtual DOM 则会对需要修改的 DOM 进行比较(DIFF),从而只选择需要修改的部分。也因此对于不需要大量...
  • React Router包含了四包: 包名 描述 react-router React Router 核心api react-router-dom React Router的DOM绑定,在浏览器中运行不需要额外安装 react-router react-router-native React Native中...
  • React路由react-router-dom

    2019-04-01 14:34:22
    文档分享 ...React router和React-router-dom React router React-router提供了一些router的核心api,包括Router, Route, Switch等,但是它没有提供dom操作进行跳转的api。 当我们使用它的时...
  • React的ref获取dom元素

    2020-01-20 21:34:33
    react 声名式开发 可以和其他框架并存 组件化。 单向数据流,即父组件可以改变子组件的数据,但是子组件一定不能直接改变父组件的数据,必须调用父组件的方法来改变父组件的数据 函数式编程 Props,State与render...
  • React提供的获取DOM元素的方法有两种,react-dom中的findDOMNode(),二是refs。 1、findDOMNode findDOMNode通常用于React组件的引用,其语法如下: import ReactDOM from 'react-dom'; ReactDOM....
  • React获取Dom高度

    2020-05-11 10:18:17
    最近这个React的问题困惑了我几天,总结下React获取Dom 高度 React可以正常获取内容为文字的元素高度,但如果要获取img元素的高度,必须对Img设置固定的高度才可以获取,否则img的高度始终是0; 在...
  • 代码实现功能:根据state 中的判断子组件显示或隐藏,因为 setState 不是及时生效的,所以不做显示隐藏判断会报错。 render() { // 客户经理循环组件 function CommentSpan(props){ const numbers = props...
  • react-dom & jsx】 react【渐进式框架】 种最流行的框架设计思想,一般框架中都包含了很多内容,这样导致框架的体积过于臃肿,拖慢加载的速度。真实项目中,我们使用一个框架,不一定用到所有的功能,此时...
  • 在这里先介绍一个比较难以描述的坑 在使用react-router的时候我首先就是先在react-router的官网上去看她的官方文档,然后按照上面的教程来在自己的react工程中使用react-router,但是基本应该会遇到问题,原因就是...
  • react-router-dom详解

    2019-04-24 18:08:18
    <BrowserRouter> <BrowserRouter> 使用 HTML5 提供的 history API (pushState, replaceState 和 popstate 事件) 来保持 ... import { BrowserRouter } from 'react-router-dom'; <BrowserRouter ba...
  • react中的虚拟DOM

    2020-06-12 17:48:20
    次生成了一个完整的DOM片段 第二次生成了一个完整的DOM片段 第二次的DOM替换第次的DOM 这三步操作都非常耗性能 简单的优化:我们应该只替换更新了的部分,而不应该股脑地替换 state数据 JSX模版
1 2 3 4 5 ... 20
收藏数 11,429
精华内容 4,571